forked from baron/baron-sso
test: 프론트엔드/백엔드 테스트 커버리지 및 시나리오 보강 (Issue #291)
- FE: Vitest 환경 구축 및 공통 UI 컴포넌트(Badge, Button) 테스트 추가 - FE: Playwright E2E 테스트(Auth, Tenant CRUD 및 Validation) 시나리오 보강 - BE: Testcontainers 기반 Repository 통합 테스트(PostgreSQL) 추가 - BE: TenantRepository 계층 구조(Hierarchy), DB 제약조건(Unique) 테스트 - BE: UserRepository 통합 테스트(CRUD, Delete) 추가 - BE: PasswordPolicy 유틸리티 테스트 보강 - BE: TenantService 엣지 케이스(중복 슬러그, 권한 등) 검증 로직 추가 - Fix: 하위 테넌트 생성 시 ParentID 누락 문제 해결
This commit is contained in:
28
adminfront/tests/auth.spec.ts
Normal file
28
adminfront/tests/auth.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Auth Flow", () => {
|
||||
test("unauthenticated user is redirected to login", async ({ page }) => {
|
||||
// Navigate to a protected route without setting localStorage
|
||||
await page.goto("/");
|
||||
|
||||
// Check if it redirects to login
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
|
||||
// Verify login page content
|
||||
await expect(page.getByText("Baron SSO")).toBeVisible();
|
||||
await expect(page.getByText("관리자 로그인")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "SSO 계정으로 로그인" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("authenticated user can access dashboard", async ({ page }) => {
|
||||
// Inject mock session
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("admin_session", "playwright-admin-session");
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
// Should stay on dashboard (or another protected route) and not redirect to login
|
||||
await expect(page).not.toHaveURL(/\/login$/);
|
||||
});
|
||||
});
|
||||
122
adminfront/tests/tenants.spec.ts
Normal file
122
adminfront/tests/tenants.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { TenantCreateRequest, TenantSummary } from "../src/lib/adminApi";
|
||||
|
||||
test.use({
|
||||
storageState: {
|
||||
cookies: [],
|
||||
origins: [
|
||||
{
|
||||
origin: "http://localhost:5173",
|
||||
localStorage: [
|
||||
{
|
||||
name: "admin_session",
|
||||
value: "playwright-admin-session",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
test("tenant create and delete flow", async ({ page }) => {
|
||||
const tenants: TenantSummary[] = [];
|
||||
let idSeq = 1;
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname;
|
||||
const isCollection = path.endsWith("/api/v1/admin/tenants");
|
||||
const isItem = path.includes("/api/v1/admin/tenants/");
|
||||
|
||||
if (request.method() === "GET" && isCollection) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: tenants,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
total: tenants.length,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === "POST" && isCollection) {
|
||||
const payload = request.postDataJSON() as TenantCreateRequest;
|
||||
const now = new Date().toISOString();
|
||||
const tenant: TenantSummary = {
|
||||
id: `tenant-${idSeq++}`,
|
||||
name: payload.name,
|
||||
type: payload.type || "COMPANY",
|
||||
slug: payload.slug || `slug-${idSeq}`,
|
||||
description: payload.description || "",
|
||||
status: payload.status || "active",
|
||||
domains: payload.domains || [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
tenants.unshift(tenant);
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(tenant),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === "DELETE" && isItem) {
|
||||
const tenantId = path.split("/").pop();
|
||||
const index = tenants.findIndex((t) => t.id === tenantId);
|
||||
if (index !== -1) {
|
||||
tenants.splice(index, 1);
|
||||
}
|
||||
await route.fulfill({ status: 204, body: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/tenants");
|
||||
await expect(page).toHaveURL(/\/tenants$/);
|
||||
await expect(page.getByRole("heading", { name: "테넌트 목록" })).toBeVisible();
|
||||
|
||||
// Create
|
||||
const addTenantLink = page.getByRole("link", { name: "테넌트 추가" });
|
||||
await addTenantLink.click();
|
||||
await expect(page).toHaveURL(/\/tenants\/new$/);
|
||||
|
||||
const uniqueName = `Test Tenant ${Date.now()}`;
|
||||
await page.getByLabel("테넌트 이름 *").fill(uniqueName);
|
||||
await page.getByLabel("테넌트 유형").selectOption("COMPANY");
|
||||
await page.getByLabel("슬러그 (Slug)").fill("test-tenant");
|
||||
await page.getByLabel("설명").fill("This is an E2E test tenant");
|
||||
await page.getByLabel("허용된 도메인 (콤마로 구분)").fill("test.com, example.com");
|
||||
|
||||
await page.getByRole("button", { name: "생성" }).click();
|
||||
await expect(page).toHaveURL(/\/tenants$/);
|
||||
|
||||
// Verify created
|
||||
const createdRow = page.locator("tbody tr").filter({ hasText: uniqueName });
|
||||
await expect(createdRow).toBeVisible();
|
||||
|
||||
// Delete
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await createdRow.getByRole("button", { name: "삭제" }).click();
|
||||
|
||||
await expect(page.locator("tbody tr").filter({ hasText: uniqueName })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("tenant creation form validation", async ({ page }) => {
|
||||
await page.goto("/tenants/new");
|
||||
|
||||
// Try to submit empty form
|
||||
await page.getByRole("button", { name: "생성" }).click();
|
||||
|
||||
// Since 'name' is required, we check if button is still disabled or form doesn't navigate
|
||||
await expect(page).toHaveURL(/\/tenants\/new$/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user