feat: 기능 추가 과정중
This commit is contained in:
@@ -3,19 +3,22 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildRouteUrl,
|
||||
parseRouteInput,
|
||||
} from "../../../src/presentation/routes/route-codecs.js";
|
||||
} from "../../../src/presentation/routes/route-codecs.ts";
|
||||
import {
|
||||
mapReferenceOperation,
|
||||
toReferenceView,
|
||||
} from "../../../src/features/reference-feature/contracts/reference-mapper.js";
|
||||
} from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
|
||||
import {
|
||||
validateReferencePayload,
|
||||
validateReferenceRequest,
|
||||
} from "../../../src/features/reference-feature/contracts/reference-schemas.js";
|
||||
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
|
||||
import {
|
||||
REFERENCE_FEATURE_CONTRACT,
|
||||
REFERENCE_FEATURE_ID,
|
||||
referenceQueryKeys,
|
||||
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
|
||||
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
|
||||
import type { ReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
|
||||
import { createTestApplication } from "../../helpers/create-test-application.ts";
|
||||
|
||||
describe("reference feature boundary contracts", () => {
|
||||
it("round-trips one canonical filter through URL and query identity", () => {
|
||||
@@ -53,11 +56,11 @@ describe("reference feature boundary contracts", () => {
|
||||
{ id: "unsafe", name: 42 },
|
||||
]),
|
||||
).toMatchObject({ success: false });
|
||||
expect(() =>
|
||||
expect(
|
||||
mapReferenceOperation("LIST_REFERENCE_RESOURCES", [
|
||||
{ id: "unsafe", name: 42 },
|
||||
]),
|
||||
).toThrow();
|
||||
).toMatchObject({ ok: false });
|
||||
});
|
||||
|
||||
it("normalizes request input and maps only owned domain fields", () => {
|
||||
@@ -71,8 +74,10 @@ describe("reference feature boundary contracts", () => {
|
||||
name: "Example",
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
});
|
||||
if (!("id" in model)) throw new Error("expected one model");
|
||||
expect(toReferenceView(model)).toEqual({
|
||||
if (!model.ok || !("id" in model.value)) {
|
||||
throw new Error("expected one model");
|
||||
}
|
||||
expect(toReferenceView(model.value)).toEqual({
|
||||
resourceId: "reference-1",
|
||||
title: "Example",
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
@@ -91,8 +96,45 @@ describe("reference feature boundary contracts", () => {
|
||||
"CREATE_REFERENCE_RESOURCE",
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
]);
|
||||
expect(
|
||||
REFERENCE_FEATURE_CONTRACT.apiOperations.GET_REFERENCE_RESOURCE,
|
||||
).toMatchObject({
|
||||
pathSchema: "ReferenceResourceParams",
|
||||
pathParameterNames: ["resourceId"],
|
||||
providerId: "PRIMARY_API",
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
csrfProfileId: "NO_CSRF_BEARER",
|
||||
});
|
||||
expect(Object.keys(REFERENCE_FEATURE_CONTRACT.queryRegistry)).toEqual([
|
||||
"REFERENCE_RESOURCE",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns the installed input through the typed application registry", () => {
|
||||
const featureInput = {
|
||||
listResources: async () => ({ ok: true as const, value: [] }),
|
||||
createResource: async ({ name }) => ({
|
||||
ok: true as const,
|
||||
value: {
|
||||
resourceId: "created",
|
||||
title: name,
|
||||
createdAt: null,
|
||||
},
|
||||
}),
|
||||
getResource: async (resourceId) => ({
|
||||
ok: true as const,
|
||||
value: {
|
||||
resourceId,
|
||||
title: "Reference",
|
||||
createdAt: null,
|
||||
},
|
||||
}),
|
||||
} satisfies ReferenceFeatureInput;
|
||||
const application = createTestApplication({
|
||||
featureInputs: { [REFERENCE_FEATURE_ID]: featureInput },
|
||||
});
|
||||
|
||||
expect(application.features.has(REFERENCE_FEATURE_ID)).toBe(true);
|
||||
expect(application.features.get(REFERENCE_FEATURE_ID)).toBe(featureInput);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createHttpClient } from "../../../src/adapters/http/client.js";
|
||||
import { createReferenceHttpGateway } from "../../../src/features/reference-feature/adapters/reference-http-gateway.js";
|
||||
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.js";
|
||||
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
|
||||
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.js";
|
||||
import { createHttpClient } from "../../../src/adapters/http/client.ts";
|
||||
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
|
||||
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
|
||||
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
|
||||
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
|
||||
import {
|
||||
validateReferencePayload,
|
||||
validateReferenceRequest,
|
||||
} from "../../../src/features/reference-feature/contracts/reference-schemas.js";
|
||||
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
|
||||
|
||||
describe("reference feature diagnostics correlation", () => {
|
||||
it("preserves route, operation and request correlation through the vertical path", async () => {
|
||||
@@ -24,6 +27,7 @@ describe("reference feature diagnostics correlation", () => {
|
||||
>;
|
||||
const client = createHttpClient({
|
||||
baseUrl: "https://api.test",
|
||||
authSession: createDemoSessionAdapter("authenticated"),
|
||||
fetcher: async () =>
|
||||
Response.json({
|
||||
success: true,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createFailure } from "../../../src/contracts/errors.ts";
|
||||
import {
|
||||
createReferenceHttpGateway,
|
||||
type RawReferenceHttpExecutor,
|
||||
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
|
||||
import type { ReferenceResource } from "../../../src/features/reference-feature/domain/reference-resource.ts";
|
||||
|
||||
const resources = Object.freeze({
|
||||
first: Object.freeze({
|
||||
id: "reference-1",
|
||||
displayName: "First",
|
||||
createdAt: null,
|
||||
}),
|
||||
created: Object.freeze({
|
||||
id: "reference-2",
|
||||
displayName: "Created",
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
}),
|
||||
}) satisfies Readonly<Record<string, ReferenceResource>>;
|
||||
|
||||
describe("reference HTTP operation gateway", () => {
|
||||
it("builds the exact registered request for every gateway operation", async () => {
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValueOnce({ ok: true, value: [resources.first] })
|
||||
.mockResolvedValueOnce({ ok: true, value: resources.created })
|
||||
.mockResolvedValueOnce({ ok: true, value: resources.first });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await expect(
|
||||
gateway.list({ cursor: "next", limit: 20, tags: ["active"] }, { signal }),
|
||||
).resolves.toEqual({ ok: true, value: [resources.first] });
|
||||
await expect(
|
||||
gateway.create({ name: "Created", note: "safe note" }),
|
||||
).resolves.toEqual({ ok: true, value: resources.created });
|
||||
await expect(
|
||||
gateway.get("reference-1", { signal }),
|
||||
).resolves.toEqual({ ok: true, value: resources.first });
|
||||
|
||||
expect(execute.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
searchParams: {
|
||||
cursor: "next",
|
||||
limit: 20,
|
||||
tags: ["active"],
|
||||
},
|
||||
signal,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
operationId: "CREATE_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_LIST",
|
||||
body: { name: "Created", note: "safe note" },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
operationId: "GET_REFERENCE_RESOURCE",
|
||||
routeId: "REFERENCE_RESOURCE_DETAIL",
|
||||
pathParams: { resourceId: "reference-1" },
|
||||
signal,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves a validated raw failure without casting it into success", async () => {
|
||||
const failure = createFailure(
|
||||
"SCHEMA_MISMATCH",
|
||||
"GET_REFERENCE_RESOURCE",
|
||||
0,
|
||||
);
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValue({ ok: false, error: failure });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
|
||||
await expect(gateway.get("invalid")).resolves.toEqual({
|
||||
ok: false,
|
||||
error: failure,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when a raw success does not match its operation result", async () => {
|
||||
const execute = vi
|
||||
.fn<RawReferenceHttpExecutor["execute"]>()
|
||||
.mockResolvedValue({ ok: true, value: { id: "not-a-list" } });
|
||||
const gateway = createReferenceHttpGateway({ execute });
|
||||
|
||||
await expect(gateway.list({ limit: 20 })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "MAPPING_CONTRACT_VIOLATION",
|
||||
code: "BOUND_RESULT_TYPE_MISMATCH",
|
||||
operationId: "LIST_REFERENCE_RESOURCES",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,21 +8,27 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.js";
|
||||
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
|
||||
import type { AuthSessionPort } from "../../../src/application/ports/auth-session-port.ts";
|
||||
import type {
|
||||
ReferenceFeatureInput,
|
||||
ReferenceResult,
|
||||
} from "../../../src/features/reference-feature/application/reference-feature-api.js";
|
||||
import { REFERENCE_FEATURE_ID } from "../../../src/features/reference-feature/contracts/reference-feature-contract.js";
|
||||
import type { ReferenceResourceView } from "../../../src/features/reference-feature/contracts/reference-mapper.js";
|
||||
import { createFailure } from "../../../src/contracts/errors.js";
|
||||
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.js";
|
||||
import { AppRouter } from "../../../src/presentation/routes/app-router.js";
|
||||
import { createTestApplication } from "../../helpers/create-test-application.js";
|
||||
} from "../../../src/features/reference-feature/application/reference-feature-api.ts";
|
||||
import { REFERENCE_FEATURE_ID } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
|
||||
import type { ReferenceResourceView } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
|
||||
import { createFailure } from "../../../src/contracts/errors.ts";
|
||||
import type { QueryInvalidationCoordinator } from "../../../src/contracts/query-invalidation.ts";
|
||||
import { QueryInvalidationProvider } from "../../../src/presentation/adapters/query/query-invalidation-provider.tsx";
|
||||
import { ServerStateScopeProvider } from "../../../src/presentation/adapters/query/server-state-scope-provider.tsx";
|
||||
import { createServerStateScopeRuntime } from "../../../src/adapters/query-cache/server-state-scope-runtime.ts";
|
||||
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx";
|
||||
import { AppRouter } from "../../../src/presentation/routes/app-router.tsx";
|
||||
import { createTestApplication } from "../../helpers/create-test-application.ts";
|
||||
|
||||
function renderReference(
|
||||
input: ReferenceFeatureInput,
|
||||
url = "/examples/reference-resources?limit=5",
|
||||
session: AuthSessionPort = createDemoSessionAdapter("authenticated"),
|
||||
) {
|
||||
window.history.pushState({}, "", url);
|
||||
const client = new QueryClient({
|
||||
@@ -31,16 +37,34 @@ function renderReference(
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
const invalidation: QueryInvalidationCoordinator = Object.freeze({
|
||||
async invalidate() {},
|
||||
beginMutation() {
|
||||
return Object.freeze({
|
||||
async release() {},
|
||||
});
|
||||
},
|
||||
async resetLocal() {},
|
||||
dispose() {},
|
||||
});
|
||||
const serverStateScope = createServerStateScopeRuntime({
|
||||
session,
|
||||
queryInvalidation: invalidation,
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
session: createDemoSessionAdapter("authenticated"),
|
||||
featureInputs: { [REFERENCE_FEATURE_ID]: input },
|
||||
})}
|
||||
>
|
||||
<AppRouter />
|
||||
</ApplicationProvider>
|
||||
<ServerStateScopeProvider runtime={serverStateScope}>
|
||||
<QueryInvalidationProvider coordinator={invalidation}>
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
session,
|
||||
featureInputs: { [REFERENCE_FEATURE_ID]: input },
|
||||
})}
|
||||
>
|
||||
<AppRouter />
|
||||
</ApplicationProvider>
|
||||
</QueryInvalidationProvider>
|
||||
</ServerStateScopeProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
@@ -120,6 +144,7 @@ describe("reference feature page states", () => {
|
||||
});
|
||||
|
||||
it("renders backend forbidden even when the client access hint allowed entry", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderReference(
|
||||
inputWith({
|
||||
listResources: async () => ({
|
||||
@@ -135,6 +160,71 @@ describe("reference feature page states", () => {
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent(
|
||||
"이 작업을 수행할 권한이 없습니다.",
|
||||
);
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "안전한 화면으로 이동" }),
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
level: 1,
|
||||
name: "Clean Architecture Frontend",
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("starts sign-in with the current route for a backend auth failure", async () => {
|
||||
const user = userEvent.setup();
|
||||
const demoSession = createDemoSessionAdapter("authenticated");
|
||||
const beginSignIn = vi.fn(async () => {});
|
||||
const session = { ...demoSession, beginSignIn };
|
||||
renderReference(
|
||||
inputWith({
|
||||
listResources: async () => ({
|
||||
ok: false,
|
||||
error: createFailure(
|
||||
"AUTH_REQUIRED",
|
||||
"LIST_REFERENCE_RESOURCES",
|
||||
0,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
"/examples/reference-resources?limit=5",
|
||||
session,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "로그인" }));
|
||||
expect(beginSignIn).toHaveBeenCalledWith(
|
||||
"/examples/reference-resources?limit=5",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a public route when starting sign-in fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const demoSession = createDemoSessionAdapter("authenticated");
|
||||
const beginSignIn = vi.fn(async () => {
|
||||
throw new Error("identity provider unavailable");
|
||||
});
|
||||
renderReference(
|
||||
inputWith({
|
||||
listResources: async () => ({
|
||||
ok: false,
|
||||
error: createFailure(
|
||||
"AUTH_REQUIRED",
|
||||
"LIST_REFERENCE_RESOURCES",
|
||||
0,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
"/examples/reference-resources?limit=5",
|
||||
{ ...demoSession, beginSignIn },
|
||||
);
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "로그인" }));
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
level: 1,
|
||||
name: "Clean Architecture Frontend",
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("deduplicates create, preserves input and surfaces a conflict", async () => {
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.js";
|
||||
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.jsx";
|
||||
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.js";
|
||||
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.js";
|
||||
import { createStrictMockServer } from "../../mocks/server.js";
|
||||
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.ts";
|
||||
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.tsx";
|
||||
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.ts";
|
||||
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.ts";
|
||||
import { createStrictMockServer } from "../../mocks/server.ts";
|
||||
|
||||
const runtimeConfig = {
|
||||
APP_ENV: "local",
|
||||
|
||||
Reference in New Issue
Block a user