feat: connect resource library workflows
This commit is contained in:
@@ -1,8 +1,61 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { GET } from "../../../app/api/libraries/[libraryType]/route";
|
||||
import { GET, POST } from "../../../app/api/libraries/[libraryType]/route";
|
||||
import { RouteError } from "@/lib/http/response";
|
||||
|
||||
const { backendRequestMock } = vi.hoisted(() => ({
|
||||
backendRequestMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/http/backend-client", () => ({
|
||||
backendRequest: backendRequestMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
backendRequestMock.mockReset();
|
||||
});
|
||||
|
||||
test("proxies backend library resources into the existing frontend view-model shape", async () => {
|
||||
backendRequestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
total: 1,
|
||||
items: [
|
||||
{
|
||||
id: 12,
|
||||
resource_type: "model",
|
||||
name: "Ava Studio",
|
||||
description: "棚拍女模特",
|
||||
tags: ["女装", "棚拍"],
|
||||
status: "active",
|
||||
gender: "female",
|
||||
age_group: "adult",
|
||||
environment: null,
|
||||
category: null,
|
||||
files: [
|
||||
{
|
||||
id: 1,
|
||||
file_role: "thumbnail",
|
||||
storage_key: "library/models/ava/thumb.png",
|
||||
public_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
bucket: "images",
|
||||
mime_type: "image/png",
|
||||
size_bytes: 2345,
|
||||
sort_order: 0,
|
||||
width: 480,
|
||||
height: 600,
|
||||
created_at: "2026-03-28T10:00:00Z",
|
||||
},
|
||||
],
|
||||
cover_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
original_url: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
created_at: "2026-03-28T10:00:00Z",
|
||||
updated_at: "2026-03-28T10:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
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" }),
|
||||
});
|
||||
@@ -10,17 +63,23 @@ test("returns honest placeholder library data for unsupported backend modules",
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toMatchObject({
|
||||
mode: "placeholder",
|
||||
mode: "proxy",
|
||||
data: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "12",
|
||||
libraryType: "models",
|
||||
isMock: true,
|
||||
name: "Ava Studio",
|
||||
previewUri: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
isMock: false,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
message: "资源库当前使用占位数据,真实后端接口尚未提供。",
|
||||
message: "资源库当前显示真实后端数据。",
|
||||
});
|
||||
expect(backendRequestMock).toHaveBeenCalledWith(
|
||||
"/library/resources?resource_type=model&limit=100",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects unsupported placeholder library types with a normalized error", async () => {
|
||||
@@ -48,3 +107,152 @@ test("rejects inherited object keys instead of treating them as valid library ty
|
||||
message: "不支持的资源库类型。",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes backend errors while proxying library resources", async () => {
|
||||
backendRequestMock.mockRejectedValue(
|
||||
new RouteError(502, "BACKEND_UNAVAILABLE", "后端暂时不可用,请稍后重试。"),
|
||||
);
|
||||
|
||||
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(502);
|
||||
expect(payload).toEqual({
|
||||
error: "BACKEND_UNAVAILABLE",
|
||||
message: "后端暂时不可用,请稍后重试。",
|
||||
});
|
||||
});
|
||||
|
||||
test("proxies library resource creation and adapts the created item into the existing view-model shape", async () => {
|
||||
backendRequestMock.mockResolvedValue({
|
||||
status: 201,
|
||||
data: {
|
||||
id: 12,
|
||||
resource_type: "model",
|
||||
name: "Ava Studio",
|
||||
description: "棚拍女模特",
|
||||
tags: ["女装", "棚拍"],
|
||||
status: "active",
|
||||
gender: "female",
|
||||
age_group: "adult",
|
||||
pose_id: null,
|
||||
environment: null,
|
||||
category: null,
|
||||
files: [
|
||||
{
|
||||
id: 1,
|
||||
file_role: "thumbnail",
|
||||
storage_key: "library/models/ava/thumb.png",
|
||||
public_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
bucket: "images",
|
||||
mime_type: "image/png",
|
||||
size_bytes: 2345,
|
||||
sort_order: 0,
|
||||
width: 480,
|
||||
height: 600,
|
||||
created_at: "2026-03-28T10:00:00Z",
|
||||
},
|
||||
],
|
||||
cover_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
original_url: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
created_at: "2026-03-28T10:00:00Z",
|
||||
updated_at: "2026-03-28T10:00:00Z",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/libraries/models", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Ava Studio",
|
||||
description: "棚拍女模特",
|
||||
tags: ["女装", "棚拍"],
|
||||
gender: "female",
|
||||
ageGroup: "adult",
|
||||
files: [
|
||||
{
|
||||
fileRole: "original",
|
||||
storageKey: "library/models/2026/ava-original.png",
|
||||
publicUrl: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
mimeType: "image/png",
|
||||
sizeBytes: 12345,
|
||||
sortOrder: 0,
|
||||
},
|
||||
{
|
||||
fileRole: "thumbnail",
|
||||
storageKey: "library/models/2026/ava-thumb.png",
|
||||
publicUrl: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
mimeType: "image/png",
|
||||
sizeBytes: 2345,
|
||||
sortOrder: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
{
|
||||
params: Promise.resolve({ libraryType: "models" }),
|
||||
},
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
item: {
|
||||
id: "12",
|
||||
libraryType: "models",
|
||||
name: "Ava Studio",
|
||||
description: "棚拍女模特",
|
||||
previewUri: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
originalUri: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
tags: ["女装", "棚拍"],
|
||||
files: [
|
||||
{
|
||||
id: 1,
|
||||
role: "thumbnail",
|
||||
url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
},
|
||||
],
|
||||
isMock: false,
|
||||
backendId: 12,
|
||||
poseId: null,
|
||||
},
|
||||
},
|
||||
message: "资源创建成功。",
|
||||
});
|
||||
expect(backendRequestMock).toHaveBeenCalledWith("/library/resources", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
resource_type: "model",
|
||||
name: "Ava Studio",
|
||||
description: "棚拍女模特",
|
||||
tags: ["女装", "棚拍"],
|
||||
gender: "female",
|
||||
age_group: "adult",
|
||||
files: [
|
||||
{
|
||||
file_role: "original",
|
||||
storage_key: "library/models/2026/ava-original.png",
|
||||
public_url: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
mime_type: "image/png",
|
||||
size_bytes: 12345,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
file_role: "thumbnail",
|
||||
storage_key: "library/models/2026/ava-thumb.png",
|
||||
public_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
mime_type: "image/png",
|
||||
size_bytes: 2345,
|
||||
sort_order: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
117
tests/app/api/library-resource-item.route.test.ts
Normal file
117
tests/app/api/library-resource-item.route.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { DELETE, PATCH } from "../../../app/api/libraries/[libraryType]/[resourceId]/route";
|
||||
|
||||
const { backendRequestMock } = vi.hoisted(() => ({
|
||||
backendRequestMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/http/backend-client", () => ({
|
||||
backendRequest: backendRequestMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
backendRequestMock.mockReset();
|
||||
});
|
||||
|
||||
test("proxies resource updates and adapts the updated item into the library view-model", async () => {
|
||||
backendRequestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
id: 12,
|
||||
resource_type: "model",
|
||||
name: "Ava Studio Updated",
|
||||
description: "新的描述",
|
||||
tags: ["女装", "更新"],
|
||||
status: "active",
|
||||
gender: "female",
|
||||
age_group: "adult",
|
||||
pose_id: null,
|
||||
environment: null,
|
||||
category: null,
|
||||
files: [],
|
||||
cover_url: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
original_url: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
created_at: "2026-03-28T10:00:00Z",
|
||||
updated_at: "2026-03-28T11:00:00Z",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await PATCH(
|
||||
new Request("http://localhost/api/libraries/models/12", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: "Ava Studio Updated",
|
||||
description: "新的描述",
|
||||
tags: ["女装", "更新"],
|
||||
}),
|
||||
}),
|
||||
{
|
||||
params: Promise.resolve({ libraryType: "models", resourceId: "12" }),
|
||||
},
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
item: {
|
||||
id: "12",
|
||||
libraryType: "models",
|
||||
name: "Ava Studio Updated",
|
||||
description: "新的描述",
|
||||
previewUri: "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
originalUri: "https://images.marcusd.me/library/models/ava/original.png",
|
||||
tags: ["女装", "更新"],
|
||||
files: [],
|
||||
isMock: false,
|
||||
backendId: 12,
|
||||
poseId: null,
|
||||
},
|
||||
},
|
||||
message: "资源更新成功。",
|
||||
});
|
||||
expect(backendRequestMock).toHaveBeenCalledWith("/library/resources/12", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
name: "Ava Studio Updated",
|
||||
description: "新的描述",
|
||||
tags: ["女装", "更新"],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("soft deletes a library resource through the backend proxy", async () => {
|
||||
backendRequestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
id: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await DELETE(
|
||||
new Request("http://localhost/api/libraries/models/12", {
|
||||
method: "DELETE",
|
||||
}),
|
||||
{
|
||||
params: Promise.resolve({ libraryType: "models", resourceId: "12" }),
|
||||
},
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
id: "12",
|
||||
},
|
||||
message: "资源已移入归档。",
|
||||
});
|
||||
expect(backendRequestMock).toHaveBeenCalledWith("/library/resources/12", {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
94
tests/app/api/library-uploads-presign.route.test.ts
Normal file
94
tests/app/api/library-uploads-presign.route.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { POST } from "../../../app/api/libraries/uploads/presign/route";
|
||||
|
||||
const { backendRequestMock } = vi.hoisted(() => ({
|
||||
backendRequestMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/http/backend-client", () => ({
|
||||
backendRequest: backendRequestMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
backendRequestMock.mockReset();
|
||||
});
|
||||
|
||||
test("proxies upload presign requests and normalizes the response for the frontend", async () => {
|
||||
backendRequestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
data: {
|
||||
method: "PUT",
|
||||
upload_url: "https://s3.example.com/presigned-put",
|
||||
headers: {
|
||||
"content-type": "image/png",
|
||||
},
|
||||
storage_key: "library/models/2026/ava-original.png",
|
||||
public_url: "https://images.example.com/library/models/2026/ava-original.png",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/libraries/uploads/presign", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resourceType: "model",
|
||||
fileRole: "original",
|
||||
fileName: "ava-original.png",
|
||||
contentType: "image/png",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(payload).toEqual({
|
||||
mode: "proxy",
|
||||
data: {
|
||||
method: "PUT",
|
||||
uploadUrl: "https://s3.example.com/presigned-put",
|
||||
headers: {
|
||||
"content-type": "image/png",
|
||||
},
|
||||
storageKey: "library/models/2026/ava-original.png",
|
||||
publicUrl: "https://images.example.com/library/models/2026/ava-original.png",
|
||||
},
|
||||
});
|
||||
expect(backendRequestMock).toHaveBeenCalledWith("/library/uploads/presign", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
resource_type: "model",
|
||||
file_role: "original",
|
||||
file_name: "ava-original.png",
|
||||
content_type: "image/png",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects invalid presign payloads before proxying", async () => {
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/libraries/uploads/presign", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resourceType: "unknown",
|
||||
fileRole: "original",
|
||||
fileName: "",
|
||||
contentType: "image/png",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload).toMatchObject({
|
||||
error: "VALIDATION_ERROR",
|
||||
message: "上传参数不合法。",
|
||||
});
|
||||
expect(backendRequestMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -37,7 +37,6 @@ test("proxies order creation to the backend and returns normalized success data"
|
||||
customer_level: "mid",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
@@ -76,7 +75,6 @@ test("rejects invalid order creation payloads before proxying", async () => {
|
||||
customer_level: "low",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
@@ -143,7 +141,6 @@ test("normalizes upstream validation errors from the backend", async () => {
|
||||
customer_level: "mid",
|
||||
service_mode: "semi_pro",
|
||||
model_id: 101,
|
||||
pose_id: 202,
|
||||
garment_asset_id: 303,
|
||||
scene_ref_asset_id: 404,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user