Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/claims/entities/evidence-flag.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export class EvidenceFlag {
@Column({ nullable: true })
flaggedBy?: string;

@Column({ default: false })
isModerator: boolean;

@CreateDateColumn()
createdAt: Date;
}
3 changes: 3 additions & 0 deletions src/claims/entities/evidence.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export class Evidence {
@Column({ default: 1 })
latestVersion: number;

@Column({ default: false })
isHidden: boolean;

@CreateDateColumn()
createdAt: Date;

Expand Down
125 changes: 125 additions & 0 deletions src/claims/evidence-flag.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EvidenceFlagService } from './evidence-flag.service';
import { EvidenceFlag } from './entities/evidence-flag.entity';
import { Evidence } from './entities/evidence.entity';
import { NotFoundException } from '@nestjs/common';

describe('EvidenceFlagService', () => {
let service: EvidenceFlagService;
let flagRepo: Repository<EvidenceFlag>;
let evidenceRepo: Repository<Evidence>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EvidenceFlagService,
{
provide: getRepositoryToken(EvidenceFlag),
useClass: Repository,
},
{
provide: getRepositoryToken(Evidence),
useClass: Repository,
},
],
}).compile();

service = module.get<EvidenceFlagService>(EvidenceFlagService);
flagRepo = module.get<Repository<EvidenceFlag>>(getRepositoryToken(EvidenceFlag));
evidenceRepo = module.get<Repository<Evidence>>(getRepositoryToken(Evidence));
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('createFlag', () => {
const evidenceId = 'evidence-1';
const reason = 'spam';
const flaggedBy = 'user-1';

it('should throw NotFoundException if evidence does not exist', async () => {
jest.spyOn(evidenceRepo, 'findOneBy').mockResolvedValue(null);

await expect(service.createFlag(evidenceId, reason)).rejects.toThrow(
NotFoundException,
);
});

it('should create a flag and not hide evidence if isModerator is false', async () => {
const evidence = { id: evidenceId, isHidden: false } as Evidence;
const flag = {
id: 'flag-1',
evidenceId,
reason,
flaggedBy,
isModerator: false,
} as any;

jest.spyOn(evidenceRepo, 'findOneBy').mockResolvedValue(evidence);
jest.spyOn(flagRepo, 'create').mockReturnValue(flag);
jest.spyOn(flagRepo, 'save').mockResolvedValue(flag);
const evidenceSaveSpy = jest.spyOn(evidenceRepo, 'save').mockResolvedValue(evidence);

const result = await service.createFlag(evidenceId, reason, flaggedBy, false);

expect(result).toEqual(flag);
expect(evidenceSaveSpy).not.toHaveBeenCalled();
expect(flagRepo.create).toHaveBeenCalledWith({
evidenceId,
reason,
flaggedBy,
});
expect(result.isModerator).toBe(false);
});

it('should create a flag and hide evidence if isModerator is true', async () => {
const evidence = { id: evidenceId, isHidden: false } as Evidence;
const flag = {
id: 'flag-1',
evidenceId,
reason,
flaggedBy,
isModerator: true,
} as any;

jest.spyOn(evidenceRepo, 'findOneBy').mockResolvedValue(evidence);
jest.spyOn(flagRepo, 'create').mockReturnValue(flag);
jest.spyOn(flagRepo, 'save').mockResolvedValue(flag);
const evidenceSaveSpy = jest.spyOn(evidenceRepo, 'save').mockResolvedValue({
...evidence,
isHidden: true,
} as Evidence);

const result = await service.createFlag(evidenceId, reason, flaggedBy, true);

expect(result).toEqual(flag);
expect(evidenceSaveSpy).toHaveBeenCalledWith(expect.objectContaining({ isHidden: true }));
expect(flagRepo.create).toHaveBeenCalledWith({
evidenceId,
reason,
flaggedBy,
});
expect(result.isModerator).toBe(true);
});
});

