process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { validateEnv } from '../../backend/src/config/env.schema'; import { BackupService } from '../../backend/src/modules/admin/system/backup.service'; 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'; import { AdminOperationTokenEntity } from '../../backend/src/database/entities/admin-operation-token.entity'; 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 { UpgradeChallengeAdminSchema1700000000500 } from '../../backend/src/database/migrations/1700000000500-UpgradeChallengeAdminSchema'; import { AddAdminOperationTokens1700000000900 } from '../../backend/src/database/migrations/1700000000900-AddAdminOperationTokens'; describe('Job 871: BackupService captures + filters', () => { let moduleRef: any; let backup: BackupService; let uploadDir: string; beforeAll(async () => { uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hipctf-sys-backup-')); process.env.UPLOAD_DIR = uploadDir; fs.writeFileSync(path.join(uploadDir, 'logo.bin'), Buffer.from('LOGO-BYTES')); fs.mkdirSync(path.join(uploadDir, 'icons'), { recursive: true }); fs.writeFileSync(path.join(uploadDir, 'icons', 'icon.png'), Buffer.from('ICON')); moduleRef = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }), TypeOrmModule.forRoot({ type: 'better-sqlite3', database: ':memory:', entities: [ UserEntity, SettingEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity, RefreshTokenEntity, BlogPostEntity, AdminOperationTokenEntity, ], migrations: [ InitSchema1700000000000, SeedSystemData1700000000100, AddCategoryTimestampsAndUniqueAbbrev1700000000200, UpdateSystemCategoryKeys1700000000300, RepairCategorySchemaAndSystemCategories1700000000400, UpgradeChallengeAdminSchema1700000000500, AddAdminOperationTokens1700000000900, ], migrationsRun: true, synchronize: false, }), ], providers: [BackupService], }).compile(); backup = moduleRef.get(BackupService); const dataSource = moduleRef.get(getDataSourceToken()); await dataSource.query( `INSERT INTO user (id,username,password_hash,role,status,created_at) VALUES (?,?,?,?,?,strftime('%Y-%m-%dT%H:%M:%fZ','now'))`, ['u-1', 'alice', 'HASH', 'player', 'enabled'], ); await dataSource.query(`INSERT INTO setting (key,value) VALUES (?,?)`, ['testCustomKey', 'HIPCTF']); }); afterAll(async () => { await moduleRef?.close(); if (uploadDir && fs.existsSync(uploadDir)) { try { fs.rmSync(uploadDir, { recursive: true, force: true }); } catch { // ignore } } }); it('captures every application table and base64-encoded uploads', async () => { const obj = await backup.build(); expect(obj.format).toBe('hipctf-system-backup'); expect(obj.tables.user.length).toBeGreaterThanOrEqual(1); expect(obj.tables.setting.length).toBeGreaterThanOrEqual(1); expect(obj.uploads.length).toBeGreaterThanOrEqual(1); const logo = obj.uploads.find((u) => u.path.includes('logo.bin')); expect(logo).toBeDefined(); expect(Buffer.from(logo!.dataBase64, 'base64').toString()).toBe('LOGO-BYTES'); }); it('never includes operational or sqlite-internal tables', async () => { const obj = await backup.build(); expect(Object.keys(obj.tables)).not.toContain('admin_operation_token'); expect(Object.keys(obj.tables)).not.toContain('migrations'); expect(Object.keys(obj.tables)).not.toContain('sqlite_master'); expect(obj.tableCounts['admin_operation_token']).toBeUndefined(); }); });