257 lines
7.3 KiB
TypeScript
257 lines
7.3 KiB
TypeScript
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { AssetsService } from './assets.service';
|
|
import type { PrismaService } from '../prisma/prisma.service';
|
|
import type { ProjectsService } from '../projects/projects.service';
|
|
import type { StorageService } from './storage.service';
|
|
import type { AuthRequestUser } from '../auth/auth.types';
|
|
|
|
const user: AuthRequestUser = {
|
|
id: '1',
|
|
email: 'user@example.com',
|
|
role: 'user'
|
|
};
|
|
|
|
function createFile(overrides: Partial<Express.Multer.File> = {}): Express.Multer.File {
|
|
return {
|
|
fieldname: 'file',
|
|
originalname: 'novel.txt',
|
|
encoding: '7bit',
|
|
mimetype: 'text/plain',
|
|
size: 12,
|
|
buffer: Buffer.from('hello novel'),
|
|
destination: '',
|
|
filename: '',
|
|
path: '',
|
|
stream: undefined as never,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe('AssetsService', () => {
|
|
let prisma: any;
|
|
let storage: Pick<StorageService, 'storePrivateFile' | 'readPrivateFile'>;
|
|
let projectsService: Pick<ProjectsService, 'assertProjectOwner'>;
|
|
let service: AssetsService;
|
|
|
|
beforeEach(() => {
|
|
prisma = {
|
|
project: {
|
|
findUnique: vi.fn()
|
|
},
|
|
asset: {
|
|
create: vi.fn(),
|
|
findUnique: vi.fn()
|
|
},
|
|
renderTask: {
|
|
findMany: vi.fn().mockResolvedValue([])
|
|
},
|
|
providerLog: {
|
|
findMany: vi.fn().mockResolvedValue([])
|
|
},
|
|
providerConfig: {
|
|
findUnique: vi.fn().mockResolvedValue(null)
|
|
},
|
|
videoClip: {
|
|
findMany: vi.fn().mockResolvedValue([])
|
|
}
|
|
};
|
|
storage = {
|
|
storePrivateFile: vi.fn().mockResolvedValue({
|
|
file_path: 'local://novels/test.txt',
|
|
size: 12n,
|
|
hash: 'hash',
|
|
backend: 'local'
|
|
}),
|
|
readPrivateFile: vi.fn().mockResolvedValue(Buffer.from('video bytes'))
|
|
};
|
|
projectsService = {
|
|
assertProjectOwner: vi.fn().mockResolvedValue(100n)
|
|
};
|
|
service = new AssetsService(
|
|
prisma as unknown as PrismaService,
|
|
storage as StorageService,
|
|
projectsService as ProjectsService
|
|
);
|
|
});
|
|
|
|
it('stores novel uploads as private assets', async () => {
|
|
prisma.asset.create.mockResolvedValue({
|
|
id: 200n,
|
|
user_id: 1n,
|
|
project_id: 100n,
|
|
asset_type: 'novel_text',
|
|
file_path: 'local://novels/test.txt',
|
|
file_url: null,
|
|
mime_type: 'text/plain',
|
|
width: null,
|
|
height: null,
|
|
duration: null,
|
|
size: 12n,
|
|
hash: 'hash',
|
|
visibility: 'private',
|
|
status: 'active',
|
|
created_at: new Date('2026-05-31T00:00:00.000Z')
|
|
});
|
|
|
|
const result = await service.uploadNovelFile(user, '100', createFile());
|
|
|
|
expect(storage.storePrivateFile).toHaveBeenCalledWith(
|
|
expect.objectContaining({ originalname: 'novel.txt' }),
|
|
'novels'
|
|
);
|
|
expect(prisma.asset.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
asset_type: 'novel_text',
|
|
visibility: 'private',
|
|
file_url: null
|
|
})
|
|
});
|
|
expect(result.asset.visibility).toBe('private');
|
|
expect(result.next_step).toBe('copyright_confirm');
|
|
});
|
|
|
|
it('rejects unsupported novel file types', async () => {
|
|
await expect(
|
|
service.uploadNovelFile(
|
|
user,
|
|
'100',
|
|
createFile({ originalname: 'novel.exe', mimetype: 'application/octet-stream' })
|
|
)
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
});
|
|
|
|
it('rejects uploads to projects owned by others', async () => {
|
|
vi.mocked(projectsService.assertProjectOwner).mockRejectedValue(
|
|
new ForbiddenException('Project is private')
|
|
);
|
|
|
|
await expect(service.uploadNovelFile(user, '100', createFile())).rejects.toBeInstanceOf(
|
|
ForbiddenException
|
|
);
|
|
});
|
|
|
|
it('returns a private file buffer for owned assets', async () => {
|
|
prisma.asset.findUnique.mockResolvedValue({
|
|
id: 300n,
|
|
user_id: 1n,
|
|
project_id: 100n,
|
|
asset_type: 'video',
|
|
file_path: 'local://videos/final.mp4',
|
|
file_url: null,
|
|
mime_type: 'video/mp4',
|
|
width: 1080,
|
|
height: 1920,
|
|
duration: 4,
|
|
size: 11n,
|
|
hash: 'video-hash',
|
|
visibility: 'private',
|
|
status: 'active',
|
|
created_at: new Date('2026-05-31T00:00:00.000Z')
|
|
});
|
|
|
|
const result = await service.downloadAssetForUser(user, '300');
|
|
|
|
expect(storage.readPrivateFile).toHaveBeenCalledWith('local://videos/final.mp4');
|
|
expect(result.asset.id).toBe('300');
|
|
expect(result.filename).toBe('video-300.mp4');
|
|
expect(result.buffer.toString()).toBe('video bytes');
|
|
});
|
|
|
|
it('returns generation provider and model for direct asset detail', async () => {
|
|
prisma.asset.findUnique.mockResolvedValue({
|
|
id: 300n,
|
|
user_id: 1n,
|
|
project_id: 100n,
|
|
asset_type: 'video',
|
|
file_path: 'local://videos/final.mp4',
|
|
file_url: null,
|
|
mime_type: 'video/mp4',
|
|
width: 1080,
|
|
height: 1920,
|
|
duration: 4,
|
|
size: 11n,
|
|
hash: 'video-hash',
|
|
visibility: 'private',
|
|
status: 'active',
|
|
created_at: new Date('2026-05-31T00:00:00.000Z')
|
|
});
|
|
prisma.renderTask.findMany.mockResolvedValue([
|
|
{
|
|
id: 20n,
|
|
project_id: 100n,
|
|
episode_id: 10n,
|
|
shot_id: 8n,
|
|
task_type: 'live_action_video_clip_generate',
|
|
provider_id: 13n,
|
|
status: 'success',
|
|
input_json: {},
|
|
input_hash: null,
|
|
idempotency_key: null,
|
|
output_asset_id: 300n,
|
|
provider_request_id: 'task-external-1',
|
|
retry_count: 0,
|
|
max_retry: 0,
|
|
cost_estimate: null,
|
|
cost_actual: { toString: () => '0.35' },
|
|
error_code: null,
|
|
error_message: null,
|
|
created_at: new Date('2026-05-31T00:00:00.000Z'),
|
|
started_at: null,
|
|
finished_at: new Date('2026-05-31T00:00:00.000Z')
|
|
}
|
|
]);
|
|
prisma.providerLog.findMany.mockResolvedValue([
|
|
{
|
|
id: 40n,
|
|
provider_id: 13n,
|
|
task_id: 20n,
|
|
project_id: 100n,
|
|
provider_type: 'VideoProvider',
|
|
provider_code: 'kling-image-to-video',
|
|
model_name: 'kling-v2-1',
|
|
request_json: {},
|
|
response_json: {},
|
|
input_size: null,
|
|
output_size: null,
|
|
cost_estimate: null,
|
|
cost_actual: null,
|
|
status: 'success',
|
|
error_code: null,
|
|
error_message: null,
|
|
started_at: null,
|
|
finished_at: null,
|
|
created_at: new Date('2026-05-31T00:00:00.000Z')
|
|
}
|
|
]);
|
|
prisma.providerConfig.findUnique.mockResolvedValue({
|
|
id: 13n,
|
|
provider_type: 'VideoProvider',
|
|
provider_code: 'kling-image-to-video',
|
|
display_name: '可灵图生视频',
|
|
mode: 'real',
|
|
model_name: 'kling-v2-1',
|
|
config_json: {},
|
|
fallback_provider_id: null,
|
|
is_enabled: true,
|
|
priority: 10,
|
|
rate_limit_json: {},
|
|
cost_rule_json: {},
|
|
created_at: new Date('2026-05-31T00:00:00.000Z'),
|
|
updated_at: new Date('2026-05-31T00:00:00.000Z')
|
|
});
|
|
|
|
const result = await service.getAssetForUser(user, '300');
|
|
|
|
expect(result.generation).toEqual(
|
|
expect.objectContaining({
|
|
provider_name: '可灵图生视频',
|
|
provider_code: 'kling-image-to-video',
|
|
model_name: 'kling-v2-1',
|
|
task_id: '20',
|
|
provider_request_id: 'task-external-1'
|
|
})
|
|
);
|
|
});
|
|
});
|