Skip to content
4 changes: 4 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ module.exports = {
coverageDirectory: '../coverage',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/$1',
'^@/(.*)$': '<rootDir>/$1',
},
};
10 changes: 2 additions & 8 deletions backend/src/modules/blockchain/ipfs.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import * as FormData from 'form-data';
import FormData from 'form-data';

type KuboRPCClient = {
add(data: string | Buffer): Promise<{ path: string }>;
Expand All @@ -21,13 +21,7 @@ export class IPFSService {

private async createClient(): Promise<KuboRPCClient> {
const ipfsUrl = this.configService.get<string>('blockchain.ipfs.url');
const dynamicImport = new Function(
'modulePath',
'return import(modulePath)',
) as (
modulePath: string,
) => Promise<{ create: (options: { url?: string }) => KuboRPCClient }>;
const kubo = await dynamicImport('kubo-rpc-client');
const kubo = await import('kubo-rpc-client') as unknown as { create: (options: { url?: string }) => KuboRPCClient };
return kubo.create({ url: ipfsUrl });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@ export class BlockingReportingService {
}

const existingBlock = await this.blockRepository.findOne({
where: { blockerId, blockedId },
where: { blocker: blockerId, blockedUser: blockedId },
});

if (existingBlock) {
throw new ConflictException('User is already blocked');
}

const block = this.blockRepository.create({ blockerId, blockedId });
return await this.blockRepository.save(block);
const block = this.blockRepository.create({ blocker: blockerId, blockedUser: blockedId });
return await this.blockRepository.save(block) as Block;
}

async unblockUser(blockerId: string, blockedId: string): Promise<void> {
const block = await this.blockRepository.findOne({
where: { blockerId, blockedId },
where: { blocker: blockerId, blockedUser: blockedId },
});

if (!block) {
Expand All @@ -51,14 +51,14 @@ export class BlockingReportingService {

async getBlockedUsers(blockerId: string): Promise<Block[]> {
return await this.blockRepository.find({
where: { blockerId },
where: { blocker: blockerId },
order: { createdAt: 'DESC' },
});
}

async isUserBlocked(blockerId: string, blockedId: string): Promise<boolean> {
const block = await this.blockRepository.findOne({
where: { blockerId, blockedId },
where: { blocker: blockerId, blockedUser: blockedId },
});
return !!block;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { IsUUID, IsNotEmpty } from 'class-validator';
export class CreateBlockDto {
@IsUUID()
@IsNotEmpty()
blockedId: string;
blockedUserId: string;
}
13 changes: 5 additions & 8 deletions backend/src/modules/blocking-reporting/dto/create-report.dto.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { IsUUID, IsEnum, IsOptional, IsString } from 'class-validator';
import { ReportTargetType, ReportReason } from '../entities/report.entity';
import { ReportCategory } from '../enums/report-category.enum';

export class CreateReportDto {
@IsUUID()
targetId: string;
reportedUserId: string;

@IsEnum(ReportTargetType)
targetType: ReportTargetType;

@IsEnum(ReportReason)
reason: ReportReason;
@IsEnum(ReportCategory)
category: ReportCategory;

@IsOptional()
@IsString()
description?: string;
}
}
12 changes: 7 additions & 5 deletions backend/src/modules/blocking-reporting/entities/block.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import {
} from 'typeorm';

@Entity('blocks')
@Unique(['blockerId', 'blockedId'])
@Index(['blockerId'])
@Index(['blockedId'])
@Unique(['blocker', 'blockedUser'])
@Index(['blocker'])
@Index(['blockedUser'])
export class Block {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'uuid' })
blockerId: string;
blocker: string;

@Column({ type: 'uuid' })
blockedId: string;
blockedUser: string;

blockedUserEntity?: any;

@CreateDateColumn()
createdAt: Date;
Expand Down
44 changes: 8 additions & 36 deletions backend/src/modules/blocking-reporting/services/report.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Report } from '../entities/report.entity';
import { Report, ReportTargetType, ReportReason } from '../entities/report.entity';
import { ReportNote } from '../entities/report-note.entity';
import { CreateReportDto } from '../dto/create-report.dto';
import { ReportFilterDto } from '../dto/report-filter.dto';
Expand All @@ -25,13 +25,14 @@ export class ReportService {
createReportDto: CreateReportDto,
): Promise<Report> {
const report = this.reportRepository.create({
reporter: { id: reporterId },
reportedUser: { id: createReportDto.reportedUserId },
category: createReportDto.category,
reporterId,
targetId: createReportDto.reportedUserId,
targetType: ReportTargetType.USER,
reason: createReportDto.category as unknown as ReportReason,
description: createReportDto.description,
});

const savedReport = await this.reportRepository.save(report);
const savedReport = await this.reportRepository.save(report) as Report;

await this.auditService.log(
reporterId,
Expand All @@ -49,9 +50,7 @@ export class ReportService {
page: number = 1,
): Promise<{ data: Report[]; total: number; page: number; limit: number }> {
const query = this.reportRepository
.createQueryBuilder('report')
.leftJoinAndSelect('report.reporter', 'reporter')
.leftJoinAndSelect('report.reportedUser', 'reportedUser');
.createQueryBuilder('report');

if (filterDto.status) {
query.andWhere('report.status = :status', { status: filterDto.status });
Expand Down Expand Up @@ -80,20 +79,6 @@ export class ReportService {

const [data, total] = await query.getManyAndCount();

// Clean sensitive user info
data.forEach((report) => {
if (report.reporter) {
delete report.reporter.password;
delete report.reporter.passwordResetToken;
delete report.reporter.emailVerificationToken;
}
if (report.reportedUser) {
delete report.reportedUser.password;
delete report.reportedUser.passwordResetToken;
delete report.reportedUser.emailVerificationToken;
}
});

return {
data,
total,
Expand All @@ -105,25 +90,12 @@ export class ReportService {
async getReportById(reportId: string): Promise<Report> {
const report = await this.reportRepository.findOne({
where: { id: reportId },
relations: ['reporter', 'reportedUser'],
});

if (!report) {
throw new NotFoundException('Report not found');
}

// clean up password
if (report.reporter) {
delete report.reporter.password;
delete report.reporter.passwordResetToken;
delete report.reporter.emailVerificationToken;
}
if (report.reportedUser) {
delete report.reportedUser.password;
delete report.reportedUser.passwordResetToken;
delete report.reportedUser.emailVerificationToken;
}

return report;
}

Expand All @@ -135,7 +107,7 @@ export class ReportService {
): Promise<Report> {
const report = await this.getReportById(reportId);

report.status = updateDto.status;
report.status = updateDto.status as unknown as Report['status'];
const updatedReport = await this.reportRepository.save(report);

if (note) {
Expand Down
15 changes: 6 additions & 9 deletions backend/src/modules/gdpr/gdpr.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { getQueueToken } from '@nestjs/bullmq';
import {
ConflictException,
NotFoundException,
TooManyRequestsException,
UnauthorizedException,
} from '@nestjs/common';
import { GdprService } from './gdpr.service';
import { GdprService, TooManyRequestsException } from './gdpr.service';
import { PasswordUtil } from '../../auth/utils/password.util';
import { UserConsent } from './entities/user-consent.entity';
import { DataDeletionRequest, DeletionStatus } from './entities/data-deletion-request.entity';
import { GdprRequest, GdprRequestStatus, GdprRequestType } from './entities/gdpr-request.entity';
Expand All @@ -30,7 +30,7 @@ describe('GdprService', () => {
{ provide: getRepositoryToken(UserConsent), useValue: mockConsentRepo },
{ provide: getRepositoryToken(DataDeletionRequest), useValue: mockDeletionRepo },
{ provide: getRepositoryToken(GdprRequest), useValue: mockGdprRequestRepo },
{ provide: 'DataSource', useValue: mockDataSource },
{ provide: DataSource, useValue: mockDataSource },
{ provide: StorageService, useValue: mockStorageService },
{ provide: getQueueToken('gdpr'), useValue: mockGdprQueue },
],
Expand All @@ -39,7 +39,7 @@ describe('GdprService', () => {
service = module.get<GdprService>(GdprService);
});

afterEach(() => jest.clearAllMocks());
afterEach(() => jest.resetAllMocks());

// ── Export ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -78,10 +78,7 @@ describe('GdprService', () => {
it('throws UnauthorizedException on wrong password', async () => {
mockGdprRequestRepo.findOne.mockResolvedValue(null);
mockDataSource.query.mockResolvedValueOnce([{ password: '$2b$12$hashedpassword' }]);

jest.spyOn(require('../../auth/utils/password.util'), 'PasswordUtil', 'get').mockReturnValue({
comparePassword: jest.fn().mockResolvedValue(false),
});
jest.spyOn(PasswordUtil, 'comparePassword').mockResolvedValue(false);

await expect(service.requestErasure('u1', 'wrongpass')).rejects.toThrow(
UnauthorizedException,
Expand Down
9 changes: 8 additions & 1 deletion backend/src/modules/gdpr/gdpr.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import {
BadRequestException,
ConflictException,
HttpException,
HttpStatus,
Injectable,
Logger,
NotFoundException,
TooManyRequestsException,
UnauthorizedException,
} from '@nestjs/common';

export class TooManyRequestsException extends HttpException {
constructor(message = 'Too Many Requests') {
super(message, HttpStatus.TOO_MANY_REQUESTS);
}
}
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, MoreThan } from 'typeorm';
import { InjectQueue } from '@nestjs/bullmq';
Expand Down
10 changes: 10 additions & 0 deletions backend/src/modules/pets/pets.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PetsService } from './pets.service';
import { Pet } from './entities/pet.entity';
import { PetShare } from './entities/pet-share.entity';
import { UsersService } from '../users/users.service';
import { CacheService } from '../cache/cache.service';

describe('PetsService', () => {
let service: PetsService;
Expand Down Expand Up @@ -41,6 +42,14 @@ describe('PetsService', () => {
findOne: jest.fn(),
};

const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
del: jest.fn().mockResolvedValue(undefined),
invalidatePet: jest.fn().mockResolvedValue(undefined),
invalidateVetVerification: jest.fn().mockResolvedValue(undefined),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
Expand All @@ -51,6 +60,7 @@ describe('PetsService', () => {
useValue: mockPetShareRepository,
},
{ provide: UsersService, useValue: mockUsersService },
{ provide: CacheService, useValue: mockCacheService },
],
}).compile();

Expand Down
13 changes: 12 additions & 1 deletion backend/src/vet-verification/vet-verification.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { Test, TestingModule } from '@nestjs/testing';
import { VetVerificationService } from './vet-verification.service';
import { CacheService } from '../modules/cache/cache.service';

jest.mock('uuid', () => ({
v4: () => 'test-uuid',
}));

const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
del: jest.fn().mockResolvedValue(undefined),
invalidatePet: jest.fn().mockResolvedValue(undefined),
};

describe('VetVerificationService', () => {
let service: VetVerificationService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [VetVerificationService],
providers: [
VetVerificationService,
{ provide: CacheService, useValue: mockCacheService },
],
}).compile();

service = module.get<VetVerificationService>(VetVerificationService);
Expand Down
Loading
Loading