import * as path from 'path'; import { MigrationInterface, QueryRunner } from 'typeorm'; import { InitSchema1700000000000 } from '../../backend/src/database/migrations/1700000000000-InitSchema'; import { SeedSystemData1700000000100 } from '../../backend/src/database/migrations/1700000000100-SeedSystemData'; import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from '../../backend/src/database/migrations/1700000000200-AddCategoryTimestampsAndUniqueAbbrev'; import { UpdateSystemCategoryKeys1700000000300 } from '../../backend/src/database/migrations/1700000000300-UpdateSystemCategoryKeys'; import { RepairCategorySchemaAndSystemCategories1700000000400 } from '../../backend/src/database/migrations/1700000000400-RepairCategorySchemaAndSystemCategories'; import { DataSource } from 'typeorm'; import { UserEntity } from '../../backend/src/database/entities/user.entity'; import { SettingEntity } from '../../backend/src/database/entities/setting.entity'; import { CategoryEntity } from '../../backend/src/database/entities/category.entity'; import { ChallengeEntity } from '../../backend/src/database/entities/challenge.entity'; import { ChallengeFileEntity } from '../../backend/src/database/entities/challenge-file.entity'; import { SolveEntity } from '../../backend/src/database/entities/solve.entity'; import { RefreshTokenEntity } from '../../backend/src/database/entities/refresh-token.entity'; import { BlogPostEntity } from '../../backend/src/database/entities/blog-post.entity'; describe('Migrations', () => { let dataSource: DataSource; beforeAll(async () => { dataSource = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [UserEntity, SettingEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity, RefreshTokenEntity, BlogPostEntity], migrations: [ InitSchema1700000000000, SeedSystemData1700000000100, AddCategoryTimestampsAndUniqueAbbrev1700000000200, UpdateSystemCategoryKeys1700000000300, RepairCategorySchemaAndSystemCategories1700000000400, ], migrationsRun: true, synchronize: false, logging: false, }); await dataSource.initialize(); }); afterAll(async () => { await dataSource.destroy(); }); it('creates all required tables', async () => { const tables = await dataSource.query("SELECT name FROM sqlite_master WHERE type='table'"); const names = tables.map((t: any) => t.name); expect(names).toEqual(expect.arrayContaining([ 'user', 'setting', 'category', 'challenge', 'challenge_file', 'solve', 'refresh_token', 'blog_post', ])); }); it('seeds exactly six canonical system categories with unique keys', async () => { const cats: any[] = await dataSource.getRepository(CategoryEntity).find(); const systemCats = cats.filter((c) => c.systemKey); expect(systemCats.length).toBe(6); const keys = systemCats.map((c) => c.systemKey); expect(new Set(keys).size).toBe(keys.length); expect([...keys].sort()).toEqual(['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB']); for (const row of systemCats) { expect(typeof row.iconPath).toBe('string'); expect(row.iconPath.length).toBeGreaterThan(0); expect(typeof row.description).toBe('string'); expect(row.description.length).toBeGreaterThan(0); } }); it('seeds default settings', async () => { const settings = await dataSource.getRepository(SettingEntity).find(); const keys = settings.map((s) => s.key); expect(keys).toEqual(expect.arrayContaining(['pageTitle', 'themeKey', 'eventStartUtc', 'eventEndUtc'])); }); it('enforces UNIQUE(challengeId, userId) on solve', async () => { const indexes = await dataSource.query("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='solve'"); const uq = indexes.find((i: any) => /UNIQUE.*challenge_id.*user_id/i.test(i.sql || '')); expect(uq).toBeDefined(); }); it('enforces ON DELETE RESTRICT FK on solve', async () => { const fks = await dataSource.query("PRAGMA foreign_key_list('solve')"); const restrict = fks.filter((f: any) => f.on_delete === 'RESTRICT'); expect(restrict.length).toBeGreaterThanOrEqual(2); }); it('creates the category table with created_at and updated_at columns (Job 889)', async () => { const cols: any[] = await dataSource.query(`PRAGMA table_info("category")`); const names = new Set(cols.map((c: any) => c.name)); expect(names.has('created_at')).toBe(true); expect(names.has('updated_at')).toBe(true); }); it('lists categories without a "no such column: c.created_at" error (Job 889)', async () => { const rows = await dataSource .getRepository(CategoryEntity) .createQueryBuilder('c') .orderBy('LOWER(c.abbreviation)', 'ASC') .addOrderBy('c.abbreviation', 'ASC') .getMany(); expect(Array.isArray(rows)).toBe(true); expect(rows.length).toBeGreaterThanOrEqual(6); }); });