feat: bootstrap auto virtual tryon admin frontend
This commit is contained in:
50
tests/app/api/libraries.route.test.ts
Normal file
50
tests/app/api/libraries.route.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { GET } from "../../../app/api/libraries/[libraryType]/route";
|
||||
|
||||
test("returns honest placeholder library data for unsupported backend modules", async () => {
|
||||
const response = await GET(new Request("http://localhost/api/libraries/models"), {
|
||||
params: Promise.resolve({ libraryType: "models" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toMatchObject({
|
||||
mode: "placeholder",
|
||||
data: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
libraryType: "models",
|
||||
isMock: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
message: "资源库当前使用占位数据,真实后端接口尚未提供。",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects unsupported placeholder library types with a normalized error", async () => {
|
||||
const response = await GET(new Request("http://localhost/api/libraries/unknown"), {
|
||||
params: Promise.resolve({ libraryType: "unknown" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(payload).toEqual({
|
||||
error: "NOT_FOUND",
|
||||
message: "不支持的资源库类型。",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects inherited object keys instead of treating them as valid library types", async () => {
|
||||
const response = await GET(new Request("http://localhost/api/libraries/toString"), {
|
||||
params: Promise.resolve({ libraryType: "toString" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(payload).toEqual({
|
||||
error: "NOT_FOUND",
|
||||
message: "不支持的资源库类型。",
|
||||
});
|
||||
});
|
||||
154
tests/app/api/order-revisions.route.test.ts
Normal file
154
tests/app/api/order-revisions.route.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
GET,
|
||||
POST,
|
||||
} from "../../../app/api/orders/[orderId]/revisions/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies manual revision registration and normalizes success data", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
order_id: 101,
|
||||
workflow_id: "wf-101",
|
||||
asset_id: 501,
|
||||
parent_asset_id: 11,
|
||||
root_asset_id: 11,
|
||||
version_no: 1,
|
||||
review_task_status: "revision_uploaded",
|
||||
latest_revision_asset_id: 501,
|
||||
revision_count: 1,
|
||||
}),
|
||||
{
|
||||
status: 201,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/orders/101/revisions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parent_asset_id: 11,
|
||||
uploaded_uri: "mock://manual-revision-v1",
|
||||
reviewer_id: 88,
|
||||
comment: "人工修订第一版",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ orderId: "101" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
orderId: 101,
|
||||
workflowId: "wf-101",
|
||||
assetId: 501,
|
||||
parentAssetId: 11,
|
||||
rootAssetId: 11,
|
||||
versionNo: 1,
|
||||
reviewTaskStatus: "revision_uploaded",
|
||||
latestRevisionAssetId: 501,
|
||||
revisionCount: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("proxies revision chain lookup and normalizes the item list", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
order_id: 101,
|
||||
latest_revision_asset_id: 502,
|
||||
revision_count: 2,
|
||||
items: [
|
||||
{
|
||||
asset_id: 501,
|
||||
order_id: 101,
|
||||
parent_asset_id: 11,
|
||||
root_asset_id: 11,
|
||||
version_no: 1,
|
||||
is_current_version: false,
|
||||
uri: "mock://manual-revision-v1",
|
||||
created_at: "2026-03-27T00:10:00Z",
|
||||
},
|
||||
{
|
||||
asset_id: 502,
|
||||
order_id: 101,
|
||||
parent_asset_id: 501,
|
||||
root_asset_id: 11,
|
||||
version_no: 2,
|
||||
is_current_version: true,
|
||||
uri: "mock://manual-revision-v2",
|
||||
created_at: "2026-03-27T00:20:00Z",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(new Request("http://localhost/api/orders/101/revisions"), {
|
||||
params: Promise.resolve({ orderId: "101" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
orderId: 101,
|
||||
latestRevisionAssetId: 502,
|
||||
revisionCount: 2,
|
||||
items: [
|
||||
{
|
||||
assetId: 501,
|
||||
orderId: 101,
|
||||
parentAssetId: 11,
|
||||
rootAssetId: 11,
|
||||
versionNo: 1,
|
||||
isCurrentVersion: false,
|
||||
uri: "mock://manual-revision-v1",
|
||||
createdAt: "2026-03-27T00:10:00Z",
|
||||
},
|
||||
{
|
||||
assetId: 502,
|
||||
orderId: 101,
|
||||
parentAssetId: 501,
|
||||
rootAssetId: 11,
|
||||
versionNo: 2,
|
||||
isCurrentVersion: true,
|
||||
uri: "mock://manual-revision-v2",
|
||||
createdAt: "2026-03-27T00:20:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
163
tests/app/api/orders-create.route.test.ts
Normal file
163
tests/app/api/orders-create.route.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { POST } from "../../../app/api/orders/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies order creation to the backend and returns normalized success data", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
order_id: 77,
|
||||
workflow_id: "wf-77",
|
||||
status: "created",
|
||||
}),
|
||||
{
|
||||
status: 201,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/orders", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customer_level: "mid",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
orderId: 77,
|
||||
workflowId: "wf-77",
|
||||
status: "created",
|
||||
},
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://backend.test/api/v1/orders",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid order creation payloads before proxying", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/orders", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customer_level: "low",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload).toMatchObject({
|
||||
error: "VALIDATION_ERROR",
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("rejects malformed JSON before validation or proxying", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/orders", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{bad json",
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload).toEqual({
|
||||
error: "INVALID_JSON",
|
||||
message: "请求体必须是合法 JSON。",
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("normalizes upstream validation errors from the backend", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
detail: "scene_ref_asset_id is invalid",
|
||||
}),
|
||||
{
|
||||
status: 422,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/orders", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customer_level: "mid",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload).toEqual({
|
||||
error: "VALIDATION_ERROR",
|
||||
message: "scene_ref_asset_id is invalid",
|
||||
details: {
|
||||
detail: "scene_ref_asset_id is invalid",
|
||||
},
|
||||
});
|
||||
});
|
||||
83
tests/app/api/orders-overview.route.test.ts
Normal file
83
tests/app/api/orders-overview.route.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { GET } from "../../../app/api/dashboard/orders-overview/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies recent orders overview from the backend list api", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
page: 2,
|
||||
limit: 3,
|
||||
total: 5,
|
||||
total_pages: 2,
|
||||
items: [
|
||||
{
|
||||
order_id: 3,
|
||||
workflow_id: "order-3",
|
||||
customer_level: "mid",
|
||||
service_mode: "semi_pro",
|
||||
status: "waiting_review",
|
||||
current_step: "review",
|
||||
updated_at: "2026-03-27T14:00:03Z",
|
||||
final_asset_id: null,
|
||||
review_task_status: "revision_uploaded",
|
||||
latest_revision_asset_id: 8,
|
||||
latest_revision_version: 1,
|
||||
revision_count: 1,
|
||||
pending_manual_confirm: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://frontend.test/api/dashboard/orders-overview?page=2&limit=3&status=waiting_review&query=order-3"),
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
page: 2,
|
||||
limit: 3,
|
||||
total: 5,
|
||||
totalPages: 2,
|
||||
items: [
|
||||
{
|
||||
orderId: 3,
|
||||
workflowId: "order-3",
|
||||
status: "waiting_review",
|
||||
statusMeta: {
|
||||
label: "待审核",
|
||||
tone: "warning",
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
updatedAt: "2026-03-27T14:00:03Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
message: "订单总览当前显示真实后端最近订单。",
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://backend.test/api/v1/orders?page=2&limit=3&status=waiting_review&query=order-3",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
64
tests/app/api/reviews-confirm-revision.route.test.ts
Normal file
64
tests/app/api/reviews-confirm-revision.route.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { POST } from "../../../app/api/reviews/[orderId]/confirm-revision/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies revision confirmation and normalizes response data", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
order_id: 101,
|
||||
workflow_id: "wf-101",
|
||||
revision_asset_id: 501,
|
||||
decision: "approve",
|
||||
status: "submitted",
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/reviews/101/confirm-revision", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reviewer_id: 88,
|
||||
comment: "确认继续流水线",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ orderId: "101" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
orderId: 101,
|
||||
workflowId: "wf-101",
|
||||
revisionAssetId: 501,
|
||||
decision: "approve",
|
||||
decisionMeta: {
|
||||
label: "通过",
|
||||
tone: "success",
|
||||
},
|
||||
status: "submitted",
|
||||
},
|
||||
});
|
||||
});
|
||||
160
tests/app/api/reviews-pending.route.test.ts
Normal file
160
tests/app/api/reviews-pending.route.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { GET } from "../../../app/api/reviews/pending/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("adapts an empty pending review list into a business-empty queue state", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(new Request("http://localhost/api/reviews/pending"));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
items: [],
|
||||
state: {
|
||||
kind: "business-empty",
|
||||
title: "暂无待审核订单",
|
||||
description: "当前没有等待人工处理的审核任务。",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes upstream server errors for pending review proxying", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
detail: "database unavailable",
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(new Request("http://localhost/api/reviews/pending"));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(payload).toEqual({
|
||||
error: "BACKEND_ERROR",
|
||||
message: "database unavailable",
|
||||
details: {
|
||||
detail: "database unavailable",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("enriches pending review items with workflow summary chips", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = input.toString();
|
||||
|
||||
if (url === "http://backend.test/api/v1/reviews/pending") {
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
{
|
||||
review_task_id: 301,
|
||||
order_id: 101,
|
||||
workflow_id: "wf-101",
|
||||
current_step: "review",
|
||||
created_at: "2026-03-27T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "http://backend.test/api/v1/workflows/101") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
order_id: 101,
|
||||
workflow_id: "wf-101",
|
||||
workflow_type: "mid_end",
|
||||
workflow_status: "waiting_review",
|
||||
current_step: "review",
|
||||
steps: [
|
||||
{
|
||||
id: 1,
|
||||
workflow_run_id: 9001,
|
||||
step_name: "fusion",
|
||||
step_status: "failed",
|
||||
input_json: null,
|
||||
output_json: null,
|
||||
error_message: "fusion failed",
|
||||
started_at: "2026-03-27T00:07:00Z",
|
||||
ended_at: "2026-03-27T00:08:00Z",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
workflow_run_id: 9001,
|
||||
step_name: "review",
|
||||
step_status: "waiting",
|
||||
input_json: {
|
||||
preview_uri: "mock://fusion-preview",
|
||||
},
|
||||
output_json: null,
|
||||
error_message: null,
|
||||
started_at: "2026-03-27T00:09:00Z",
|
||||
ended_at: null,
|
||||
},
|
||||
],
|
||||
created_at: "2026-03-27T00:00:00Z",
|
||||
updated_at: "2026-03-27T00:10:00Z",
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled fetch url: ${url}`);
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(new Request("http://localhost/api/reviews/pending"));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload.data.items[0]).toMatchObject({
|
||||
orderId: 101,
|
||||
workflowType: "mid_end",
|
||||
hasMockAssets: true,
|
||||
failureCount: 1,
|
||||
});
|
||||
});
|
||||
144
tests/app/api/reviews-submit.route.test.ts
Normal file
144
tests/app/api/reviews-submit.route.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { POST } from "../../../app/api/reviews/[orderId]/submit/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies review submission through a dynamic route and normalizes success data", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
order_id: 101,
|
||||
workflow_id: "wf-101",
|
||||
decision: "approve",
|
||||
status: "queued",
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/reviews/101/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
decision: "approve",
|
||||
reviewer_id: 7,
|
||||
selected_asset_id: null,
|
||||
comment: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ orderId: "101" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
orderId: 101,
|
||||
workflowId: "wf-101",
|
||||
decision: "approve",
|
||||
decisionMeta: {
|
||||
label: "通过",
|
||||
tone: "success",
|
||||
},
|
||||
status: "queued",
|
||||
},
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://backend.test/api/v1/reviews/101/submit",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid dynamic path params before proxying review submission", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/reviews/not-a-number/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
decision: "approve",
|
||||
reviewer_id: 7,
|
||||
selected_asset_id: null,
|
||||
comment: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ orderId: "not-a-number" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload).toEqual({
|
||||
error: "VALIDATION_ERROR",
|
||||
message: "orderId 必须是正整数。",
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("normalizes upstream not-found responses on review submission", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
detail: "review task not found",
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const request = new Request("http://localhost/api/reviews/999/submit", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
decision: "approve",
|
||||
reviewer_id: 7,
|
||||
selected_asset_id: null,
|
||||
comment: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ orderId: "999" }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(payload).toEqual({
|
||||
error: "NOT_FOUND",
|
||||
message: "review task not found",
|
||||
});
|
||||
});
|
||||
83
tests/app/api/workflow-lookup.route.test.ts
Normal file
83
tests/app/api/workflow-lookup.route.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { GET } from "../../../app/api/dashboard/workflow-lookup/route";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("proxies workflow lookup items from the backend workflows list api", async () => {
|
||||
vi.stubEnv("BACKEND_BASE_URL", "http://backend.test/api/v1");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
page: 2,
|
||||
limit: 4,
|
||||
total: 7,
|
||||
total_pages: 2,
|
||||
items: [
|
||||
{
|
||||
order_id: 3,
|
||||
workflow_id: "order-3",
|
||||
workflow_type: "MidEndPipelineWorkflow",
|
||||
workflow_status: "waiting_review",
|
||||
current_step: "review",
|
||||
updated_at: "2026-03-27T14:00:03Z",
|
||||
failure_count: 0,
|
||||
review_task_status: "revision_uploaded",
|
||||
latest_revision_asset_id: 8,
|
||||
latest_revision_version: 1,
|
||||
revision_count: 1,
|
||||
pending_manual_confirm: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://frontend.test/api/dashboard/workflow-lookup?page=2&limit=4&status=waiting_review&query=4201"),
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
page: 2,
|
||||
limit: 4,
|
||||
total: 7,
|
||||
totalPages: 2,
|
||||
items: [
|
||||
{
|
||||
orderId: 3,
|
||||
workflowId: "order-3",
|
||||
workflowType: "MidEndPipelineWorkflow",
|
||||
status: "waiting_review",
|
||||
statusMeta: {
|
||||
label: "待审核",
|
||||
tone: "warning",
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
updatedAt: "2026-03-27T14:00:03Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
message: "流程追踪首页当前显示真实后端最近流程。",
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://backend.test/api/v1/workflows?page=2&limit=4&status=waiting_review&query=4201",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user