diff --git a/packages/backend/test/unit/entities/DriveFileEntityService.ts b/packages/backend/test/unit/entities/DriveFileEntityService.ts new file mode 100644 index 0000000000..2e416326ee --- /dev/null +++ b/packages/backend/test/unit/entities/DriveFileEntityService.ts @@ -0,0 +1,227 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; +import { Test } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import type { DriveFilesRepository, DriveFoldersRepository, UsersRepository } from '@/models/_.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { genAidx } from '@/misc/id/aidx.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +const describeBenchmark = process.env.RUN_BENCHMARKS === '1' ? describe : describe.skip; + +describe('DriveFileEntityService', () => { + let app: TestingModule; + let service: DriveFileEntityService; + let driveFolderEntityService: DriveFolderEntityService; + let driveFilesRepository: DriveFilesRepository; + let driveFoldersRepository: DriveFoldersRepository; + let usersRepository: UsersRepository; + let idCounter = 0; + + const userEntityServiceMock = { + packMany: jest.fn(async (users: Array) => { + return users.map(u => ({ + id: typeof u === 'string' ? u : u.id, + username: 'user', + })); + }), + pack: jest.fn(async (user: string | { id: string }) => { + return { + id: typeof user === 'string' ? user : user.id, + username: 'user', + }; + }), + }; + + const nextId = () => genAidx(Date.now() + (idCounter++)); + + const createUser = async () => { + const un = secureRndstr(16); + const id = nextId(); + await usersRepository.insert({ + id, + username: un, + usernameLower: un.toLowerCase(), + }); + return usersRepository.findOneByOrFail({ id }); + }; + + const createFolder = async (name: string, parentId: string | null) => { + const id = nextId(); + await driveFoldersRepository.insert({ + id, + name, + userId: null, + parentId, + }); + return driveFoldersRepository.findOneByOrFail({ id }); + }; + + const createFile = async (folderId: string | null, userId: string | null) => { + const id = nextId(); + await driveFilesRepository.insert({ + id, + userId, + userHost: null, + md5: secureRndstr(32), + name: `file-${id}`, + type: 'text/plain', + size: 1, + comment: null, + blurhash: null, + properties: {}, + storedInternal: true, + url: `https://example.com/${id}`, + thumbnailUrl: null, + webpublicUrl: null, + webpublicType: null, + accessKey: null, + thumbnailAccessKey: null, + webpublicAccessKey: null, + uri: null, + src: null, + folderId, + isSensitive: false, + maybeSensitive: false, + maybePorn: false, + isLink: false, + requestHeaders: null, + requestIp: null, + }); + return driveFilesRepository.findOneByOrFail({ id }); + }; + + beforeAll(async () => { + const moduleBuilder = Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }); + moduleBuilder.overrideProvider(UserEntityService).useValue(userEntityServiceMock as any); + + app = await moduleBuilder.compile(); + await app.init(); + app.enableShutdownHooks(); + + service = app.get(DriveFileEntityService); + driveFolderEntityService = app.get(DriveFolderEntityService); + driveFilesRepository = app.get(DI.driveFilesRepository); + driveFoldersRepository = app.get(DI.driveFoldersRepository); + usersRepository = app.get(DI.usersRepository); + }); + + beforeEach(() => { + userEntityServiceMock.packMany.mockClear(); + userEntityServiceMock.pack.mockClear(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('pack', () => { + test('detail: false', async () => { + const user = await createUser(); + const folder = await createFolder('pack-root', null); + const file = await createFile(folder.id, user.id); + + const packed = await service.pack(file, { detail: false, self: true }) as any; + expect(packed.id).toBe(file.id); + expect(packed.folder).toBeNull(); + expect(packed.user).toBeNull(); + expect(packed.userId).toBeNull(); + }); + + test('detail: true', async () => { + const folder = await createFolder('pack-parent', null); + const child = await createFolder('pack-child', folder.id); + const file = await createFile(child.id, null); + + const packed = await service.pack(file, { detail: true, self: true }) as any; + expect(packed.folder?.id).toBe(child.id); + expect(packed.folder?.parent?.id).toBe(folder.id); + }); + }); + + describe('packNullable', () => { + test('returns null for missing', async () => { + const packed = await service.packNullable('non-existent' as any, { detail: false }); + expect(packed).toBeNull(); + }); + + test('uses packedUser hint when withUser', async () => { + const user = await createUser(); + const file = await createFile(null, user.id); + + const packed = await service.packNullable(file, { withUser: true, self: true }, { + packedUser: { id: user.id, username: 'hint' } as any, + }); + expect(packed?.user?.id).toBe(user.id); + expect(packed?.user?.username).toBe('hint'); + }); + }); + + describe('packMany', () => { + test('withUser: true uses deduped packMany', async () => { + const user = await createUser(); + const fileA = await createFile(null, user.id); + const fileB = await createFile(null, user.id); + + const packed = await service.packMany([fileA, fileB], { withUser: true, self: true }); + expect(packed.length).toBe(2); + expect(userEntityServiceMock.packMany).toHaveBeenCalledTimes(1); + expect(userEntityServiceMock.packMany.mock.calls[0]?.[0]?.length).toBe(1); + expect(packed[0]?.user?.id).toBe(user.id); + }); + + test('detail: true packs folder', async () => { + const folder = await createFolder('packmany-root', null); + const file = await createFile(folder.id, null); + + const packed = await service.packMany([file], { detail: true, self: true }); + expect(packed[0]?.folder?.id).toBe(folder.id); + expect(packed[0]?.folder?.parent).toBeUndefined(); + }); + + test('detail: true uses DriveFolderEntityService pack', async () => { + const folder = await createFolder('packmany-folder', null); + const file = await createFile(folder.id, null); + const packSpy = jest.spyOn(driveFolderEntityService, 'pack'); + + await service.packMany([file], { detail: true, self: true }); + expect(packSpy).toHaveBeenCalled(); + packSpy.mockRestore(); + }); + }); + + describeBenchmark('benchmark', () => { + test('packMany', async () => { + const user = await createUser(); + const folders = []; + for (let i = 0; i < 100; i++) { + folders.push(await createFolder(`bench-${i}`, null)); + } + const files = []; + for (const folder of folders) { + for (let j = 0; j < 20; j++) { + files.push(await createFile(folder.id, user.id)); + } + } + + const start = Date.now(); + await service.packMany(files, { detail: true, withUser: true, self: true }); + const elapsed = Date.now() - start; + + console.log(`DriveFileEntityService.packMany benchmark: ${elapsed}ms`); + }); + }); +}); diff --git a/packages/backend/test/unit/entities/DriveFolderEntityService.ts b/packages/backend/test/unit/entities/DriveFolderEntityService.ts new file mode 100644 index 0000000000..81399cdc54 --- /dev/null +++ b/packages/backend/test/unit/entities/DriveFolderEntityService.ts @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'; +import { Test } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { genAidx } from '@/misc/id/aidx.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +const describeBenchmark = process.env.RUN_BENCHMARKS === '1' ? describe : describe.skip; + +describe('DriveFolderEntityService', () => { + let app: TestingModule; + let service: DriveFolderEntityService; + let driveFoldersRepository: DriveFoldersRepository; + let driveFilesRepository: DriveFilesRepository; + let idCounter = 0; + + const nextId = () => genAidx(Date.now() + (idCounter++)); + + const createFolder = async (name: string, parentId: string | null) => { + const id = nextId(); + await driveFoldersRepository.insert({ + id, + name, + userId: null, + parentId, + }); + return driveFoldersRepository.findOneByOrFail({ id }); + }; + + const createFile = async (folderId: string | null) => { + const id = nextId(); + await driveFilesRepository.insert({ + id, + userId: null, + userHost: null, + md5: secureRndstr(32), + name: `file-${id}`, + type: 'text/plain', + size: 1, + comment: null, + blurhash: null, + properties: {}, + storedInternal: true, + url: `https://example.com/${id}`, + thumbnailUrl: null, + webpublicUrl: null, + webpublicType: null, + accessKey: null, + thumbnailAccessKey: null, + webpublicAccessKey: null, + uri: null, + src: null, + folderId, + isSensitive: false, + maybeSensitive: false, + maybePorn: false, + isLink: false, + requestHeaders: null, + requestIp: null, + }); + }; + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + await app.init(); + app.enableShutdownHooks(); + + service = app.get(DriveFolderEntityService); + driveFoldersRepository = app.get(DI.driveFoldersRepository); + driveFilesRepository = app.get(DI.driveFilesRepository); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('pack', () => { + test('detail: false', async () => { + const root = await createFolder('root', null); + const child = await createFolder('child', root.id); + + const packed = await service.pack(child, { detail: false }) as any; + expect(packed.id).toBe(child.id); + expect(packed.parentId).toBe(root.id); + expect(packed.parent).toBeUndefined(); + expect(packed.foldersCount).toBeUndefined(); + expect(packed.filesCount).toBeUndefined(); + }); + + test('detail: true', async () => { + const root = await createFolder('root-detail', null); + const child = await createFolder('child-detail', root.id); + await createFolder('grandchild-detail', child.id); + await createFile(child.id); + await createFile(child.id); + + const packed = await service.pack(child, { detail: true }) as any; + expect(packed.id).toBe(child.id); + expect(packed.foldersCount).toBe(1); + expect(packed.filesCount).toBe(2); + expect(packed.parent?.id).toBe(root.id); + expect(packed.parent?.parent).toBeUndefined(); + }); + + test('detail: true reaches root for deep hierarchy', async () => { + const root = await createFolder('root-deep', null); + const level1 = await createFolder('level-1', root.id); + const level2 = await createFolder('level-2', level1.id); + const level3 = await createFolder('level-3', level2.id); + const level4 = await createFolder('level-4', level3.id); + const level5 = await createFolder('level-5', level4.id); + + const packed = await service.pack(level5, { detail: true }) as any; + expect(packed.id).toBe(level5.id); + expect(packed.parent?.id).toBe(level4.id); + expect(packed.parent?.parent?.id).toBe(level3.id); + expect(packed.parent?.parent?.parent?.id).toBe(level2.id); + expect(packed.parent?.parent?.parent?.parent?.id).toBe(level1.id); + expect(packed.parent?.parent?.parent?.parent?.parent?.id).toBe(root.id); + expect(packed.parent?.parent?.parent?.parent?.parent?.parent).toBeUndefined(); + }); + }); + + describe('packMany', () => { + test('preserves order and packs parents', async () => { + const root = await createFolder('root-many', null); + const childA = await createFolder('child-a', root.id); + const childB = await createFolder('child-b', root.id); + await createFile(childA.id); + + const packed = await service.packMany([childB, childA], { detail: true }) as any; + expect(packed[0].id).toBe(childB.id); + expect(packed[1].id).toBe(childA.id); + expect(packed[0].parent?.id).toBe(root.id); + expect(packed[1].parent?.id).toBe(root.id); + expect(packed[0].filesCount).toBe(0); + expect(packed[1].filesCount).toBe(1); + }); + }); + + describeBenchmark('benchmark', () => { + test('packMany', async () => { + const root = await createFolder('bench-root', null); + const folders = []; + for (let i = 0; i < 200; i++) { + folders.push(await createFolder(`bench-${i}`, root.id)); + } + + const start = Date.now(); + await service.packMany(folders, { detail: true }); + const elapsed = Date.now() - start; + + console.log(`DriveFolderEntityService.packMany benchmark: ${elapsed}ms`); + }); + }); +});