// tests/e2e/lease-approval.spec.ts import { expect, Page, test } from "@playwright/test"; import { applyDeterministicPage, e2eHeaders, useE2EUser } from "./helpers.ts"; test("tenant can request lease approval", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Tenant requests lease approval await useE2EUser(page, "tenant"); // Create a tenant invite first const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); expect(inviteResponse.ok()).toBeTruthy(); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; expect(inviteId).toBeTruthy(); // Request lease approval const approvalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); expect(approvalResponse.ok()).toBeTruthy(); const approvalPayload = await approvalResponse.json(); expect(approvalPayload?.success).toBeTruthy(); expect(approvalPayload?.lease).toBeDefined(); expect(approvalPayload?.lease?.approval_status).toBe("pending_approval"); }); test("landlord can view pending approvals on dashboard", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); // Create a pending lease approval const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; await page.request.post("/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }); // Navigate to landlord dashboard await page.goto("/landlord/dashboard"); // Check for pending approvals section await expect(page.getByRole("heading", { name: /pending lease approvals/i })) .toBeVisible(); // Verify approval card is displayed await expect(page.getByText(/taylor\.tenant/i)).toBeVisible(); await expect(page.getByText(/skyline lofts/i)).toBeVisible(); }); test("landlord can view pending approvals on dedicated page", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); // Create a pending lease approval const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; await page.request.post("/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }); // Navigate to dedicated approvals page await page.goto("/landlord/approvals"); // Check for page heading await expect(page.getByRole("heading", { name: /lease approvals/i })) .toBeVisible(); // Verify approval card is displayed await expect(page.getByText(/taylor\.tenant/i)).toBeVisible(); await expect(page.getByText(/skyline lofts/i)).toBeVisible(); // Verify approve and reject buttons are present await expect(page.getByRole("button", { name: /approve/i })).toBeVisible(); await expect(page.getByRole("button", { name: /reject/i })).toBeVisible(); }); test("landlord can approve a lease", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); // Create a pending lease approval const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; const requestApprovalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); const requestApprovalPayload = await requestApprovalResponse.json(); const leaseId = requestApprovalPayload?.lease?.id || ""; // Navigate to approvals page await page.goto("/landlord/approvals"); // Submit approval via API const approveResponse = await page.request.post("/api/lease/approve", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: leaseId, }, }); expect(approveResponse.ok()).toBeTruthy(); const approveData = await approveResponse.json(); expect(approveData?.success).toBeTruthy(); expect(approveData?.lease?.approval_status).toBe("approved"); expect(approveData?.lease?.approved_at).toBeDefined(); }); test("landlord can reject a lease with reason", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); // Create a pending lease approval const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; const requestApprovalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); const requestApprovalPayload = await requestApprovalResponse.json(); const leaseId = requestApprovalPayload?.lease?.id || ""; // Navigate to approvals page await page.goto("/landlord/approvals"); // Submit rejection via API const rejectionReason = "Insufficient income verification"; const rejectResponse = await page.request.post("/api/lease/reject", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: leaseId, rejection_reason: rejectionReason, }, }); expect(rejectResponse.ok()).toBeTruthy(); const rejectData = await rejectResponse.json(); expect(rejectData?.success).toBeTruthy(); expect(rejectData?.lease?.approval_status).toBe("rejected"); expect(rejectData?.lease?.rejection_reason).toBe(rejectionReason); }); test("tenant sees updated approval status on invite page", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Create and approve a lease const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; const requestApprovalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); const requestApprovalPayload = await requestApprovalResponse.json(); const leaseId = requestApprovalPayload?.lease?.id || ""; // Approve lease as landlord await page.request.post("/api/lease/approve", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: leaseId, }, }); // Navigate to tenant invite page await useE2EUser(page, "tenant"); await page.goto(`/tenant/invite/${inviteId}`); // Verify approved status is displayed await expect(page.getByText(/lease approved/i)).toBeVisible(); await expect(page.getByText(/congratulations/i)).toBeVisible(); await expect(page.getByText(/tenant portal/i)).toBeVisible(); }); test("tenant sees rejected status on invite page", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Create and reject a lease const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; const requestApprovalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); const requestApprovalPayload = await requestApprovalResponse.json(); const leaseId = requestApprovalPayload?.lease?.id || ""; const rejectionReason = "Property not available"; await page.request.post("/api/lease/reject", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: leaseId, rejection_reason: rejectionReason, }, }); // Navigate to tenant invite page await useE2EUser(page, "tenant"); await page.goto(`/tenant/invite/${inviteId}`); // Verify rejected status is displayed await expect(page.getByText(/lease rejected/i)).toBeVisible(); await expect(page.getByText(/declined/i)).toBeVisible(); await expect(page.getByText(rejectionReason)).toBeVisible(); }); test("unauthorized user cannot request approval", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Try to request approval without authentication const response = await page.request.post("/api/lease/request-approval", { headers: { "Content-Type": "application/json" }, data: { invite_id: "test-invite-id", property_id: "test-property-id", }, }); expect(response.status()).toBe(401); const errorData = await response.json(); expect(errorData?.error).toBe("Unauthorized"); }); test("unauthorized user cannot approve lease", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Try to approve without authentication const response = await page.request.post("/api/lease/approve", { headers: { "Content-Type": "application/json" }, data: { lease_id: "test-lease-id", }, }); expect(response.status()).toBe(401); const errorData = await response.json(); expect(errorData?.error).toBe("Unauthorized"); }); test("unauthorized user cannot reject lease", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Try to reject without authentication const response = await page.request.post("/api/lease/reject", { headers: { "Content-Type": "application/json" }, data: { lease_id: "test-lease-id", rejection_reason: "Test rejection", }, }); expect(response.status()).toBe(401); const errorData = await response.json(); expect(errorData?.error).toBe("Unauthorized"); }); test("invalid lease_id returns error", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); // Try to approve with invalid lease_id const response = await page.request.post("/api/lease/approve", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: "invalid-lease-id", }, }); expect(response.status()).toBe(404); const errorData = await response.json(); expect(errorData?.error).toBe("Lease not found"); }); test("landlord cannot approve lease they don't own", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); // Create a lease as tenant const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; const requestApprovalResponse = await page.request.post( "/api/lease/request-approval", { headers: { "Content-Type": "application/json", ...e2eHeaders("tenant") }, data: { invite_id: inviteId, property_id: "e2e-prop-1", }, }, ); const requestApprovalPayload = await requestApprovalResponse.json(); const leaseId = requestApprovalPayload?.lease?.id || ""; // Try to approve as landlord (should fail) await useE2EUser(page, "landlord"); const response = await page.request.post("/api/lease/approve", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { lease_id: leaseId, }, }); expect(response.status()).toBe(403); const errorData = await response.json(); expect(errorData?.error).toBe("Unauthorized: You do not own this lease"); });