diff --git a/src/claims/entities/evidence-flag.entity.ts b/src/claims/entities/evidence-flag.entity.ts index 3cfe37f..9258435 100644 --- a/src/claims/entities/evidence-flag.entity.ts +++ b/src/claims/entities/evidence-flag.entity.ts @@ -30,6 +30,9 @@ export class EvidenceFlag { @Column({ nullable: true }) flaggedBy?: string; + @Column({ default: false }) + isModerator: boolean; + @CreateDateColumn() createdAt: Date; } diff --git a/src/claims/entities/evidence.entity.ts b/src/claims/entities/evidence.entity.ts index f265c33..fe6079a 100644 --- a/src/claims/entities/evidence.entity.ts +++ b/src/claims/entities/evidence.entity.ts @@ -27,6 +27,9 @@ export class Evidence { @Column({ default: 1 }) latestVersion: number; + @Column({ default: false }) + isHidden: boolean; + @CreateDateColumn() createdAt: Date; diff --git a/src/claims/evidence-flag.service.spec.ts b/src/claims/evidence-flag.service.spec.ts new file mode 100644 index 0000000..c550fb8 --- /dev/null +++ b/src/claims/evidence-flag.service.spec.ts @@ -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; + let evidenceRepo: Repository; + + 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); + flagRepo = module.get>(getRepositoryToken(EvidenceFlag)); + evidenceRepo = module.get>(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' }, + }); + }); + }); +}); diff --git a/src/claims/evidence-flag.service.ts b/src/claims/evidence-flag.service.ts index 4038ce9..09dfe33 100644 --- a/src/claims/evidence-flag.service.ts +++ b/src/claims/evidence-flag.service.ts @@ -13,13 +13,29 @@ export class EvidenceFlagService { private readonly evidenceRepo: Repository, ) {} - async createFlag(evidenceId: string, reason: string, flaggedBy?: string): Promise { + async createFlag( + evidenceId: string, + reason: string, + flaggedBy?: string, + isModerator: boolean = false, + ): Promise { 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); } diff --git a/src/claims/evidence.service.spec.ts b/src/claims/evidence.service.spec.ts index a408beb..12fe2f7 100644 --- a/src/claims/evidence.service.spec.ts +++ b/src/claims/evidence.service.spec.ts @@ -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; + let evidenceVersionRepo: Repository; + let auditTrailService: AuditTrailService; const makeEvidence = (overrides: Partial = {}): Evidence => ({ id: 'ev-1', @@ -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(), @@ -64,6 +80,10 @@ describe('EvidenceService', () => { ], }).compile(); + service = module.get(EvidenceService); + evidenceRepo = module.get>(getRepositoryToken(Evidence)); + evidenceVersionRepo = module.get>(getRepositoryToken(EvidenceVersion)); + auditTrailService = module.get(AuditTrailService); service = module.get(EvidenceService); evidenceRepo = module.get(getRepositoryToken(Evidence)); versionRepo = module.get(getRepositoryToken(EvidenceVersion)); @@ -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(); @@ -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); diff --git a/src/claims/evidence.service.ts b/src/claims/evidence.service.ts index 9ebedab..2a435be 100644 --- a/src/claims/evidence.service.ts +++ b/src/claims/evidence.service.ts @@ -84,14 +84,39 @@ export class EvidenceService { return savedVersion; } + /** + * Get evidence with all versions + */ + async getEvidence(evidenceId: string, includeHidden: boolean = false): Promise { + const where: any = { id: evidenceId }; + if (!includeHidden) { + where.isHidden = false; + } + async getEvidence(evidenceId: string): Promise { 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 { + 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 { const evidence = await this.evidenceRepository.findOneBy({ id: evidenceId }); if (!evidence) return null; @@ -101,14 +126,34 @@ export class EvidenceService { }); } + /** + * Get all evidence for a claim + */ + async getEvidenceForClaim(claimId: string, includeHidden: boolean = false): Promise { + const where: any = { claimId }; + if (!includeHidden) { + where.isHidden = false; + } + async getEvidenceForClaim(claimId: string): Promise { 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 { + const evidences = await this.getEvidenceForClaim(claimId, includeHidden); + if (evidences.length === 0) { + return null; + } async getLatestEvidenceForClaim(claimId: string): Promise { const evidences = await this.getEvidenceForClaim(claimId); if (evidences.length === 0) return null;