AI Implementation feature(871): Admin Area System: Database Backup/Restore and Danger Zone (#61)

This commit was merged in pull request #61.
This commit is contained in:
2026-07-23 12:10:41 +00:00
parent 6bac67fad7
commit 470ddd30c3
42 changed files with 3996 additions and 55 deletions
+107
View File
@@ -0,0 +1,107 @@
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();
});
});