/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
import {randomUUID} from 'node:crypto';
import type {PhotoDnaMatchResult} from '@fluxer/api/src/csam/CsamTypes';
import type {ISynchronousCsamScanner} from '@fluxer/api/src/csam/ISynchronousCsamScanner';
import type {
ScanBase64Params,
ScanMediaParams,
SynchronousCsamScanResult,
} from '@fluxer/api/src/csam/SynchronousCsamScanner';
import {vi} from 'vitest';
export interface MockSynchronousCsamScannerConfig {
shouldMatch?: boolean;
matchResult?: PhotoDnaMatchResult;
omitMatchResult?: boolean;
shouldFail?: boolean;
}
export interface ScanCall {
readonly type: 'media' | 'base64';
readonly contentType: string;
}
function createDefaultMatchResult(): PhotoDnaMatchResult {
return {
isMatch: true,
trackingId: randomUUID(),
matchDetails: [
{
source: 'test-database',
violations: ['CSAM'],
matchDistance: 0.01,
matchId: randomUUID(),
},
],
timestamp: new Date().toISOString(),
};
}
export class MockSynchronousCsamScanner implements ISynchronousCsamScanner {
readonly scanMediaSpy = vi.fn();
readonly scanBase64Spy = vi.fn();
private config: MockSynchronousCsamScannerConfig;
private scanCalls: Array = [];
constructor(config: MockSynchronousCsamScannerConfig = {}) {
this.config = config;
}
configure(config: MockSynchronousCsamScannerConfig): void {
this.config = {...this.config, ...config};
}
async scanMedia(params: ScanMediaParams): Promise {
this.scanMediaSpy(params);
this.scanCalls.push({type: 'media', contentType: params.contentType ?? 'unknown'});
if (this.config.shouldFail) {
throw new Error('Mock CSAM scan failure');
}
if (this.config.shouldMatch) {
if (this.config.omitMatchResult) {
return {isMatch: true};
}
return {
isMatch: true,
matchResult: this.config.matchResult ?? createDefaultMatchResult(),
};
}
return {isMatch: false};
}
async scanBase64(params: ScanBase64Params): Promise {
this.scanBase64Spy(params);
this.scanCalls.push({type: 'base64', contentType: params.mimeType});
if (this.config.shouldFail) {
throw new Error('Mock CSAM scan failure');
}
if (this.config.shouldMatch) {
if (this.config.omitMatchResult) {
return {isMatch: true};
}
return {
isMatch: true,
matchResult: this.config.matchResult ?? createDefaultMatchResult(),
};
}
return {isMatch: false};
}
getScanCalls(): Array {
return [...this.scanCalls];
}
reset(): void {
this.config = {};
this.scanCalls = [];
this.scanMediaSpy.mockClear();
this.scanBase64Spy.mockClear();
}
}