diff --git a/devfront/tests/devfront-developer-request.spec.ts b/devfront/tests/devfront-developer-request.spec.ts new file mode 100644 index 00000000..ee34e677 --- /dev/null +++ b/devfront/tests/devfront-developer-request.spec.ts @@ -0,0 +1,144 @@ +import { expect, test } from "@playwright/test"; +import { + type DeveloperRequest, + installDevApiMock, + seedAuth, +} from "./helpers/devfront-fixtures"; +import { captureEvidence } from "./helpers/evidence"; + +test.describe("DevFront developer request and management", () => { + test.afterEach(async ({ page }, testInfo) => { + if (testInfo.status === "passed") { + await captureEvidence(page, testInfo, testInfo.title); + } + }); + + test.beforeEach(async ({ page }) => { + page.on("dialog", async (dialog) => { + await dialog.accept(); + }); + }); + + test("user can request developer access when no RP exists", async ({ page }) => { + const state = { + clients: [], + consents: [], + developerRequests: [], + }; + await seedAuth(page, "user"); + await installDevApiMock(page, state); + + await page.goto("/clients"); + + // Click Request Button + const requestBtn = page.getByRole('button', { name: /개발자 등록 신청/ }); + await requestBtn.waitFor({ state: 'visible' }); + await requestBtn.click(); + + // Fill Form (using direct selectors for reliability) + await page.locator("#org").fill("QA Team"); + await page.locator("#reason").fill("Need to test OIDC integration"); + + // Submit + await page.getByRole('button', { name: /신청하기|Submit/ }).click(); + + // Verify Status - Look for "Pending" or "대기" anywhere + await expect(page.locator("body")).toContainText(/대기|Pending/); + }); + + test("super admin can approve, reject and cancel developer requests", async ({ page }) => { + const request: DeveloperRequest = { + id: "req-admin-test", + userId: "user-1", + userName: "Requester User", + name: "Requester User", + userEmail: "user1@example.com", + organization: "Dev Team", + reason: "API Test", + status: "pending", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const state = { + clients: [], + consents: [], + developerRequests: [request], + }; + + await seedAuth(page, "super_admin"); + await installDevApiMock(page, state); + + await page.goto("/developer-requests"); + + // Wait for data to load + await page.waitForLoadState('networkidle'); + await expect(page.locator("table")).toContainText("Requester User", { timeout: 10000 }); + + // Approve + const approveBtn = page.getByRole('button', { name: '승인' }).first(); + await approveBtn.click(); + await expect(page.locator("table")).toContainText(/승인됨|Approved/); + + // Cancel approval (Requires notes) + await page.locator("input.h-8").first().fill("Cancellation reason"); + await page.getByRole('button', { name: '승인 취소' }).click(); + await expect(page.locator("table")).toContainText(/대기|Pending/); + + // Reject (Requires notes) + await page.locator("input.h-8").first().fill("Rejection reason"); + await page.getByRole('button', { name: '반려' }).click(); + await expect(page.locator("table")).toContainText(/반려됨|Rejected/); + }); + + test("approved user can see 'Add App' guidance and create RP", async ({ page }) => { + const request: DeveloperRequest = { + id: "req-approved", + userId: "playwright-user", + userName: "Playwright User", + name: "Playwright User", + userEmail: "playwright@example.com", + organization: "QA", + reason: "Test", + status: "approved", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + approvedAt: new Date().toISOString(), + }; + + const state = { + clients: [], + consents: [], + developerRequests: [request], + }; + + await seedAuth(page, "rp_admin"); + await installDevApiMock(page, state); + + await page.goto("/clients"); + + // Click Add App + const createBtn = page.getByRole('button', { name: /연동 앱 추가/ }).first(); + await createBtn.click(); + + // Fill Form (Must fill all mandatory fields to enable Submit) + await expect(page).toHaveURL(/\/clients\/new$/); + + const nameInput = page.locator("input[placeholder*='Awesome']"); + await nameInput.fill("E2E Test RP"); + await nameInput.press('Tab'); + + const uriInput = page.locator("textarea.font-mono"); + await uriInput.fill("https://example.com/callback"); + await uriInput.press('Tab'); + + // Submit + const submitBtn = page.getByRole('button', { name: /생성/ }).filter({ hasNotText: '취소' }); + await expect(submitBtn).toBeEnabled({ timeout: 10000 }); + await submitBtn.click(); + + // Verification + await expect(page).toHaveURL(/\/clients\/client-\d+\/settings$/); + await expect(page.locator("h1")).toContainText(/설정|Settings/); + }); +}); diff --git a/devfront/tests/helpers/devfront-fixtures.ts b/devfront/tests/helpers/devfront-fixtures.ts index e8200f9b..e29e9016 100644 --- a/devfront/tests/helpers/devfront-fixtures.ts +++ b/devfront/tests/helpers/devfront-fixtures.ts @@ -53,6 +53,25 @@ export type Consent = { tenantName: string; }; +export type DeveloperRequestStatus = "pending" | "approved" | "rejected"; + +export type DeveloperRequest = { + id: string; + userId: string; + userName: string; + name?: string; // 추가 + userEmail: string; + organization: string; + reason: string; + status: DeveloperRequestStatus; + createdAt: string; + updatedAt: string; + approvedAt?: string; + rejectedAt?: string; + comment?: string; + adminNotes?: string; // 추가 +}; + export type ClientRelation = { relation: string; subject: string; @@ -84,6 +103,7 @@ export type AuditLog = { export type DevApiMockState = { clients: Client[]; consents: Consent[]; + developerRequests?: DeveloperRequest[]; relations?: Record; users?: DevAssignableUser[]; auditLogsByCursor?: Record< @@ -241,6 +261,79 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) { const { pathname, searchParams } = url; const method = request.method(); + if (pathname === "/api/v1/dev/requests" && method === "GET") { + return json(route, { items: state.developerRequests ?? [] }); + } + + if (pathname === "/api/v1/dev/requests" && method === "POST") { + const payload = (request.postDataJSON() as { + organization?: string; + reason?: string; + }) || {}; + const created: DeveloperRequest = { + id: `req-${Date.now()}`, + userId: "playwright-user", + userName: "Playwright User", + userEmail: "playwright@example.com", + organization: payload.organization ?? "Unknown", + reason: payload.reason ?? "No reason", + status: "pending", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + if (!state.developerRequests) { + state.developerRequests = []; + } + state.developerRequests.push(created); + return json(route, created, 201); + } + + if (pathname === "/api/v1/dev/requests/status" && method === "GET") { + const myRequest = (state.developerRequests ?? []).find( + (r) => r.userId === "playwright-user", + ); + return json(route, myRequest || null); + } + + if ( + pathname.startsWith("/api/v1/dev/requests/") && + pathname.endsWith("/approve") && + method === "POST" + ) { + const reqId = pathname.split("/")[5] ?? ""; + const found = state.developerRequests?.find((r) => r.id === reqId); + if (!found) return json(route, { error: "not found" }, 404); + found.status = "approved"; + found.approvedAt = new Date().toISOString(); + return json(route, found); + } + + if ( + pathname.startsWith("/api/v1/dev/requests/") && + pathname.endsWith("/reject") && + method === "POST" + ) { + const reqId = pathname.split("/")[5] ?? ""; + const found = state.developerRequests?.find((r) => r.id === reqId); + if (!found) return json(route, { error: "not found" }, 404); + found.status = "rejected"; + found.rejectedAt = new Date().toISOString(); + return json(route, found); + } + + if ( + pathname.startsWith("/api/v1/dev/requests/") && + pathname.endsWith("/cancel") && + method === "POST" + ) { + const reqId = pathname.split("/")[5] ?? ""; + const found = state.developerRequests?.find((r) => r.id === reqId); + if (!found) return json(route, { error: "not found" }, 404); + found.status = "pending"; + found.approvedAt = undefined; + return json(route, found); + } + if (pathname === "/api/v1/dev/my-tenants" && method === "GET") { return json(route, [ { id: "tenant-a", name: "Tenant A", slug: "tenant-a" },