413 lines
14 KiB
TypeScript
413 lines
14 KiB
TypeScript
// 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 form
|
|
const approveForm = page.locator('form').filter({ hasText: "approve" }).first();
|
|
await approveForm.fill({});
|
|
|
|
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 form with reason
|
|
const rejectForm = page.locator('form').filter({ hasText: "reject" }).first();
|
|
await rejectForm.fill({});
|
|
|
|
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");
|
|
});
|