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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user