Files
Genarrative/src/services/rpg-entry/rpgEntryLibraryClient.test.ts
T
2026-05-21 20:20:06 +08:00

259 lines
7.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const { requestJsonMock } = vi.hoisted(() => ({
requestJsonMock: vi.fn(),
}));
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetail,
getRpgEntryWorldLibraryDetail,
listRpgEntryWorldGallery,
listRpgEntryWorldLibrary,
publishRpgEntryWorldProfile,
unpublishRpgEntryWorldProfile,
upsertRpgEntryWorldProfile,
} from './rpgEntryLibraryClient';
vi.mock('../apiClient', () => ({
BACKGROUND_AUTH_REQUEST_OPTIONS: {
authImpact: 'local',
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
requestJson: requestJsonMock,
}));
describe('rpgEntryLibraryClient world library routes', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({ entries: [] });
});
it('reads world library from the runtime entry route', async () => {
await listRpgEntryWorldLibrary();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library',
expect.objectContaining({ method: 'GET' }),
'读取自定义世界库失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('reads world gallery from the public runtime entry route', async () => {
await listRpgEntryWorldGallery();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery',
expect.objectContaining({ method: 'GET' }),
'读取作品广场失败',
expect.objectContaining({
skipAuth: true,
skipRefresh: true,
}),
);
});
it('reads gallery detail from the public runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'owner-1',
profileId: 'profile-1',
},
});
await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-gallery/owner-1/profile-1',
expect.objectContaining({ method: 'GET' }),
'读取作品详情失败',
expect.objectContaining({
skipAuth: true,
skipRefresh: true,
}),
);
});
it('normalizes detail profiles before runtime launch consumes them', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'owner-1',
profileId: 'profile-1',
publicWorkCode: 'CW-1',
authorPublicUserCode: 'U-1',
profile: {
id: 'profile-1',
name: '旧数据世界',
summary: '只有摘要字段的旧 profile。',
},
visibility: 'published',
publishedAt: '2026-05-21T00:00:00.000Z',
updatedAt: '2026-05-21T00:00:00.000Z',
authorDisplayName: '作者',
worldName: '旧数据世界',
subtitle: '旧数据',
summaryText: '只有摘要字段的旧 profile。',
coverImageSrc: null,
themeMode: 'martial',
playableNpcCount: 0,
landmarkCount: 0,
},
});
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
expect(Array.isArray(entry.profile.storyNpcs)).toBe(true);
expect(Array.isArray(entry.profile.landmarks)).toBe(true);
expect(entry.profile.attributeSchema.schemaVersion).toBe(1);
});
it('falls back to entry summary when old detail profile cannot be normalized', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'owner-1',
profileId: 'profile-1',
publicWorkCode: 'CW-1',
authorPublicUserCode: 'U-1',
profile: {
id: 'profile-1',
summary: '缺少 name 的旧 profile。',
},
visibility: 'published',
publishedAt: '2026-05-21T00:00:00.000Z',
updatedAt: '2026-05-21T00:00:00.000Z',
authorDisplayName: '作者',
worldName: '摘要兜底世界',
subtitle: '旧数据',
summaryText: '缺少 name 的旧 profile。',
coverImageSrc: null,
themeMode: 'martial',
playableNpcCount: 0,
landmarkCount: 0,
},
});
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
expect(entry.profile.id).toBe('profile-1');
expect(entry.profile.name).toBe('摘要兜底世界');
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
});
it('reads owned library detail from the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
ownerUserId: 'owner-1',
profileId: 'profile-1',
},
});
await getRpgEntryWorldLibraryDetail('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1',
expect.objectContaining({ method: 'GET' }),
'读取作品详情失败',
expect.objectContaining({
retry: expect.objectContaining({ maxRetries: 1 }),
}),
);
});
it('writes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await upsertRpgEntryWorldProfile({
id: 'profile-1',
name: '测试世界',
} as never);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1',
expect.objectContaining({
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
}),
'保存自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('deletes world profile through the runtime entry route', async () => {
await deleteRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1',
expect.objectContaining({ method: 'DELETE' }),
'删除自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('publishes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await publishRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1/publish',
expect.objectContaining({ method: 'POST' }),
'发布自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
it('unpublishes world profile through the runtime entry route', async () => {
requestJsonMock.mockResolvedValueOnce({
entry: {
profileId: 'profile-1',
},
entries: [],
});
await unpublishRpgEntryWorldProfile('profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world-library/profile-1/unpublish',
expect.objectContaining({ method: 'POST' }),
'下架自定义世界失败',
expect.objectContaining({
retry: expect.objectContaining({
maxRetries: 1,
retryUnsafeMethods: true,
}),
}),
);
});
});