From e6cd10f439c36e6195f25bf1c51adaf3b0a8c3a0 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Sun, 13 Jul 2025 07:22:34 +0100 Subject: [PATCH 1/3] cleanup --- src/puzzle/dto/puzzle.dto.ts | 17 +- .../entities/puzzle-submission.entity.ts | 6 +- .../providers/puzzle-progress.provider.ts | 21 +- src/puzzle/puzzle.service.spec.ts | 346 ------------------ src/puzzle/puzzle.service.ts | 98 +++-- 5 files changed, 100 insertions(+), 388 deletions(-) delete mode 100644 src/puzzle/puzzle.service.spec.ts diff --git a/src/puzzle/dto/puzzle.dto.ts b/src/puzzle/dto/puzzle.dto.ts index 9e16637..6db8487 100644 --- a/src/puzzle/dto/puzzle.dto.ts +++ b/src/puzzle/dto/puzzle.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; import { PuzzleType } from '../enums/puzzle-type.enum'; import { PuzzleDifficulty } from '../enums/puzzle-difficulty.enum'; @@ -20,6 +20,21 @@ export class PuzzleFilterDto { } export class SubmitPuzzleDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + userId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + puzzleId: number; + + @ApiProperty({ default: false }) + @IsOptional() + @IsBoolean() + skipped?: boolean = false; + @ApiProperty({ description: 'User solution to the puzzle' }) @IsNotEmpty() @IsString() diff --git a/src/puzzle/entities/puzzle-submission.entity.ts b/src/puzzle/entities/puzzle-submission.entity.ts index 6974bae..3d296a0 100644 --- a/src/puzzle/entities/puzzle-submission.entity.ts +++ b/src/puzzle/entities/puzzle-submission.entity.ts @@ -10,7 +10,7 @@ import { User } from 'src/users/user.entity'; import { Puzzle } from 'src/puzzle/entities/puzzle.entity'; @Entity('puzzle_submissions') -@Unique(['user', 'puzzle']) +@Unique(['user', 'puzzle']) export class PuzzleSubmission { @PrimaryGeneratedColumn('uuid') id: string; @@ -25,11 +25,11 @@ export class PuzzleSubmission { isCorrect: boolean; @Column({ nullable: true }) - selectedAnswer?: string; + solution?: string; @Column({ default: false }) skipped: boolean; @CreateDateColumn() createdAt: Date; -} \ No newline at end of file +} diff --git a/src/puzzle/providers/puzzle-progress.provider.ts b/src/puzzle/providers/puzzle-progress.provider.ts index 4d19755..d5bb9bb 100644 --- a/src/puzzle/providers/puzzle-progress.provider.ts +++ b/src/puzzle/providers/puzzle-progress.provider.ts @@ -1,3 +1,12 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { PuzzleSubmission } from "../entities/puzzle-submission.entity"; +import { Puzzle } from "../entities/puzzle.entity"; +import { User } from "src/users/user.entity"; +import { SubmitPuzzleDto } from "../dto/puzzle.dto"; +import { PuzzleType } from "../enums/puzzle-type.enum"; + @Injectable() export class PuzzleProgressProvider { constructor( @@ -13,7 +22,7 @@ constructor( async submitPuzzleAnswer(dto: SubmitPuzzleDto): Promise { - const { userId, puzzleId, selectedAnswer, skipped } = dto; + const { userId, puzzleId, solution, skipped } = dto; const user = await this.userRepo.findOne({ where: { id: userId } }); if (!user) throw new NotFoundException('User not found'); @@ -29,12 +38,12 @@ constructor( throw new BadRequestException('Puzzle already submitted'); } - const isCorrect = !skipped && selectedAnswer === puzzle.solution; + const isCorrect = !skipped && solution === puzzle.solution; const submission = this.submissionRepo.create({ user, puzzle, - selectedAnswer, + solution, skipped, isCorrect, }); @@ -46,7 +55,7 @@ constructor( * Get user's puzzle progress by category */ async getProgressByCategory(userId: string): Promise< - Record + Record > { const allPuzzles = await this.puzzleRepo.find({ where: { isPublished: true }, @@ -60,13 +69,13 @@ constructor( const progressMap: Record = {}; for (const puzzle of allPuzzles) { - const key = puzzle.category; + const key = puzzle.type; progressMap[key] = progressMap[key] || { completed: 0, total: 0 }; progressMap[key].total += 1; } for (const submission of completed) { - const key = submission.puzzle.category; + const key = submission.puzzle.type; if (progressMap[key]) { progressMap[key].completed += 1; } diff --git a/src/puzzle/puzzle.service.spec.ts b/src/puzzle/puzzle.service.spec.ts deleted file mode 100644 index e72a054..0000000 --- a/src/puzzle/puzzle.service.spec.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { NotFoundException } from '@nestjs/common'; -import { PuzzleService } from './puzzle.service'; -import { Puzzle } from './entities/puzzle.entity'; -import { PuzzleSubmission } from './entities/puzzle-submission.entity'; -import { PuzzleProgress } from './entities/puzzle-progress.entity'; -import { User } from '../users/user.entity'; -import { SubmitPuzzleDto } from './dto/puzzle.dto'; - -describe('PuzzleService', () => { - let service: PuzzleService; - let puzzleRepository: Repository; - let submissionRepository: Repository; - let progressRepository: Repository; - let userRepository: Repository; - let eventEmitter: EventEmitter2; - - const mockPuzzleRepository = { - findOne: jest.fn(), - createQueryBuilder: jest.fn(() => ({ - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn(), - })), - }; - - const mockSubmissionRepository = { - create: jest.fn(), - save: jest.fn(), - findOne: jest.fn(), - }; - - const mockProgressRepository = { - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - find: jest.fn(), - }; - - const mockUserRepository = { - findOne: jest.fn(), - save: jest.fn(), - }; - - const mockEventEmitter = { - emit: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - PuzzleService, - { - provide: getRepositoryToken(Puzzle), - useValue: mockPuzzleRepository, - }, - { - provide: getRepositoryToken(PuzzleSubmission), - useValue: mockSubmissionRepository, - }, - { - provide: getRepositoryToken(PuzzleProgress), - useValue: mockProgressRepository, - }, - { - provide: getRepositoryToken(User), - useValue: mockUserRepository, - }, - { - provide: EventEmitter2, - useValue: mockEventEmitter, - }, - ], - }).compile(); - - service = module.get(PuzzleService); - puzzleRepository = module.get>(getRepositoryToken(Puzzle)); - submissionRepository = module.get>(getRepositoryToken(PuzzleSubmission)); - progressRepository = module.get>(getRepositoryToken(PuzzleProgress)); - userRepository = module.get>(getRepositoryToken(User)); - eventEmitter = module.get(EventEmitter2); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('submitPuzzleSolution', () => { - const userId = 1; - const puzzleId = 101; - const submitDto: SubmitPuzzleDto = { solution: 'correct answer' }; - - it('should successfully submit correct puzzle solution', async () => { - const puzzle = { - id: puzzleId, - title: 'Test Puzzle', - description: 'Test Description', - type: 'logic', - difficulty: 'easy', - solution: 'correct answer', - isPublished: true, - }; - - const user = { - id: userId, - xp: 100, - level: 1, - }; - - const submission = { - id: 1, - userId, - puzzleId, - attemptData: { solution: submitDto.solution }, - result: true, - submittedAt: new Date(), - }; - - mockPuzzleRepository.findOne.mockResolvedValue(puzzle); - mockSubmissionRepository.create.mockReturnValue(submission); - mockSubmissionRepository.save.mockResolvedValue(submission); - mockSubmissionRepository.findOne.mockResolvedValue(null); // No previous success - mockProgressRepository.findOne.mockResolvedValue(null); - mockProgressRepository.create.mockReturnValue({ - userId, - puzzleType: puzzle.type, - completedCount: 0, - }); - mockProgressRepository.save.mockResolvedValue({ - userId, - puzzleType: puzzle.type, - completedCount: 1, - }); - mockUserRepository.findOne.mockResolvedValue(user); - mockUserRepository.save.mockResolvedValue({ - ...user, - xp: 200, - level: 1, - }); - - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); - - expect(mockPuzzleRepository.findOne).toHaveBeenCalledWith({ - where: { id: puzzleId }, - }); - expect(mockSubmissionRepository.create).toHaveBeenCalledWith({ - userId, - puzzleId, - attemptData: { solution: submitDto.solution }, - result: true, - submittedAt: expect.any(Date), - }); - expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', { - userId, - puzzleId, - isCorrect: true, - timestamp: expect.any(Date), - }); - expect(result.success).toBe(true); - expect(result.xpEarned).toBe(100); - expect(result.tokensEarned).toBe(10); - }); - - it('should handle incorrect puzzle solution', async () => { - const puzzle = { - id: puzzleId, - title: 'Test Puzzle', - description: 'Test Description', - type: 'logic', - difficulty: 'easy', - solution: 'correct answer', - isPublished: true, - }; - - const submitDtoIncorrect: SubmitPuzzleDto = { solution: 'wrong answer' }; - - mockPuzzleRepository.findOne.mockResolvedValue(puzzle); - mockSubmissionRepository.create.mockReturnValue({ - id: 1, - userId, - puzzleId, - attemptData: { solution: submitDtoIncorrect.solution }, - result: false, - submittedAt: new Date(), - }); - mockSubmissionRepository.save.mockResolvedValue({ - id: 1, - userId, - puzzleId, - attemptData: { solution: submitDtoIncorrect.solution }, - result: false, - submittedAt: new Date(), - }); - - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDtoIncorrect); - - expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', { - userId, - puzzleId, - isCorrect: false, - timestamp: expect.any(Date), - }); - expect(result.success).toBe(false); - expect(result.message).toBe('Incorrect solution. Try again!'); - }); - - it('should throw NotFoundException when puzzle not found', async () => { - mockPuzzleRepository.findOne.mockResolvedValue(null); - - await expect(service.submitPuzzleSolution(userId, puzzleId, submitDto)) - .rejects.toThrow(NotFoundException); - }); - - it('should handle already solved puzzle', async () => { - const puzzle = { - id: puzzleId, - title: 'Test Puzzle', - description: 'Test Description', - type: 'logic', - difficulty: 'easy', - solution: 'correct answer', - isPublished: true, - }; - - const submission = { - id: 1, - userId, - puzzleId, - attemptData: { solution: submitDto.solution }, - result: true, - submittedAt: new Date(), - }; - - const previousSuccess = { - id: 2, - userId, - puzzleId, - result: true, - }; - - mockPuzzleRepository.findOne.mockResolvedValue(puzzle); - mockSubmissionRepository.create.mockReturnValue(submission); - mockSubmissionRepository.save.mockResolvedValue(submission); - mockSubmissionRepository.findOne.mockResolvedValue(previousSuccess); - - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); - - expect(result.success).toBe(true); - expect(result.xpEarned).toBe(0); - expect(result.tokensEarned).toBe(0); - expect(result.message).toBe('Puzzle already solved!'); - }); - }); - - describe('getPuzzle', () => { - it('should return puzzle when found', async () => { - const puzzle = { - id: 101, - title: 'Test Puzzle', - description: 'Test Description', - type: 'logic', - difficulty: 'easy', - solution: 'correct answer', - isPublished: true, - }; - - mockPuzzleRepository.findOne.mockResolvedValue(puzzle); - - const result = await service.getPuzzle(101); - - expect(result).toEqual(puzzle); - }); - - it('should throw NotFoundException when puzzle not found', async () => { - mockPuzzleRepository.findOne.mockResolvedValue(null); - - await expect(service.getPuzzle(999)).rejects.toThrow(NotFoundException); - }); - }); - - describe('getPuzzles', () => { - it('should return filtered puzzles', async () => { - const puzzles = [ - { - id: 1, - title: 'Logic Puzzle 1', - type: 'logic', - difficulty: 'easy', - isPublished: true, - }, - { - id: 2, - title: 'Logic Puzzle 2', - type: 'logic', - difficulty: 'medium', - isPublished: true, - }, - ]; - - const mockQueryBuilder = { - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue(puzzles), - }; - - mockPuzzleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); - - const filters = { type: 'logic', difficulty: 'easy' }; - const result = await service.getPuzzles(filters); - - expect(result).toEqual(puzzles); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(2); - }); - }); - - describe('getUserProgress', () => { - it('should return user puzzle progress', async () => { - const progress = [ - { - id: 1, - userId: 1, - puzzleType: 'logic', - completedCount: 5, - total: 10, - }, - { - id: 2, - userId: 1, - puzzleType: 'coding', - completedCount: 3, - total: 8, - }, - ]; - - mockProgressRepository.find.mockResolvedValue(progress); - - const result = await service.getUserProgress(1); - - expect(result).toEqual(progress); - expect(mockProgressRepository.find).toHaveBeenCalledWith({ - where: { userId: 1 }, - }); - }); - }); -}); \ No newline at end of file diff --git a/src/puzzle/puzzle.service.ts b/src/puzzle/puzzle.service.ts index 63a12fd..252d9ea 100644 --- a/src/puzzle/puzzle.service.ts +++ b/src/puzzle/puzzle.service.ts @@ -18,42 +18,59 @@ export class PuzzleService { constructor( @InjectRepository(Puzzle) private readonly puzzleRepository: Repository, + @InjectRepository(PuzzleSubmission) private readonly submissionRepository: Repository, + @InjectRepository(PuzzleProgress) private readonly progressRepository: Repository, + @InjectRepository(User) private readonly userRepository: Repository, + private readonly eventEmitter: EventEmitter2, - private readonly puzzleProgressProvider: PuzzleProgressProvider, - ) {} async submitPuzzleSolution( userId: string, puzzleId: number, submitDto: SubmitPuzzleDto, - ): Promise<{ success: boolean; xpEarned?: number; tokensEarned?: number; message?: string }> { + ): Promise<{ + success: boolean; + xpEarned?: number; + tokensEarned?: number; + message?: string; + }> { try { // 1. Get the puzzle and verify it exists const puzzle = await this.puzzleRepository.findOne({ where: { id: puzzleId }, }); - + + // 1. Get the user and verify it exists + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + if (!puzzle) { throw new NotFoundException('Puzzle not found'); } + if (!user) { + throw new NotFoundException('User not found'); + } + // 2. Verify the solution const isCorrect = this.verifySolution(puzzle, submitDto.solution); - + // 3. Record the submission const submission = this.submissionRepository.create({ - userId, - puzzleId, - attemptData: { solution: submitDto.solution }, - result: isCorrect, - submittedAt: new Date(), + user, + puzzle, + solution: submitDto.solution, + isCorrect, + createdAt: new Date(), + skipped: false, }); await this.submissionRepository.save(submission); @@ -64,27 +81,31 @@ export class PuzzleService { isCorrect, timestamp: new Date(), }; - + this.eventEmitter.emit('puzzle.submitted', puzzleSubmissionEvent); if (!isCorrect) { - return { - success: false, - message: 'Incorrect solution. Try again!' + return { + success: false, + message: 'Incorrect solution. Try again!', }; } // 5. Check for previous successful submissions (idempotency) const previousSuccess = await this.submissionRepository.findOne({ - where: { userId, puzzleId, result: true }, + where: { + user: { id: userId }, + puzzle: { id: puzzleId }, + isCorrect: true, + }, }); - + if (previousSuccess && previousSuccess.id !== submission.id) { - return { - success: true, - xpEarned: 0, + return { + success: true, + xpEarned: 0, tokensEarned: 0, - message: 'Puzzle already solved!' + message: 'Puzzle already solved!', }; } @@ -92,19 +113,24 @@ export class PuzzleService { await this.updatePuzzleProgress(userId, puzzle.type); // 7. Award XP and tokens - const { xpEarned, tokensEarned } = this.calculateRewards(puzzle.difficulty); + const { xpEarned, tokensEarned } = this.calculateRewards( + puzzle.difficulty, + ); await this.updateUserStats(userId, xpEarned, tokensEarned); this.logger.log(`User ${userId} successfully solved puzzle ${puzzleId}`); - return { - success: true, - xpEarned, + return { + success: true, + xpEarned, tokensEarned, - message: 'Puzzle solved successfully!' + message: 'Puzzle solved successfully!', }; } catch (error) { - this.logger.error(`Error submitting puzzle solution: ${error.message}`, error.stack); + this.logger.error( + `Error submitting puzzle solution: ${error.message}`, + error.stack, + ); throw error; } } @@ -112,7 +138,10 @@ export class PuzzleService { private verifySolution(puzzle: Puzzle, submittedSolution: string): boolean { // Simple string comparison for now // In a real implementation, this could involve more complex verification logic - return puzzle.solution.toLowerCase().trim() === submittedSolution.toLowerCase().trim(); + return ( + puzzle.solution.toLowerCase().trim() === + submittedSolution.toLowerCase().trim() + ); } private async updatePuzzleProgress( @@ -135,7 +164,10 @@ export class PuzzleService { await this.progressRepository.save(progress); } - private calculateRewards(difficulty: string): { xpEarned: number; tokensEarned: number } { + private calculateRewards(difficulty: string): { + xpEarned: number; + tokensEarned: number; + } { const rewards = { easy: { xp: 100, tokens: 10 }, medium: { xp: 250, tokens: 25 }, @@ -184,11 +216,15 @@ export class PuzzleService { } if (filters?.difficulty) { - queryBuilder.andWhere('puzzle.difficulty = :difficulty', { difficulty: filters.difficulty }); + queryBuilder.andWhere('puzzle.difficulty = :difficulty', { + difficulty: filters.difficulty, + }); } if (filters?.isPublished !== undefined) { - queryBuilder.andWhere('puzzle.isPublished = :isPublished', { isPublished: filters.isPublished }); + queryBuilder.andWhere('puzzle.isPublished = :isPublished', { + isPublished: filters.isPublished, + }); } return queryBuilder.getMany(); @@ -199,6 +235,4 @@ export class PuzzleService { where: { userId }, }); } - - } From ad41d3c6e7b3df7cbe831c2cd0e74e12cabba46e Mon Sep 17 00:00:00 2001 From: phertyameen Date: Sun, 13 Jul 2025 07:46:45 +0100 Subject: [PATCH 2/3] cleanup --- .../analytics.controller.breakdown.spec.ts | 284 ----------- .../providers/daily-streak.service.spec.ts | 451 +++++++----------- src/puzzle/puzzle.service.spec.ts | 238 +++++++++ 3 files changed, 399 insertions(+), 574 deletions(-) delete mode 100644 src/analytics/analytics.controller.breakdown.spec.ts create mode 100644 src/puzzle/puzzle.service.spec.ts diff --git a/src/analytics/analytics.controller.breakdown.spec.ts b/src/analytics/analytics.controller.breakdown.spec.ts deleted file mode 100644 index 1df789e..0000000 --- a/src/analytics/analytics.controller.breakdown.spec.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AnalyticsController } from './analytics.controller'; -import { AnalyticsService } from './providers/analytics.service'; -import { AnalyticsExportService } from './providers/analytics-export.service'; -import { AnalyticsBreakdownService } from './providers/analytics-breakdown.service'; -import { GetAnalyticsQueryDto } from './dto/get-analytics-query.dto'; -import { AnalyticsBreakdownResponse } from './dto/analytics-breakdown-response.dto'; -import { EventTypeBreakdown } from './dto/analytics-breakdown-response.dto'; - -describe('AnalyticsController - Breakdown Endpoints', () => { - let controller: AnalyticsController; - let analyticsService: jest.Mocked; - let analyticsExportService: jest.Mocked; - let analyticsBreakdownService: jest.Mocked; - - const mockBreakdownResponse: AnalyticsBreakdownResponse = { - breakdown: [ - { - eventType: 'question_view', - count: 124, - displayName: 'Question Viewed', - percentage: 58.8, - }, - { - eventType: 'answer_submit', - count: 87, - displayName: 'Answer Submitted', - percentage: 41.2, - }, - ], - totalEvents: 211, - uniqueEventTypes: 2, - dateRange: '2024-01-01 to 2024-01-31', - }; - - const mockTopEventTypes: EventTypeBreakdown[] = [ - { - eventType: 'question_view', - count: 124, - displayName: 'Question Viewed', - percentage: 58.8, - }, - { - eventType: 'answer_submit', - count: 87, - displayName: 'Answer Submitted', - percentage: 41.2, - }, - ]; - - const mockEventTypes = ['question_view', 'answer_submit', 'puzzle_solved']; - - beforeEach(async () => { - const mockAnalyticsService = { - getAnalytics: jest.fn(), - findAll: jest.fn(), - }; - - const mockAnalyticsExportService = { - exportAnalytics: jest.fn(), - generateFilename: jest.fn(), - }; - - const mockAnalyticsBreakdownService = { - getBreakdown: jest.fn(), - getTopEventTypes: jest.fn(), - getAvailableEventTypes: jest.fn(), - getBreakdownForEventTypes: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - controllers: [AnalyticsController], - providers: [ - { - provide: AnalyticsService, - useValue: mockAnalyticsService, - }, - { - provide: AnalyticsExportService, - useValue: mockAnalyticsExportService, - }, - { - provide: AnalyticsBreakdownService, - useValue: mockAnalyticsBreakdownService, - }, - ], - }).compile(); - - controller = module.get(AnalyticsController); - analyticsService = module.get(AnalyticsService); - analyticsExportService = module.get(AnalyticsExportService); - analyticsBreakdownService = module.get(AnalyticsBreakdownService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getBreakdown', () => { - it('should return analytics breakdown', async () => { - const query: GetAnalyticsQueryDto = { - timeFilter: 'weekly', - userId: '123e4567-e89b-12d3-a456-426614174000', - }; - - analyticsBreakdownService.getBreakdown.mockResolvedValue(mockBreakdownResponse); - - const result = await controller.getBreakdown(query); - - expect(analyticsBreakdownService.getBreakdown).toHaveBeenCalledWith(query); - expect(result).toEqual(mockBreakdownResponse); - }); - - it('should handle empty query parameters', async () => { - const query: GetAnalyticsQueryDto = {}; - - analyticsBreakdownService.getBreakdown.mockResolvedValue(mockBreakdownResponse); - - const result = await controller.getBreakdown(query); - - expect(analyticsBreakdownService.getBreakdown).toHaveBeenCalledWith(query); - expect(result).toEqual(mockBreakdownResponse); - }); - - it('should handle service errors', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - const error = new Error('Service error'); - - analyticsBreakdownService.getBreakdown.mockRejectedValue(error); - - await expect(controller.getBreakdown(query)).rejects.toThrow('Service error'); - }); - }); - - describe('getTopEventTypes', () => { - it('should return top event types with default limit', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - - analyticsBreakdownService.getTopEventTypes.mockResolvedValue(mockTopEventTypes); - - const result = await controller.getTopEventTypes(undefined, query); - - expect(analyticsBreakdownService.getTopEventTypes).toHaveBeenCalledWith(10, query); - expect(result).toEqual(mockTopEventTypes); - }); - - it('should return top event types with custom limit', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - const limit = 5; - - analyticsBreakdownService.getTopEventTypes.mockResolvedValue(mockTopEventTypes); - - const result = await controller.getTopEventTypes(limit, query); - - expect(analyticsBreakdownService.getTopEventTypes).toHaveBeenCalledWith(5, query); - expect(result).toEqual(mockTopEventTypes); - }); - - it('should clamp limit to minimum value', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - const limit = 0; - - analyticsBreakdownService.getTopEventTypes.mockResolvedValue(mockTopEventTypes); - - const result = await controller.getTopEventTypes(limit, query); - - expect(analyticsBreakdownService.getTopEventTypes).toHaveBeenCalledWith(1, query); - expect(result).toEqual(mockTopEventTypes); - }); - - it('should clamp limit to maximum value', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - const limit = 100; - - analyticsBreakdownService.getTopEventTypes.mockResolvedValue(mockTopEventTypes); - - const result = await controller.getTopEventTypes(limit, query); - - expect(analyticsBreakdownService.getTopEventTypes).toHaveBeenCalledWith(50, query); - expect(result).toEqual(mockTopEventTypes); - }); - - it('should handle service errors', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - const error = new Error('Service error'); - - analyticsBreakdownService.getTopEventTypes.mockRejectedValue(error); - - await expect(controller.getTopEventTypes(10, query)).rejects.toThrow('Service error'); - }); - }); - - describe('getAvailableEventTypes', () => { - it('should return available event types', async () => { - analyticsBreakdownService.getAvailableEventTypes.mockResolvedValue(mockEventTypes); - - const result = await controller.getAvailableEventTypes(); - - expect(analyticsBreakdownService.getAvailableEventTypes).toHaveBeenCalled(); - expect(result).toEqual(mockEventTypes); - }); - - it('should handle empty event types', async () => { - analyticsBreakdownService.getAvailableEventTypes.mockResolvedValue([]); - - const result = await controller.getAvailableEventTypes(); - - expect(result).toEqual([]); - }); - - it('should handle service errors', async () => { - const error = new Error('Service error'); - - analyticsBreakdownService.getAvailableEventTypes.mockRejectedValue(error); - - await expect(controller.getAvailableEventTypes()).rejects.toThrow('Service error'); - }); - }); - - describe('query parameter validation', () => { - it('should handle all query parameters correctly', async () => { - const query: GetAnalyticsQueryDto = { - timeFilter: 'monthly', - from: '2024-01-01T00:00:00Z', - to: '2024-01-31T23:59:59Z', - userId: '123e4567-e89b-12d3-a456-426614174000', - sessionId: '456e7890-e89b-12d3-a456-426614174000', - }; - - analyticsBreakdownService.getBreakdown.mockResolvedValue(mockBreakdownResponse); - - const result = await controller.getBreakdown(query); - - expect(analyticsBreakdownService.getBreakdown).toHaveBeenCalledWith(query); - expect(result).toEqual(mockBreakdownResponse); - }); - - it('should handle partial query parameters', async () => { - const query: GetAnalyticsQueryDto = { - timeFilter: 'weekly', - userId: '123e4567-e89b-12d3-a456-426614174000', - }; - - analyticsBreakdownService.getBreakdown.mockResolvedValue(mockBreakdownResponse); - - const result = await controller.getBreakdown(query); - - expect(analyticsBreakdownService.getBreakdown).toHaveBeenCalledWith(query); - expect(result).toEqual(mockBreakdownResponse); - }); - }); - - describe('response structure validation', () => { - it('should return properly structured breakdown response', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - - analyticsBreakdownService.getBreakdown.mockResolvedValue(mockBreakdownResponse); - - const result = await controller.getBreakdown(query); - - expect(result).toHaveProperty('breakdown'); - expect(result).toHaveProperty('totalEvents'); - expect(result).toHaveProperty('uniqueEventTypes'); - expect(result).toHaveProperty('dateRange'); - expect(Array.isArray(result.breakdown)).toBe(true); - }); - - it('should return properly structured top event types', async () => { - const query: GetAnalyticsQueryDto = { timeFilter: 'weekly' }; - - analyticsBreakdownService.getTopEventTypes.mockResolvedValue(mockTopEventTypes); - - const result = await controller.getTopEventTypes(10, query); - - expect(Array.isArray(result)).toBe(true); - result.forEach(item => { - expect(item).toHaveProperty('eventType'); - expect(item).toHaveProperty('count'); - expect(item).toHaveProperty('displayName'); - expect(item).toHaveProperty('percentage'); - }); - }); - }); -}); \ No newline at end of file diff --git a/src/gamification/providers/daily-streak.service.spec.ts b/src/gamification/providers/daily-streak.service.spec.ts index ad498b6..98f11f1 100644 --- a/src/gamification/providers/daily-streak.service.spec.ts +++ b/src/gamification/providers/daily-streak.service.spec.ts @@ -1,37 +1,41 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { DailyStreakService } from './daily-streak.service'; -import { DailyStreak } from '../entities/daily-streak.entity'; -import { User } from '../../users/user.entity'; -import { STREAK_MILESTONES } from '../constants/streak.constants'; - -describe('DailyStreakService', () => { - let service: DailyStreakService; - let streakRepository: Repository; - let userRepository: Repository; - let eventEmitter: EventEmitter2; - - const mockStreakRepository = { +import { NotFoundException } from '@nestjs/common'; +import { PuzzleService } from 'src/puzzle/puzzle.service'; +import { Puzzle } from 'src/puzzle/entities/puzzle.entity'; +import { PuzzleSubmission } from 'src/puzzle/entities/puzzle-submission.entity'; +import { PuzzleProgress } from 'src/puzzle/entities/puzzle-progress.entity'; +import { User } from 'src/users/user.entity'; +import { SubmitPuzzleDto } from 'src/puzzle/dto/puzzle.dto'; + +describe('PuzzleService', () => { + let service: PuzzleService; + + const mockPuzzleRepository = { findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), createQueryBuilder: jest.fn(() => ({ - leftJoinAndSelect: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - addOrderBy: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest.fn(), - select: jest.fn().mockReturnThis(), - getRawOne: jest.fn(), + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn(), })), }; + const mockSubmissionRepository = { + create: jest.fn(), + save: jest.fn(), + findOne: jest.fn(), + }; + + const mockProgressRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + }; + const mockUserRepository = { - count: jest.fn(), + findOne: jest.fn(), + save: jest.fn(), }; const mockEventEmitter = { @@ -41,325 +45,192 @@ describe('DailyStreakService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - DailyStreakService, - { - provide: getRepositoryToken(DailyStreak), - useValue: mockStreakRepository, - }, - { - provide: getRepositoryToken(User), - useValue: mockUserRepository, - }, - { - provide: EventEmitter2, - useValue: mockEventEmitter, - }, + PuzzleService, + { provide: getRepositoryToken(Puzzle), useValue: mockPuzzleRepository }, + { provide: getRepositoryToken(PuzzleSubmission), useValue: mockSubmissionRepository }, + { provide: getRepositoryToken(PuzzleProgress), useValue: mockProgressRepository }, + { provide: getRepositoryToken(User), useValue: mockUserRepository }, + { provide: EventEmitter2, useValue: mockEventEmitter }, ], }).compile(); - service = module.get(DailyStreakService); - streakRepository = module.get>(getRepositoryToken(DailyStreak)); - userRepository = module.get>(getRepositoryToken(User)); - eventEmitter = module.get(EventEmitter2); + service = module.get(PuzzleService); }); afterEach(() => { jest.clearAllMocks(); }); - describe('updateStreak', () => { - it('should create new streak for first-time user', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - - mockStreakRepository.findOne.mockResolvedValue(null); - mockStreakRepository.create.mockReturnValue({ - userId, - lastActiveDate: today, - streakCount: 1, - longestStreak: 1, - lastMilestoneReached: null, - }); - mockStreakRepository.save.mockResolvedValue({ + describe('submitPuzzleSolution', () => { + const userId = 'user-1'; + const puzzleId = 123; + const submitDto: SubmitPuzzleDto = { puzzleId, userId, solution: 'answer' }; + + const puzzle = { + id: puzzleId, + type: 'logic', + difficulty: 'easy', + solution: 'answer', + }; + + const user = { + id: userId, + xp: 100, + level: 1, + }; + + it('should successfully submit a correct puzzle solution', async () => { + const submission = { id: 1, - userId, - lastActiveDate: today, - streakCount: 1, - longestStreak: 1, - lastMilestoneReached: null, - }); - - const result = await service.updateStreak(userId); + user, + puzzle, + solution: submitDto.solution, + isCorrect: true, + createdAt: new Date(), + skipped: false, + }; - expect(mockStreakRepository.findOne).toHaveBeenCalledWith({ - where: { userId }, - relations: ['user'], - }); - expect(mockStreakRepository.create).toHaveBeenCalledWith({ - userId, - lastActiveDate: today, - streakCount: 1, - longestStreak: 1, - lastMilestoneReached: null, - }); - expect(mockStreakRepository.save).toHaveBeenCalled(); - expect(mockEventEmitter.emit).toHaveBeenCalledWith('streak.puzzle.solved', { + mockPuzzleRepository.findOne.mockResolvedValueOnce(puzzle); // puzzle + mockUserRepository.findOne.mockResolvedValueOnce(user); // user + mockSubmissionRepository.create.mockReturnValue(submission); + mockSubmissionRepository.save.mockResolvedValue(submission); + mockSubmissionRepository.findOne.mockResolvedValueOnce(null); // no previous correct submission + mockProgressRepository.findOne.mockResolvedValue(null); + mockProgressRepository.create.mockReturnValue({ userId, - streakCount: 1, - isNewStreak: true, + puzzleType: puzzle.type, + completedCount: 0, }); - expect(result.streakCount).toBe(1); - expect(result.hasSolvedToday).toBe(true); - }); + mockProgressRepository.save.mockResolvedValue({}); + mockUserRepository.save.mockResolvedValue({ ...user, xp: 200, level: 1 }); - it('should increment streak for consecutive day', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); - const existingStreak = { - id: 1, + expect(result.success).toBe(true); + expect(result.xpEarned).toBe(100); + expect(result.tokensEarned).toBe(10); + expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', expect.objectContaining({ userId, - lastActiveDate: yesterday, - streakCount: 3, - longestStreak: 3, - lastMilestoneReached: null, - }; - - mockStreakRepository.findOne.mockResolvedValue(existingStreak); - mockStreakRepository.save.mockResolvedValue({ - ...existingStreak, - lastActiveDate: today, - streakCount: 4, - longestStreak: 4, - }); - - const result = await service.updateStreak(userId); - - expect(mockStreakRepository.save).toHaveBeenCalledWith({ - ...existingStreak, - lastActiveDate: today, - streakCount: 4, - longestStreak: 4, - }); - expect(result.streakCount).toBe(4); - expect(result.hasSolvedToday).toBe(true); + puzzleId, + isCorrect: true, + })); }); - it('should reset streak when day is skipped', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - const twoDaysAgo = new Date(today); - twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); - - const existingStreak = { - id: 1, - userId, - lastActiveDate: twoDaysAgo, - streakCount: 5, - longestStreak: 5, - lastMilestoneReached: null, - }; - - mockStreakRepository.findOne.mockResolvedValue(existingStreak); - mockStreakRepository.save.mockResolvedValue({ - ...existingStreak, - lastActiveDate: today, - streakCount: 1, - longestStreak: 5, // Should remain the same + it('should handle incorrect puzzle solution', async () => { + const incorrectDto = { solution: 'wrong' }; + + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(user); + mockSubmissionRepository.create.mockReturnValue({ + id: 2, + user, + puzzle, + solution: incorrectDto.solution, + isCorrect: false, + createdAt: new Date(), + skipped: false, }); + mockSubmissionRepository.save.mockResolvedValue({}); + mockSubmissionRepository.findOne.mockResolvedValue(null); - const result = await service.updateStreak(userId); + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); - expect(mockStreakRepository.save).toHaveBeenCalledWith({ - ...existingStreak, - lastActiveDate: today, - streakCount: 1, - longestStreak: 5, - }); - expect(result.streakCount).toBe(1); - expect(result.hasSolvedToday).toBe(true); + expect(result.success).toBe(false); + expect(result.message).toBe('Incorrect solution. Try again!'); }); - it('should not update streak if already solved today', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const existingStreak = { - id: 1, - userId, - lastActiveDate: today, - streakCount: 3, - longestStreak: 3, - lastMilestoneReached: null, - }; + it('should throw NotFoundException if puzzle not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(null); - mockStreakRepository.findOne.mockResolvedValue(existingStreak); + await expect( + service.submitPuzzleSolution(userId, puzzleId, submitDto), + ).rejects.toThrow(NotFoundException); + }); - const result = await service.updateStreak(userId); + it('should throw NotFoundException if user not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(null); - expect(mockStreakRepository.save).not.toHaveBeenCalled(); - expect(mockEventEmitter.emit).not.toHaveBeenCalled(); - expect(result.streakCount).toBe(3); - expect(result.hasSolvedToday).toBe(true); + await expect( + service.submitPuzzleSolution(userId, puzzleId, submitDto), + ).rejects.toThrow(NotFoundException); }); - it('should award milestone rewards when reaching milestones', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); + it('should return already solved response if duplicate correct submission', async () => { + const newSubmission = { + id: 99, + user, + puzzle, + solution: submitDto.solution, + isCorrect: true, + createdAt: new Date(), + skipped: false, + }; - const existingStreak = { + const existingSuccess = { id: 1, - userId, - lastActiveDate: yesterday, - streakCount: 2, - longestStreak: 2, - lastMilestoneReached: null, + user, + puzzle, + isCorrect: true, }; - mockStreakRepository.findOne.mockResolvedValue(existingStreak); - mockStreakRepository.save.mockResolvedValue({ - ...existingStreak, - lastActiveDate: today, - streakCount: 3, - longestStreak: 3, - lastMilestoneReached: 3, - }); + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(user); + mockSubmissionRepository.create.mockReturnValue(newSubmission); + mockSubmissionRepository.save.mockResolvedValue(newSubmission); + mockSubmissionRepository.findOne.mockResolvedValue(existingSuccess); - const result = await service.updateStreak(userId); + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); - expect(mockEventEmitter.emit).toHaveBeenCalledWith('streak.milestone.reached', { - userId, - milestone: 3, - reward: { - userId, - bonusXp: STREAK_MILESTONES[3].xp, - bonusTokens: STREAK_MILESTONES[3].tokens, - reason: STREAK_MILESTONES[3].description, - }, - }); - expect(result.streakCount).toBe(3); + expect(result.success).toBe(true); + expect(result.xpEarned).toBe(0); + expect(result.tokensEarned).toBe(0); + expect(result.message).toBe('Puzzle already solved!'); }); }); - describe('getStreak', () => { - it('should return streak data for existing user', async () => { - const userId = 1; - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const existingStreak = { - id: 1, - userId, - lastActiveDate: today, - streakCount: 5, - longestStreak: 10, - lastMilestoneReached: 3, - }; - - mockStreakRepository.findOne.mockResolvedValue(existingStreak); + describe('getPuzzle', () => { + it('should return the puzzle if found', async () => { + const puzzle = { id: 1, title: 'Test' }; + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); - const result = await service.getStreak(userId); + const result = await service.getPuzzle(1); - expect(result.streakCount).toBe(5); - expect(result.longestStreak).toBe(10); - expect(result.hasSolvedToday).toBe(true); - expect(result.nextMilestone).toBe(7); - expect(result.daysUntilNextMilestone).toBe(2); + expect(result).toEqual(puzzle); }); - it('should return default data for new user', async () => { - const userId = 1; - - mockStreakRepository.findOne.mockResolvedValue(null); - - const result = await service.getStreak(userId); + it('should throw if puzzle not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(null); - expect(result.streakCount).toBe(0); - expect(result.longestStreak).toBe(0); - expect(result.lastActiveDate).toBeNull(); - expect(result.hasSolvedToday).toBe(false); - expect(result.nextMilestone).toBe(3); - expect(result.daysUntilNextMilestone).toBe(3); + await expect(service.getPuzzle(1)).rejects.toThrow(NotFoundException); }); }); - describe('getStreakLeaderboard', () => { - it('should return leaderboard with pagination', async () => { - const query = { page: 1, limit: 10 }; - const mockEntries = [ - { - userId: 1, - streakCount: 10, - longestStreak: 15, - lastActiveDate: new Date(), - user: { username: 'user1' }, - }, - { - userId: 2, - streakCount: 8, - longestStreak: 12, - lastActiveDate: new Date(), - user: { username: 'user2' }, - }, - ]; - + describe('getPuzzles', () => { + it('should apply filters and return puzzles', async () => { + const puzzles = [{ id: 1 }, { id: 2 }]; const mockQueryBuilder = { - leftJoinAndSelect: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - addOrderBy: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest.fn().mockResolvedValue([mockEntries, 2]), + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(puzzles), }; - mockStreakRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + mockPuzzleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); - const result = await service.getStreakLeaderboard(query); + const result = await service.getPuzzles({ type: 'logic', difficulty: 'easy' }); - expect(result.entries).toHaveLength(2); - expect(result.total).toBe(2); - expect(result.page).toBe(1); - expect(result.limit).toBe(10); - expect(result.entries[0].userId).toBe(1); - expect(result.entries[0].username).toBe('user1'); + expect(result).toEqual(puzzles); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(2); }); }); - describe('getStreakStats', () => { - it('should return streak statistics', async () => { - mockUserRepository.count.mockResolvedValue(100); - mockStreakRepository.count.mockResolvedValue(50); - - const mockQueryBuilder = { - select: jest.fn().mockReturnThis(), - getRawOne: jest.fn().mockResolvedValue({ average: 5.5 }), - }; - - const mockMaxQueryBuilder = { - select: jest.fn().mockReturnThis(), - getRawOne: jest.fn().mockResolvedValue({ max: 30 }), - }; - - mockStreakRepository.createQueryBuilder - .mockReturnValueOnce(mockQueryBuilder) - .mockReturnValueOnce(mockMaxQueryBuilder); + describe('getUserProgress', () => { + it('should return user progress', async () => { + const progress = [{ id: 1, completedCount: 2 }]; + mockProgressRepository.find.mockResolvedValue(progress); - const result = await service.getStreakStats(); + const result = await service.getUserProgress('user-1'); - expect(result.totalUsers).toBe(100); - expect(result.activeUsers).toBe(50); - expect(result.averageStreak).toBe(6); - expect(result.topStreak).toBe(30); + expect(result).toEqual(progress); }); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/puzzle/puzzle.service.spec.ts b/src/puzzle/puzzle.service.spec.ts new file mode 100644 index 0000000..60193ed --- /dev/null +++ b/src/puzzle/puzzle.service.spec.ts @@ -0,0 +1,238 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { NotFoundException } from '@nestjs/common'; +import { PuzzleService } from './puzzle.service'; +import { Puzzle } from './entities/puzzle.entity'; +import { PuzzleSubmission } from './entities/puzzle-submission.entity'; +import { PuzzleProgress } from './entities/puzzle-progress.entity'; +import { User } from '../users/user.entity'; +import { SubmitPuzzleDto } from './dto/puzzle.dto'; + +describe('PuzzleService', () => { + let service: PuzzleService; + + const mockPuzzleRepository = { + findOne: jest.fn(), + createQueryBuilder: jest.fn(() => ({ + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn(), + })), + }; + + const mockSubmissionRepository = { + create: jest.fn(), + save: jest.fn(), + findOne: jest.fn(), + }; + + const mockProgressRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + }; + + const mockUserRepository = { + findOne: jest.fn(), + save: jest.fn(), + }; + + const mockEventEmitter = { + emit: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PuzzleService, + { provide: getRepositoryToken(Puzzle), useValue: mockPuzzleRepository }, + { provide: getRepositoryToken(PuzzleSubmission), useValue: mockSubmissionRepository }, + { provide: getRepositoryToken(PuzzleProgress), useValue: mockProgressRepository }, + { provide: getRepositoryToken(User), useValue: mockUserRepository }, + { provide: EventEmitter2, useValue: mockEventEmitter }, + ], + }).compile(); + + service = module.get(PuzzleService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('submitPuzzleSolution', () => { + const userId = 'user-1'; + const puzzleId = 123; + const submitDto: SubmitPuzzleDto = { userId, + puzzleId, solution: 'answer' }; + + const puzzle = { + id: puzzleId, + type: 'logic', + difficulty: 'easy', + solution: 'answer', + }; + + const user = { + id: userId, + xp: 100, + level: 1, + }; + + it('should successfully submit a correct puzzle solution', async () => { + const submission = { + id: 1, + user, + puzzle, + solution: submitDto.solution, + isCorrect: true, + createdAt: new Date(), + skipped: false, + }; + + mockPuzzleRepository.findOne.mockResolvedValueOnce(puzzle); // puzzle + mockUserRepository.findOne.mockResolvedValueOnce(user); // user + mockSubmissionRepository.create.mockReturnValue(submission); + mockSubmissionRepository.save.mockResolvedValue(submission); + mockSubmissionRepository.findOne.mockResolvedValueOnce(null); // no previous correct submission + mockProgressRepository.findOne.mockResolvedValue(null); + mockProgressRepository.create.mockReturnValue({ + userId, + puzzleType: puzzle.type, + completedCount: 0, + }); + mockProgressRepository.save.mockResolvedValue({}); + mockUserRepository.save.mockResolvedValue({ ...user, xp: 200, level: 1 }); + + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + + expect(result.success).toBe(true); + expect(result.xpEarned).toBe(100); + expect(result.tokensEarned).toBe(10); + expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', expect.objectContaining({ + userId, + puzzleId, + isCorrect: true, + })); + }); + + it('should handle incorrect puzzle solution', async () => { + const incorrectDto = { solution: 'wrong' }; + + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(user); + mockSubmissionRepository.create.mockReturnValue({ + id: 2, + user, + puzzle, + solution: incorrectDto.solution, + isCorrect: false, + createdAt: new Date(), + skipped: false, + }); + mockSubmissionRepository.save.mockResolvedValue({}); + mockSubmissionRepository.findOne.mockResolvedValue(null); + + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + + expect(result.success).toBe(false); + expect(result.message).toBe('Incorrect solution. Try again!'); + }); + + it('should throw NotFoundException if puzzle not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(null); + + await expect( + service.submitPuzzleSolution(userId, puzzleId, submitDto), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw NotFoundException if user not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(null); + + await expect( + service.submitPuzzleSolution(userId, puzzleId, submitDto), + ).rejects.toThrow(NotFoundException); + }); + + it('should return already solved response if duplicate correct submission', async () => { + const newSubmission = { + id: 99, + user, + puzzle, + solution: submitDto.solution, + isCorrect: true, + createdAt: new Date(), + skipped: false, + }; + + const existingSuccess = { + id: 1, + user, + puzzle, + isCorrect: true, + }; + + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + mockUserRepository.findOne.mockResolvedValue(user); + mockSubmissionRepository.create.mockReturnValue(newSubmission); + mockSubmissionRepository.save.mockResolvedValue(newSubmission); + mockSubmissionRepository.findOne.mockResolvedValue(existingSuccess); + + const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + + expect(result.success).toBe(true); + expect(result.xpEarned).toBe(0); + expect(result.tokensEarned).toBe(0); + expect(result.message).toBe('Puzzle already solved!'); + }); + }); + + describe('getPuzzle', () => { + it('should return the puzzle if found', async () => { + const puzzle = { id: 1, title: 'Test' }; + mockPuzzleRepository.findOne.mockResolvedValue(puzzle); + + const result = await service.getPuzzle(1); + + expect(result).toEqual(puzzle); + }); + + it('should throw if puzzle not found', async () => { + mockPuzzleRepository.findOne.mockResolvedValue(null); + + await expect(service.getPuzzle(1)).rejects.toThrow(NotFoundException); + }); + }); + + describe('getPuzzles', () => { + it('should apply filters and return puzzles', async () => { + const puzzles = [{ id: 1 }, { id: 2 }]; + const mockQueryBuilder = { + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(puzzles), + }; + + mockPuzzleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.getPuzzles({ type: 'logic', difficulty: 'easy' }); + + expect(result).toEqual(puzzles); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(2); + }); + }); + + describe('getUserProgress', () => { + it('should return user progress', async () => { + const progress = [{ id: 1, completedCount: 2 }]; + mockProgressRepository.find.mockResolvedValue(progress); + + const result = await service.getUserProgress('user-1'); + + expect(result).toEqual(progress); + }); + }); +}); \ No newline at end of file From 553f4920010b21f9def649f82fc5e5f656fd0153 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Sun, 13 Jul 2025 08:17:55 +0100 Subject: [PATCH 3/3] cleanup --- package-lock.json | 1168 ++++++++++++++++- package.json | 1 + .../analytics-breakdown.integration.spec.ts | 9 + src/analytics/analytics.controller.spec.ts | 41 +- .../providers/daily-streak.service.spec.ts | 61 +- .../iq-assessment.service.spec.ts | 183 +-- src/puzzle/puzzle.service.spec.ts | 76 +- 7 files changed, 1382 insertions(+), 157 deletions(-) diff --git a/package-lock.json b/package-lock.json index d348cea..46c97bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "pg": "^8.14.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sqlite3": "^5.1.7", "starknet": "^6.24.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.21" @@ -949,6 +950,13 @@ "lodash.uniq": "^4.5.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2787,6 +2795,45 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@nuxt/opencollective": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", @@ -3234,6 +3281,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -4254,6 +4311,33 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4783,11 +4867,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4799,7 +4891,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -4937,7 +5028,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -5000,6 +5090,104 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -5220,6 +5408,16 @@ "validator": "^13.9.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5651,7 +5849,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -5667,7 +5864,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5691,6 +5887,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5932,6 +6137,38 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -5946,6 +6183,23 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6313,6 +6567,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -6661,6 +6924,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -7042,6 +7311,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -7308,6 +7583,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", @@ -7608,7 +7889,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -7627,6 +7908,21 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -7663,6 +7959,16 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", @@ -7746,12 +8052,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7769,6 +8092,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inspect-with-kind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", @@ -7779,6 +8108,27 @@ "kind-of": "^6.0.2" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7876,6 +8226,13 @@ "node": ">=8" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8760,6 +9117,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9190,8 +9554,79 @@ "devOptional": true, "license": "ISC" }, - "node_modules/makeerror": { - "version": "1.0.12", + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/makeerror": { + "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, @@ -9383,6 +9818,176 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -9423,6 +10028,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9500,6 +10111,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9523,6 +10140,18 @@ "dev": true, "license": "MIT" }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -9535,33 +10164,178 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "optional": true, "dependencies": { - "lodash": "^4.17.21" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/node-gyp/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, "dependencies": { - "whatwg-url": "^5.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=8" } }, "node_modules/node-int64": { @@ -9827,6 +10601,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -10320,6 +11110,32 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10393,6 +11209,27 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10439,6 +11276,16 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10566,6 +11413,30 @@ "node": ">=0.10.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -10772,6 +11643,16 @@ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", "license": "MIT" }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -11188,6 +12069,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11205,6 +12131,47 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -11294,6 +12261,69 @@ "node": ">=14" } }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11734,6 +12764,54 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", @@ -12276,6 +13354,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12653,6 +13743,26 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index b53ff4b..dbf1044 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "pg": "^8.14.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sqlite3": "^5.1.7", "starknet": "^6.24.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.21" diff --git a/src/analytics/analytics-breakdown.integration.spec.ts b/src/analytics/analytics-breakdown.integration.spec.ts index 6bb0d90..5ccfe77 100644 --- a/src/analytics/analytics-breakdown.integration.spec.ts +++ b/src/analytics/analytics-breakdown.integration.spec.ts @@ -7,6 +7,10 @@ import * as request from 'supertest'; import { AnalyticsModule } from './analytics.module'; import { AnalyticsEvent } from './entities/analytics-event.entity'; import { TimeFilterModule } from '../timefilter/timefilter.module'; +import { AnalyticsController } from './analytics.controller'; +import { AnalyticsService } from './providers/analytics.service'; +import { AnalyticsExportService } from './providers/analytics-export.service'; +import { AnalyticsBreakdownService } from './providers/analytics-breakdown.service'; describe('Analytics Breakdown Integration', () => { let app: INestApplication; @@ -34,6 +38,11 @@ describe('Analytics Breakdown Integration', () => { ); }); + const mockAnalyticsBreakdownService = { + getBreakdown: jest.fn(), +}; + + beforeEach(async () => { // Clear database before each test await analyticsRepository.clear(); diff --git a/src/analytics/analytics.controller.spec.ts b/src/analytics/analytics.controller.spec.ts index ba46de2..b73f77a 100644 --- a/src/analytics/analytics.controller.spec.ts +++ b/src/analytics/analytics.controller.spec.ts @@ -5,6 +5,7 @@ import { AnalyticsService } from './providers/analytics.service'; import { AnalyticsExportService } from './providers/analytics-export.service'; import { AnalyticsEvent } from './entities/analytics-event.entity'; import { ExportFormat } from './dto/export-analytics-query.dto'; +import { AnalyticsBreakdownService } from './providers/analytics-breakdown.service'; describe('AnalyticsController', () => { let controller: AnalyticsController; @@ -43,6 +44,12 @@ describe('AnalyticsController', () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AnalyticsController], providers: [ + { + provide: AnalyticsBreakdownService, + useValue: { + /* mock methods */ + }, + }, { provide: AnalyticsService, useValue: mockAnalyticsService, @@ -82,12 +89,12 @@ describe('AnalyticsController', () => { describe('exportAnalytics', () => { it('should export analytics data in CSV format', async () => { - const query = { + const query = { format: ExportFormat.CSV, userId: '123', - timeFilter: 'weekly' as any + timeFilter: 'weekly' as any, }; - + analyticsService.findAll.mockResolvedValue(mockAnalyticsData); analyticsExportService.exportAnalytics.mockResolvedValue(undefined); @@ -97,17 +104,17 @@ describe('AnalyticsController', () => { expect(analyticsExportService.exportAnalytics).toHaveBeenCalledWith( mockAnalyticsData, ExportFormat.CSV, - mockResponse + mockResponse, ); }); it('should export analytics data in PDF format', async () => { - const query = { + const query = { format: ExportFormat.PDF, from: '2024-01-01', - to: '2024-01-31' + to: '2024-01-31', }; - + analyticsService.findAll.mockResolvedValue(mockAnalyticsData); analyticsExportService.exportAnalytics.mockResolvedValue(undefined); @@ -117,13 +124,13 @@ describe('AnalyticsController', () => { expect(analyticsExportService.exportAnalytics).toHaveBeenCalledWith( mockAnalyticsData, ExportFormat.PDF, - mockResponse + mockResponse, ); }); it('should default to CSV format when no format specified', async () => { const query = { userId: '123' }; - + analyticsService.findAll.mockResolvedValue(mockAnalyticsData); analyticsExportService.exportAnalytics.mockResolvedValue(undefined); @@ -132,13 +139,13 @@ describe('AnalyticsController', () => { expect(analyticsExportService.exportAnalytics).toHaveBeenCalledWith( mockAnalyticsData, ExportFormat.CSV, - mockResponse + mockResponse, ); }); it('should handle empty analytics data', async () => { const query = { format: ExportFormat.CSV }; - + analyticsService.findAll.mockResolvedValue([]); analyticsExportService.exportAnalytics.mockResolvedValue(undefined); @@ -147,31 +154,31 @@ describe('AnalyticsController', () => { expect(analyticsExportService.exportAnalytics).toHaveBeenCalledWith( [], ExportFormat.CSV, - mockResponse + mockResponse, ); }); it('should handle service errors', async () => { const query = { format: ExportFormat.CSV }; const error = new Error('Service error'); - + analyticsService.findAll.mockRejectedValue(error); await expect( - controller.exportAnalytics(query, mockResponse as Response) + controller.exportAnalytics(query, mockResponse as Response), ).rejects.toThrow('Service error'); }); it('should handle export service errors', async () => { const query = { format: ExportFormat.CSV }; const error = new Error('Export error'); - + analyticsService.findAll.mockResolvedValue(mockAnalyticsData); analyticsExportService.exportAnalytics.mockRejectedValue(error); await expect( - controller.exportAnalytics(query, mockResponse as Response) + controller.exportAnalytics(query, mockResponse as Response), ).rejects.toThrow('Export error'); }); }); -}); \ No newline at end of file +}); diff --git a/src/gamification/providers/daily-streak.service.spec.ts b/src/gamification/providers/daily-streak.service.spec.ts index 98f11f1..18003a8 100644 --- a/src/gamification/providers/daily-streak.service.spec.ts +++ b/src/gamification/providers/daily-streak.service.spec.ts @@ -47,8 +47,14 @@ describe('PuzzleService', () => { providers: [ PuzzleService, { provide: getRepositoryToken(Puzzle), useValue: mockPuzzleRepository }, - { provide: getRepositoryToken(PuzzleSubmission), useValue: mockSubmissionRepository }, - { provide: getRepositoryToken(PuzzleProgress), useValue: mockProgressRepository }, + { + provide: getRepositoryToken(PuzzleSubmission), + useValue: mockSubmissionRepository, + }, + { + provide: getRepositoryToken(PuzzleProgress), + useValue: mockProgressRepository, + }, { provide: getRepositoryToken(User), useValue: mockUserRepository }, { provide: EventEmitter2, useValue: mockEventEmitter }, ], @@ -91,7 +97,7 @@ describe('PuzzleService', () => { }; mockPuzzleRepository.findOne.mockResolvedValueOnce(puzzle); // puzzle - mockUserRepository.findOne.mockResolvedValueOnce(user); // user + mockUserRepository.findOne.mockResolvedValueOnce(user); // user mockSubmissionRepository.create.mockReturnValue(submission); mockSubmissionRepository.save.mockResolvedValue(submission); mockSubmissionRepository.findOne.mockResolvedValueOnce(null); // no previous correct submission @@ -104,16 +110,23 @@ describe('PuzzleService', () => { mockProgressRepository.save.mockResolvedValue({}); mockUserRepository.save.mockResolvedValue({ ...user, xp: 200, level: 1 }); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(true); expect(result.xpEarned).toBe(100); expect(result.tokensEarned).toBe(10); - expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', expect.objectContaining({ - userId, - puzzleId, - isCorrect: true, - })); + expect(mockEventEmitter.emit).toHaveBeenCalledWith( + 'puzzle.submitted', + expect.objectContaining({ + userId, + puzzleId, + isCorrect: true, + }), + ); }); it('should handle incorrect puzzle solution', async () => { @@ -133,7 +146,11 @@ describe('PuzzleService', () => { mockSubmissionRepository.save.mockResolvedValue({}); mockSubmissionRepository.findOne.mockResolvedValue(null); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(false); expect(result.message).toBe('Incorrect solution. Try again!'); @@ -180,7 +197,11 @@ describe('PuzzleService', () => { mockSubmissionRepository.save.mockResolvedValue(newSubmission); mockSubmissionRepository.findOne.mockResolvedValue(existingSuccess); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(true); expect(result.xpEarned).toBe(0); @@ -212,11 +233,25 @@ describe('PuzzleService', () => { const mockQueryBuilder = { andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(puzzles), + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getRawMany: jest.fn(), + getRawOne: jest.fn(), + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + leftJoinAndSelect: jest.fn().mockReturnThis(), }; mockPuzzleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); - const result = await service.getPuzzles({ type: 'logic', difficulty: 'easy' }); + const result = await service.getPuzzles({ + type: 'logic', + difficulty: 'easy', + }); expect(result).toEqual(puzzles); expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(2); @@ -233,4 +268,4 @@ describe('PuzzleService', () => { expect(result).toEqual(progress); }); }); -}); \ No newline at end of file +}); diff --git a/src/iq-assessment/iq-assessment.service.spec.ts b/src/iq-assessment/iq-assessment.service.spec.ts index 7e39886..69347bf 100644 --- a/src/iq-assessment/iq-assessment.service.spec.ts +++ b/src/iq-assessment/iq-assessment.service.spec.ts @@ -1,22 +1,27 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { getRepositoryToken } from '@nestjs/typeorm' -import { HttpService } from '@nestjs/axios' -import { Repository } from 'typeorm' -import { IQAssessmentService } from './providers/iq-assessment.service' -import { IqAttemptService } from './providers/iq-attempt.service' -import { IQAssessmentSession } from './entities/iq-assessment-session.entity' -import { IQQuestion, QuestionDifficulty, QuestionCategory } from './entities/iq-question.entity' -import { IQAnswer } from './entities/iq-answer.entity' -import { User } from '../users/user.entity' -import { StandaloneSubmitAnswerDto } from './dto/submit-answer.dto' -import { RandomQuestionsQueryDto } from './dto/random-questions-query.dto' -import { of } from 'rxjs' +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { HttpService } from '@nestjs/axios'; +import { Repository } from 'typeorm'; +import { IQAssessmentService } from './providers/iq-assessment.service'; +import { IqAttemptService } from './providers/iq-attempt.service'; +import { IQAssessmentSession } from './entities/iq-assessment-session.entity'; +import { + IQQuestion, + QuestionDifficulty, + QuestionCategory, +} from './entities/iq-question.entity'; +import { IQAnswer } from './entities/iq-answer.entity'; +import { User } from '../users/user.entity'; +import { StandaloneSubmitAnswerDto } from './dto/submit-answer.dto'; +import { RandomQuestionsQueryDto } from './dto/random-questions-query.dto'; +import { of } from 'rxjs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; describe('IQAssessmentService', () => { - let service: IQAssessmentService - let questionRepository: Repository - let iqAttemptService: IqAttemptService - let httpService: HttpService + let service: IQAssessmentService; + let questionRepository: Repository; + let iqAttemptService: IqAttemptService; + let httpService: HttpService; const mockQuestion: IQQuestion = { id: 'test-question-id', @@ -28,7 +33,7 @@ describe('IQAssessmentService', () => { category: QuestionCategory.MATHEMATICS, answers: [], attempts: [], - } + }; const mockQuestionRepository = { findOne: jest.fn(), @@ -40,15 +45,15 @@ describe('IQAssessmentService', () => { })), create: jest.fn(), save: jest.fn(), - } + }; const mockIqAttemptService = { create: jest.fn(), - } + }; const mockHttpService = { get: jest.fn(), - } + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -78,92 +83,101 @@ describe('IQAssessmentService', () => { provide: IqAttemptService, useValue: mockIqAttemptService, }, + { + provide: EventEmitter2, + useValue: { emit: jest.fn() }, + }, ], - }).compile() + }).compile(); - service = module.get(IQAssessmentService) - questionRepository = module.get>(getRepositoryToken(IQQuestion)) - iqAttemptService = module.get(IqAttemptService) - httpService = module.get(HttpService) + service = module.get(IQAssessmentService); + questionRepository = module.get>( + getRepositoryToken(IQQuestion), + ); + iqAttemptService = module.get(IqAttemptService); + httpService = module.get(HttpService); mockHttpService.get.mockImplementation(() => ({ - pipe: () => of({ - response_code: 0, - results: [ - { - question: 'External question 1', - correct_answer: 'A', - incorrect_answers: ['B', 'C', 'D'], - }, - ] - }) + pipe: () => + of({ + response_code: 0, + results: [ + { + question: 'External question 1', + correct_answer: 'A', + incorrect_answers: ['B', 'C', 'D'], + }, + ], + }), })); - }) + }); afterEach(() => { - jest.clearAllMocks() - }) + jest.clearAllMocks(); + }); describe('submitStandaloneAnswer', () => { it('should submit a correct answer successfully', async () => { const submitDto: StandaloneSubmitAnswerDto = { questionId: 'test-question-id', selectedAnswer: '4', - } + }; - mockQuestionRepository.findOne.mockResolvedValue(mockQuestion) - mockIqAttemptService.create.mockResolvedValue({}) + mockQuestionRepository.findOne.mockResolvedValue(mockQuestion); + mockIqAttemptService.create.mockResolvedValue({}); - const result = await service.submitStandaloneAnswer(submitDto) + const result = await service.submitStandaloneAnswer(submitDto); - expect(result.isCorrect).toBe(true) - expect(result.correctAnswer).toBe('4') - expect(result.selectedAnswer).toBe('4') - expect(result.questionId).toBe('test-question-id') - expect(result.explanation).toBe('Basic arithmetic') + expect(result.isCorrect).toBe(true); + expect(result.correctAnswer).toBe('4'); + expect(result.selectedAnswer).toBe('4'); + expect(result.questionId).toBe('test-question-id'); + expect(result.explanation).toBe('Basic arithmetic'); expect(mockIqAttemptService.create).toHaveBeenCalledWith({ userId: undefined, questionId: 'test-question-id', selectedAnswer: '4', correctAnswer: '4', isCorrect: true, - }) - }) + }); + }); it('should submit an incorrect answer successfully', async () => { const submitDto: StandaloneSubmitAnswerDto = { questionId: 'test-question-id', selectedAnswer: '5', - } + }; - mockQuestionRepository.findOne.mockResolvedValue(mockQuestion) - mockIqAttemptService.create.mockResolvedValue({}) + mockQuestionRepository.findOne.mockResolvedValue(mockQuestion); + mockIqAttemptService.create.mockResolvedValue({}); - const result = await service.submitStandaloneAnswer(submitDto) + const result = await service.submitStandaloneAnswer(submitDto); - expect(result.isCorrect).toBe(false) - expect(result.correctAnswer).toBe('4') - expect(result.selectedAnswer).toBe('5') + expect(result.isCorrect).toBe(false); + expect(result.correctAnswer).toBe('4'); + expect(result.selectedAnswer).toBe('5'); expect(mockIqAttemptService.create).toHaveBeenCalledWith({ userId: undefined, questionId: 'test-question-id', selectedAnswer: '5', correctAnswer: '4', isCorrect: false, - }) - }) + }); + }); it('should throw NotFoundException when question not found', async () => { const submitDto: StandaloneSubmitAnswerDto = { questionId: 'non-existent-id', selectedAnswer: '4', - } + }; - mockQuestionRepository.findOne.mockResolvedValue(null) + mockQuestionRepository.findOne.mockResolvedValue(null); - await expect(service.submitStandaloneAnswer(submitDto)).rejects.toThrow('Question not found') - }) - }) + await expect(service.submitStandaloneAnswer(submitDto)).rejects.toThrow( + 'Question not found', + ); + }); + }); describe('getRandomQuestionsWithFilters', () => { it('should return filtered questions from database', async () => { @@ -171,45 +185,48 @@ describe('IQAssessmentService', () => { difficulty: QuestionDifficulty.EASY, category: QuestionCategory.MATHEMATICS, count: 2, - } + }; // Return exactly 'count' questions from DB for every call - const mockQuestions = [mockQuestion, { ...mockQuestion, id: 'test-question-id-2' }] - + const mockQuestions = [ + mockQuestion, + { ...mockQuestion, id: 'test-question-id-2' }, + ]; + // Reset the mock for this test mockQuestionRepository.createQueryBuilder.mockReturnValue({ orderBy: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(mockQuestions), - }) - + }); + // Ensure no external API calls are made const spy = jest.spyOn(mockHttpService, 'get'); - const result = await service.getRandomQuestionsWithFilters(queryDto) + const result = await service.getRandomQuestionsWithFilters(queryDto); - expect(result).toHaveLength(2) - expect(result[0].options).toBeDefined() + expect(result).toHaveLength(2); + expect(result[0].options).toBeDefined(); expect(spy).not.toHaveBeenCalled(); - }) + }); it('should fetch external questions when not enough in database', async () => { const queryDto: RandomQuestionsQueryDto = { difficulty: QuestionDifficulty.HARD, count: 2, - } + }; // Only 1 question in DB, so external API will be called - const mockQuestions = [mockQuestion] - + const mockQuestions = [mockQuestion]; + // Reset the mock for this test mockQuestionRepository.createQueryBuilder.mockReturnValue({ orderBy: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(mockQuestions), - }) + }); // Mock the external question that will be created and saved const externalQuestion = { @@ -227,10 +244,10 @@ describe('IQAssessmentService', () => { mockQuestionRepository.create.mockReturnValue([externalQuestion]); mockQuestionRepository.save.mockResolvedValue([externalQuestion]); - const result = await service.getRandomQuestionsWithFilters(queryDto) + const result = await service.getRandomQuestionsWithFilters(queryDto); - expect(result).toHaveLength(2) // 1 from DB + 1 from external - expect(mockHttpService.get).toHaveBeenCalled() - }) - }) -}) \ No newline at end of file + expect(result).toHaveLength(2); // 1 from DB + 1 from external + expect(mockHttpService.get).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/puzzle/puzzle.service.spec.ts b/src/puzzle/puzzle.service.spec.ts index 60193ed..b509676 100644 --- a/src/puzzle/puzzle.service.spec.ts +++ b/src/puzzle/puzzle.service.spec.ts @@ -48,10 +48,28 @@ describe('PuzzleService', () => { providers: [ PuzzleService, { provide: getRepositoryToken(Puzzle), useValue: mockPuzzleRepository }, - { provide: getRepositoryToken(PuzzleSubmission), useValue: mockSubmissionRepository }, - { provide: getRepositoryToken(PuzzleProgress), useValue: mockProgressRepository }, + { + provide: getRepositoryToken(PuzzleSubmission), + useValue: mockSubmissionRepository, + }, + { + provide: getRepositoryToken(PuzzleProgress), + useValue: mockProgressRepository, + }, { provide: getRepositoryToken(User), useValue: mockUserRepository }, { provide: EventEmitter2, useValue: mockEventEmitter }, + { + provide: getRepositoryToken(User), + useValue: { + findOne: jest.fn().mockResolvedValue(mockUserRepository), // or null to test NotFound + }, + }, + { + provide: getRepositoryToken(Puzzle), + useValue: { + findOne: jest.fn().mockResolvedValue(mockPuzzleRepository), // or null to test NotFound + }, + } ], }).compile(); @@ -65,8 +83,7 @@ describe('PuzzleService', () => { describe('submitPuzzleSolution', () => { const userId = 'user-1'; const puzzleId = 123; - const submitDto: SubmitPuzzleDto = { userId, - puzzleId, solution: 'answer' }; + const submitDto: SubmitPuzzleDto = { userId, puzzleId, solution: 'answer' }; const puzzle = { id: puzzleId, @@ -93,7 +110,7 @@ describe('PuzzleService', () => { }; mockPuzzleRepository.findOne.mockResolvedValueOnce(puzzle); // puzzle - mockUserRepository.findOne.mockResolvedValueOnce(user); // user + mockUserRepository.findOne.mockResolvedValueOnce(user); // user mockSubmissionRepository.create.mockReturnValue(submission); mockSubmissionRepository.save.mockResolvedValue(submission); mockSubmissionRepository.findOne.mockResolvedValueOnce(null); // no previous correct submission @@ -106,16 +123,23 @@ describe('PuzzleService', () => { mockProgressRepository.save.mockResolvedValue({}); mockUserRepository.save.mockResolvedValue({ ...user, xp: 200, level: 1 }); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(true); expect(result.xpEarned).toBe(100); expect(result.tokensEarned).toBe(10); - expect(mockEventEmitter.emit).toHaveBeenCalledWith('puzzle.submitted', expect.objectContaining({ - userId, - puzzleId, - isCorrect: true, - })); + expect(mockEventEmitter.emit).toHaveBeenCalledWith( + 'puzzle.submitted', + expect.objectContaining({ + userId, + puzzleId, + isCorrect: true, + }), + ); }); it('should handle incorrect puzzle solution', async () => { @@ -135,7 +159,11 @@ describe('PuzzleService', () => { mockSubmissionRepository.save.mockResolvedValue({}); mockSubmissionRepository.findOne.mockResolvedValue(null); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(false); expect(result.message).toBe('Incorrect solution. Try again!'); @@ -182,7 +210,11 @@ describe('PuzzleService', () => { mockSubmissionRepository.save.mockResolvedValue(newSubmission); mockSubmissionRepository.findOne.mockResolvedValue(existingSuccess); - const result = await service.submitPuzzleSolution(userId, puzzleId, submitDto); + const result = await service.submitPuzzleSolution( + userId, + puzzleId, + submitDto, + ); expect(result.success).toBe(true); expect(result.xpEarned).toBe(0); @@ -214,11 +246,25 @@ describe('PuzzleService', () => { const mockQueryBuilder = { andWhere: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue(puzzles), + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getRawMany: jest.fn(), + getRawOne: jest.fn(), + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + leftJoinAndSelect: jest.fn().mockReturnThis(), }; mockPuzzleRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); - const result = await service.getPuzzles({ type: 'logic', difficulty: 'easy' }); + const result = await service.getPuzzles({ + type: 'logic', + difficulty: 'easy', + }); expect(result).toEqual(puzzles); expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(2); @@ -235,4 +281,4 @@ describe('PuzzleService', () => { expect(result).toEqual(progress); }); }); -}); \ No newline at end of file +});