Initial AI manga platform

This commit is contained in:
www
2026-06-15 17:45:28 +08:00
commit 7a8191650f
267 changed files with 105987 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
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: {
project: { findUnique: ReturnType<typeof vi.fn> };
asset: { create: ReturnType<typeof vi.fn>; findUnique: ReturnType<typeof vi.fn> };
};
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()
}
};
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');
});
});