Improve ISAC libraries, roles, and public profiles

This commit is contained in:
JB
2026-07-27 18:54:46 -04:00
parent fe4acec191
commit 6e12b2f3b7
40 changed files with 1196 additions and 271 deletions
+139
View File
@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { rmMock } = vi.hoisted(() => ({ rmMock: vi.fn() }));
vi.mock('node:fs/promises', async (importOriginal) => ({
...await importOriginal<typeof import('node:fs/promises')>(),
rm: rmMock
}));
import { GAME_ICON_TYPES } from './game-data-icons';
import { ManagedImageDeletionError, deleteManagedImage } from './managed-images';
type Row = Record<string, unknown>;
class FakeDatabase {
images = new Map<string, Row>();
icons = new Map<string, Row>();
executed: string[] = [];
prepare(sql: string) {
return Promise.resolve({
get: async (...args: unknown[]) => {
if (sql.startsWith('SELECT * FROM managed_images')) return this.images.get(String(args[0]));
if (sql.startsWith('SELECT * FROM game_data_icons')) {
return this.icons.get(`${String(args[0])}:${String(args[1])}`);
}
throw new Error(`Unexpected get: ${sql}`);
},
run: async (...args: unknown[]) => {
if (sql.startsWith('DELETE FROM game_data_icons')) {
const key = `${String(args[0])}:${String(args[1])}`;
const row = this.icons.get(key);
if (row?.storage_key === args[2]) this.icons.delete(key);
return;
}
if (sql.startsWith('DELETE FROM managed_images')) {
const row = this.images.get(String(args[0]));
if (row?.storage_key === args[1]) this.images.delete(String(args[0]));
return;
}
throw new Error(`Unexpected run: ${sql}`);
}
});
}
async exec(sql: string) {
this.executed.push(sql);
}
}
const imageId = '11111111-1111-4111-8111-111111111111';
const sourceId = '22222222-2222-4222-8222-222222222222';
describe('deleteManagedImage', () => {
beforeEach(() => rmMock.mockReset().mockResolvedValue(undefined));
it.each(GAME_ICON_TYPES)('detaches and deletes an assigned %s image', async (sourceType) => {
const database = new FakeDatabase();
const storageKey = `${sourceType}-${sourceId}.webp`;
database.images.set(imageId, {
id: imageId, storage_key: storageKey,
source_entity_type: sourceType, source_entity_id: sourceId
});
database.icons.set(`${sourceType}:${sourceId}`, {
entity_type: sourceType, entity_id: sourceId, storage_key: storageKey
});
const deleted = await deleteManagedImage(database as never, imageId);
expect(deleted?.sourceIcon).toMatchObject({ entity_type: sourceType, entity_id: sourceId });
expect(database.images.has(imageId)).toBe(false);
expect(database.icons.has(`${sourceType}:${sourceId}`)).toBe(false);
expect(database.executed).toEqual(['BEGIN', 'COMMIT']);
expect(rmMock).toHaveBeenCalledOnce();
});
it('deletes a standalone library image', async () => {
const database = new FakeDatabase();
database.images.set(imageId, {
id: imageId, storage_key: `library-${imageId}.webp`,
source_entity_type: null, source_entity_id: null
});
const deleted = await deleteManagedImage(database as never, imageId);
expect(deleted?.sourceIcon).toBeNull();
expect(database.images.has(imageId)).toBe(false);
expect(database.icons.size).toBe(0);
expect(rmMock).toHaveBeenCalledOnce();
});
it('deletes an orphaned linked image without touching another assignment', async () => {
const database = new FakeDatabase();
database.images.set(imageId, {
id: imageId, storage_key: `talent-${sourceId}.webp`,
source_entity_type: 'talent', source_entity_id: sourceId
});
const deleted = await deleteManagedImage(database as never, imageId);
expect(deleted?.sourceIcon).toBeNull();
expect(database.images.has(imageId)).toBe(false);
expect(database.executed).toEqual(['BEGIN', 'COMMIT']);
});
it('fails closed when the library and source rows reference different files', async () => {
const database = new FakeDatabase();
database.images.set(imageId, {
id: imageId, storage_key: `talent-${sourceId}.webp`,
source_entity_type: 'talent', source_entity_id: sourceId
});
database.icons.set(`talent:${sourceId}`, {
entity_type: 'talent', entity_id: sourceId,
storage_key: 'talent-33333333-3333-4333-8333-333333333333.webp'
});
await expect(deleteManagedImage(database as never, imageId)).rejects.toThrow(ManagedImageDeletionError);
expect(database.images.has(imageId)).toBe(true);
expect(database.icons.has(`talent:${sourceId}`)).toBe(true);
expect(database.executed).toEqual([]);
expect(rmMock).not.toHaveBeenCalled();
});
it.each([
{ source_entity_type: 'talent', source_entity_id: null },
{ source_entity_type: null, source_entity_id: sourceId },
{ source_entity_type: 'unknown', source_entity_id: sourceId }
])('rejects an invalid source relationship: $source_entity_type / $source_entity_id', async (source) => {
const database = new FakeDatabase();
database.images.set(imageId, {
id: imageId, storage_key: `library-${imageId}.webp`, ...source
});
await expect(deleteManagedImage(database as never, imageId)).rejects.toThrow(ManagedImageDeletionError);
expect(database.images.has(imageId)).toBe(true);
expect(database.executed).toEqual([]);
expect(rmMock).not.toHaveBeenCalled();
});
});