130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
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);
|
|
});
|
|
});
|