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