describe('getFlagsForEvidence', () => {
it('should return flags for an evidence', async () => {
const evidenceId = 'evidence-1';
const flags = [{ id: 'flag-1', evidenceId }] as EvidenceFlag[];

jest.spyOn(flagRepo, 'find').mockResolvedValue(flags);

const result = await service.getFlagsForEvidence(evidenceId);

expect(result).toEqual(flags);
expect(flagRepo.find).toHaveBeenCalledWith({
where: { evidenceId },
order: { createdAt: 'ASC' },
});
});
});
});
20 changes: 18 additions & 2 deletions src/claims/evidence-flag.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,29 @@ export class EvidenceFlagService {
private readonly evidenceRepo: Repository<Evidence>,
) {}

async createFlag(evidenceId: string, reason: string, flaggedBy?: string): Promise<EvidenceFlag> {
async createFlag(
evidenceId: string,
reason: string,
flaggedBy?: string,
isModerator: boolean = false,
): Promise<EvidenceFlag> {
const evidence = await this.evidenceRepo.findOneBy({ id: evidenceId });
if (!evidence) {
throw new NotFoundException(`Evidence with ID ${evidenceId} not found`);
}

const flag = this.flagRepo.create({ evidenceId, reason, flaggedBy });
// If a moderator flags evidence, we automatically hide it
if (isModerator) {
evidence.isHidden = true;
await this.evidenceRepo.save(evidence);
}

const flag = this.flagRepo.create({
evidenceId,
reason,
flaggedBy,
});
(flag as any).isModerator = isModerator;
return this.flagRepo.save(flag);
}

Expand Down
91 changes: 91 additions & 0 deletions src/claims/evidence.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { Evidence } from './entities/evidence.entity';
import { EvidenceVersion } from './entities/evidence-version.entity';
import { AuditTrailService } from '../audit/services/audit-trail.service';

describe('EvidenceService', () => {
let service: EvidenceService;
let evidenceRepo: Repository<Evidence>;
let evidenceVersionRepo: Repository<EvidenceVersion>;
let auditTrailService: AuditTrailService;
const makeEvidence = (overrides: Partial<Evidence> = {}): Evidence =>
({
id: 'ev-1',
Expand Down Expand Up @@ -41,6 +46,17 @@ describe('EvidenceService', () => {
EvidenceService,
{
provide: getRepositoryToken(Evidence),
useClass: Repository,
},
{
provide: getRepositoryToken(EvidenceVersion),
useClass: Repository,
},
{
provide: AuditTrailService,
useValue: {
log: jest.fn(),
},
useValue: {
create: jest.fn(),
save: jest.fn(),
Expand All @@ -64,6 +80,10 @@ describe('EvidenceService', () => {
],
}).compile();

service = module.get<EvidenceService>(EvidenceService);
evidenceRepo = module.get<Repository<Evidence>>(getRepositoryToken(Evidence));
evidenceVersionRepo = module.get<Repository<EvidenceVersion>>(getRepositoryToken(EvidenceVersion));
auditTrailService = module.get<AuditTrailService>(AuditTrailService);
service = module.get(EvidenceService);
evidenceRepo = module.get(getRepositoryToken(Evidence));
versionRepo = module.get(getRepositoryToken(EvidenceVersion));
Expand All @@ -74,6 +94,46 @@ describe('EvidenceService', () => {
expect(service).toBeDefined();
});

describe('getEvidence', () => {
it('should return evidence if not hidden', async () => {
const evidence = { id: 'ev-1', isHidden: false } as Evidence;
jest.spyOn(evidenceRepo, 'findOne').mockResolvedValue(evidence);

const result = await service.getEvidence('ev-1');

expect(result).toEqual(evidence);
expect(evidenceRepo.findOne).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'ev-1', isHidden: false },
}),
);
});

it('should return null if hidden and includeHidden is false', async () => {
jest.spyOn(evidenceRepo, 'findOne').mockResolvedValue(null);

const result = await service.getEvidence('ev-1');

expect(result).toBeNull();
expect(evidenceRepo.findOne).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'ev-1', isHidden: false },
}),
);
});

it('should return evidence if hidden and includeHidden is true', async () => {
const evidence = { id: 'ev-1', isHidden: true } as Evidence;
jest.spyOn(evidenceRepo, 'findOne').mockResolvedValue(evidence);

const result = await service.getEvidence('ev-1', true);

expect(result).toEqual(evidence);
expect(evidenceRepo.findOne).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'ev-1' },
}),
);
describe('createEvidence', () => {
it('creates evidence with version 1', async () => {
const evidence = makeEvidence();
Expand Down Expand Up @@ -230,6 +290,37 @@ describe('EvidenceService', () => {
});

describe('getEvidenceForClaim', () => {
it('should filter out hidden evidence by default', async () => {
const claimId = 'claim-1';
const evidences = [{ id: 'ev-1', isHidden: false }] as Evidence[];
jest.spyOn(evidenceRepo, 'find').mockResolvedValue(evidences);

const result = await service.getEvidenceForClaim(claimId);

expect(result).toEqual(evidences);
expect(evidenceRepo.find).toHaveBeenCalledWith(
expect.objectContaining({
where: { claimId, isHidden: false },
}),
);
});

it('should include hidden evidence if includeHidden is true', async () => {
const claimId = 'claim-1';
const evidences = [
{ id: 'ev-1', isHidden: false },
{ id: 'ev-2', isHidden: true },
] as Evidence[];
jest.spyOn(evidenceRepo, 'find').mockResolvedValue(evidences);

const result = await service.getEvidenceForClaim(claimId, true);

expect(result).toEqual(evidences);
expect(evidenceRepo.find).toHaveBeenCalledWith(
expect.objectContaining({
where: { claimId },
}),
);
it('returns all evidence for a claim', async () => {
const evidences = [makeEvidence()];
evidenceRepo.find.mockResolvedValue(evidences);
Expand Down
49 changes: 47 additions & 2 deletions src/claims/evidence.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,39 @@ export class EvidenceService {
return savedVersion;
}

/**
* Get evidence with all versions
*/
async getEvidence(evidenceId: string, includeHidden: boolean = false): Promise<Evidence | null> {
const where: any = { id: evidenceId };
if (!includeHidden) {
where.isHidden = false;
}

async getEvidence(evidenceId: string): Promise<Evidence | null> {
return this.evidenceRepository.findOne({
where: { id: evidenceId },
where,
relations: ['versions'],
order: { versions: { version: 'ASC' } },
});
}

/**
* Get latest version of evidence
*/
async getLatestEvidenceVersion(
evidenceId: string,
includeHidden: boolean = false,
): Promise<EvidenceVersion | null> {
const where: any = { id: evidenceId };
if (!includeHidden) {
where.isHidden = false;
}

const evidence = await this.evidenceRepository.findOneBy(where);
if (!evidence) {
return null;
}
async getLatestEvidenceVersion(evidenceId: string): Promise<EvidenceVersion | null> {
const evidence = await this.evidenceRepository.findOneBy({ id: evidenceId });
if (!evidence) return null;
Expand All @@ -101,14 +126,34 @@ export class EvidenceService {
});
}

/**
* Get all evidence for a claim
*/
async getEvidenceForClaim(claimId: string, includeHidden: boolean = false): Promise<Evidence[]> {
const where: any = { claimId };
if (!includeHidden) {
where.isHidden = false;
}

async getEvidenceForClaim(claimId: string): Promise<Evidence[]> {
return this.evidenceRepository.find({
where: { claimId },
where,
relations: ['versions'],
order: { createdAt: 'ASC', versions: { version: 'ASC' } },
});
}

/**
* Get latest evidence version for a claim (assuming one evidence per claim for simplicity)
*/
async getLatestEvidenceForClaim(
claimId: string,
includeHidden: boolean = false,
): Promise<EvidenceVersion | null> {
const evidences = await this.getEvidenceForClaim(claimId, includeHidden);
if (evidences.length === 0) {
return null;
}
async getLatestEvidenceForClaim(claimId: string): Promise<EvidenceVersion | null> {
const evidences = await this.getEvidenceForClaim(claimId);
if (evidences.length === 0) return null;
Expand Down
Loading