process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; process.env.UPLOAD_SIZE_LIMIT = '5mb'; process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { getDataSourceToken } from '@nestjs/typeorm'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { validateEnv } from '../../backend/src/config/env.schema'; import { RestoreService } from '../../backend/src/modules/admin/system/restore.service'; import { FilesystemTransactionService } from '../../backend/src/modules/admin/system/filesystem-transaction.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'; describe('Job 871: RestoreService validation rejects malformed archives before mutation', () => { let moduleRef: any; let restore: RestoreService; let dataSource: any; beforeAll(async () => { process.env.UPLOAD_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'hipctf-restore-uploads-')); 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, ], synchronize: true, }), ], providers: [RestoreService, FilesystemTransactionService], }).compile(); moduleRef.init(); dataSource = moduleRef.get(getDataSourceToken()); restore = moduleRef.get(RestoreService); }); afterAll(async () => { const dir = process.env.UPLOAD_DIR!; if (fs.existsSync(dir)) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { // ignore } } await moduleRef?.close(); }); it('rejects empty JSON', async () => { await expect(restore.stageArchive('', 'admin-1')).rejects.toMatchObject({ code: 'SYSTEM_RESTORE_VALIDATION_FAILED', }); }); it('rejects wrong format', async () => { const bad = JSON.stringify({ format: 'wrong', version: 1, exportedAt: 'x', tables: {}, uploads: [] }); await expect(restore.stageArchive(bad, 'admin-1')).rejects.toMatchObject({ code: 'SYSTEM_RESTORE_VALIDATION_FAILED', }); }); it('rejects base64 size mismatch', async () => { const bad = { format: 'hipctf-system-backup', version: 1, exportedAt: 'x', tables: {}, uploads: [ { path: 'icons/mismatch.png', originalFilename: 'm.png', mimeType: 'image/png', sizeBytes: 100, dataBase64: Buffer.from('short').toString('base64'), }, ], }; await expect(restore.stageArchive(JSON.stringify(bad), 'admin-1')).rejects.toMatchObject({ code: 'SYSTEM_RESTORE_VALIDATION_FAILED', }); }); it('rejects path traversal', async () => { const data = Buffer.alloc(0).toString('base64'); const bad = { format: 'hipctf-system-backup', version: 1, exportedAt: 'x', tables: {}, uploads: [ { path: '../../../etc/passwd', originalFilename: 'x', mimeType: 'application/octet-stream', sizeBytes: 0, dataBase64: data, }, ], }; await expect(restore.stageArchive(JSON.stringify(bad), 'admin-1')).rejects.toMatchObject({ code: 'SYSTEM_RESTORE_VALIDATION_FAILED', }); }); it('does not mutate live data after rejected validation', async () => { const users = await dataSource.query('SELECT COUNT(*) AS c FROM user'); expect(Number((users as any[])[0]?.c ?? 0)).toBeGreaterThanOrEqual(0); const stagedCount = (restore as any).stages?.size ?? 0; expect(stagedCount).toBe(0); }); });