From 58badf65c41c17d0d497541c40a685cd8923e492 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:59:46 -0400 Subject: [PATCH 01/23] E2E test stubs --- apps/backend/test/donations.e2e-spec.ts | 194 ++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 apps/backend/test/donations.e2e-spec.ts diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts new file mode 100644 index 0000000..9ad2435 --- /dev/null +++ b/apps/backend/test/donations.e2e-spec.ts @@ -0,0 +1,194 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as request from 'supertest'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +// Import entities directly from src so tests run against the same models +import { Donation } from '../src/donations/donation.entity'; +import { User } from '../src/users/user.entity'; + +describe('Donations (e2e) - expanded stubs', () => { + let app: INestApplication; + let donationRepo: Repository | null = null; + + beforeAll(async () => { + // Create a testing module using an in-memory SQLite database so tests + // exercise TypeORM and the repository layer without touching Postgres. + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + type: 'sqlite', + database: ':memory:', + dropSchema: true, + entities: [Donation, User], + synchronize: true, + logging: false, + }), + TypeOrmModule.forFeature([Donation]), + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + + // Repository is available for DB assertions. When tests are enabled + // (not skipped) you can use this to verify DB state after POSTs. + try { + donationRepo = moduleFixture.get>(getRepositoryToken(Donation)); + } catch (e) { + donationRepo = null; + } + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + // Helper sample payloads used in the stubs below + const oneTimePayload = { + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + amount: 50, + isAnonymous: false, + donationType: 'ONE_TIME', + }; + + const recurringPayload = { + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + amount: 25, + isAnonymous: false, + donationType: 'RECURRING', + recurringInterval: 'MONTHLY', + }; + + // Each test is currently skipped. They contain concrete, ready-to-run + // supertest calls and DB assertions; remove `.skip` to enable a test + // once the corresponding controller/routes are implemented. + + it.skip('POST /api/donations - successful one-time donation creation', async () => { + // Arrange + const payload = { ...oneTimePayload }; + // prevent unused-var/unused-local complaints while this test is skipped + void payload; + void donationRepo; + + // Act (example supertest call) + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(201); + + // Assert response shape (example) + // expect(res.body).toHaveProperty('id'); + // expect(res.body.amount).toBe(payload.amount); + + // Verify DB state (example) + // const created = await donationRepo!.findOne({ where: { email: payload.email } }); + // expect(created).toBeDefined(); + // expect(created!.amount).toBe(payload.amount); + }); + + it.skip('POST /api/donations - successful recurring donation with interval', async () => { + const payload = { ...recurringPayload }; + void payload; + void donationRepo; + + // Example supertest + assertions (commented until route exists) + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(201); + + // expect(res.body.donationType).toBe('RECURRING'); + // expect(res.body.recurringInterval).toBe('MONTHLY'); + }); + + describe('POST /api/donations - validation errors', () => { + it.skip('Negative amount returns 400', async () => { + const payload = { ...oneTimePayload, amount: -10 }; + void payload; + void donationRepo; + + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(400); + + // expect(res.body).toHaveProperty('message'); + }); + + it.skip('Invalid email format returns 400', async () => { + const payload = { ...oneTimePayload, email: 'not-an-email' }; + void payload; + void donationRepo; + + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(400); + + // expect(res.body).toHaveProperty('message'); + }); + + it.skip('Recurring without interval returns 400', async () => { + const payload = { ...recurringPayload }; + // @ts-expect-error - simulate missing interval + delete payload.recurringInterval; + + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(400); + }); + + it.skip('One-time with interval returns 400', async () => { + const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; + void payload; + void donationRepo; + + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(400); + }); + }); + + it.skip('GET /api/donations/public - returns only non-anonymous donations', async () => { + // Example setup: insert public and anonymous donations, then call endpoint + // await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); + // await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); + + // const res = await request(app.getHttpServer()) + // .get('/api/donations/public') + // .expect(200); + + // expect(Array.isArray(res.body)).toBe(true); + // expect(res.body.every((d: any) => d.isAnonymous === false)).toBe(true); + }); + + it.skip('GET /api/donations/stats - returns correct total and count', async () => { + // Example: seed two donations and verify totals endpoint + // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); + // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); + + // const res = await request(app.getHttpServer()) + // .get('/api/donations/stats') + // .expect(200); + + // expect(res.body).toEqual({ total: 25, count: 2 }); + }); + + // Small smoke test to ensure supertest and the app wiring are working. + it('smoke: GET / (should 404 or 200 depending on routes)', async () => { + const res = await request(app.getHttpServer()).get('/'); + // We don't make assumptions about the root route; assert we get a response + expect([200, 404]).toContain(res.status); + }); +}); From 2d3469fc4e72b7349d4034572c4f02aa9116c483 Mon Sep 17 00:00:00 2001 From: Ben Petrillo Date: Mon, 27 Oct 2025 16:51:36 -0400 Subject: [PATCH 02/23] feat: donations controller impl --- apps/backend/src/app.module.ts | 2 + apps/backend/src/auth/auth.controller.ts | 10 +- .../donations/donations.controller.spec.ts | 297 ++++++++++++++++++ .../src/donations/donations.controller.ts | 264 ++++++++++++++++ .../backend/src/donations/donations.module.ts | 14 + .../src/donations/donations.service.spec.ts | 291 +++++++++++++++++ .../src/donations/donations.service.ts | 96 ++++++ .../migrations/1759151447065-add_donations.ts | 4 +- .../src/strategies/plural-naming.strategy.ts | 6 +- apps/backend/src/users/user.entity.ts | 2 +- 10 files changed, 969 insertions(+), 17 deletions(-) create mode 100644 apps/backend/src/donations/donations.controller.spec.ts create mode 100644 apps/backend/src/donations/donations.controller.ts create mode 100644 apps/backend/src/donations/donations.module.ts create mode 100644 apps/backend/src/donations/donations.service.spec.ts create mode 100644 apps/backend/src/donations/donations.service.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index b5fd0d3..3d5a9ee 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -5,6 +5,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; +import { DonationsModule } from './donations/donations.module'; import AppDataSource from './data-source'; @Module({ @@ -12,6 +13,7 @@ import AppDataSource from './data-source'; TypeOrmModule.forRoot(AppDataSource.options), UsersModule, AuthModule, + DonationsModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index bb04b6b..83965a2 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -1,11 +1,4 @@ -import { - BadRequestException, - Body, - Controller, - Post, - Request, - UseGuards, -} from '@nestjs/common'; +import { BadRequestException, Body, Controller, Post } from '@nestjs/common'; import { SignInDto } from './dtos/sign-in.dto'; import { SignUpDto } from './dtos/sign-up.dto'; @@ -16,7 +9,6 @@ import { DeleteUserDto } from './dtos/delete-user.dto'; import { User } from '../users/user.entity'; import { SignInResponseDto } from './dtos/sign-in-response.dto'; import { RefreshTokenDto } from './dtos/refresh-token.dto'; -import { AuthGuard } from '@nestjs/passport'; import { ConfirmPasswordDto } from './dtos/confirm-password.dto'; import { ForgotPasswordDto } from './dtos/forgot-password.dto'; import { ApiTags } from '@nestjs/swagger'; diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts new file mode 100644 index 0000000..9d48d18 --- /dev/null +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -0,0 +1,297 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DonationsController } from './donations.controller'; +import { DonationsService } from './donations.service'; +import { DonationsRepository } from './donations.repository'; +import { CreateDonationDto } from './dtos/create-donation-dto'; +import { + DonationType, + RecurringInterval, + DonationStatus, +} from './donation.entity'; +import { Donation as DomainDonation } from './mappers'; + +describe('DonationsController', () => { + let controller: DonationsController; + let service: DonationsService; + let repository: DonationsRepository; + + const mockDomainDonation: DomainDonation = { + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: 'one_time', + showDedicationPublicly: false, + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockService = { + create: jest.fn(), + findPublic: jest.fn(), + getTotalDonations: jest.fn(), + }; + + const mockRepository = { + findPaginated: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DonationsController], + providers: [ + { + provide: DonationsService, + useValue: mockService, + }, + { + provide: DonationsRepository, + useValue: mockRepository, + }, + ], + }).compile(); + + controller = module.get(DonationsController); + service = module.get(DonationsService); + repository = module.get(DonationsRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should create a donation and return response DTO', async () => { + const createDto: CreateDonationDto = { + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + showDedicationPublicly: false, + }; + + mockService.create.mockResolvedValue(mockDomainDonation); + + const result = await controller.create(createDto); + + expect(service.create).toHaveBeenCalledWith({ + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: 'one_time', + showDedicationPublicly: false, + }); + + expect(result).toEqual({ + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + showDedicationPublicly: false, + status: DonationStatus.PENDING, + createdAt: mockDomainDonation.createdAt, + updatedAt: mockDomainDonation.updatedAt, + }); + }); + + it('should handle recurring donations', async () => { + const createDto: CreateDonationDto = { + firstName: 'Jane', + lastName: 'Smith', + email: 'jane@example.com', + amount: 50, + isAnonymous: true, + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + showDedicationPublicly: false, + }; + + const recurringDomainDonation: DomainDonation = { + ...mockDomainDonation, + firstName: 'Jane', + lastName: 'Smith', + email: 'jane@example.com', + amount: 50, + isAnonymous: true, + donationType: 'recurring', + recurringInterval: 'monthly', + }; + + mockService.create.mockResolvedValue(recurringDomainDonation); + + const result = await controller.create(createDto); + + expect(service.create).toHaveBeenCalledWith({ + firstName: 'Jane', + lastName: 'Smith', + email: 'jane@example.com', + amount: 50, + isAnonymous: true, + donationType: 'recurring', + recurringInterval: 'monthly', + showDedicationPublicly: false, + }); + + expect(result.donationType).toBe(DonationType.RECURRING); + expect(result.recurringInterval).toBe(RecurringInterval.MONTHLY); + }); + }); + + describe('findPublic', () => { + it('should return public donations', async () => { + mockService.findPublic.mockResolvedValue([mockDomainDonation]); + + const result = await controller.findPublic(10); + + expect(service.findPublic).toHaveBeenCalledWith(10); + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('id', 1); + expect(result[0]).toHaveProperty('amount', 100); + }); + + it('should use default limit when not provided', async () => { + mockService.findPublic.mockResolvedValue([]); + + await controller.findPublic(undefined); + + expect(service.findPublic).toHaveBeenCalledWith(undefined); + }); + + it('should hide donor name for anonymous donations', async () => { + const anonymousDonation: DomainDonation = { + ...mockDomainDonation, + isAnonymous: true, + }; + + mockService.findPublic.mockResolvedValue([anonymousDonation]); + + const result = await controller.findPublic(10); + + expect(result[0]).not.toHaveProperty('donorName'); + expect(result[0].isAnonymous).toBe(true); + }); + }); + + describe('getStats', () => { + it('should return donation statistics', async () => { + const mockStats = { total: 10000, count: 50 }; + mockService.getTotalDonations.mockResolvedValue(mockStats); + + const result = await controller.getStats(); + + expect(service.getTotalDonations).toHaveBeenCalled(); + expect(result).toEqual(mockStats); + }); + + it('should handle zero donations', async () => { + const mockStats = { total: 0, count: 0 }; + mockService.getTotalDonations.mockResolvedValue(mockStats); + + const result = await controller.getStats(); + + expect(result.total).toBe(0); + expect(result.count).toBe(0); + }); + }); + + describe('findAll', () => { + it('should return paginated donations with default parameters', async () => { + const mockEntity = { + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.PENDING, + createdAt: new Date(), + updatedAt: new Date(), + transactionId: null, + }; + + mockRepository.findPaginated.mockResolvedValue({ + rows: [mockEntity], + total: 1, + page: 1, + perPage: 20, + totalPages: 1, + }); + + const result = await controller.findAll(1, 20); + + expect(repository.findPaginated).toHaveBeenCalledWith(1, 20, { + donationType: undefined, + status: undefined, + isAnonymous: undefined, + recurringInterval: undefined, + minAmount: undefined, + maxAmount: undefined, + }); + + expect(result.rows).toHaveLength(1); + expect(result.total).toBe(1); + expect(result.page).toBe(1); + expect(result.perPage).toBe(20); + expect(result.totalPages).toBe(1); + }); + + it('should apply filters when provided', async () => { + mockRepository.findPaginated.mockResolvedValue({ + rows: [], + total: 0, + page: 1, + perPage: 20, + totalPages: 0, + }); + + await controller.findAll( + 1, + 20, + DonationType.RECURRING, + DonationStatus.SUCCEEDED, + false, + RecurringInterval.MONTHLY, + 50, + 500, + ); + + expect(repository.findPaginated).toHaveBeenCalledWith(1, 20, { + donationType: DonationType.RECURRING, + status: DonationStatus.SUCCEEDED, + isAnonymous: false, + recurringInterval: RecurringInterval.MONTHLY, + minAmount: 50, + maxAmount: 500, + }); + }); + + it('should handle empty results', async () => { + mockRepository.findPaginated.mockResolvedValue({ + rows: [], + total: 0, + page: 1, + perPage: 20, + totalPages: 0, + }); + + const result = await controller.findAll(1, 20); + + expect(result.rows).toEqual([]); + expect(result.total).toBe(0); + }); + }); +}); diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts new file mode 100644 index 0000000..0043183 --- /dev/null +++ b/apps/backend/src/donations/donations.controller.ts @@ -0,0 +1,264 @@ +import { + Controller, + Get, + Post, + Body, + Query, + ParseIntPipe, + UseGuards, + ValidationPipe, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { DonationsService } from './donations.service'; +import { DonationsRepository, PaginationFilters } from './donations.repository'; +import { CreateDonationDto } from './dtos/create-donation-dto'; +import { DonationResponseDto } from './dtos/donation-response-dto'; +import { PublicDonationDto } from './dtos/public-donation-dto'; +import { DonationMappers } from './mappers'; +import { + DonationType, + RecurringInterval, + DonationStatus, +} from './donation.entity'; + +@ApiTags('Donations') +@Controller('donations') +export class DonationsController { + constructor( + private readonly donationsService: DonationsService, + private readonly donationsRepository: DonationsRepository, + ) {} + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: 'create a new donation', + description: + 'submit a new donation with donor information and donation details', + }) + @ApiResponse({ + status: 201, + description: 'donation successfully created', + type: DonationResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'validation error', + }) + async create( + @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) + createDonationDto: CreateDonationDto, + ): Promise { + const request = DonationMappers.toCreateDonationRequest(createDonationDto); + const donation = await this.donationsService.create(request); + return DonationMappers.toDonationResponseDto(donation); + } + + @Get('public') + @ApiOperation({ + summary: 'get public donations', + description: + 'retrieve a list of recent donations for public display, respecting privacy settings', + }) + @ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: 'maximum number of donations to return', + example: 50, + }) + @ApiResponse({ + status: 200, + description: 'list of public donations', + type: [PublicDonationDto], + }) + async findPublic( + @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, + ): Promise { + const donations = await this.donationsService.findPublic(limit); + return DonationMappers.toPublicDonationDtos(donations); + } + + @Get('stats') + @ApiOperation({ + summary: 'get donation statistics', + description: + 'retrieve aggregate donation statistics including total amount and count', + }) + @ApiResponse({ + status: 200, + description: 'donation statistics', + schema: { + type: 'object', + properties: { + total: { + type: 'number', + description: 'total donation amount in dollars', + example: 25000.0, + }, + count: { + type: 'number', + description: 'total number of donations', + example: 150, + }, + }, + }, + }) + async getStats(): Promise<{ total: number; count: number }> { + return this.donationsService.getTotalDonations(); + } + + @Get() + @UseGuards(AuthGuard('jwt')) + @ApiBearerAuth() + @ApiOperation({ + summary: 'get paginated donation list (admin)', + description: + 'retrieve a paginated list of all donations with optional filters. Requires authentication.', + }) + @ApiQuery({ + name: 'page', + required: false, + type: Number, + description: 'page number (one-indexed)', + example: 1, + }) + @ApiQuery({ + name: 'perPage', + required: false, + type: Number, + description: 'number of items per page', + example: 20, + }) + @ApiQuery({ + name: 'donationType', + required: false, + enum: DonationType, + description: 'filter by donation type', + }) + @ApiQuery({ + name: 'status', + required: false, + enum: DonationStatus, + description: 'filter by donation status', + }) + @ApiQuery({ + name: 'isAnonymous', + required: false, + type: Boolean, + description: 'filter by anonymous status', + }) + @ApiQuery({ + name: 'recurringInterval', + required: false, + enum: RecurringInterval, + description: 'filter by recurring interval', + }) + @ApiQuery({ + name: 'minAmount', + required: false, + type: Number, + description: 'minimum donation amount', + }) + @ApiQuery({ + name: 'maxAmount', + required: false, + type: Number, + description: 'maximum donation amount', + }) + @ApiResponse({ + status: 200, + description: 'paginated donation list', + schema: { + type: 'object', + properties: { + rows: { + type: 'array', + items: { $ref: '#/components/schemas/DonationResponseDto' }, + }, + total: { type: 'number', example: 100 }, + page: { type: 'number', example: 1 }, + perPage: { type: 'number', example: 20 }, + totalPages: { type: 'number', example: 5 }, + }, + }, + }) + @ApiResponse({ + status: 401, + description: 'unauthorized', + }) + async findAll( + @Query('page', new ParseIntPipe({ optional: true })) page: number = 1, + @Query('perPage', new ParseIntPipe({ optional: true })) + perPage: number = 20, + @Query('donationType') donationType?: DonationType, + @Query('status') status?: DonationStatus, + @Query('isAnonymous') isAnonymous?: boolean, + @Query('recurringInterval') recurringInterval?: RecurringInterval, + @Query('minAmount', new ParseIntPipe({ optional: true })) + minAmount?: number, + @Query('maxAmount', new ParseIntPipe({ optional: true })) + maxAmount?: number, + ): Promise<{ + rows: DonationResponseDto[]; + total: number; + page: number; + perPage: number; + totalPages: number; + }> { + const filters: PaginationFilters = { + donationType, + status, + isAnonymous, + recurringInterval, + minAmount, + maxAmount, + }; + + const result = await this.donationsRepository.findPaginated( + page, + perPage, + filters, + ); + + const domainDonations = result.rows.map((entity) => ({ + id: entity.id, + firstName: entity.firstName, + lastName: entity.lastName, + email: entity.email, + amount: entity.amount, + isAnonymous: entity.isAnonymous, + donationType: entity.donationType as 'one_time' | 'recurring', + recurringInterval: entity.recurringInterval as + | 'weekly' + | 'monthly' + | 'bimonthly' + | 'quarterly' + | 'annually' + | undefined, + dedicationMessage: entity.dedicationMessage ?? undefined, + showDedicationPublicly: entity.showDedicationPublicly, + status: entity.status as 'pending' | 'succeeded' | 'failed' | 'cancelled', + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + transactionId: entity.transactionId ?? undefined, + })); + + return { + rows: DonationMappers.toDonationResponseDtos(domainDonations), + total: result.total, + page: result.page, + perPage: result.perPage, + totalPages: result.totalPages, + }; + } +} diff --git a/apps/backend/src/donations/donations.module.ts b/apps/backend/src/donations/donations.module.ts new file mode 100644 index 0000000..e088743 --- /dev/null +++ b/apps/backend/src/donations/donations.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Donation } from './donation.entity'; +import { DonationsController } from './donations.controller'; +import { DonationsService } from './donations.service'; +import { DonationsRepository } from './donations.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([Donation])], + controllers: [DonationsController], + providers: [DonationsService, DonationsRepository], + exports: [DonationsService, DonationsRepository], +}) +export class DonationsModule {} diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts new file mode 100644 index 0000000..c30a3c1 --- /dev/null +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -0,0 +1,291 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DonationsService } from './donations.service'; +import { DonationsRepository } from './donations.repository'; +import { + Donation, + DonationType, + DonationStatus, + RecurringInterval, +} from './donation.entity'; +import { CreateDonationRequest } from './mappers'; + +describe('DonationsService', () => { + let service: DonationsService; + let donationRepository: Repository; + + const mockDonationEntity: Donation = { + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.PENDING, + createdAt: new Date(), + updatedAt: new Date(), + transactionId: null, + }; + + const mockRepository = { + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + createQueryBuilder: jest.fn(), + }; + + const mockDonationsRepository = { + findRecentPublic: jest.fn(), + getTotalsByDateRange: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DonationsService, + { + provide: getRepositoryToken(Donation), + useValue: mockRepository, + }, + { + provide: DonationsRepository, + useValue: mockDonationsRepository, + }, + ], + }).compile(); + + service = module.get(DonationsService); + donationRepository = module.get>( + getRepositoryToken(Donation), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should create a one-time donation', async () => { + const request: CreateDonationRequest = { + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: 'one_time', + showDedicationPublicly: false, + }; + + mockRepository.create.mockReturnValue(mockDonationEntity); + mockRepository.save.mockResolvedValue(mockDonationEntity); + + const result = await service.create(request); + + expect(donationRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + status: DonationStatus.PENDING, + }), + ); + + expect(donationRepository.save).toHaveBeenCalled(); + + expect(result).toMatchObject({ + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + amount: 100, + isAnonymous: false, + donationType: 'one_time', + status: 'pending', + }); + }); + + it('should create a recurring donation', async () => { + const request: CreateDonationRequest = { + firstName: 'Jane', + lastName: 'Smith', + email: 'jane@example.com', + amount: 50, + isAnonymous: true, + donationType: 'recurring', + recurringInterval: 'monthly', + showDedicationPublicly: false, + }; + + const recurringEntity = { + ...mockDonationEntity, + firstName: 'Jane', + lastName: 'Smith', + email: 'jane@example.com', + amount: 50, + isAnonymous: true, + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }; + + mockRepository.create.mockReturnValue(recurringEntity); + mockRepository.save.mockResolvedValue(recurringEntity); + + const result = await service.create(request); + + expect(donationRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + donationType: DonationType.RECURRING, + recurringInterval: 'monthly', + }), + ); + + expect(result.donationType).toBe('recurring'); + expect(result.recurringInterval).toBe('monthly'); + }); + + it('should handle dedication messages', async () => { + const request: CreateDonationRequest = { + firstName: 'Bob', + lastName: 'Johnson', + email: 'bob@example.com', + amount: 200, + isAnonymous: false, + donationType: 'one_time', + dedicationMessage: 'In memory of...', + showDedicationPublicly: true, + }; + + const entityWithDedication = { + ...mockDonationEntity, + dedicationMessage: 'In memory of...', + showDedicationPublicly: true, + }; + + mockRepository.create.mockReturnValue(entityWithDedication); + mockRepository.save.mockResolvedValue(entityWithDedication); + + const result = await service.create(request); + + expect(result.dedicationMessage).toBe('In memory of...'); + expect(result.showDedicationPublicly).toBe(true); + }); + }); + + describe('findPublic', () => { + it('should return public donations with default limit', async () => { + mockRepository.find.mockResolvedValue([mockDonationEntity]); + + const result = await service.findPublic(); + + expect(donationRepository.find).toHaveBeenCalledWith({ + where: { status: DonationStatus.SUCCEEDED }, + order: { createdAt: 'DESC' }, + take: 50, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + id: 1, + firstName: 'John', + lastName: 'Doe', + amount: 100, + }); + }); + + it('should respect custom limit', async () => { + mockRepository.find.mockResolvedValue([]); + + await service.findPublic(10); + + expect(donationRepository.find).toHaveBeenCalledWith({ + where: { status: DonationStatus.SUCCEEDED }, + order: { createdAt: 'DESC' }, + take: 10, + }); + }); + + it('should map multiple donations correctly', async () => { + const donations = [ + mockDonationEntity, + { ...mockDonationEntity, id: 2, amount: 200 }, + { ...mockDonationEntity, id: 3, amount: 300 }, + ]; + + mockRepository.find.mockResolvedValue(donations); + + const result = await service.findPublic(3); + + expect(result).toHaveLength(3); + expect(result[0].id).toBe(1); + expect(result[1].id).toBe(2); + expect(result[2].id).toBe(3); + }); + }); + + describe('getTotalDonations', () => { + it('should return total amount and count', async () => { + const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getRawOne: jest + .fn() + .mockResolvedValue({ total: '10000.00', count: '50' }), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.getTotalDonations(); + + expect(result).toEqual({ total: 10000, count: 50 }); + expect(mockQueryBuilder.select).toHaveBeenCalledWith( + 'SUM(donation.amount)', + 'total', + ); + expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith( + 'COUNT(donation.id)', + 'count', + ); + }); + + it('should handle zero donations', async () => { + const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValue({ total: null, count: '0' }), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.getTotalDonations(); + + expect(result).toEqual({ total: 0, count: 0 }); + }); + + it('should parse numeric values correctly', async () => { + const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getRawOne: jest + .fn() + .mockResolvedValue({ total: '12345.67', count: '123' }), + }; + + mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); + + const result = await service.getTotalDonations(); + + expect(result.total).toBe(12345.67); + expect(result.count).toBe(123); + expect(typeof result.total).toBe('number'); + expect(typeof result.count).toBe('number'); + }); + }); +}); diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts new file mode 100644 index 0000000..83359b6 --- /dev/null +++ b/apps/backend/src/donations/donations.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { + Donation, + DonationType, + RecurringInterval, + DonationStatus, +} from './donation.entity'; +import { CreateDonationRequest, Donation as DomainDonation } from './mappers'; +import { DonationsRepository } from './donations.repository'; + +export interface DonationsServiceInterface { + create(request: CreateDonationRequest): Promise; + findPublic(limit?: number): Promise; + getTotalDonations(): Promise<{ total: number; count: number }>; +} + +@Injectable() +export class DonationsService implements DonationsServiceInterface { + constructor( + @InjectRepository(Donation) + private readonly donationRepository: Repository, + private readonly donationsRepository: DonationsRepository, + ) {} + + async create(request: CreateDonationRequest): Promise { + const donation = this.donationRepository.create({ + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + amount: request.amount, + isAnonymous: request.isAnonymous ?? false, + donationType: + request.donationType === 'one_time' + ? DonationType.ONE_TIME + : DonationType.RECURRING, + recurringInterval: request.recurringInterval + ? (request.recurringInterval as RecurringInterval) + : null, + dedicationMessage: request.dedicationMessage ?? null, + showDedicationPublicly: request.showDedicationPublicly ?? false, + status: DonationStatus.PENDING, + createdAt: new Date(), + updatedAt: new Date(), + }); + const saved = await this.donationRepository.save(donation); + return this.mapEntityToDomain(saved); + } + + async findPublic(limit: number = 50): Promise { + const donations = await this.donationRepository.find({ + where: { status: DonationStatus.SUCCEEDED }, + order: { createdAt: 'DESC' }, + take: limit, + }); + return donations.map((d) => this.mapEntityToDomain(d)); + } + + async getTotalDonations(): Promise<{ total: number; count: number }> { + const result = await this.donationRepository + .createQueryBuilder('donation') + .select('SUM(donation.amount)', 'total') + .addSelect('COUNT(donation.id)', 'count') + .getRawOne(); + return { + total: parseFloat(result.total) || 0, + count: parseInt(result.count, 10) || 0, + }; + } + + private mapEntityToDomain(entity: Donation): DomainDonation { + return { + id: entity.id, + firstName: entity.firstName, + lastName: entity.lastName, + email: entity.email, + amount: entity.amount, + isAnonymous: entity.isAnonymous, + donationType: entity.donationType as 'one_time' | 'recurring', + recurringInterval: entity.recurringInterval as + | 'weekly' + | 'monthly' + | 'bimonthly' + | 'quarterly' + | 'annually' + | undefined, + dedicationMessage: entity.dedicationMessage ?? undefined, + showDedicationPublicly: entity.showDedicationPublicly, + status: entity.status as 'pending' | 'succeeded' | 'failed' | 'cancelled', + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + transactionId: entity.transactionId ?? undefined, + }; + } +} diff --git a/apps/backend/src/migrations/1759151447065-add_donations.ts b/apps/backend/src/migrations/1759151447065-add_donations.ts index 838e762..acc9503 100644 --- a/apps/backend/src/migrations/1759151447065-add_donations.ts +++ b/apps/backend/src/migrations/1759151447065-add_donations.ts @@ -5,7 +5,7 @@ export class AddDonations1759151447065 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "donation" ( + `CREATE TABLE "donations" ( "id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, @@ -26,6 +26,6 @@ export class AddDonations1759151447065 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "donation"`); + await queryRunner.query(`DROP TABLE "donations"`); } } diff --git a/apps/backend/src/strategies/plural-naming.strategy.ts b/apps/backend/src/strategies/plural-naming.strategy.ts index bf3a512..1a7c0a1 100644 --- a/apps/backend/src/strategies/plural-naming.strategy.ts +++ b/apps/backend/src/strategies/plural-naming.strategy.ts @@ -8,11 +8,7 @@ export class PluralNamingStrategy return userSpecifiedName || targetName.toLowerCase() + 's'; // Pluralize the table name } - columnName( - propertyName: string, - customName: string, - embeddedPrefixes: string[], - ): string { + columnName(propertyName: string): string { return propertyName; } diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/user.entity.ts index 3224019..daacc81 100644 --- a/apps/backend/src/users/user.entity.ts +++ b/apps/backend/src/users/user.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Column, ObjectIdColumn, ObjectId } from 'typeorm'; +import { Entity, Column } from 'typeorm'; import type { Status } from './types'; From fdc5e305dfce2ed62869c5229b38001af466defe Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:38:56 -0400 Subject: [PATCH 03/23] Some failure tests, adding supertest and expanding jest.config.ts to recognize tests in /test folder --- apps/backend/jest.config.ts | 1 + apps/backend/test/donations.e2e-spec.ts | 225 ++++++++++--------- package.json | 2 + yarn.lock | 279 +++++++++++++++++++++++- 4 files changed, 403 insertions(+), 104 deletions(-) diff --git a/apps/backend/jest.config.ts b/apps/backend/jest.config.ts index 3a31ab8..a7aadd0 100644 --- a/apps/backend/jest.config.ts +++ b/apps/backend/jest.config.ts @@ -8,4 +8,5 @@ export default { }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../../coverage/apps/backend', + testMatch: ['/src/**/*.spec.ts', '/test/**/*.spec.ts', '/test/**/*.e2e-spec.ts'], }; diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 9ad2435..7d33efe 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -1,11 +1,9 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import * as request from 'supertest'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; - -// Import entities directly from src so tests run against the same models +import request from 'supertest'; import { Donation } from '../src/donations/donation.entity'; import { User } from '../src/users/user.entity'; @@ -68,127 +66,150 @@ describe('Donations (e2e) - expanded stubs', () => { recurringInterval: 'MONTHLY', }; - // Each test is currently skipped. They contain concrete, ready-to-run - // supertest calls and DB assertions; remove `.skip` to enable a test - // once the corresponding controller/routes are implemented. - - it.skip('POST /api/donations - successful one-time donation creation', async () => { - // Arrange - const payload = { ...oneTimePayload }; - // prevent unused-var/unused-local complaints while this test is skipped - void payload; - void donationRepo; - - // Act (example supertest call) - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(201); - - // Assert response shape (example) - // expect(res.body).toHaveProperty('id'); - // expect(res.body.amount).toBe(payload.amount); - - // Verify DB state (example) - // const created = await donationRepo!.findOne({ where: { email: payload.email } }); - // expect(created).toBeDefined(); - // expect(created!.amount).toBe(payload.amount); + // Small smoke test to ensure supertest and the app wiring are working. + it('smoke: GET / (should 404 or 200 depending on routes)', async () => { + const res = await request(app.getHttpServer()).get('/'); + // We don't make assumptions about the root route; assert we get a response + expect([200, 404]).toContain(res.status); }); + + describe('POST /api/donations', () => { + it.skip('POST /api/donations - successful one-time donation creation', async () => { + // Arrange + const payload = { ...oneTimePayload }; + // prevent unused-var/unused-local complaints while this test is skipped + void payload; + void donationRepo; + + // Act (example supertest call) + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(201); - it.skip('POST /api/donations - successful recurring donation with interval', async () => { - const payload = { ...recurringPayload }; - void payload; - void donationRepo; - - // Example supertest + assertions (commented until route exists) - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(201); + // Assert response shape (example) + // expect(res.body).toHaveProperty('id'); + // expect(res.body.amount).toBe(payload.amount); - // expect(res.body.donationType).toBe('RECURRING'); - // expect(res.body.recurringInterval).toBe('MONTHLY'); - }); + // Verify DB state (example) + // const created = await donationRepo!.findOne({ where: { email: payload.email } }); + // expect(created).toBeDefined(); + // expect(created!.amount).toBe(payload.amount); + }); - describe('POST /api/donations - validation errors', () => { - it.skip('Negative amount returns 400', async () => { - const payload = { ...oneTimePayload, amount: -10 }; - void payload; - void donationRepo; + it.skip('POST /api/donations - successful recurring donation with interval', async () => { + const payload = { ...recurringPayload }; + void payload; + void donationRepo; - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(400); + // Example supertest + assertions (commented until route exists) + // const res = await request(app.getHttpServer()) + // .post('/api/donations') + // .send(payload) + // .expect(201); - // expect(res.body).toHaveProperty('message'); + // expect(res.body.donationType).toBe('RECURRING'); + // expect(res.body.recurringInterval).toBe('MONTHLY'); }); - it.skip('Invalid email format returns 400', async () => { - const payload = { ...oneTimePayload, email: 'not-an-email' }; - void payload; - void donationRepo; + it('Negative amount returns 400', async () => { + const payload = { ...oneTimePayload, amount: -10 }; - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); - // expect(res.body).toHaveProperty('message'); - }); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); - it.skip('Recurring without interval returns 400', async () => { - const payload = { ...recurringPayload }; - // @ts-expect-error - simulate missing interval - delete payload.recurringInterval; + it('Invalid email format returns 400', async () => { + const payload = { ...oneTimePayload, email: 'not-an-email' }; - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); }); - it.skip('One-time with interval returns 400', async () => { - const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; - void payload; - void donationRepo; + it('Recurring without interval returns 400', async () => { + const payload: Partial = { ...recurringPayload }; + delete payload.recurringInterval; - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(400); - }); - }); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); - it.skip('GET /api/donations/public - returns only non-anonymous donations', async () => { - // Example setup: insert public and anonymous donations, then call endpoint - // await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); - // await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); + it('One-time with interval returns 400', async () => { + const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; - // const res = await request(app.getHttpServer()) - // .get('/api/donations/public') - // .expect(200); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); - // expect(Array.isArray(res.body)).toBe(true); - // expect(res.body.every((d: any) => d.isAnonymous === false)).toBe(true); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('POST /api/donations - when DB save throws, returns 500', async () => { + // Simulate a DB failure by spying on the repository save method. + if (!donationRepo) { + // If the repo isn't available, fail the test to make the problem visible + throw new Error('donationRepo not available for mocking'); + } + + const payload = { ...oneTimePayload }; + + const saveSpy = jest.spyOn(donationRepo, 'save').mockRejectedValueOnce(new Error('Simulated DB failure')); + try { + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(500); + + // Expect standard Nest error shape for unhandled errors + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + saveSpy.mockRestore(); + } + }); }); - it.skip('GET /api/donations/stats - returns correct total and count', async () => { - // Example: seed two donations and verify totals endpoint - // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); - // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); + describe('GET /api/donations/public', () => { + it.skip('GET /api/donations/public - returns only non-anonymous donations', async () => { + // Example setup: insert public and anonymous donations, then call endpoint + // await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); + // await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); - // const res = await request(app.getHttpServer()) - // .get('/api/donations/stats') - // .expect(200); + // const res = await request(app.getHttpServer()) + // .get('/api/donations/public') + // .expect(200); - // expect(res.body).toEqual({ total: 25, count: 2 }); - }); + // expect(Array.isArray(res.body)).toBe(true); + // expect(res.body.every((d: any) => d.isAnonymous === false)).toBe(true); + }); - // Small smoke test to ensure supertest and the app wiring are working. - it('smoke: GET / (should 404 or 200 depending on routes)', async () => { - const res = await request(app.getHttpServer()).get('/'); - // We don't make assumptions about the root route; assert we get a response - expect([200, 404]).toContain(res.status); + it.skip('GET /api/donations/stats - returns correct total and count', async () => { + // Example: seed two donations and verify totals endpoint + // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); + // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); + + // const res = await request(app.getHttpServer()) + // .get('/api/donations/stats') + // .expect(200); + + // expect(res.body).toEqual({ total: 25, count: 2 }); + }); }); }); diff --git a/package.json b/package.json index 91112ed..76a7740 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@nestjs/swagger": "^7.1.12", "@nestjs/typeorm": "^10.0.0", "@types/pg": "^8.15.5", + "@types/supertest": "^6.0.3", "amazon-cognito-identity-js": "^6.3.5", "axios": "^1.5.0", "class-transformer": "^0.5.1", @@ -56,6 +57,7 @@ "react-router-dom": "^6.15.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", + "supertest": "^7.1.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typeorm": "^0.3.17" diff --git a/yarn.lock b/yarn.lock index d4b8cf7..e823b9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,6 +2594,11 @@ dependencies: uuid "9.0.0" +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3015,6 +3020,13 @@ tslib "^2.3.0" yargs-parser "21.1.1" +"@paralleldrive/cuid2@^2.2.2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" + integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== + dependencies: + "@noble/hashes" "^1.1.5" + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" @@ -3717,6 +3729,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/eslint-scope@^3.7.3": version "3.7.6" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.6.tgz#585578b368ed170e67de8aae7b93f54a1b2fdc26" @@ -3833,6 +3850,11 @@ dependencies: "@types/node" "*" +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.3.tgz#886674659ce55fe7c6c06ec5ca7c0eb276a08f91" @@ -3975,6 +3997,24 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/superagent@^8.1.0": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.3.tgz#d736f0e994b195b63e1c93e80271a2faf927388c" + integrity sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/validator@^13.7.10": version "13.11.5" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.5.tgz#1911964fd5556b08d3479d1ded977c06f89a44a7" @@ -4796,6 +4836,11 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -5339,6 +5384,14 @@ cachedir@^2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" @@ -5348,6 +5401,14 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -5718,6 +5779,11 @@ common-tags@^1.8.0: resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== +component-emitter@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -5795,6 +5861,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + copy-anything@^2.0.1: version "2.0.6" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" @@ -6135,6 +6206,13 @@ debug@^4, debug@^4.3.1: dependencies: ms "^2.1.3" +debug@^4.3.7: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -6271,6 +6349,14 @@ detect-port@^1.5.1: address "^1.0.1" debug "4" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^29.4.3, diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -6384,6 +6470,15 @@ dotenv@~10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -6550,6 +6645,16 @@ es-abstract@^1.22.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.13" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-get-iterator@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" @@ -6590,6 +6695,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" @@ -6599,6 +6711,16 @@ es-set-tostringtag@^2.0.1: has-tostringtag "^1.0.0" hasown "^2.0.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -7105,7 +7227,7 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-safe-stringify@2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -7327,6 +7449,17 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7336,6 +7469,15 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formidable@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" + integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -7451,11 +7593,35 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -7642,6 +7808,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -7699,6 +7870,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -7706,6 +7882,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" @@ -7718,6 +7901,13 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -9743,6 +9933,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mdn-data@2.0.28: version "2.0.28" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" @@ -9780,7 +9975,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -9818,6 +10013,11 @@ mime@1.6.0, mime@^1.4.1, mime@^1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -10243,6 +10443,11 @@ object-inspect@^1.13.1, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -11218,6 +11423,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.2: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + qs@^6.4.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -11847,6 +12059,35 @@ shelljs@0.8.5: interpret "^1.0.0" rechoir "^0.6.2" +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -11856,6 +12097,17 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + siginfo@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" @@ -12254,6 +12506,29 @@ stylus@^0.59.0: sax "~1.2.4" source-map "^0.7.3" +superagent@^10.2.3: + version "10.2.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.2.3.tgz#d1e4986f2caac423c37e38077f9073ccfe73a59b" + integrity sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig== + dependencies: + component-emitter "^1.3.1" + cookiejar "^2.1.4" + debug "^4.3.7" + fast-safe-stringify "^2.1.1" + form-data "^4.0.4" + formidable "^3.5.4" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.2" + +supertest@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.1.4.tgz#3175e2539f517ca72fdc7992ffff35b94aca7d34" + integrity sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg== + dependencies: + methods "^1.1.2" + superagent "^10.2.3" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" From de85e254740a8d8fd9eaaceedd5be1805f24959e Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:54:43 -0400 Subject: [PATCH 04/23] SqLite setup and longer setup timeout --- apps/backend/test/donations.e2e-spec.ts | 4 + package.json | 1 + yarn.lock | 522 +++++++++++++++++++++++- 3 files changed, 514 insertions(+), 13 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 7d33efe..f3f8b31 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -8,6 +8,10 @@ import { Donation } from '../src/donations/donation.entity'; import { User } from '../src/users/user.entity'; describe('Donations (e2e) - expanded stubs', () => { + // Increase Jest timeout for slower CI/initialization (DB + Nest app init) + // Default is 5000ms which is often too small for integration tests. + jest.setTimeout(30000); + let app: INestApplication; let donationRepo: Repository | null = null; diff --git a/package.json b/package.json index 76a7740..d55dee1 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-router-dom": "^6.15.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", + "sqlite3": "^5.1.7", "supertest": "^7.1.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/yarn.lock b/yarn.lock index e823b9e..66eaecf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1966,6 +1966,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -2620,6 +2625,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@nrwl/cypress@16.10.0": version "16.10.0" resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-16.10.0.tgz#89a073cdc4e87b57eb10b98d212e1e96bb4a33d3" @@ -3601,6 +3622,11 @@ "@testing-library/dom" "^9.0.0" "@types/react-dom" "^18.0.0" +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -4522,6 +4548,11 @@ abab@^2.0.5, abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -4555,13 +4586,20 @@ address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" +agentkeepalive@^4.1.3: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4701,11 +4739,24 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== +"aproba@^1.0.3 || ^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.1.0.tgz#75500a190313d95c64e871e7e4284c6ac219f0b1" + integrity sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew== + arch@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -5172,6 +5223,13 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -5379,6 +5437,30 @@ cac@^6.7.14: resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + 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" + cachedir@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" @@ -5536,6 +5618,11 @@ chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -5708,6 +5795,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colord@^2.9.1: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -5834,6 +5926,11 @@ consola@^2.15.0: resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== +console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -6206,7 +6303,7 @@ debug@^4, debug@^4.3.1: dependencies: ms "^2.1.3" -debug@^4.3.7: +debug@^4.3.3, debug@^4.3.7: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -6218,6 +6315,13 @@ decimal.js@^10.4.3: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" @@ -6259,6 +6363,11 @@ deep-equal@^2.0.5: which-collection "^1.0.1" which-typed-array "^1.1.9" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -6311,6 +6420,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -6331,6 +6445,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + detect-newline@^3.0.0, detect-newline@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -6551,6 +6670,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -6586,6 +6712,16 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errno@^0.1.1: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -7090,6 +7226,11 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expect@30.1.2: version "30.1.2" resolved "https://registry.yarnpkg.com/expect/-/expect-30.1.2.tgz#094909c2443f76b9e208fafac4a315aaaf924580" @@ -7294,6 +7435,11 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filelist@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -7568,6 +7714,20 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + 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" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -7656,6 +7816,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -7889,6 +8054,11 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + has@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" @@ -7952,6 +8122,11 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-cache-semantics@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -7983,6 +8158,15 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -8040,7 +8224,7 @@ http-signature@~1.3.6: jsprim "^2.0.2" sshpk "^1.14.1" -https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -8063,6 +8247,13 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" @@ -8075,7 +8266,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -8150,6 +8341,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -8173,6 +8369,11 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + inquirer@8.2.4: version "8.2.4" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" @@ -8229,6 +8430,11 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +ip-address@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed" + integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -8375,6 +8581,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" @@ -9926,6 +10137,28 @@ make-error@1.x, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + 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" + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -10028,6 +10261,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -10089,12 +10327,51 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^3.0.0: +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -10106,6 +10383,11 @@ minipass@^4.2.4: resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" @@ -10116,7 +10398,7 @@ minipass@^7.1.2: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -minizlib@^2.1.1: +minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -10124,6 +10406,11 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^0.5.4, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -10131,7 +10418,7 @@ mkdirp@^0.5.4, mkdirp@^0.5.6: dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -10166,7 +10453,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -10211,6 +10498,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== + napi-postinstall@^0.3.0: version "0.3.3" resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz#93d045c6b576803ead126711d3093995198c6eb9" @@ -10235,6 +10527,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^0.6.2: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -10248,6 +10545,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-abi@^3.3.0: + version "3.79.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.79.0.tgz#a5d9129828898f99366ee39c1d9c6ef74f2f1076" + integrity sha512-Pr/5KdBQGG8TirdkS0qN3B+f3eo8zTOfZQWAxHoJqopMz2/uvRnG+S4fWu/6AZxKei2CP2p/psdQ5HFC2Ap5BA== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -10258,6 +10562,11 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-emoji@1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -10282,6 +10591,22 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + 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" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -10318,6 +10643,13 @@ nodemon@^3.0.1: touch "^3.1.0" undefsafe "^2.0.5" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -10352,6 +10684,16 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -11282,6 +11624,24 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== + 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" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -11339,6 +11699,19 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -11479,6 +11852,16 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -11554,7 +11937,7 @@ readable-stream@^2.0.1, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -11734,6 +12117,11 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -11996,6 +12384,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + set-function-length@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" @@ -12123,6 +12516,20 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-update-notifier@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" @@ -12175,6 +12582,11 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -12192,6 +12604,23 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.8.7" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + dependencies: + ip-address "^10.0.1" + smart-buffer "^4.2.0" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -12273,6 +12702,18 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sqlite3@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7" + integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== + 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" + sshpk@^1.14.1: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" @@ -12288,6 +12729,13 @@ sshpk@^1.14.1: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -12340,7 +12788,7 @@ string-length@^4.0.1, string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12453,6 +12901,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + strip-literal@^1.0.1: version "1.3.0" resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" @@ -12599,7 +13052,17 @@ tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-stream@~2.2.0: +tar-fs@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -12622,6 +13085,18 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terser-webpack-plugin@^5.3.3, terser-webpack-plugin@^5.3.7: version "5.3.9" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" @@ -13090,6 +13565,20 @@ union@~0.5.0: dependencies: qs "^6.4.0" +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -13528,7 +14017,7 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -13543,6 +14032,13 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" +wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + windows-release@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" From d0f5d02c50d10250e46eb8b5da005db4f1d5f9ac Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:12:13 -0400 Subject: [PATCH 05/23] Using enums, testing for umbrella db throws error, GET cases if db is empty --- apps/backend/test/donations.e2e-spec.ts | 184 +++++++++++++++++++----- 1 file changed, 149 insertions(+), 35 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index f3f8b31..41a5ba0 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import request from 'supertest'; -import { Donation } from '../src/donations/donation.entity'; +import { Donation, DonationType, RecurringInterval } from '../src/donations/donation.entity'; import { User } from '../src/users/user.entity'; describe('Donations (e2e) - expanded stubs', () => { @@ -57,7 +57,7 @@ describe('Donations (e2e) - expanded stubs', () => { email: 'jane.doe@example.com', amount: 50, isAnonymous: false, - donationType: 'ONE_TIME', + donationType: DonationType.ONE_TIME, }; const recurringPayload = { @@ -66,10 +66,77 @@ describe('Donations (e2e) - expanded stubs', () => { email: 'alice.smith@example.com', amount: 25, isAnonymous: false, - donationType: 'RECURRING', - recurringInterval: 'MONTHLY', + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, }; + // Helper to mock all common repository methods (reads and writes) to throw once. + // This ensures any call into the repository will reject and can be used to test + // global DB failure handling without needing to know which method will be invoked. + function mockAllRepoMethodsThrow(repo: Repository, message = 'Simulated DB failure') { + const spies: Array = []; + + // Common repo method names to mock + const methodNames = [ + 'find', + 'findOne', + 'findOneBy', + 'findBy', + 'findAndCount', + 'save', + 'insert', + 'update', + 'delete', + 'remove', + 'softRemove', + 'softDelete', + 'clear', + 'count', + 'query', + 'create', + ]; + + for (const name of methodNames) { + // dynamic access for testing - repository methods are dynamic and tested at runtime + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fn = (repo as any)[name]; + if (typeof fn === 'function') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + spies.push(jest.spyOn(repo as any, name).mockRejectedValueOnce(new Error(message))); + } + } + + // Also mock createQueryBuilder if present + let qbSpy: jest.SpyInstance | null = null; + if ((repo as unknown as Record).createQueryBuilder) { + // The query-builder stub is only used to mock chainable methods in tests. + // We scope-disable the explicit any rule for this mock object only. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const qb = { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + orWhere: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getMany: jest.fn().mockRejectedValueOnce(new Error(message)), + getManyAndCount: jest.fn().mockRejectedValueOnce(new Error(message)), + getRawOne: jest.fn().mockRejectedValueOnce(new Error(message)), + getRawMany: jest.fn().mockRejectedValueOnce(new Error(message)), + } as unknown as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + qbSpy = jest.spyOn(repo as any, 'createQueryBuilder').mockReturnValue(qb as any); + } + + return () => { + for (const s of spies) s.mockRestore(); + if (qbSpy) qbSpy.mockRestore(); + }; + } + // Small smoke test to ensure supertest and the app wiring are working. it('smoke: GET / (should 404 or 200 depending on routes)', async () => { const res = await request(app.getHttpServer()).get('/'); @@ -78,7 +145,7 @@ describe('Donations (e2e) - expanded stubs', () => { }); describe('POST /api/donations', () => { - it.skip('POST /api/donations - successful one-time donation creation', async () => { + it.skip('Successfuly commits a one-time donation creation', async () => { // Arrange const payload = { ...oneTimePayload }; // prevent unused-var/unused-local complaints while this test is skipped @@ -86,37 +153,37 @@ describe('Donations (e2e) - expanded stubs', () => { void donationRepo; // Act (example supertest call) - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(201); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); // Assert response shape (example) - // expect(res.body).toHaveProperty('id'); - // expect(res.body.amount).toBe(payload.amount); + expect(res.body).toHaveProperty('id'); + expect(res.body.amount).toBe(payload.amount); // Verify DB state (example) - // const created = await donationRepo!.findOne({ where: { email: payload.email } }); - // expect(created).toBeDefined(); - // expect(created!.amount).toBe(payload.amount); + const created = await donationRepo!.findOne({ where: { email: payload.email } }); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); }); - it.skip('POST /api/donations - successful recurring donation with interval', async () => { + it.skip('Successful creates a recurring donation with interval', async () => { const payload = { ...recurringPayload }; void payload; void donationRepo; // Example supertest + assertions (commented until route exists) - // const res = await request(app.getHttpServer()) - // .post('/api/donations') - // .send(payload) - // .expect(201); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); - // expect(res.body.donationType).toBe('RECURRING'); - // expect(res.body.recurringInterval).toBe('MONTHLY'); + expect(res.body.donationType).toBe('RECURRING'); + expect(res.body.recurringInterval).toBe('MONTHLY'); }); - it('Negative amount returns 400', async () => { + it('rejects a negative amount (returns 400)', async () => { const payload = { ...oneTimePayload, amount: -10 }; const res = await request(app.getHttpServer()) @@ -128,7 +195,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('Invalid email format returns 400', async () => { + it('rejects an invalid email format amount (returns 400)', async () => { const payload = { ...oneTimePayload, email: 'not-an-email' }; const res = await request(app.getHttpServer()) @@ -140,7 +207,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('Recurring without interval returns 400', async () => { + it('rejects a negative amount (returns 400)', async () => { const payload: Partial = { ...recurringPayload }; delete payload.recurringInterval; @@ -153,7 +220,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('One-time with interval returns 400', async () => { + it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; const res = await request(app.getHttpServer()) @@ -165,7 +232,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('POST /api/donations - when DB save throws, returns 500', async () => { + it('throws 500 server error if the database errors', async () => { // Simulate a DB failure by spying on the repository save method. if (!donationRepo) { // If the repo isn't available, fail the test to make the problem visible @@ -191,20 +258,45 @@ describe('Donations (e2e) - expanded stubs', () => { }); describe('GET /api/donations/public', () => { - it.skip('GET /api/donations/public - returns only non-anonymous donations', async () => { + it.skip('returns only non-anonymous donations', async () => { // Example setup: insert public and anonymous donations, then call endpoint - // await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); - // await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); + await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); + await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); - // const res = await request(app.getHttpServer()) - // .get('/api/donations/public') - // .expect(200); + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); + }); + + it.skip('returns no donations if there are none in the database', async () => { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); + }); - // expect(Array.isArray(res.body)).toBe(true); - // expect(res.body.every((d: any) => d.isAnonymous === false)).toBe(true); + it('throws 500 server error if the database errors', async () => { + // Simulate DB find/query failures by mocking find variants to throw. + if (!donationRepo) throw new Error('donationRepo not available for mocking'); + + const restore = mockAllRepoMethodsThrow(donationRepo); + try { + const res = await request(app.getHttpServer()).get('/api/donations/public').expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + restore(); + } }); + }); - it.skip('GET /api/donations/stats - returns correct total and count', async () => { + describe('GET /api/donations/stats', () => { + it.skip('successfully returns the correct total and count', async () => { // Example: seed two donations and verify totals endpoint // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); @@ -215,5 +307,27 @@ describe('Donations (e2e) - expanded stubs', () => { // expect(res.body).toEqual({ total: 25, count: 2 }); }); + + it.skip('successfully returns the correct total and count even if the database is empty', async () => { + // const res = await request(app.getHttpServer()) + // .get('/api/donations/stats') + // .expect(200); + + // expect(res.body).toEqual({ total: 0, count: 0 }); + }); + + it('throws 500 server error if the database errors', async () => { + // Simulate DB find/query failures by mocking find variants to throw. + if (!donationRepo) throw new Error('donationRepo not available for mocking'); + + const restore = mockAllRepoMethodsThrow(donationRepo); + try { + const res = await request(app.getHttpServer()).get('/api/donations/stats').expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + restore(); + } + }); }); }); From 68724c63c5d43f993408f8f1ca110a30c2924bd8 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:07:53 -0400 Subject: [PATCH 06/23] Passing success tests --- apps/backend/test/donations.e2e-spec.ts | 251 ++++++++++-------------- 1 file changed, 109 insertions(+), 142 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 41a5ba0..bae5198 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -1,11 +1,28 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { getRepositoryToken } from '@nestjs/typeorm'; import request from 'supertest'; -import { Donation, DonationType, RecurringInterval } from '../src/donations/donation.entity'; -import { User } from '../src/users/user.entity'; +import { DonationType, RecurringInterval, DonationStatus } from '../src/donations/donation.entity'; +import { DonationsController } from '../src/donations/donations.controller'; +import { DonationsService } from '../src/donations/donations.service'; +import { DonationsRepository } from '../src/donations/donations.repository'; +// import { User } from '../src/users/user.entity'; + +interface TestDonation { + id: number; + firstName: string; + lastName: string; + email: string; + amount: number; + isAnonymous: boolean; + donationType: DonationType; + recurringInterval: RecurringInterval | null; + dedicationMessage?: string | null; + showDedicationPublicly: boolean; + status: DonationStatus; + createdAt: Date; + updatedAt: Date; + transactionId?: string | null; +} describe('Donations (e2e) - expanded stubs', () => { // Increase Jest timeout for slower CI/initialization (DB + Nest app init) @@ -13,35 +30,64 @@ describe('Donations (e2e) - expanded stubs', () => { jest.setTimeout(30000); let app: INestApplication; - let donationRepo: Repository | null = null; + // We use an in-memory array to simulate stored donations for controller tests + let inMemoryDonations: TestDonation[] = []; + let nextId = 1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockService: any; beforeAll(async () => { - // Create a testing module using an in-memory SQLite database so tests - // exercise TypeORM and the repository layer without touching Postgres. + // Create a testing module that instantiates the DonationsController but + // uses a simple in-memory mock for the DonationsService so tests don't + // depend on TypeORM behavior during controller-level validation tests. + inMemoryDonations = []; + nextId = 1; + + mockService = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create: jest.fn(async (request: any) => { + const now = new Date(); + const donation = { + id: nextId++, + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + amount: request.amount, + isAnonymous: request.isAnonymous ?? false, + donationType: + request.donationType === 'one_time' ? DonationType.ONE_TIME : DonationType.RECURRING, + recurringInterval: request.recurringInterval ?? null, + dedicationMessage: request.dedicationMessage ?? undefined, + showDedicationPublicly: request.showDedicationPublicly ?? false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + }; + inMemoryDonations.push(donation); + return donation; + }), + findPublic: jest.fn(async (limit?: number) => { + return inMemoryDonations.filter((d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous).slice(0, limit ?? 50); + }), + getTotalDonations: jest.fn(async () => { + const succeeded = inMemoryDonations.filter((d) => d.status === DonationStatus.SUCCEEDED); + const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); + return { total, count: succeeded.length }; + }), + }; + const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ - TypeOrmModule.forRoot({ - type: 'sqlite', - database: ':memory:', - dropSchema: true, - entities: [Donation, User], - synchronize: true, - logging: false, - }), - TypeOrmModule.forFeature([Donation]), + controllers: [DonationsController], + providers: [ + { provide: DonationsService, useValue: mockService }, + { provide: DonationsRepository, useValue: {} }, ], }).compile(); app = moduleFixture.createNestApplication(); + // Match runtime API prefix used by the real application + app.setGlobalPrefix('api'); await app.init(); - - // Repository is available for DB assertions. When tests are enabled - // (not skipped) you can use this to verify DB state after POSTs. - try { - donationRepo = moduleFixture.get>(getRepositoryToken(Donation)); - } catch (e) { - donationRepo = null; - } }); afterAll(async () => { @@ -70,72 +116,9 @@ describe('Donations (e2e) - expanded stubs', () => { recurringInterval: RecurringInterval.MONTHLY, }; - // Helper to mock all common repository methods (reads and writes) to throw once. - // This ensures any call into the repository will reject and can be used to test - // global DB failure handling without needing to know which method will be invoked. - function mockAllRepoMethodsThrow(repo: Repository, message = 'Simulated DB failure') { - const spies: Array = []; - - // Common repo method names to mock - const methodNames = [ - 'find', - 'findOne', - 'findOneBy', - 'findBy', - 'findAndCount', - 'save', - 'insert', - 'update', - 'delete', - 'remove', - 'softRemove', - 'softDelete', - 'clear', - 'count', - 'query', - 'create', - ]; - - for (const name of methodNames) { - // dynamic access for testing - repository methods are dynamic and tested at runtime - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fn = (repo as any)[name]; - if (typeof fn === 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - spies.push(jest.spyOn(repo as any, name).mockRejectedValueOnce(new Error(message))); - } - } - - // Also mock createQueryBuilder if present - let qbSpy: jest.SpyInstance | null = null; - if ((repo as unknown as Record).createQueryBuilder) { - // The query-builder stub is only used to mock chainable methods in tests. - // We scope-disable the explicit any rule for this mock object only. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const qb = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - orWhere: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - addSelect: jest.fn().mockReturnThis(), - getMany: jest.fn().mockRejectedValueOnce(new Error(message)), - getManyAndCount: jest.fn().mockRejectedValueOnce(new Error(message)), - getRawOne: jest.fn().mockRejectedValueOnce(new Error(message)), - getRawMany: jest.fn().mockRejectedValueOnce(new Error(message)), - } as unknown as Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - qbSpy = jest.spyOn(repo as any, 'createQueryBuilder').mockReturnValue(qb as any); - } - - return () => { - for (const s of spies) s.mockRestore(); - if (qbSpy) qbSpy.mockRestore(); - }; - } + // Note: repo-level mocking helpers remo + // ved — controller tests use the mocked + // DonationsService above to simulate DB errors where needed. // Small smoke test to ensure supertest and the app wiring are working. it('smoke: GET / (should 404 or 200 depending on routes)', async () => { @@ -145,12 +128,11 @@ describe('Donations (e2e) - expanded stubs', () => { }); describe('POST /api/donations', () => { - it.skip('Successfuly commits a one-time donation creation', async () => { + it('Successfuly commits a one-time donation creation', async () => { // Arrange const payload = { ...oneTimePayload }; - // prevent unused-var/unused-local complaints while this test is skipped - void payload; - void donationRepo; + // prevent unused-var/unused-local complaints + void payload; // Act (example supertest call) const res = await request(app.getHttpServer()) @@ -162,16 +144,15 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('id'); expect(res.body.amount).toBe(payload.amount); - // Verify DB state (example) - const created = await donationRepo!.findOne({ where: { email: payload.email } }); - expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); + // Verify in-memory state recorded by the mocked service + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); }); - it.skip('Successful creates a recurring donation with interval', async () => { + it('Successful creates a recurring donation with interval', async () => { const payload = { ...recurringPayload }; - void payload; - void donationRepo; + void payload; // Example supertest + assertions (commented until route exists) const res = await request(app.getHttpServer()) @@ -179,8 +160,8 @@ describe('Donations (e2e) - expanded stubs', () => { .send(payload) .expect(201); - expect(res.body.donationType).toBe('RECURRING'); - expect(res.body.recurringInterval).toBe('MONTHLY'); + expect(res.body.donationType).toBe('recurring'); + expect(res.body.recurringInterval).toBe('monthly'); }); it('rejects a negative amount (returns 400)', async () => { @@ -233,35 +214,25 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('throws 500 server error if the database errors', async () => { - // Simulate a DB failure by spying on the repository save method. - if (!donationRepo) { - // If the repo isn't available, fail the test to make the problem visible - throw new Error('donationRepo not available for mocking'); - } - + // Simulate a DB failure by making the mocked service throw + mockService.create.mockRejectedValueOnce(new Error('Simulated DB failure')); const payload = { ...oneTimePayload }; - - const saveSpy = jest.spyOn(donationRepo, 'save').mockRejectedValueOnce(new Error('Simulated DB failure')); try { - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(500); - - // Expect standard Nest error shape for unhandled errors + const res = await request(app.getHttpServer()).post('/api/donations').send(payload).expect(500); expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { - saveSpy.mockRestore(); + mockService.create.mockReset(); } }); }); describe('GET /api/donations/public', () => { - it.skip('returns only non-anonymous donations', async () => { + it('returns only non-anonymous donations', async () => { // Example setup: insert public and anonymous donations, then call endpoint - await donationRepo!.save({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false }); - await donationRepo!.save({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true }); + const now = new Date(); + inMemoryDonations.push({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false, status: DonationStatus.SUCCEEDED, createdAt: now, updatedAt: now, id: nextId++ } as TestDonation); + inMemoryDonations.push({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true, status: DonationStatus.SUCCEEDED, createdAt: now, updatedAt: now, id: nextId++ } as TestDonation); const res = await request(app.getHttpServer()) .get('/api/donations/public') @@ -271,7 +242,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); }); - it.skip('returns no donations if there are none in the database', async () => { + it('returns no donations if there are none in the database', async () => { const res = await request(app.getHttpServer()) .get('/api/donations/public') .expect(200); @@ -281,22 +252,20 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by mocking find variants to throw. - if (!donationRepo) throw new Error('donationRepo not available for mocking'); - - const restore = mockAllRepoMethodsThrow(donationRepo); + // Simulate DB find/query failures by making the mocked service throw + mockService.findPublic.mockRejectedValueOnce(new Error('Simulated DB failure')); try { const res = await request(app.getHttpServer()).get('/api/donations/public').expect(500); expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { - restore(); + mockService.findPublic.mockReset(); } }); }); describe('GET /api/donations/stats', () => { - it.skip('successfully returns the correct total and count', async () => { + it('successfully returns the correct total and count', async () => { // Example: seed two donations and verify totals endpoint // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); @@ -308,7 +277,7 @@ describe('Donations (e2e) - expanded stubs', () => { // expect(res.body).toEqual({ total: 25, count: 2 }); }); - it.skip('successfully returns the correct total and count even if the database is empty', async () => { + it('successfully returns the correct total and count even if the database is empty', async () => { // const res = await request(app.getHttpServer()) // .get('/api/donations/stats') // .expect(200); @@ -317,17 +286,15 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by mocking find variants to throw. - if (!donationRepo) throw new Error('donationRepo not available for mocking'); - - const restore = mockAllRepoMethodsThrow(donationRepo); - try { - const res = await request(app.getHttpServer()).get('/api/donations/stats').expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - restore(); - } + // Simulate DB find/query failures by making the mocked service throw + mockService.getTotalDonations.mockRejectedValueOnce(new Error('Simulated DB failure')); + try { + const res = await request(app.getHttpServer()).get('/api/donations/stats').expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.getTotalDonations.mockReset(); + } }); }); }); From 05ba4528d789cfef03325600f0b5bfbeff35c32f Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:13:46 -0400 Subject: [PATCH 07/23] Test cases for missing responses and expanded DTO validation --- apps/backend/test/donations.e2e-spec.ts | 446 ++++++++++++++++++++++-- 1 file changed, 412 insertions(+), 34 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index bae5198..8ef036a 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -116,9 +116,148 @@ describe('Donations (e2e) - expanded stubs', () => { recurringInterval: RecurringInterval.MONTHLY, }; - // Note: repo-level mocking helpers remo - // ved — controller tests use the mocked - // DonationsService above to simulate DB errors where needed. + // Reusable seeds and helpers for in-memory donation construction + const donationASeed: Pick = { + email: 'a@example.com', + amount: 10, + }; + const donationBSeed: Pick = { + email: 'b@example.com', + amount: 15, + }; + + function buildTestDonation( + seed: Pick, + now: Date, + overrides: Partial = {}, + ): TestDonation { + return { + id: nextId++, + firstName: oneTimePayload.firstName, + lastName: oneTimePayload.lastName, + email: seed.email, + amount: seed.amount, + isAnonymous: oneTimePayload.isAnonymous, + donationType: oneTimePayload.donationType, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + transactionId: null, + ...overrides, + }; + } + + // ---------- DTO shape validators ---------- + const isISODateString = (value: unknown): boolean => { + if (typeof value !== 'string') return false; + const dt = new Date(value); + return !Number.isNaN(dt.getTime()); + }; + + const expectDonationResponseDtoShape = ( + obj: Record, + expected: { + donationType: DonationType; + recurringInterval?: RecurringInterval | null; + email?: string; + firstName?: string; + lastName?: string; + transactionIdPresent?: boolean; + }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.firstName).toBe('string'); + expect(typeof obj.lastName).toBe('string'); + expect(typeof obj.email).toBe('string'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect(obj.donationType).toBe(expected.donationType); + if (expected.recurringInterval) { + expect(obj.recurringInterval).toBe(expected.recurringInterval); + } else { + // Could be null or undefined depending on mapper; both acceptable + expect(['undefined', 'string']).toContain(typeof obj.recurringInterval as string); + if (typeof obj.recurringInterval === 'string') { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval); + } + } + // dedicationMessage is optional + if (obj.dedicationMessage !== undefined && obj.dedicationMessage !== null) { + expect(typeof obj.dedicationMessage).toBe('string'); + } + expect(typeof obj.showDedicationPublicly).toBe('boolean'); + expect([ + 'pending', + 'succeeded', + 'failed', + 'cancelled', + ]).toContain(obj.status as string); + expect(isISODateString(String(obj.createdAt))).toBe(true); + expect(isISODateString(String(obj.updatedAt))).toBe(true); + if (expected.transactionIdPresent) { + expect(typeof obj.transactionId).toBe('string'); + } else { + // Can be absent or null + expect(['undefined', 'string']).toContain(typeof obj.transactionId as string); + } + }; + + const expectPublicDonationDtoShape = ( + obj: Record, + opts: { anonymous: boolean; hasDedication: boolean }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect([ + DonationType.ONE_TIME, + DonationType.RECURRING, + ]).toContain(obj.donationType as DonationType); + if (obj.recurringInterval !== undefined && obj.recurringInterval !== null) { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval as RecurringInterval); + } + expect([ + 'pending', + 'succeeded', + 'failed', + 'cancelled', + ]).toContain(obj.status as string); + expect(isISODateString(String(obj.createdAt))).toBe(true); + + if (opts.anonymous) { + expect(obj).not.toHaveProperty('donorName'); + } else { + expect(typeof obj.donorName).toBe('string'); + expect((obj.donorName as string).length).toBeGreaterThan(0); + } + + if (opts.hasDedication) { + expect(typeof obj.dedicationMessage).toBe('string'); + } else { + expect(obj).not.toHaveProperty('dedicationMessage'); + } + + // Ensure sensitive fields are not leaked in public DTO + expect(obj).not.toHaveProperty('email'); + expect(obj).not.toHaveProperty('firstName'); + expect(obj).not.toHaveProperty('lastName'); + expect(obj).not.toHaveProperty('transactionId'); + }; // Small smoke test to ensure supertest and the app wiring are working. it('smoke: GET / (should 404 or 200 depending on routes)', async () => { @@ -131,8 +270,6 @@ describe('Donations (e2e) - expanded stubs', () => { it('Successfuly commits a one-time donation creation', async () => { // Arrange const payload = { ...oneTimePayload }; - // prevent unused-var/unused-local complaints - void payload; // Act (example supertest call) const res = await request(app.getHttpServer()) @@ -141,18 +278,20 @@ describe('Donations (e2e) - expanded stubs', () => { .expect(201); // Assert response shape (example) - expect(res.body).toHaveProperty('id'); + expectDonationResponseDtoShape(res.body, { + donationType: DonationType.ONE_TIME, + recurringInterval: null, + }); expect(res.body.amount).toBe(payload.amount); - // Verify in-memory state recorded by the mocked service - const created = inMemoryDonations.find((d) => d.email === payload.email); - expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); + // Verify in-memory state recorded by the mocked service + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); }); - it('Successful creates a recurring donation with interval', async () => { + it('Successfuly creates a recurring donation with interval', async () => { const payload = { ...recurringPayload }; - void payload; // Example supertest + assertions (commented until route exists) const res = await request(app.getHttpServer()) @@ -160,8 +299,10 @@ describe('Donations (e2e) - expanded stubs', () => { .send(payload) .expect(201); - expect(res.body.donationType).toBe('recurring'); - expect(res.body.recurringInterval).toBe('monthly'); + expectDonationResponseDtoShape(res.body, { + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }); }); it('rejects a negative amount (returns 400)', async () => { @@ -188,18 +329,20 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('rejects a negative amount (returns 400)', async () => { + it('accepts a recurring donation even if recurringInterval is missing (DTO allows optional)', async () => { const payload: Partial = { ...recurringPayload }; delete payload.recurringInterval; - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); + expectDonationResponseDtoShape(res.body, { + donationType: DonationType.RECURRING, + recurringInterval: undefined, }); + }); it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; @@ -222,9 +365,153 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { - mockService.create.mockReset(); + mockService.create.mockClear(); } }); + + it('gracefully rejects a payload that is missing the first name', async () => { + // Note: using the enum values so the controller validation sees correct types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.firstName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('firstName'.toLowerCase()); + }); + + it('gracefully rejects a payload that is missing the last name', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.lastName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('lastName'.toLowerCase()); + }); + + it('gracefully rejects a payload that is missing the email', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.email; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('email'.toLowerCase()); + }); + + it('gracefully rejects a payload that is missing the amount', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.amount; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('amount'.toLowerCase()); + }); + + it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.isAnonymous + + // Act (example supertest call) + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); + + // Assert response shape (example) + expect(res.body).toHaveProperty('id'); + expect(res.body.amount).toBe(payload.amount); + + // Verify in-memory state recorded by the mocked service + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); + }); + + it('gracefully rejects a payload that is missing the donationType', async () => { + // Note: using the enum values so the controller validation sees correct types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.donationType; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + }); + + it('gracefully rejects a payload that is missing the donationType', async () => { + // Note: using the enum values so the controller validation sees correct types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.donationType; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + }); + + it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { + // Note: using the enum values so the controller validation sees correct types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, recurring: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('recurring'.toLowerCase()); + }); + + it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { + // Note: using the enum values so the controller validation sees correct types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, donationType: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + }); }); describe('GET /api/donations/public', () => { @@ -240,6 +527,10 @@ describe('Donations (e2e) - expanded stubs', () => { expect(Array.isArray(res.body)).toBe(true); expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); + // Validate public DTO shape for the first item + if (res.body.length > 0) { + expectPublicDonationDtoShape(res.body[0], { anonymous: false, hasDedication: false }); + } }); it('returns no donations if there are none in the database', async () => { @@ -251,6 +542,36 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); }); + it('includes donorName and dedicationMessage when allowed, and hides PII', async () => { + inMemoryDonations.length = 0; + const now = new Date(); + // Visible donor with dedication shown + inMemoryDonations.push( + buildTestDonation({ email: 'pub1@example.com', amount: 42 }, now, { + isAnonymous: false, + showDedicationPublicly: true, + dedicationMessage: 'For the kids', + }), + ); + // Anonymous donor should not have donorName + inMemoryDonations.push( + buildTestDonation({ email: 'anon1@example.com', amount: 12 }, now, { + isAnonymous: true, + showDedicationPublicly: false, + }), + ); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(res.body).toHaveLength(1); // anonymous filtered out by service + const item = res.body[0]; + expectPublicDonationDtoShape(item, { anonymous: false, hasDedication: true }); + expect(item.donorName).toBe('Jane Doe'); // based on default names in builder + expect(item.dedicationMessage).toBe('For the kids'); + }); + it('throws 500 server error if the database errors', async () => { // Simulate DB find/query failures by making the mocked service throw mockService.findPublic.mockRejectedValueOnce(new Error('Simulated DB failure')); @@ -259,30 +580,35 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { - mockService.findPublic.mockReset(); + mockService.findPublic.mockClear(); } }); + + }); describe('GET /api/donations/stats', () => { it('successfully returns the correct total and count', async () => { // Example: seed two donations and verify totals endpoint - // await donationRepo!.save({ ...oneTimePayload, email: 'a@example.com', amount: 10 }); - // await donationRepo!.save({ ...oneTimePayload, email: 'b@example.com', amount: 15 }); + inMemoryDonations.length = 0; // reset + const now = new Date(); + inMemoryDonations.push(buildTestDonation(donationASeed, now)); + inMemoryDonations.push(buildTestDonation(donationBSeed, now)); - // const res = await request(app.getHttpServer()) - // .get('/api/donations/stats') - // .expect(200); + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); - // expect(res.body).toEqual({ total: 25, count: 2 }); + expect(res.body).toEqual({ total: 25, count: 2 }); }); it('successfully returns the correct total and count even if the database is empty', async () => { - // const res = await request(app.getHttpServer()) - // .get('/api/donations/stats') - // .expect(200); + inMemoryDonations.length = 0; // ensure empty + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); - // expect(res.body).toEqual({ total: 0, count: 0 }); + expect(res.body).toEqual({ total: 0, count: 0 }); }); it('throws 500 server error if the database errors', async () => { @@ -293,8 +619,60 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { - mockService.getTotalDonations.mockReset(); + mockService.getTotalDonations.mockClear(); } }); }); + + // Attribute-only verification (no content validation) + describe('GET response attribute presence only', () => { + it('GET /api/donations/public returns items with expected keys', async () => { + inMemoryDonations.length = 0; + const now = new Date(); + // Create a non-anonymous donation that can include donorName and dedication + inMemoryDonations.push( + buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { + isAnonymous: false, + showDedicationPublicly: true, + dedicationMessage: 'Nice work', + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }), + ); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + if (res.body.length > 0) { + const item = res.body[0]; + const keys = Object.keys(item); + const required = [ + 'id', + 'amount', + 'isAnonymous', + 'donationType', + 'status', + 'createdAt', + ]; + const optional = ['donorName', 'recurringInterval', 'dedicationMessage']; + const allowed = [...required, ...optional]; + + // Has all required keys + required.forEach((k) => expect(keys).toContain(k)); + // Does not include unexpected keys + keys.forEach((k) => expect(allowed).toContain(k)); + } + }); + + it('GET /api/donations/stats returns object with total and count', async () => { + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toHaveProperty('total'); + expect(res.body).toHaveProperty('count'); + }); + }); }); From 7d4373c1aff870d270b6b70875617a74f03ef1bd Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:23:14 -0400 Subject: [PATCH 08/23] reformatting --- apps/backend/test/donations.e2e-spec.ts | 301 ++++++++++++++---------- 1 file changed, 177 insertions(+), 124 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 8ef036a..87c2fb0 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -1,7 +1,11 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; -import { DonationType, RecurringInterval, DonationStatus } from '../src/donations/donation.entity'; +import { + DonationType, + RecurringInterval, + DonationStatus, +} from '../src/donations/donation.entity'; import { DonationsController } from '../src/donations/donations.controller'; import { DonationsService } from '../src/donations/donations.service'; import { DonationsRepository } from '../src/donations/donations.repository'; @@ -55,7 +59,9 @@ describe('Donations (e2e) - expanded stubs', () => { amount: request.amount, isAnonymous: request.isAnonymous ?? false, donationType: - request.donationType === 'one_time' ? DonationType.ONE_TIME : DonationType.RECURRING, + request.donationType === 'one_time' + ? DonationType.ONE_TIME + : DonationType.RECURRING, recurringInterval: request.recurringInterval ?? null, dedicationMessage: request.dedicationMessage ?? undefined, showDedicationPublicly: request.showDedicationPublicly ?? false, @@ -67,14 +73,20 @@ describe('Donations (e2e) - expanded stubs', () => { return donation; }), findPublic: jest.fn(async (limit?: number) => { - return inMemoryDonations.filter((d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous).slice(0, limit ?? 50); + return inMemoryDonations + .filter( + (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, + ) + .slice(0, limit ?? 50); }), getTotalDonations: jest.fn(async () => { - const succeeded = inMemoryDonations.filter((d) => d.status === DonationStatus.SUCCEEDED); + const succeeded = inMemoryDonations.filter( + (d) => d.status === DonationStatus.SUCCEEDED, + ); const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); return { total, count: succeeded.length }; }), - }; + }; const moduleFixture: TestingModule = await Test.createTestingModule({ controllers: [DonationsController], @@ -179,7 +191,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(obj.recurringInterval).toBe(expected.recurringInterval); } else { // Could be null or undefined depending on mapper; both acceptable - expect(['undefined', 'string']).toContain(typeof obj.recurringInterval as string); + expect(['undefined', 'string']).toContain( + typeof obj.recurringInterval as string, + ); if (typeof obj.recurringInterval === 'string') { expect([ RecurringInterval.WEEKLY, @@ -195,19 +209,18 @@ describe('Donations (e2e) - expanded stubs', () => { expect(typeof obj.dedicationMessage).toBe('string'); } expect(typeof obj.showDedicationPublicly).toBe('boolean'); - expect([ - 'pending', - 'succeeded', - 'failed', - 'cancelled', - ]).toContain(obj.status as string); + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); expect(isISODateString(String(obj.createdAt))).toBe(true); expect(isISODateString(String(obj.updatedAt))).toBe(true); if (expected.transactionIdPresent) { expect(typeof obj.transactionId).toBe('string'); } else { // Can be absent or null - expect(['undefined', 'string']).toContain(typeof obj.transactionId as string); + expect(['undefined', 'string']).toContain( + typeof obj.transactionId as string, + ); } }; @@ -218,10 +231,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(typeof obj.id).toBe('number'); expect(typeof obj.amount).toBe('number'); expect(typeof obj.isAnonymous).toBe('boolean'); - expect([ - DonationType.ONE_TIME, - DonationType.RECURRING, - ]).toContain(obj.donationType as DonationType); + expect([DonationType.ONE_TIME, DonationType.RECURRING]).toContain( + obj.donationType as DonationType, + ); if (obj.recurringInterval !== undefined && obj.recurringInterval !== null) { expect([ RecurringInterval.WEEKLY, @@ -231,12 +243,9 @@ describe('Donations (e2e) - expanded stubs', () => { RecurringInterval.ANNUALLY, ]).toContain(obj.recurringInterval as RecurringInterval); } - expect([ - 'pending', - 'succeeded', - 'failed', - 'cancelled', - ]).toContain(obj.status as string); + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); expect(isISODateString(String(obj.createdAt))).toBe(true); if (opts.anonymous) { @@ -265,26 +274,22 @@ describe('Donations (e2e) - expanded stubs', () => { // We don't make assumptions about the root route; assert we get a response expect([200, 404]).toContain(res.status); }); - + describe('POST /api/donations', () => { it('Successfuly commits a one-time donation creation', async () => { - // Arrange const payload = { ...oneTimePayload }; - // Act (example supertest call) const res = await request(app.getHttpServer()) .post('/api/donations') .send(payload) .expect(201); - // Assert response shape (example) expectDonationResponseDtoShape(res.body, { donationType: DonationType.ONE_TIME, recurringInterval: null, }); expect(res.body.amount).toBe(payload.amount); - // Verify in-memory state recorded by the mocked service const created = inMemoryDonations.find((d) => d.email === payload.email); expect(created).toBeDefined(); expect(created!.amount).toBe(payload.amount); @@ -293,7 +298,6 @@ describe('Donations (e2e) - expanded stubs', () => { it('Successfuly creates a recurring donation with interval', async () => { const payload = { ...recurringPayload }; - // Example supertest + assertions (commented until route exists) const res = await request(app.getHttpServer()) .post('/api/donations') .send(payload) @@ -306,27 +310,27 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('rejects a negative amount (returns 400)', async () => { - const payload = { ...oneTimePayload, amount: -10 }; + const payload = { ...oneTimePayload, amount: -10 }; - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - }); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); it('rejects an invalid email format amount (returns 400)', async () => { - const payload = { ...oneTimePayload, email: 'not-an-email' }; + const payload = { ...oneTimePayload, email: 'not-an-email' }; - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); }); it('accepts a recurring donation even if recurringInterval is missing (DTO allows optional)', async () => { @@ -345,23 +349,31 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { - const payload: Record = { ...oneTimePayload, recurringInterval: 'MONTHLY' }; + const payload: Record = { + ...oneTimePayload, + recurringInterval: 'MONTHLY', + }; - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); }); it('throws 500 server error if the database errors', async () => { // Simulate a DB failure by making the mocked service throw - mockService.create.mockRejectedValueOnce(new Error('Simulated DB failure')); + mockService.create.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); const payload = { ...oneTimePayload }; try { - const res = await request(app.getHttpServer()).post('/api/donations').send(payload).expect(500); + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(500); expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { @@ -370,7 +382,6 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('gracefully rejects a payload that is missing the first name', async () => { - // Note: using the enum values so the controller validation sees correct types // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; delete payload.firstName; @@ -382,7 +393,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('firstName'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'firstName'.toLowerCase(), + ); }); it('gracefully rejects a payload that is missing the last name', async () => { @@ -397,7 +410,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('lastName'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'lastName'.toLowerCase(), + ); }); it('gracefully rejects a payload that is missing the email', async () => { @@ -412,7 +427,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('email'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'email'.toLowerCase(), + ); }); it('gracefully rejects a payload that is missing the amount', async () => { @@ -427,25 +444,24 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('amount'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'amount'.toLowerCase(), + ); }); it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; - delete payload.isAnonymous + delete payload.isAnonymous; - // Act (example supertest call) const res = await request(app.getHttpServer()) .post('/api/donations') .send(payload) .expect(201); - // Assert response shape (example) expect(res.body).toHaveProperty('id'); expect(res.body.amount).toBe(payload.amount); - // Verify in-memory state recorded by the mocked service const created = inMemoryDonations.find((d) => d.email === payload.email); expect(created).toBeDefined(); expect(created!.amount).toBe(payload.amount); @@ -464,11 +480,12 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); }); it('gracefully rejects a payload that is missing the donationType', async () => { - // Note: using the enum values so the controller validation sees correct types // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; delete payload.donationType; @@ -480,11 +497,12 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); }); it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { - // Note: using the enum values so the controller validation sees correct types // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload, recurring: 'invalid' }; @@ -495,11 +513,12 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('recurring'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'recurring'.toLowerCase(), + ); }); it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { - // Note: using the enum values so the controller validation sees correct types // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload, donationType: 'invalid' }; @@ -510,26 +529,50 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain('donationType'.toLowerCase()); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); }); }); describe('GET /api/donations/public', () => { it('returns only non-anonymous donations', async () => { - // Example setup: insert public and anonymous donations, then call endpoint const now = new Date(); - inMemoryDonations.push({ ...oneTimePayload, email: 'public@example.com', isAnonymous: false, status: DonationStatus.SUCCEEDED, createdAt: now, updatedAt: now, id: nextId++ } as TestDonation); - inMemoryDonations.push({ ...oneTimePayload, email: 'anon@example.com', isAnonymous: true, status: DonationStatus.SUCCEEDED, createdAt: now, updatedAt: now, id: nextId++ } as TestDonation); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'public@example.com', + isAnonymous: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'anon@example.com', + isAnonymous: true, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); const res = await request(app.getHttpServer()) .get('/api/donations/public') .expect(200); expect(Array.isArray(res.body)).toBe(true); - expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); // Validate public DTO shape for the first item if (res.body.length > 0) { - expectPublicDonationDtoShape(res.body[0], { anonymous: false, hasDedication: false }); + expectPublicDonationDtoShape(res.body[0], { + anonymous: false, + hasDedication: false, + }); } }); @@ -539,7 +582,11 @@ describe('Donations (e2e) - expanded stubs', () => { .expect(200); expect(Array.isArray(res.body)).toBe(true); - expect(res.body.every((d: { isAnonymous: boolean }) => d.isAnonymous === false)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); }); it('includes donorName and dedicationMessage when allowed, and hides PII', async () => { @@ -567,16 +614,23 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveLength(1); // anonymous filtered out by service const item = res.body[0]; - expectPublicDonationDtoShape(item, { anonymous: false, hasDedication: true }); + expectPublicDonationDtoShape(item, { + anonymous: false, + hasDedication: true, + }); expect(item.donorName).toBe('Jane Doe'); // based on default names in builder expect(item.dedicationMessage).toBe('For the kids'); }); it('throws 500 server error if the database errors', async () => { // Simulate DB find/query failures by making the mocked service throw - mockService.findPublic.mockRejectedValueOnce(new Error('Simulated DB failure')); + mockService.findPublic.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); try { - const res = await request(app.getHttpServer()).get('/api/donations/public').expect(500); + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(500); expect(res.body).toHaveProperty('statusCode', 500); expect(res.body).toHaveProperty('message'); } finally { @@ -584,52 +638,9 @@ describe('Donations (e2e) - expanded stubs', () => { } }); - - }); - - describe('GET /api/donations/stats', () => { - it('successfully returns the correct total and count', async () => { - // Example: seed two donations and verify totals endpoint - inMemoryDonations.length = 0; // reset - const now = new Date(); - inMemoryDonations.push(buildTestDonation(donationASeed, now)); - inMemoryDonations.push(buildTestDonation(donationBSeed, now)); - - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(200); - - expect(res.body).toEqual({ total: 25, count: 2 }); - }); - - it('successfully returns the correct total and count even if the database is empty', async () => { - inMemoryDonations.length = 0; // ensure empty - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(200); - - expect(res.body).toEqual({ total: 0, count: 0 }); - }); - - it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by making the mocked service throw - mockService.getTotalDonations.mockRejectedValueOnce(new Error('Simulated DB failure')); - try { - const res = await request(app.getHttpServer()).get('/api/donations/stats').expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.getTotalDonations.mockClear(); - } - }); - }); - - // Attribute-only verification (no content validation) - describe('GET response attribute presence only', () => { it('GET /api/donations/public returns items with expected keys', async () => { inMemoryDonations.length = 0; const now = new Date(); - // Create a non-anonymous donation that can include donorName and dedication inMemoryDonations.push( buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { isAnonymous: false, @@ -656,15 +667,57 @@ describe('Donations (e2e) - expanded stubs', () => { 'status', 'createdAt', ]; - const optional = ['donorName', 'recurringInterval', 'dedicationMessage']; + const optional = [ + 'donorName', + 'recurringInterval', + 'dedicationMessage', + ]; const allowed = [...required, ...optional]; - // Has all required keys required.forEach((k) => expect(keys).toContain(k)); - // Does not include unexpected keys keys.forEach((k) => expect(allowed).toContain(k)); } }); + }); + + describe('GET /api/donations/stats', () => { + it('successfully returns the correct total and count', async () => { + // Example: seed two donations and verify totals endpoint + inMemoryDonations.length = 0; // reset + const now = new Date(); + inMemoryDonations.push(buildTestDonation(donationASeed, now)); + inMemoryDonations.push(buildTestDonation(donationBSeed, now)); + + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 25, count: 2 }); + }); + + it('successfully returns the correct total and count even if the database is empty', async () => { + inMemoryDonations.length = 0; + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 0, count: 0 }); + }); + + it('throws 500 server error if the database errors', async () => { + mockService.getTotalDonations.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.getTotalDonations.mockClear(); + } + }); it('GET /api/donations/stats returns object with total and count', async () => { const res = await request(app.getHttpServer()) From 07c69dfcbd68333b19bff8dd6c7c5de48ab64615 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:45:44 -0400 Subject: [PATCH 09/23] Fixed DTO field test --- apps/backend/test/donations.e2e-spec.ts | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 87c2fb0..9fe18be 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -191,7 +191,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(obj.recurringInterval).toBe(expected.recurringInterval); } else { // Could be null or undefined depending on mapper; both acceptable - expect(['undefined', 'string']).toContain( + expect(['undefined', 'string', 'object']).toContain( typeof obj.recurringInterval as string, ); if (typeof obj.recurringInterval === 'string') { @@ -202,6 +202,9 @@ describe('Donations (e2e) - expanded stubs', () => { RecurringInterval.QUARTERLY, RecurringInterval.ANNUALLY, ]).toContain(obj.recurringInterval); + } else if (typeof obj.recurringInterval === 'object') { + // JSON null + expect(obj.recurringInterval).toBeNull(); } } // dedicationMessage is optional @@ -268,10 +271,8 @@ describe('Donations (e2e) - expanded stubs', () => { expect(obj).not.toHaveProperty('transactionId'); }; - // Small smoke test to ensure supertest and the app wiring are working. it('smoke: GET / (should 404 or 200 depending on routes)', async () => { const res = await request(app.getHttpServer()).get('/'); - // We don't make assumptions about the root route; assert we get a response expect([200, 404]).toContain(res.status); }); @@ -467,24 +468,6 @@ describe('Donations (e2e) - expanded stubs', () => { expect(created!.amount).toBe(payload.amount); }); - it('gracefully rejects a payload that is missing the donationType', async () => { - // Note: using the enum values so the controller validation sees correct types - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.donationType; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'donationType'.toLowerCase(), - ); - }); - it('gracefully rejects a payload that is missing the donationType', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; From 43c7236137a8ed4ff8ced374531ebe75b287459d Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:57:48 -0400 Subject: [PATCH 10/23] Removing unnecessary cases --- apps/backend/test/donations.e2e-spec.ts | 54 +++---------------------- 1 file changed, 5 insertions(+), 49 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 9fe18be..94aaeca 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -334,19 +334,17 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('accepts a recurring donation even if recurringInterval is missing (DTO allows optional)', async () => { + it('rejects a recurring donation if recurringInterval is missing (DTO allows optional)', async () => { const payload: Partial = { ...recurringPayload }; delete payload.recurringInterval; const res = await request(app.getHttpServer()) .post('/api/donations') .send(payload) - .expect(201); + .expect(400); - expectDonationResponseDtoShape(res.body, { - donationType: DonationType.RECURRING, - recurringInterval: undefined, - }); + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); }); it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { @@ -572,39 +570,6 @@ describe('Donations (e2e) - expanded stubs', () => { ).toBe(true); }); - it('includes donorName and dedicationMessage when allowed, and hides PII', async () => { - inMemoryDonations.length = 0; - const now = new Date(); - // Visible donor with dedication shown - inMemoryDonations.push( - buildTestDonation({ email: 'pub1@example.com', amount: 42 }, now, { - isAnonymous: false, - showDedicationPublicly: true, - dedicationMessage: 'For the kids', - }), - ); - // Anonymous donor should not have donorName - inMemoryDonations.push( - buildTestDonation({ email: 'anon1@example.com', amount: 12 }, now, { - isAnonymous: true, - showDedicationPublicly: false, - }), - ); - - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(200); - - expect(res.body).toHaveLength(1); // anonymous filtered out by service - const item = res.body[0]; - expectPublicDonationDtoShape(item, { - anonymous: false, - hasDedication: true, - }); - expect(item.donorName).toBe('Jane Doe'); // based on default names in builder - expect(item.dedicationMessage).toBe('For the kids'); - }); - it('throws 500 server error if the database errors', async () => { // Simulate DB find/query failures by making the mocked service throw mockService.findPublic.mockRejectedValueOnce( @@ -621,7 +586,7 @@ describe('Donations (e2e) - expanded stubs', () => { } }); - it('GET /api/donations/public returns items with expected keys', async () => { + it('Returns items with correct DTO (expected keys)', async () => { inMemoryDonations.length = 0; const now = new Date(); inMemoryDonations.push( @@ -701,14 +666,5 @@ describe('Donations (e2e) - expanded stubs', () => { mockService.getTotalDonations.mockClear(); } }); - - it('GET /api/donations/stats returns object with total and count', async () => { - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(200); - - expect(res.body).toHaveProperty('total'); - expect(res.body).toHaveProperty('count'); - }); }); }); From 0397faf42790c8a30f2fd27a6ce748b7bc137a40 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:11:56 -0500 Subject: [PATCH 11/23] Changed wording to be clearer --- apps/backend/test/donations.e2e-spec.ts | 2 +- yarn.lock | 686 +++++++++++++++++++++++- 2 files changed, 663 insertions(+), 25 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 94aaeca..59248aa 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -334,7 +334,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('rejects a recurring donation if recurringInterval is missing (DTO allows optional)', async () => { + it('rejects a donation marked recurring if recurringInterval is missing', async () => { const payload: Partial = { ...recurringPayload }; delete payload.recurringInterval; diff --git a/yarn.lock b/yarn.lock index 7014637..f764208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1913,6 +1913,11 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" @@ -2959,6 +2964,11 @@ dependencies: uuid "9.0.0" +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -2980,6 +2990,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@nuxtjs/opencollective@0.3.2": version "0.3.2" resolved "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz" @@ -3295,6 +3321,13 @@ tslib "^2.3.0" yargs-parser "21.1.1" +"@paralleldrive/cuid2@^2.2.2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" + integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== + dependencies: + "@noble/hashes" "^1.1.5" + "@parcel/watcher-android-arm64@2.5.1": version "2.5.1" resolved "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz" @@ -4193,6 +4226,11 @@ dependencies: "@babel/runtime" "^7.12.5" +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -4300,6 +4338,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/eslint-scope@^3.7.3", "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz" @@ -4412,6 +4455,11 @@ dependencies: "@types/node" "*" +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@^1": version "1.3.4" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz" @@ -4547,6 +4595,24 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/superagent@^8.1.0": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.3.tgz#d736f0e994b195b63e1c93e80271a2faf927388c" + integrity sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/validator@^13.7.10": version "13.11.5" resolved "https://registry.npmjs.org/@types/validator/-/validator-13.11.5.tgz" @@ -5058,6 +5124,11 @@ abab@^2.0.6: resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + accepts@^1.3.8, accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" @@ -5103,13 +5174,20 @@ adm-zip@^0.5.10: resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz" integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" +agentkeepalive@^4.1.3: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -5252,11 +5330,24 @@ append-field@^1.0.0: resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== +"aproba@^1.0.3 || ^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.1.0.tgz#75500a190313d95c64e871e7e4284c6ac219f0b1" + integrity sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew== + arch@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + arg@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" @@ -5378,6 +5469,11 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" @@ -5679,6 +5775,13 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" @@ -5891,6 +5994,30 @@ cac@^6.7.14: resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + 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" + cachedir@^2.3.0: version "2.4.0" resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz" @@ -6050,6 +6177,16 @@ chokidar@^4.0.0: dependencies: readdirp "^4.0.1" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" @@ -6204,6 +6341,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colord@^2.9.1: version "2.9.3" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" @@ -6295,6 +6437,11 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +component-emitter@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + compressible@~2.0.18: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" @@ -6357,6 +6504,11 @@ consola@^2.15.0: resolved "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== +console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + content-disposition@0.5.4, content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -6389,6 +6541,11 @@ cookie@0.7.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + cookies@~0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz" @@ -6833,9 +6990,9 @@ debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.6, debug@^4.4.1: +debug@^4.3.3, debug@^4.3.6, debug@^4.3.7, debug@^4.4.1: version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" @@ -6845,6 +7002,13 @@ decimal.js@^10.4.3: resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^1.6.0: version "1.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz" @@ -6862,6 +7026,11 @@ deep-equal@~1.0.1: resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz" integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -6950,6 +7119,11 @@ detect-libc@^1.0.3: resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +detect-libc@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + detect-newline@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -6968,6 +7142,14 @@ detect-port@^1.5.1: address "^1.0.1" debug "4" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" @@ -7176,9 +7358,9 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encoding@^0.1.13: +encoding@^0.1.12, encoding@^0.1.13: version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: iconv-lite "^0.6.2" @@ -7215,6 +7397,16 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errno@^0.1.1: version "0.1.8" resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz" @@ -7773,6 +7965,11 @@ exit-x@^0.2.2: resolved "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz" integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz" @@ -7939,7 +8136,7 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-safe-stringify@2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -8016,6 +8213,11 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filelist@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" @@ -8202,6 +8404,15 @@ form-data@^4.0.0, form-data@^4.0.4, form-data@~4.0.4: hasown "^2.0.2" mime-types "^2.1.12" +formidable@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" + integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -8257,6 +8468,13 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-monkey@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz" @@ -8294,6 +8512,20 @@ functions-have-names@^1.2.3: resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + 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" + generator-function@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz" @@ -8390,6 +8622,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -8646,6 +8883,11 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" @@ -8700,6 +8942,11 @@ http-assert@^1.5.0: deep-equal "~1.0.1" http-errors "~1.8.0" +http-cache-semantics@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" @@ -8742,6 +8989,15 @@ http-parser-js@>=0.5.1: resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" @@ -8811,9 +9067,9 @@ http-signature@~1.4.0: jsprim "^2.0.2" sshpk "^1.18.0" -https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -8839,6 +9095,13 @@ human-signals@^5.0.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^8.0.3: version "8.0.3" resolved "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz" @@ -8950,6 +9213,11 @@ indent-string@^4.0.0: resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -8973,9 +9241,9 @@ ini@2.0.0: resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@8.2.4: @@ -9034,6 +9302,11 @@ interpret@^1.0.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +ip-address@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed" + integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" @@ -9195,6 +9468,11 @@ is-interactive@^1.0.0: resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-map@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" @@ -10801,6 +11079,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + "lru-cache@^9.1.1 || ^10.0.0": version "10.0.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz" @@ -10873,6 +11158,28 @@ make-error@^1.1.1, make-error@^1.3.6: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + 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" + makeerror@1.0.12: version "1.0.12" resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" @@ -10949,7 +11256,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -10999,6 +11306,11 @@ mime@1.6.0, mime@^1.4.1, mime@^1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -11009,6 +11321,11 @@ mimic-fn@^4.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz" @@ -11068,21 +11385,85 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + minipass@^4.2.4: version "4.2.8" resolved "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz" integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" @@ -11090,6 +11471,11 @@ mkdirp@^0.5.4: dependencies: minimist "^1.2.6" +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@^2.1.3: version "2.1.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz" @@ -11120,7 +11506,7 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -11165,6 +11551,11 @@ nanoid@^3.3.11: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== + napi-postinstall@^0.3.0: version "0.3.4" resolved "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz" @@ -11189,9 +11580,9 @@ negotiator@0.6.3: resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -negotiator@~0.6.4: +negotiator@^0.6.2, negotiator@~0.6.4: version "0.6.4" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== neo-async@^2.6.2: @@ -11207,6 +11598,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-abi@^3.3.0: + version "3.80.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.80.0.tgz#d7390951f27caa129cceeec01e1c20fc9f07581c" + integrity sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz" @@ -11236,6 +11634,22 @@ node-forge@^1: resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + 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" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -11276,6 +11690,13 @@ nodemon@^3.0.1: touch "^3.1.0" undefsafe "^2.0.5" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -11305,6 +11726,16 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" @@ -12480,6 +12911,24 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== + 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" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -12532,6 +12981,19 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise.series@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz" @@ -12616,9 +13078,9 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -qs@6.14.0: +qs@6.14.0, qs@^6.11.2: version "6.14.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: side-channel "^1.1.0" @@ -12670,6 +13132,16 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" @@ -12735,7 +13207,7 @@ readable-stream@^2.0.1, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -12937,6 +13409,11 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + retry@^0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" @@ -13426,6 +13903,11 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" @@ -13556,6 +14038,20 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-update-notifier@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" @@ -13608,6 +14104,11 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" @@ -13625,6 +14126,23 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.8.7" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + dependencies: + ip-address "^10.0.1" + smart-buffer "^4.2.0" + sorted-array-functions@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz" @@ -13710,6 +14228,18 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sqlite3@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7" + integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== + 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" + sshpk@^1.18.0: version "1.18.0" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz" @@ -13725,6 +14255,13 @@ sshpk@^1.18.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stable@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" @@ -13797,7 +14334,16 @@ string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13878,7 +14424,14 @@ string_decoder@^1.1.1, string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13917,6 +14470,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + strip-literal@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz" @@ -13962,6 +14520,29 @@ stylehacks@^6.0.0: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" +superagent@^10.2.3: + version "10.2.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.2.3.tgz#d1e4986f2caac423c37e38077f9073ccfe73a59b" + integrity sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig== + dependencies: + component-emitter "^1.3.1" + cookiejar "^2.1.4" + debug "^4.3.7" + fast-safe-stringify "^2.1.1" + form-data "^4.0.4" + formidable "^3.5.4" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.2" + +supertest@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.1.4.tgz#3175e2539f517ca72fdc7992ffff35b94aca7d34" + integrity sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg== + dependencies: + methods "^1.1.2" + superagent "^10.2.3" + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -14057,9 +14638,19 @@ tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== -tar-stream@~2.2.0: +tar-fs@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4, tar-stream@~2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: bl "^4.0.3" @@ -14068,6 +14659,18 @@ tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.3, terser-webpack-plugin@^5.3.7: version "5.3.14" resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz" @@ -14587,6 +15190,20 @@ union@~0.5.0: dependencies: qs "^6.4.0" +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" @@ -15064,7 +15681,7 @@ which@^1.2.14: dependencies: isexe "^2.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -15079,6 +15696,13 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" +wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + windows-release@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz" @@ -15091,7 +15715,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15109,6 +15733,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -15173,6 +15806,11 @@ yallist@^3.0.2: resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz" From 0c76af0776f5f1f1bf4bb1239de2c556ea0223e6 Mon Sep 17 00:00:00 2001 From: thaninbew Date: Sun, 9 Nov 2025 19:11:19 -0500 Subject: [PATCH 12/23] validation to match real service behavior --- apps/backend/test/donations.e2e-spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 59248aa..8cdf6c4 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { INestApplication, BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { @@ -50,6 +50,16 @@ describe('Donations (e2e) - expanded stubs', () => { mockService = { // eslint-disable-next-line @typescript-eslint/no-explicit-any create: jest.fn(async (request: any) => { + // Validation to match real service behavior + if ( + request.donationType === 'recurring' && + !request.recurringInterval + ) { + throw new BadRequestException( + 'Recurring donation must specify interval.', + ); + } + const now = new Date(); const donation = { id: nextId++, From 8d8203df1b7d3020b440ce3b61b0d40a644c1831 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:38:02 -0500 Subject: [PATCH 13/23] Rename donations.e2e-spec.ts to donations.controller.spec.ts --- .../test/{donations.e2e-spec.ts => donations.controller.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/backend/test/{donations.e2e-spec.ts => donations.controller.spec.ts} (100%) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.controller.spec.ts similarity index 100% rename from apps/backend/test/donations.e2e-spec.ts rename to apps/backend/test/donations.controller.spec.ts From 4eeecadeb9be4970bef923425fafbf1da85ea24f Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:35:58 -0500 Subject: [PATCH 14/23] Tests pass e2e without db state check or cleanup yet --- .../backend/test/donations.controller.spec.ts | 119 +-- apps/backend/test/donations.e2e-spec.ts | 680 ++++++++++++++++++ 2 files changed, 681 insertions(+), 118 deletions(-) create mode 100644 apps/backend/test/donations.e2e-spec.ts diff --git a/apps/backend/test/donations.controller.spec.ts b/apps/backend/test/donations.controller.spec.ts index 8cdf6c4..c3b11bc 100644 --- a/apps/backend/test/donations.controller.spec.ts +++ b/apps/backend/test/donations.controller.spec.ts @@ -7,9 +7,6 @@ import { DonationStatus, } from '../src/donations/donation.entity'; import { DonationsController } from '../src/donations/donations.controller'; -import { DonationsService } from '../src/donations/donations.service'; -import { DonationsRepository } from '../src/donations/donations.repository'; -// import { User } from '../src/users/user.entity'; interface TestDonation { id: number; @@ -34,76 +31,11 @@ describe('Donations (e2e) - expanded stubs', () => { jest.setTimeout(30000); let app: INestApplication; - // We use an in-memory array to simulate stored donations for controller tests - let inMemoryDonations: TestDonation[] = []; - let nextId = 1; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let mockService: any; beforeAll(async () => { - // Create a testing module that instantiates the DonationsController but - // uses a simple in-memory mock for the DonationsService so tests don't - // depend on TypeORM behavior during controller-level validation tests. - inMemoryDonations = []; - nextId = 1; - - mockService = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - create: jest.fn(async (request: any) => { - // Validation to match real service behavior - if ( - request.donationType === 'recurring' && - !request.recurringInterval - ) { - throw new BadRequestException( - 'Recurring donation must specify interval.', - ); - } - - const now = new Date(); - const donation = { - id: nextId++, - firstName: request.firstName, - lastName: request.lastName, - email: request.email, - amount: request.amount, - isAnonymous: request.isAnonymous ?? false, - donationType: - request.donationType === 'one_time' - ? DonationType.ONE_TIME - : DonationType.RECURRING, - recurringInterval: request.recurringInterval ?? null, - dedicationMessage: request.dedicationMessage ?? undefined, - showDedicationPublicly: request.showDedicationPublicly ?? false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - }; - inMemoryDonations.push(donation); - return donation; - }), - findPublic: jest.fn(async (limit?: number) => { - return inMemoryDonations - .filter( - (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, - ) - .slice(0, limit ?? 50); - }), - getTotalDonations: jest.fn(async () => { - const succeeded = inMemoryDonations.filter( - (d) => d.status === DonationStatus.SUCCEEDED, - ); - const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); - return { total, count: succeeded.length }; - }), - }; - const moduleFixture: TestingModule = await Test.createTestingModule({ controllers: [DonationsController], - providers: [ - { provide: DonationsService, useValue: mockService }, - { provide: DonationsRepository, useValue: {} }, - ], + providers: [], }).compile(); app = moduleFixture.createNestApplication(); @@ -372,24 +304,6 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('throws 500 server error if the database errors', async () => { - // Simulate a DB failure by making the mocked service throw - mockService.create.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - const payload = { ...oneTimePayload }; - try { - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.create.mockClear(); - } - }); - it('gracefully rejects a payload that is missing the first name', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; @@ -580,22 +494,6 @@ describe('Donations (e2e) - expanded stubs', () => { ).toBe(true); }); - it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by making the mocked service throw - mockService.findPublic.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.findPublic.mockClear(); - } - }); - it('Returns items with correct DTO (expected keys)', async () => { inMemoryDonations.length = 0; const now = new Date(); @@ -661,20 +559,5 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toEqual({ total: 0, count: 0 }); }); - - it('throws 500 server error if the database errors', async () => { - mockService.getTotalDonations.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.getTotalDonations.mockClear(); - } - }); }); }); diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts new file mode 100644 index 0000000..8cdf6c4 --- /dev/null +++ b/apps/backend/test/donations.e2e-spec.ts @@ -0,0 +1,680 @@ +import { INestApplication, BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { + DonationType, + RecurringInterval, + DonationStatus, +} from '../src/donations/donation.entity'; +import { DonationsController } from '../src/donations/donations.controller'; +import { DonationsService } from '../src/donations/donations.service'; +import { DonationsRepository } from '../src/donations/donations.repository'; +// import { User } from '../src/users/user.entity'; + +interface TestDonation { + id: number; + firstName: string; + lastName: string; + email: string; + amount: number; + isAnonymous: boolean; + donationType: DonationType; + recurringInterval: RecurringInterval | null; + dedicationMessage?: string | null; + showDedicationPublicly: boolean; + status: DonationStatus; + createdAt: Date; + updatedAt: Date; + transactionId?: string | null; +} + +describe('Donations (e2e) - expanded stubs', () => { + // Increase Jest timeout for slower CI/initialization (DB + Nest app init) + // Default is 5000ms which is often too small for integration tests. + jest.setTimeout(30000); + + let app: INestApplication; + // We use an in-memory array to simulate stored donations for controller tests + let inMemoryDonations: TestDonation[] = []; + let nextId = 1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockService: any; + + beforeAll(async () => { + // Create a testing module that instantiates the DonationsController but + // uses a simple in-memory mock for the DonationsService so tests don't + // depend on TypeORM behavior during controller-level validation tests. + inMemoryDonations = []; + nextId = 1; + + mockService = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create: jest.fn(async (request: any) => { + // Validation to match real service behavior + if ( + request.donationType === 'recurring' && + !request.recurringInterval + ) { + throw new BadRequestException( + 'Recurring donation must specify interval.', + ); + } + + const now = new Date(); + const donation = { + id: nextId++, + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + amount: request.amount, + isAnonymous: request.isAnonymous ?? false, + donationType: + request.donationType === 'one_time' + ? DonationType.ONE_TIME + : DonationType.RECURRING, + recurringInterval: request.recurringInterval ?? null, + dedicationMessage: request.dedicationMessage ?? undefined, + showDedicationPublicly: request.showDedicationPublicly ?? false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + }; + inMemoryDonations.push(donation); + return donation; + }), + findPublic: jest.fn(async (limit?: number) => { + return inMemoryDonations + .filter( + (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, + ) + .slice(0, limit ?? 50); + }), + getTotalDonations: jest.fn(async () => { + const succeeded = inMemoryDonations.filter( + (d) => d.status === DonationStatus.SUCCEEDED, + ); + const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); + return { total, count: succeeded.length }; + }), + }; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + controllers: [DonationsController], + providers: [ + { provide: DonationsService, useValue: mockService }, + { provide: DonationsRepository, useValue: {} }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + // Match runtime API prefix used by the real application + app.setGlobalPrefix('api'); + await app.init(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + // Helper sample payloads used in the stubs below + const oneTimePayload = { + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + amount: 50, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + }; + + const recurringPayload = { + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + amount: 25, + isAnonymous: false, + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }; + + // Reusable seeds and helpers for in-memory donation construction + const donationASeed: Pick = { + email: 'a@example.com', + amount: 10, + }; + const donationBSeed: Pick = { + email: 'b@example.com', + amount: 15, + }; + + function buildTestDonation( + seed: Pick, + now: Date, + overrides: Partial = {}, + ): TestDonation { + return { + id: nextId++, + firstName: oneTimePayload.firstName, + lastName: oneTimePayload.lastName, + email: seed.email, + amount: seed.amount, + isAnonymous: oneTimePayload.isAnonymous, + donationType: oneTimePayload.donationType, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + transactionId: null, + ...overrides, + }; + } + + // ---------- DTO shape validators ---------- + const isISODateString = (value: unknown): boolean => { + if (typeof value !== 'string') return false; + const dt = new Date(value); + return !Number.isNaN(dt.getTime()); + }; + + const expectDonationResponseDtoShape = ( + obj: Record, + expected: { + donationType: DonationType; + recurringInterval?: RecurringInterval | null; + email?: string; + firstName?: string; + lastName?: string; + transactionIdPresent?: boolean; + }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.firstName).toBe('string'); + expect(typeof obj.lastName).toBe('string'); + expect(typeof obj.email).toBe('string'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect(obj.donationType).toBe(expected.donationType); + if (expected.recurringInterval) { + expect(obj.recurringInterval).toBe(expected.recurringInterval); + } else { + // Could be null or undefined depending on mapper; both acceptable + expect(['undefined', 'string', 'object']).toContain( + typeof obj.recurringInterval as string, + ); + if (typeof obj.recurringInterval === 'string') { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval); + } else if (typeof obj.recurringInterval === 'object') { + // JSON null + expect(obj.recurringInterval).toBeNull(); + } + } + // dedicationMessage is optional + if (obj.dedicationMessage !== undefined && obj.dedicationMessage !== null) { + expect(typeof obj.dedicationMessage).toBe('string'); + } + expect(typeof obj.showDedicationPublicly).toBe('boolean'); + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); + expect(isISODateString(String(obj.createdAt))).toBe(true); + expect(isISODateString(String(obj.updatedAt))).toBe(true); + if (expected.transactionIdPresent) { + expect(typeof obj.transactionId).toBe('string'); + } else { + // Can be absent or null + expect(['undefined', 'string']).toContain( + typeof obj.transactionId as string, + ); + } + }; + + const expectPublicDonationDtoShape = ( + obj: Record, + opts: { anonymous: boolean; hasDedication: boolean }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect([DonationType.ONE_TIME, DonationType.RECURRING]).toContain( + obj.donationType as DonationType, + ); + if (obj.recurringInterval !== undefined && obj.recurringInterval !== null) { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval as RecurringInterval); + } + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); + expect(isISODateString(String(obj.createdAt))).toBe(true); + + if (opts.anonymous) { + expect(obj).not.toHaveProperty('donorName'); + } else { + expect(typeof obj.donorName).toBe('string'); + expect((obj.donorName as string).length).toBeGreaterThan(0); + } + + if (opts.hasDedication) { + expect(typeof obj.dedicationMessage).toBe('string'); + } else { + expect(obj).not.toHaveProperty('dedicationMessage'); + } + + // Ensure sensitive fields are not leaked in public DTO + expect(obj).not.toHaveProperty('email'); + expect(obj).not.toHaveProperty('firstName'); + expect(obj).not.toHaveProperty('lastName'); + expect(obj).not.toHaveProperty('transactionId'); + }; + + it('smoke: GET / (should 404 or 200 depending on routes)', async () => { + const res = await request(app.getHttpServer()).get('/'); + expect([200, 404]).toContain(res.status); + }); + + describe('POST /api/donations', () => { + it('Successfuly commits a one-time donation creation', async () => { + const payload = { ...oneTimePayload }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); + + expectDonationResponseDtoShape(res.body, { + donationType: DonationType.ONE_TIME, + recurringInterval: null, + }); + expect(res.body.amount).toBe(payload.amount); + + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); + }); + + it('Successfuly creates a recurring donation with interval', async () => { + const payload = { ...recurringPayload }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); + + expectDonationResponseDtoShape(res.body, { + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }); + }); + + it('rejects a negative amount (returns 400)', async () => { + const payload = { ...oneTimePayload, amount: -10 }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects an invalid email format amount (returns 400)', async () => { + const payload = { ...oneTimePayload, email: 'not-an-email' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects a donation marked recurring if recurringInterval is missing', async () => { + const payload: Partial = { ...recurringPayload }; + delete payload.recurringInterval; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { + const payload: Record = { + ...oneTimePayload, + recurringInterval: 'MONTHLY', + }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('throws 500 server error if the database errors', async () => { + // Simulate a DB failure by making the mocked service throw + mockService.create.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + const payload = { ...oneTimePayload }; + try { + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.create.mockClear(); + } + }); + + it('gracefully rejects a payload that is missing the first name', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.firstName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'firstName'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the last name', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.lastName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'lastName'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the email', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.email; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'email'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the amount', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.amount; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'amount'.toLowerCase(), + ); + }); + + it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.isAnonymous; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); + + expect(res.body).toHaveProperty('id'); + expect(res.body.amount).toBe(payload.amount); + + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); + }); + + it('gracefully rejects a payload that is missing the donationType', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.donationType; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, recurring: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'recurring'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, donationType: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); + }); + }); + + describe('GET /api/donations/public', () => { + it('returns only non-anonymous donations', async () => { + const now = new Date(); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'public@example.com', + isAnonymous: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'anon@example.com', + isAnonymous: true, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); + // Validate public DTO shape for the first item + if (res.body.length > 0) { + expectPublicDonationDtoShape(res.body[0], { + anonymous: false, + hasDedication: false, + }); + } + }); + + it('returns no donations if there are none in the database', async () => { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); + }); + + it('throws 500 server error if the database errors', async () => { + // Simulate DB find/query failures by making the mocked service throw + mockService.findPublic.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.findPublic.mockClear(); + } + }); + + it('Returns items with correct DTO (expected keys)', async () => { + inMemoryDonations.length = 0; + const now = new Date(); + inMemoryDonations.push( + buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { + isAnonymous: false, + showDedicationPublicly: true, + dedicationMessage: 'Nice work', + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }), + ); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + if (res.body.length > 0) { + const item = res.body[0]; + const keys = Object.keys(item); + const required = [ + 'id', + 'amount', + 'isAnonymous', + 'donationType', + 'status', + 'createdAt', + ]; + const optional = [ + 'donorName', + 'recurringInterval', + 'dedicationMessage', + ]; + const allowed = [...required, ...optional]; + + required.forEach((k) => expect(keys).toContain(k)); + keys.forEach((k) => expect(allowed).toContain(k)); + } + }); + }); + + describe('GET /api/donations/stats', () => { + it('successfully returns the correct total and count', async () => { + // Example: seed two donations and verify totals endpoint + inMemoryDonations.length = 0; // reset + const now = new Date(); + inMemoryDonations.push(buildTestDonation(donationASeed, now)); + inMemoryDonations.push(buildTestDonation(donationBSeed, now)); + + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 25, count: 2 }); + }); + + it('successfully returns the correct total and count even if the database is empty', async () => { + inMemoryDonations.length = 0; + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 0, count: 0 }); + }); + + it('throws 500 server error if the database errors', async () => { + mockService.getTotalDonations.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.getTotalDonations.mockClear(); + } + }); + }); +}); From fa38a8081c597572bad7cc6f49a541a6fac20be9 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:22:48 -0500 Subject: [PATCH 15/23] Tests don't actually pass e2e, no db state check or cleanup yet --- .../backend/test/donations.controller.spec.ts | 119 +++++++++++- apps/backend/test/donations.e2e-spec.ts | 183 +++--------------- 2 files changed, 145 insertions(+), 157 deletions(-) diff --git a/apps/backend/test/donations.controller.spec.ts b/apps/backend/test/donations.controller.spec.ts index c3b11bc..8cdf6c4 100644 --- a/apps/backend/test/donations.controller.spec.ts +++ b/apps/backend/test/donations.controller.spec.ts @@ -7,6 +7,9 @@ import { DonationStatus, } from '../src/donations/donation.entity'; import { DonationsController } from '../src/donations/donations.controller'; +import { DonationsService } from '../src/donations/donations.service'; +import { DonationsRepository } from '../src/donations/donations.repository'; +// import { User } from '../src/users/user.entity'; interface TestDonation { id: number; @@ -31,11 +34,76 @@ describe('Donations (e2e) - expanded stubs', () => { jest.setTimeout(30000); let app: INestApplication; + // We use an in-memory array to simulate stored donations for controller tests + let inMemoryDonations: TestDonation[] = []; + let nextId = 1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockService: any; beforeAll(async () => { + // Create a testing module that instantiates the DonationsController but + // uses a simple in-memory mock for the DonationsService so tests don't + // depend on TypeORM behavior during controller-level validation tests. + inMemoryDonations = []; + nextId = 1; + + mockService = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create: jest.fn(async (request: any) => { + // Validation to match real service behavior + if ( + request.donationType === 'recurring' && + !request.recurringInterval + ) { + throw new BadRequestException( + 'Recurring donation must specify interval.', + ); + } + + const now = new Date(); + const donation = { + id: nextId++, + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + amount: request.amount, + isAnonymous: request.isAnonymous ?? false, + donationType: + request.donationType === 'one_time' + ? DonationType.ONE_TIME + : DonationType.RECURRING, + recurringInterval: request.recurringInterval ?? null, + dedicationMessage: request.dedicationMessage ?? undefined, + showDedicationPublicly: request.showDedicationPublicly ?? false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + }; + inMemoryDonations.push(donation); + return donation; + }), + findPublic: jest.fn(async (limit?: number) => { + return inMemoryDonations + .filter( + (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, + ) + .slice(0, limit ?? 50); + }), + getTotalDonations: jest.fn(async () => { + const succeeded = inMemoryDonations.filter( + (d) => d.status === DonationStatus.SUCCEEDED, + ); + const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); + return { total, count: succeeded.length }; + }), + }; + const moduleFixture: TestingModule = await Test.createTestingModule({ controllers: [DonationsController], - providers: [], + providers: [ + { provide: DonationsService, useValue: mockService }, + { provide: DonationsRepository, useValue: {} }, + ], }).compile(); app = moduleFixture.createNestApplication(); @@ -304,6 +372,24 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); + it('throws 500 server error if the database errors', async () => { + // Simulate a DB failure by making the mocked service throw + mockService.create.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + const payload = { ...oneTimePayload }; + try { + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.create.mockClear(); + } + }); + it('gracefully rejects a payload that is missing the first name', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; @@ -494,6 +580,22 @@ describe('Donations (e2e) - expanded stubs', () => { ).toBe(true); }); + it('throws 500 server error if the database errors', async () => { + // Simulate DB find/query failures by making the mocked service throw + mockService.findPublic.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.findPublic.mockClear(); + } + }); + it('Returns items with correct DTO (expected keys)', async () => { inMemoryDonations.length = 0; const now = new Date(); @@ -559,5 +661,20 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toEqual({ total: 0, count: 0 }); }); + + it('throws 500 server error if the database errors', async () => { + mockService.getTotalDonations.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.getTotalDonations.mockClear(); + } + }); }); }); diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 8cdf6c4..ab2639b 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -9,7 +9,9 @@ import { import { DonationsController } from '../src/donations/donations.controller'; import { DonationsService } from '../src/donations/donations.service'; import { DonationsRepository } from '../src/donations/donations.repository'; -// import { User } from '../src/users/user.entity'; +import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { Donation } from '../src/donations/donation.entity'; // adjust path if needed interface TestDonation { id: number; @@ -34,82 +36,38 @@ describe('Donations (e2e) - expanded stubs', () => { jest.setTimeout(30000); let app: INestApplication; - // We use an in-memory array to simulate stored donations for controller tests - let inMemoryDonations: TestDonation[] = []; let nextId = 1; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let mockService: any; + let donationRepository: Repository; + let dataSource: DataSource; beforeAll(async () => { - // Create a testing module that instantiates the DonationsController but - // uses a simple in-memory mock for the DonationsService so tests don't - // depend on TypeORM behavior during controller-level validation tests. - inMemoryDonations = []; nextId = 1; - mockService = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - create: jest.fn(async (request: any) => { - // Validation to match real service behavior - if ( - request.donationType === 'recurring' && - !request.recurringInterval - ) { - throw new BadRequestException( - 'Recurring donation must specify interval.', - ); - } - - const now = new Date(); - const donation = { - id: nextId++, - firstName: request.firstName, - lastName: request.lastName, - email: request.email, - amount: request.amount, - isAnonymous: request.isAnonymous ?? false, - donationType: - request.donationType === 'one_time' - ? DonationType.ONE_TIME - : DonationType.RECURRING, - recurringInterval: request.recurringInterval ?? null, - dedicationMessage: request.dedicationMessage ?? undefined, - showDedicationPublicly: request.showDedicationPublicly ?? false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - }; - inMemoryDonations.push(donation); - return donation; - }), - findPublic: jest.fn(async (limit?: number) => { - return inMemoryDonations - .filter( - (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, - ) - .slice(0, limit ?? 50); - }), - getTotalDonations: jest.fn(async () => { - const succeeded = inMemoryDonations.filter( - (d) => d.status === DonationStatus.SUCCEEDED, - ); - const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); - return { total, count: succeeded.length }; - }), - }; - const moduleFixture: TestingModule = await Test.createTestingModule({ - controllers: [DonationsController], - providers: [ - { provide: DonationsService, useValue: mockService }, - { provide: DonationsRepository, useValue: {} }, + imports: [ + TypeOrmModule.forRoot({ + type: 'sqlite', + database: ':memory:', + entities: [__dirname + '/../src/**/*.entity{.ts,.js}'], + synchronize: true, // OK for tests; creates schema automatically + dropSchema: true, // drop schema on connect (clean) + logging: false, + }), + TypeOrmModule.forFeature([Donation]), ], + controllers: [DonationsController], + providers: [DonationsService, DonationsRepository], }).compile(); app = moduleFixture.createNestApplication(); // Match runtime API prefix used by the real application app.setGlobalPrefix('api'); await app.init(); + + donationRepository = moduleFixture.get>( + getRepositoryToken(Donation), + ); + dataSource = moduleFixture.get(DataSource); }); afterAll(async () => { @@ -295,13 +253,16 @@ describe('Donations (e2e) - expanded stubs', () => { .send(payload) .expect(201); + // HTTP response shape checks as you already do... expectDonationResponseDtoShape(res.body, { donationType: DonationType.ONE_TIME, recurringInterval: null, }); - expect(res.body.amount).toBe(payload.amount); - const created = inMemoryDonations.find((d) => d.email === payload.email); + // Validate DB state (repository query) + const created = await donationRepository.findOne({ + where: { email: payload.email }, + }); expect(created).toBeDefined(); expect(created!.amount).toBe(payload.amount); }); @@ -372,24 +333,6 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('message'); }); - it('throws 500 server error if the database errors', async () => { - // Simulate a DB failure by making the mocked service throw - mockService.create.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - const payload = { ...oneTimePayload }; - try { - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.create.mockClear(); - } - }); - it('gracefully rejects a payload that is missing the first name', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = { ...oneTimePayload }; @@ -470,10 +413,6 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('id'); expect(res.body.amount).toBe(payload.amount); - - const created = inMemoryDonations.find((d) => d.email === payload.email); - expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); }); it('gracefully rejects a payload that is missing the donationType', async () => { @@ -528,26 +467,6 @@ describe('Donations (e2e) - expanded stubs', () => { describe('GET /api/donations/public', () => { it('returns only non-anonymous donations', async () => { - const now = new Date(); - inMemoryDonations.push({ - ...oneTimePayload, - email: 'public@example.com', - isAnonymous: false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - id: nextId++, - } as TestDonation); - inMemoryDonations.push({ - ...oneTimePayload, - email: 'anon@example.com', - isAnonymous: true, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - id: nextId++, - } as TestDonation); - const res = await request(app.getHttpServer()) .get('/api/donations/public') .expect(200); @@ -580,34 +499,8 @@ describe('Donations (e2e) - expanded stubs', () => { ).toBe(true); }); - it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by making the mocked service throw - mockService.findPublic.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.findPublic.mockClear(); - } - }); - it('Returns items with correct DTO (expected keys)', async () => { - inMemoryDonations.length = 0; const now = new Date(); - inMemoryDonations.push( - buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { - isAnonymous: false, - showDedicationPublicly: true, - dedicationMessage: 'Nice work', - donationType: DonationType.RECURRING, - recurringInterval: RecurringInterval.MONTHLY, - }), - ); const res = await request(app.getHttpServer()) .get('/api/donations/public') @@ -640,12 +533,6 @@ describe('Donations (e2e) - expanded stubs', () => { describe('GET /api/donations/stats', () => { it('successfully returns the correct total and count', async () => { - // Example: seed two donations and verify totals endpoint - inMemoryDonations.length = 0; // reset - const now = new Date(); - inMemoryDonations.push(buildTestDonation(donationASeed, now)); - inMemoryDonations.push(buildTestDonation(donationBSeed, now)); - const res = await request(app.getHttpServer()) .get('/api/donations/stats') .expect(200); @@ -654,27 +541,11 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('successfully returns the correct total and count even if the database is empty', async () => { - inMemoryDonations.length = 0; const res = await request(app.getHttpServer()) .get('/api/donations/stats') .expect(200); expect(res.body).toEqual({ total: 0, count: 0 }); }); - - it('throws 500 server error if the database errors', async () => { - mockService.getTotalDonations.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.getTotalDonations.mockClear(); - } - }); }); }); From 58e1983f96c1b27cee362ee0f1f5a1a27cb2e4fe Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:21:30 -0500 Subject: [PATCH 16/23] Temporary logging, using actual db --- .../src/donations/donations.controller.ts | 7 ++++- .../src/donations/donations.service.ts | 25 +++++++++++++++++- apps/backend/test/donations.e2e-spec.ts | 26 ++++++++++++++----- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 83ce6d4..0870e13 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -58,6 +58,7 @@ export class DonationsController { @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) createDonationDto: CreateDonationDto, ): Promise { + console.log('[DonationsController.create] payload:', createDonationDto); const request = DonationMappers.toCreateDonationRequest(createDonationDto); const donation = await this.donationsService.create(request); return DonationMappers.toDonationResponseDto(donation); @@ -84,6 +85,7 @@ export class DonationsController { async findPublic( @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, ): Promise { + console.log('[DonationsController.findPublic] limit=', limit); const donations = await this.donationsService.findPublic(limit); return DonationMappers.toPublicDonationDtos(donations); } @@ -114,7 +116,10 @@ export class DonationsController { }, }) async getStats(): Promise<{ total: number; count: number }> { - return this.donationsService.getTotalDonations(); + console.log('[DonationsController.getStats] fetching totals'); + const stats = await this.donationsService.getTotalDonations(); + console.log('[DonationsController.getStats] result=', stats); + return stats; } @Get() diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 3bff2f0..f340a04 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -15,6 +15,10 @@ export class DonationsService { async create( createDonationRequest: CreateDonationRequest, ): Promise { + console.log( + '[DonationsService.create] incoming request:', + createDonationRequest, + ); if (createDonationRequest.amount <= 0) { throw new BadRequestException('Donation amount must be positive.'); } @@ -64,6 +68,11 @@ export class DonationsService { const savedDonation = await this.donationRepository.save(donation); + console.log( + `[DonationsService.create] savedDonation id=${savedDonation.id} email=${savedDonation.email} amount=${savedDonation.amount}`, + savedDonation, + ); + return { id: savedDonation.id, firstName: savedDonation.firstName, @@ -120,6 +129,7 @@ export class DonationsService { } async findPublic(limit = 50): Promise { + console.log('[DonationsService.findPublic] limit=', limit); const donations: Donation[] = await this.donationRepository.find({ take: limit, order: { createdAt: 'DESC' }, @@ -181,10 +191,23 @@ export class DonationsService { } async getTotalDonations(): Promise<{ total: number; count: number }> { + console.log('[DonationsService.getTotalDonations] querying totals'); const [donations] = await this.donationRepository.manager.query( `SELECT COUNT(amount) AS count, SUM(amount) AS total FROM donations`, ); - return { total: donations.total, count: donations.count }; + console.log('[DonationsService.getTotalDonations] result=', donations); + + // SQL SUM returns null when no rows exist; coerce to numbers with sensible defaults + const total = + donations.total !== null && donations.total !== undefined + ? Number(donations.total) + : 0; + const count = + donations.count !== null && donations.count !== undefined + ? Number(donations.count) + : 0; + + return { total, count }; } } diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index ab2639b..41cd9ec 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -9,6 +9,7 @@ import { import { DonationsController } from '../src/donations/donations.controller'; import { DonationsService } from '../src/donations/donations.service'; import { DonationsRepository } from '../src/donations/donations.repository'; +import { DonationsModule } from '../src/donations/donations.module'; import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { Donation } from '../src/donations/donation.entity'; // adjust path if needed @@ -43,20 +44,28 @@ describe('Donations (e2e) - expanded stubs', () => { beforeAll(async () => { nextId = 1; + // Use actual Postgres DB for tests. IMPORTANT: Make sure these env vars + // point to a dedicated test database (never run tests against production DB). const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ - type: 'sqlite', - database: ':memory:', + type: 'postgres', + host: process.env.NX_DB_HOST || 'localhost', + port: parseInt(process.env.NX_DB_PORT || '5432', 10), + username: 'postgres', + password: '12345678', + database: 'fcc_dev', entities: [__dirname + '/../src/**/*.entity{.ts,.js}'], - synchronize: true, // OK for tests; creates schema automatically - dropSchema: true, // drop schema on connect (clean) + // Prefer running migrations in tests for parity; set migrationsRun to true + // if you keep migrations up-to-date. If you want schema auto-sync for + // a test DB, set `synchronize: true` manually here. + synchronize: false, + migrationsRun: true, + migrations: [__dirname + '/../src/migrations/*{.ts,.js}'], logging: false, }), - TypeOrmModule.forFeature([Donation]), + DonationsModule, ], - controllers: [DonationsController], - providers: [DonationsService, DonationsRepository], }).compile(); app = moduleFixture.createNestApplication(); @@ -74,6 +83,9 @@ describe('Donations (e2e) - expanded stubs', () => { if (app) { await app.close(); } + if (dataSource) { + await dataSource.destroy(); + } }); // Helper sample payloads used in the stubs below From 275aa4b24e9c2425449b5222d0942b6c319c01b9 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:10:15 -0500 Subject: [PATCH 17/23] Database entry validation, uses donation transactions --- apps/backend/test/donations.e2e-spec.ts | 230 +++++++++++++++++++----- 1 file changed, 188 insertions(+), 42 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 41cd9ec..f99b195 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -1,4 +1,4 @@ -import { INestApplication, BadRequestException } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { @@ -6,12 +6,9 @@ import { RecurringInterval, DonationStatus, } from '../src/donations/donation.entity'; -import { DonationsController } from '../src/donations/donations.controller'; -import { DonationsService } from '../src/donations/donations.service'; -import { DonationsRepository } from '../src/donations/donations.repository'; import { DonationsModule } from '../src/donations/donations.module'; import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; +import { Repository, DataSource, QueryRunner } from 'typeorm'; import { Donation } from '../src/donations/donation.entity'; // adjust path if needed interface TestDonation { @@ -40,6 +37,7 @@ describe('Donations (e2e) - expanded stubs', () => { let nextId = 1; let donationRepository: Repository; let dataSource: DataSource; + let queryRunner: QueryRunner; beforeAll(async () => { nextId = 1; @@ -79,6 +77,18 @@ describe('Donations (e2e) - expanded stubs', () => { dataSource = moduleFixture.get(DataSource); }); + beforeEach(async () => { + queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + await donationRepository.clear(); + }); + + afterEach(async () => { + await queryRunner.rollbackTransaction(); + await queryRunner.release(); + }); + afterAll(async () => { if (app) { await app.close(); @@ -108,40 +118,6 @@ describe('Donations (e2e) - expanded stubs', () => { recurringInterval: RecurringInterval.MONTHLY, }; - // Reusable seeds and helpers for in-memory donation construction - const donationASeed: Pick = { - email: 'a@example.com', - amount: 10, - }; - const donationBSeed: Pick = { - email: 'b@example.com', - amount: 15, - }; - - function buildTestDonation( - seed: Pick, - now: Date, - overrides: Partial = {}, - ): TestDonation { - return { - id: nextId++, - firstName: oneTimePayload.firstName, - lastName: oneTimePayload.lastName, - email: seed.email, - amount: seed.amount, - isAnonymous: oneTimePayload.isAnonymous, - donationType: oneTimePayload.donationType, - recurringInterval: null, - dedicationMessage: null, - showDedicationPublicly: false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - transactionId: null, - ...overrides, - }; - } - // ---------- DTO shape validators ---------- const isISODateString = (value: unknown): boolean => { if (typeof value !== 'string') return false; @@ -265,7 +241,6 @@ describe('Donations (e2e) - expanded stubs', () => { .send(payload) .expect(201); - // HTTP response shape checks as you already do... expectDonationResponseDtoShape(res.body, { donationType: DonationType.ONE_TIME, recurringInterval: null, @@ -276,7 +251,23 @@ describe('Donations (e2e) - expanded stubs', () => { where: { email: payload.email }, }); expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); + expect(created).toBeInstanceOf(Donation); + if (created instanceof Donation) { + expect((created as Donation).id).toBeDefined(); + expect((created as Donation).firstName).toBe(payload.firstName); + expect((created as Donation).lastName).toBe(payload.lastName); + expect((created as Donation).email).toBe(payload.email); + expect((created as Donation).amount).toBe(payload.amount); + expect((created as Donation).isAnonymous).toBe(payload.isAnonymous); + expect((created as Donation).donationType).toBe(payload.donationType); + expect((created as Donation).recurringInterval).toBeUndefined(); + expect((created as Donation).dedicationMessage).toBeUndefined(); + expect((created as Donation).showDedicationPublicly).toBe(false); + expect((created as Donation).status).toBe('pending'); + expect(isISODateString((created as Donation).createdAt)).toBe(true); + expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect((created as Donation).transactionId).toBeInstanceOf(Number); + } }); it('Successfuly creates a recurring donation with interval', async () => { @@ -291,6 +282,32 @@ describe('Donations (e2e) - expanded stubs', () => { donationType: DonationType.RECURRING, recurringInterval: RecurringInterval.MONTHLY, }); + + // Validate DB state (repository query) + const created = await donationRepository.findOne({ + where: { email: payload.email }, + }); + + expect(created).toBeDefined(); + expect(created).toBeInstanceOf(Donation); + if (created instanceof Donation) { + expect((created as Donation).id).toBeDefined(); + expect((created as Donation).firstName).toBe(payload.firstName); + expect((created as Donation).lastName).toBe(payload.lastName); + expect((created as Donation).email).toBe(payload.email); + expect((created as Donation).amount).toBe(payload.amount); + expect((created as Donation).isAnonymous).toBe(payload.isAnonymous); + expect((created as Donation).donationType).toBe(payload.donationType); + expect((created as Donation).recurringInterval).toBe( + payload.recurringInterval, + ); + expect((created as Donation).dedicationMessage).toBeUndefined(); + expect((created as Donation).showDedicationPublicly).toBe(false); + expect((created as Donation).status).toBe('pending'); + expect(isISODateString((created as Donation).createdAt)).toBe(true); + expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect((created as Donation).transactionId).toBeInstanceOf(Number); + } }); it('rejects a negative amount (returns 400)', async () => { @@ -303,6 +320,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('rejects an invalid email format amount (returns 400)', async () => { @@ -315,6 +335,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('rejects a donation marked recurring if recurringInterval is missing', async () => { @@ -328,6 +351,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { @@ -343,6 +369,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('statusCode', 400); expect(res.body).toHaveProperty('message'); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is missing the first name', async () => { @@ -360,6 +389,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'firstName'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is missing the last name', async () => { @@ -377,6 +409,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'lastName'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is missing the email', async () => { @@ -394,6 +429,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'email'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is missing the amount', async () => { @@ -411,6 +449,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'amount'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { @@ -425,6 +466,30 @@ describe('Donations (e2e) - expanded stubs', () => { expect(res.body).toHaveProperty('id'); expect(res.body.amount).toBe(payload.amount); + + // Validate DB state (repository query) + const created = await donationRepository.findOne({ + where: { email: payload.email }, + }); + + expect(created).toBeDefined(); + expect(created).toBeInstanceOf(Donation); + if (created instanceof Donation) { + expect((created as Donation).id).toBeDefined(); + expect((created as Donation).firstName).toBe(payload.firstName); + expect((created as Donation).lastName).toBe(payload.lastName); + expect((created as Donation).email).toBe(payload.email); + expect((created as Donation).amount).toBe(payload.amount); + expect((created as Donation).isAnonymous).toBe(false); + expect((created as Donation).donationType).toBe(payload.donationType); + expect((created as Donation).recurringInterval).toBeUndefined(); + expect((created as Donation).dedicationMessage).toBeUndefined(); + expect((created as Donation).showDedicationPublicly).toBe(false); + expect((created as Donation).status).toBe('pending'); + expect(isISODateString((created as Donation).createdAt)).toBe(true); + expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect((created as Donation).transactionId).toBeInstanceOf(Number); + } }); it('gracefully rejects a payload that is missing the donationType', async () => { @@ -442,6 +507,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'donationType'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { @@ -458,6 +526,9 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'recurring'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { @@ -474,21 +545,59 @@ describe('Donations (e2e) - expanded stubs', () => { expect(String(res.body.message).toLowerCase()).toContain( 'donationType'.toLowerCase(), ); + + const dbCount = await donationRepository.count(); + expect(dbCount).toBe(0); }); }); describe('GET /api/donations/public', () => { it('returns only non-anonymous donations', async () => { + // Seed anonymous and non-anonymous donations + const now = new Date(); + await donationRepository.save([ + { + firstName: 'Sam', + lastName: 'Nie', + email: 'nie.sa@example.com', + amount: 10, + isAnonymous: true, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + transactionId: null, + createdAt: now, + updatedAt: now, + }, + { + firstName: 'Rex', + lastName: 'Jeff', + email: 'Re.Je@example.com', + amount: 15, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + transactionId: null, + createdAt: now, + updatedAt: now, + }, + ] as Partial[]); + const res = await request(app.getHttpServer()) .get('/api/donations/public') .expect(200); - expect(Array.isArray(res.body)).toBe(true); expect( res.body.every( (d: { isAnonymous: boolean }) => d.isAnonymous === false, ), ).toBe(true); + // Validate public DTO shape for the first item if (res.body.length > 0) { expectPublicDonationDtoShape(res.body[0], { @@ -545,6 +654,41 @@ describe('Donations (e2e) - expanded stubs', () => { describe('GET /api/donations/stats', () => { it('successfully returns the correct total and count', async () => { + // Seed two donations so the total is 25 and count is 2 + const now = new Date(); + await donationRepository.save([ + { + firstName: 'Sam', + lastName: 'Nie', + email: 'nie.sa@example.com', + amount: 10, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + transactionId: null, + createdAt: now, + updatedAt: now, + }, + { + firstName: 'Rex', + lastName: 'Jeff', + email: 'Re.Je@example.com', + amount: 15, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + transactionId: null, + createdAt: now, + updatedAt: now, + }, + ] as Partial[]); + const res = await request(app.getHttpServer()) .get('/api/donations/stats') .expect(200); @@ -553,6 +697,8 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('successfully returns the correct total and count even if the database is empty', async () => { + await donationRepository.clear(); + const res = await request(app.getHttpServer()) .get('/api/donations/stats') .expect(200); From 1d9f8b3b9ffed985697947030f2f4cc885181bba Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:15:08 -0500 Subject: [PATCH 18/23] Removing console.logs and moving most of the donation with mock tests into donation.spec.ts --- .../donations/donations.controller.spec.ts | 638 +++++++++++++++- .../src/donations/donations.controller.ts | 4 - .../src/donations/donations.service.ts | 13 - .../backend/test/donations.controller.spec.ts | 680 ------------------ apps/backend/test/donations.e2e-spec.ts | 38 +- 5 files changed, 658 insertions(+), 715 deletions(-) delete mode 100644 apps/backend/test/donations.controller.spec.ts diff --git a/apps/backend/src/donations/donations.controller.spec.ts b/apps/backend/src/donations/donations.controller.spec.ts index 9d48d18..7ab7a9c 100644 --- a/apps/backend/src/donations/donations.controller.spec.ts +++ b/apps/backend/src/donations/donations.controller.spec.ts @@ -9,7 +9,8 @@ import { DonationStatus, } from './donation.entity'; import { Donation as DomainDonation } from './mappers'; - +import { INestApplication, BadRequestException } from '@nestjs/common'; +import request from 'supertest'; describe('DonationsController', () => { let controller: DonationsController; let service: DonationsService; @@ -295,3 +296,638 @@ describe('DonationsController', () => { }); }); }); + +interface TestDonation { + id: number; + firstName: string; + lastName: string; + email: string; + amount: number; + isAnonymous: boolean; + donationType: DonationType; + recurringInterval: RecurringInterval | null; + dedicationMessage?: string | null; + showDedicationPublicly: boolean; + status: DonationStatus; + createdAt: Date; + updatedAt: Date; + transactionId?: string | null; +} + +describe('Donation Integration', () => { + // Increase Jest timeout for slower CI/initialization (DB + Nest app init) + // Default is 5000ms which is often too small for integration tests. + jest.setTimeout(30000); + + let app: INestApplication; + // We use an in-memory array to simulate stored donations for controller tests + let inMemoryDonations: TestDonation[] = []; + let nextId = 1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockService: any; + + beforeAll(async () => { + // Create a testing module that instantiates the DonationsController but + // uses a simple in-memory mock for the DonationsService so tests don't + // depend on TypeORM behavior during controller-level validation tests. + inMemoryDonations = []; + nextId = 1; + + mockService = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create: jest.fn(async (request: any) => { + // Validation to match real service behavior + if ( + request.donationType === 'recurring' && + !request.recurringInterval + ) { + throw new BadRequestException( + 'Recurring donation must specify interval.', + ); + } + + const now = new Date(); + const donation = { + id: nextId++, + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + amount: request.amount, + isAnonymous: request.isAnonymous ?? false, + donationType: + request.donationType === 'one_time' + ? DonationType.ONE_TIME + : DonationType.RECURRING, + recurringInterval: request.recurringInterval ?? null, + dedicationMessage: request.dedicationMessage ?? undefined, + showDedicationPublicly: request.showDedicationPublicly ?? false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + }; + inMemoryDonations.push(donation); + return donation; + }), + findPublic: jest.fn(async (limit?: number) => { + return inMemoryDonations + .filter( + (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, + ) + .slice(0, limit ?? 50); + }), + getTotalDonations: jest.fn(async () => { + const succeeded = inMemoryDonations.filter( + (d) => d.status === DonationStatus.SUCCEEDED, + ); + const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); + return { total, count: succeeded.length }; + }), + }; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + controllers: [DonationsController], + providers: [ + { provide: DonationsService, useValue: mockService }, + { provide: DonationsRepository, useValue: {} }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + // Match runtime API prefix used by the real application + app.setGlobalPrefix('api'); + await app.init(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + // Helper sample payloads used in the stubs below + const oneTimePayload = { + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + amount: 50, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + }; + + const recurringPayload = { + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + amount: 25, + isAnonymous: false, + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }; + + // Reusable seeds and helpers for in-memory donation construction + const donationASeed: Pick = { + email: 'a@example.com', + amount: 10, + }; + const donationBSeed: Pick = { + email: 'b@example.com', + amount: 15, + }; + + function buildTestDonation( + seed: Pick, + now: Date, + overrides: Partial = {}, + ): TestDonation { + return { + id: nextId++, + firstName: oneTimePayload.firstName, + lastName: oneTimePayload.lastName, + email: seed.email, + amount: seed.amount, + isAnonymous: oneTimePayload.isAnonymous, + donationType: oneTimePayload.donationType, + recurringInterval: null, + dedicationMessage: null, + showDedicationPublicly: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + transactionId: null, + ...overrides, + }; + } + + // ---------- DTO shape validators ---------- + const isISODateString = (value: unknown): boolean => { + if (typeof value !== 'string') return false; + const dt = new Date(value); + return !Number.isNaN(dt.getTime()); + }; + + const expectDonationResponseDtoShape = ( + obj: Record, + expected: { + donationType: DonationType; + recurringInterval?: RecurringInterval | null; + email?: string; + firstName?: string; + lastName?: string; + transactionIdPresent?: boolean; + }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.firstName).toBe('string'); + expect(typeof obj.lastName).toBe('string'); + expect(typeof obj.email).toBe('string'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect(obj.donationType).toBe(expected.donationType); + if (expected.recurringInterval) { + expect(obj.recurringInterval).toBe(expected.recurringInterval); + } else { + // Could be null or undefined depending on mapper; both acceptable + expect(['undefined', 'string', 'object']).toContain( + typeof obj.recurringInterval as string, + ); + if (typeof obj.recurringInterval === 'string') { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval); + } else if (typeof obj.recurringInterval === 'object') { + // JSON null + expect(obj.recurringInterval).toBeNull(); + } + } + // dedicationMessage is optional + if (obj.dedicationMessage !== undefined && obj.dedicationMessage !== null) { + expect(typeof obj.dedicationMessage).toBe('string'); + } + expect(typeof obj.showDedicationPublicly).toBe('boolean'); + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); + expect(isISODateString(String(obj.createdAt))).toBe(true); + expect(isISODateString(String(obj.updatedAt))).toBe(true); + if (expected.transactionIdPresent) { + expect(typeof obj.transactionId).toBe('string'); + } else { + // Can be absent or null + expect(['undefined', 'string']).toContain( + typeof obj.transactionId as string, + ); + } + }; + + const expectPublicDonationDtoShape = ( + obj: Record, + opts: { anonymous: boolean; hasDedication: boolean }, + ) => { + expect(typeof obj.id).toBe('number'); + expect(typeof obj.amount).toBe('number'); + expect(typeof obj.isAnonymous).toBe('boolean'); + expect([DonationType.ONE_TIME, DonationType.RECURRING]).toContain( + obj.donationType as DonationType, + ); + if (obj.recurringInterval !== undefined && obj.recurringInterval !== null) { + expect([ + RecurringInterval.WEEKLY, + RecurringInterval.MONTHLY, + RecurringInterval.BIMONTHLY, + RecurringInterval.QUARTERLY, + RecurringInterval.ANNUALLY, + ]).toContain(obj.recurringInterval as RecurringInterval); + } + expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( + obj.status as string, + ); + expect(isISODateString(String(obj.createdAt))).toBe(true); + + if (opts.anonymous) { + expect(obj).not.toHaveProperty('donorName'); + } else { + expect(typeof obj.donorName).toBe('string'); + expect((obj.donorName as string).length).toBeGreaterThan(0); + } + + if (opts.hasDedication) { + expect(typeof obj.dedicationMessage).toBe('string'); + } else { + expect(obj).not.toHaveProperty('dedicationMessage'); + } + + // Ensure sensitive fields are not leaked in public DTO + expect(obj).not.toHaveProperty('email'); + expect(obj).not.toHaveProperty('firstName'); + expect(obj).not.toHaveProperty('lastName'); + expect(obj).not.toHaveProperty('transactionId'); + }; + + it('smoke: GET / (should 404 or 200 depending on routes)', async () => { + const res = await request(app.getHttpServer()).get('/'); + expect([200, 404]).toContain(res.status); + }); + + describe('POST /api/donations', () => { + it('rejects a negative amount (returns 400)', async () => { + const payload = { ...oneTimePayload, amount: -10 }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects an invalid email format amount (returns 400)', async () => { + const payload = { ...oneTimePayload, email: 'not-an-email' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects a donation marked recurring if recurringInterval is missing', async () => { + const payload: Partial = { ...recurringPayload }; + delete payload.recurringInterval; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { + const payload: Record = { + ...oneTimePayload, + recurringInterval: 'MONTHLY', + }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + }); + + it('throws 500 server error if the database errors', async () => { + // Simulate a DB failure by making the mocked service throw + mockService.create.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + const payload = { ...oneTimePayload }; + try { + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.create.mockClear(); + } + }); + + it('gracefully rejects a payload that is missing the first name', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.firstName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'firstName'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the last name', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.lastName; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'lastName'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the email', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.email; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'email'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is missing the amount', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.amount; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'amount'.toLowerCase(), + ); + }); + + it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.isAnonymous; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(201); + + expect(res.body).toHaveProperty('id'); + expect(res.body.amount).toBe(payload.amount); + + const created = inMemoryDonations.find((d) => d.email === payload.email); + expect(created).toBeDefined(); + expect(created!.amount).toBe(payload.amount); + }); + + it('gracefully rejects a payload that is missing the donationType', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload }; + delete payload.donationType; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, recurring: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'recurring'.toLowerCase(), + ); + }); + + it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const payload: any = { ...oneTimePayload, donationType: 'invalid' }; + + const res = await request(app.getHttpServer()) + .post('/api/donations') + .send(payload) + .expect(400); + + expect(res.body).toHaveProperty('statusCode', 400); + expect(res.body).toHaveProperty('message'); + expect(String(res.body.message).toLowerCase()).toContain( + 'donationType'.toLowerCase(), + ); + }); + }); + + describe('GET /api/donations/public', () => { + it('returns only non-anonymous donations', async () => { + const now = new Date(); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'public@example.com', + isAnonymous: false, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); + inMemoryDonations.push({ + ...oneTimePayload, + email: 'anon@example.com', + isAnonymous: true, + status: DonationStatus.SUCCEEDED, + createdAt: now, + updatedAt: now, + id: nextId++, + } as TestDonation); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); + // Validate public DTO shape for the first item + if (res.body.length > 0) { + expectPublicDonationDtoShape(res.body[0], { + anonymous: false, + hasDedication: false, + }); + } + }); + + it('returns no donations if there are none in the database', async () => { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect( + res.body.every( + (d: { isAnonymous: boolean }) => d.isAnonymous === false, + ), + ).toBe(true); + }); + + it('throws 500 server error if the database errors', async () => { + // Simulate DB find/query failures by making the mocked service throw + mockService.findPublic.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.findPublic.mockClear(); + } + }); + + it('Returns items with correct DTO (expected keys)', async () => { + inMemoryDonations.length = 0; + const now = new Date(); + inMemoryDonations.push( + buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { + isAnonymous: false, + showDedicationPublicly: true, + dedicationMessage: 'Nice work', + donationType: DonationType.RECURRING, + recurringInterval: RecurringInterval.MONTHLY, + }), + ); + + const res = await request(app.getHttpServer()) + .get('/api/donations/public') + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + if (res.body.length > 0) { + const item = res.body[0]; + const keys = Object.keys(item); + const required = [ + 'id', + 'amount', + 'isAnonymous', + 'donationType', + 'status', + 'createdAt', + ]; + const optional = [ + 'donorName', + 'recurringInterval', + 'dedicationMessage', + ]; + const allowed = [...required, ...optional]; + + required.forEach((k) => expect(keys).toContain(k)); + keys.forEach((k) => expect(allowed).toContain(k)); + } + }); + }); + + describe('GET /api/donations/stats', () => { + it('successfully returns the correct total and count', async () => { + // Example: seed two donations and verify totals endpoint + inMemoryDonations.length = 0; // reset + const now = new Date(); + inMemoryDonations.push(buildTestDonation(donationASeed, now)); + inMemoryDonations.push(buildTestDonation(donationBSeed, now)); + + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 25, count: 2 }); + }); + + it('successfully returns the correct total and count even if the database is empty', async () => { + inMemoryDonations.length = 0; + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(200); + + expect(res.body).toEqual({ total: 0, count: 0 }); + }); + + it('throws 500 server error if the database errors', async () => { + mockService.getTotalDonations.mockRejectedValueOnce( + new Error('Simulated DB failure'), + ); + try { + const res = await request(app.getHttpServer()) + .get('/api/donations/stats') + .expect(500); + expect(res.body).toHaveProperty('statusCode', 500); + expect(res.body).toHaveProperty('message'); + } finally { + mockService.getTotalDonations.mockClear(); + } + }); + }); +}); diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 0870e13..cba94bc 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -58,7 +58,6 @@ export class DonationsController { @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) createDonationDto: CreateDonationDto, ): Promise { - console.log('[DonationsController.create] payload:', createDonationDto); const request = DonationMappers.toCreateDonationRequest(createDonationDto); const donation = await this.donationsService.create(request); return DonationMappers.toDonationResponseDto(donation); @@ -85,7 +84,6 @@ export class DonationsController { async findPublic( @Query('limit', new ParseIntPipe({ optional: true })) limit?: number, ): Promise { - console.log('[DonationsController.findPublic] limit=', limit); const donations = await this.donationsService.findPublic(limit); return DonationMappers.toPublicDonationDtos(donations); } @@ -116,9 +114,7 @@ export class DonationsController { }, }) async getStats(): Promise<{ total: number; count: number }> { - console.log('[DonationsController.getStats] fetching totals'); const stats = await this.donationsService.getTotalDonations(); - console.log('[DonationsController.getStats] result=', stats); return stats; } diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index f340a04..e2fda9c 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -15,10 +15,6 @@ export class DonationsService { async create( createDonationRequest: CreateDonationRequest, ): Promise { - console.log( - '[DonationsService.create] incoming request:', - createDonationRequest, - ); if (createDonationRequest.amount <= 0) { throw new BadRequestException('Donation amount must be positive.'); } @@ -68,11 +64,6 @@ export class DonationsService { const savedDonation = await this.donationRepository.save(donation); - console.log( - `[DonationsService.create] savedDonation id=${savedDonation.id} email=${savedDonation.email} amount=${savedDonation.amount}`, - savedDonation, - ); - return { id: savedDonation.id, firstName: savedDonation.firstName, @@ -129,7 +120,6 @@ export class DonationsService { } async findPublic(limit = 50): Promise { - console.log('[DonationsService.findPublic] limit=', limit); const donations: Donation[] = await this.donationRepository.find({ take: limit, order: { createdAt: 'DESC' }, @@ -191,13 +181,10 @@ export class DonationsService { } async getTotalDonations(): Promise<{ total: number; count: number }> { - console.log('[DonationsService.getTotalDonations] querying totals'); const [donations] = await this.donationRepository.manager.query( `SELECT COUNT(amount) AS count, SUM(amount) AS total FROM donations`, ); - console.log('[DonationsService.getTotalDonations] result=', donations); - // SQL SUM returns null when no rows exist; coerce to numbers with sensible defaults const total = donations.total !== null && donations.total !== undefined diff --git a/apps/backend/test/donations.controller.spec.ts b/apps/backend/test/donations.controller.spec.ts deleted file mode 100644 index 8cdf6c4..0000000 --- a/apps/backend/test/donations.controller.spec.ts +++ /dev/null @@ -1,680 +0,0 @@ -import { INestApplication, BadRequestException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import request from 'supertest'; -import { - DonationType, - RecurringInterval, - DonationStatus, -} from '../src/donations/donation.entity'; -import { DonationsController } from '../src/donations/donations.controller'; -import { DonationsService } from '../src/donations/donations.service'; -import { DonationsRepository } from '../src/donations/donations.repository'; -// import { User } from '../src/users/user.entity'; - -interface TestDonation { - id: number; - firstName: string; - lastName: string; - email: string; - amount: number; - isAnonymous: boolean; - donationType: DonationType; - recurringInterval: RecurringInterval | null; - dedicationMessage?: string | null; - showDedicationPublicly: boolean; - status: DonationStatus; - createdAt: Date; - updatedAt: Date; - transactionId?: string | null; -} - -describe('Donations (e2e) - expanded stubs', () => { - // Increase Jest timeout for slower CI/initialization (DB + Nest app init) - // Default is 5000ms which is often too small for integration tests. - jest.setTimeout(30000); - - let app: INestApplication; - // We use an in-memory array to simulate stored donations for controller tests - let inMemoryDonations: TestDonation[] = []; - let nextId = 1; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let mockService: any; - - beforeAll(async () => { - // Create a testing module that instantiates the DonationsController but - // uses a simple in-memory mock for the DonationsService so tests don't - // depend on TypeORM behavior during controller-level validation tests. - inMemoryDonations = []; - nextId = 1; - - mockService = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - create: jest.fn(async (request: any) => { - // Validation to match real service behavior - if ( - request.donationType === 'recurring' && - !request.recurringInterval - ) { - throw new BadRequestException( - 'Recurring donation must specify interval.', - ); - } - - const now = new Date(); - const donation = { - id: nextId++, - firstName: request.firstName, - lastName: request.lastName, - email: request.email, - amount: request.amount, - isAnonymous: request.isAnonymous ?? false, - donationType: - request.donationType === 'one_time' - ? DonationType.ONE_TIME - : DonationType.RECURRING, - recurringInterval: request.recurringInterval ?? null, - dedicationMessage: request.dedicationMessage ?? undefined, - showDedicationPublicly: request.showDedicationPublicly ?? false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - }; - inMemoryDonations.push(donation); - return donation; - }), - findPublic: jest.fn(async (limit?: number) => { - return inMemoryDonations - .filter( - (d) => d.status === DonationStatus.SUCCEEDED && !d.isAnonymous, - ) - .slice(0, limit ?? 50); - }), - getTotalDonations: jest.fn(async () => { - const succeeded = inMemoryDonations.filter( - (d) => d.status === DonationStatus.SUCCEEDED, - ); - const total = succeeded.reduce((s, d) => s + (d.amount || 0), 0); - return { total, count: succeeded.length }; - }), - }; - - const moduleFixture: TestingModule = await Test.createTestingModule({ - controllers: [DonationsController], - providers: [ - { provide: DonationsService, useValue: mockService }, - { provide: DonationsRepository, useValue: {} }, - ], - }).compile(); - - app = moduleFixture.createNestApplication(); - // Match runtime API prefix used by the real application - app.setGlobalPrefix('api'); - await app.init(); - }); - - afterAll(async () => { - if (app) { - await app.close(); - } - }); - - // Helper sample payloads used in the stubs below - const oneTimePayload = { - firstName: 'Jane', - lastName: 'Doe', - email: 'jane.doe@example.com', - amount: 50, - isAnonymous: false, - donationType: DonationType.ONE_TIME, - }; - - const recurringPayload = { - firstName: 'Alice', - lastName: 'Smith', - email: 'alice.smith@example.com', - amount: 25, - isAnonymous: false, - donationType: DonationType.RECURRING, - recurringInterval: RecurringInterval.MONTHLY, - }; - - // Reusable seeds and helpers for in-memory donation construction - const donationASeed: Pick = { - email: 'a@example.com', - amount: 10, - }; - const donationBSeed: Pick = { - email: 'b@example.com', - amount: 15, - }; - - function buildTestDonation( - seed: Pick, - now: Date, - overrides: Partial = {}, - ): TestDonation { - return { - id: nextId++, - firstName: oneTimePayload.firstName, - lastName: oneTimePayload.lastName, - email: seed.email, - amount: seed.amount, - isAnonymous: oneTimePayload.isAnonymous, - donationType: oneTimePayload.donationType, - recurringInterval: null, - dedicationMessage: null, - showDedicationPublicly: false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - transactionId: null, - ...overrides, - }; - } - - // ---------- DTO shape validators ---------- - const isISODateString = (value: unknown): boolean => { - if (typeof value !== 'string') return false; - const dt = new Date(value); - return !Number.isNaN(dt.getTime()); - }; - - const expectDonationResponseDtoShape = ( - obj: Record, - expected: { - donationType: DonationType; - recurringInterval?: RecurringInterval | null; - email?: string; - firstName?: string; - lastName?: string; - transactionIdPresent?: boolean; - }, - ) => { - expect(typeof obj.id).toBe('number'); - expect(typeof obj.firstName).toBe('string'); - expect(typeof obj.lastName).toBe('string'); - expect(typeof obj.email).toBe('string'); - expect(typeof obj.amount).toBe('number'); - expect(typeof obj.isAnonymous).toBe('boolean'); - expect(obj.donationType).toBe(expected.donationType); - if (expected.recurringInterval) { - expect(obj.recurringInterval).toBe(expected.recurringInterval); - } else { - // Could be null or undefined depending on mapper; both acceptable - expect(['undefined', 'string', 'object']).toContain( - typeof obj.recurringInterval as string, - ); - if (typeof obj.recurringInterval === 'string') { - expect([ - RecurringInterval.WEEKLY, - RecurringInterval.MONTHLY, - RecurringInterval.BIMONTHLY, - RecurringInterval.QUARTERLY, - RecurringInterval.ANNUALLY, - ]).toContain(obj.recurringInterval); - } else if (typeof obj.recurringInterval === 'object') { - // JSON null - expect(obj.recurringInterval).toBeNull(); - } - } - // dedicationMessage is optional - if (obj.dedicationMessage !== undefined && obj.dedicationMessage !== null) { - expect(typeof obj.dedicationMessage).toBe('string'); - } - expect(typeof obj.showDedicationPublicly).toBe('boolean'); - expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( - obj.status as string, - ); - expect(isISODateString(String(obj.createdAt))).toBe(true); - expect(isISODateString(String(obj.updatedAt))).toBe(true); - if (expected.transactionIdPresent) { - expect(typeof obj.transactionId).toBe('string'); - } else { - // Can be absent or null - expect(['undefined', 'string']).toContain( - typeof obj.transactionId as string, - ); - } - }; - - const expectPublicDonationDtoShape = ( - obj: Record, - opts: { anonymous: boolean; hasDedication: boolean }, - ) => { - expect(typeof obj.id).toBe('number'); - expect(typeof obj.amount).toBe('number'); - expect(typeof obj.isAnonymous).toBe('boolean'); - expect([DonationType.ONE_TIME, DonationType.RECURRING]).toContain( - obj.donationType as DonationType, - ); - if (obj.recurringInterval !== undefined && obj.recurringInterval !== null) { - expect([ - RecurringInterval.WEEKLY, - RecurringInterval.MONTHLY, - RecurringInterval.BIMONTHLY, - RecurringInterval.QUARTERLY, - RecurringInterval.ANNUALLY, - ]).toContain(obj.recurringInterval as RecurringInterval); - } - expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( - obj.status as string, - ); - expect(isISODateString(String(obj.createdAt))).toBe(true); - - if (opts.anonymous) { - expect(obj).not.toHaveProperty('donorName'); - } else { - expect(typeof obj.donorName).toBe('string'); - expect((obj.donorName as string).length).toBeGreaterThan(0); - } - - if (opts.hasDedication) { - expect(typeof obj.dedicationMessage).toBe('string'); - } else { - expect(obj).not.toHaveProperty('dedicationMessage'); - } - - // Ensure sensitive fields are not leaked in public DTO - expect(obj).not.toHaveProperty('email'); - expect(obj).not.toHaveProperty('firstName'); - expect(obj).not.toHaveProperty('lastName'); - expect(obj).not.toHaveProperty('transactionId'); - }; - - it('smoke: GET / (should 404 or 200 depending on routes)', async () => { - const res = await request(app.getHttpServer()).get('/'); - expect([200, 404]).toContain(res.status); - }); - - describe('POST /api/donations', () => { - it('Successfuly commits a one-time donation creation', async () => { - const payload = { ...oneTimePayload }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(201); - - expectDonationResponseDtoShape(res.body, { - donationType: DonationType.ONE_TIME, - recurringInterval: null, - }); - expect(res.body.amount).toBe(payload.amount); - - const created = inMemoryDonations.find((d) => d.email === payload.email); - expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); - }); - - it('Successfuly creates a recurring donation with interval', async () => { - const payload = { ...recurringPayload }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(201); - - expectDonationResponseDtoShape(res.body, { - donationType: DonationType.RECURRING, - recurringInterval: RecurringInterval.MONTHLY, - }); - }); - - it('rejects a negative amount (returns 400)', async () => { - const payload = { ...oneTimePayload, amount: -10 }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - }); - - it('rejects an invalid email format amount (returns 400)', async () => { - const payload = { ...oneTimePayload, email: 'not-an-email' }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - }); - - it('rejects a donation marked recurring if recurringInterval is missing', async () => { - const payload: Partial = { ...recurringPayload }; - delete payload.recurringInterval; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - }); - - it('rejects a one-time donation that has a recurring interval (returns 400)', async () => { - const payload: Record = { - ...oneTimePayload, - recurringInterval: 'MONTHLY', - }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - }); - - it('throws 500 server error if the database errors', async () => { - // Simulate a DB failure by making the mocked service throw - mockService.create.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - const payload = { ...oneTimePayload }; - try { - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.create.mockClear(); - } - }); - - it('gracefully rejects a payload that is missing the first name', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.firstName; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'firstName'.toLowerCase(), - ); - }); - - it('gracefully rejects a payload that is missing the last name', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.lastName; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'lastName'.toLowerCase(), - ); - }); - - it('gracefully rejects a payload that is missing the email', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.email; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'email'.toLowerCase(), - ); - }); - - it('gracefully rejects a payload that is missing the amount', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.amount; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'amount'.toLowerCase(), - ); - }); - - it('Successfuly commits a one-time donation creation even if isAnonymous is missing', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.isAnonymous; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(201); - - expect(res.body).toHaveProperty('id'); - expect(res.body.amount).toBe(payload.amount); - - const created = inMemoryDonations.find((d) => d.email === payload.email); - expect(created).toBeDefined(); - expect(created!.amount).toBe(payload.amount); - }); - - it('gracefully rejects a payload that is missing the donationType', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload }; - delete payload.donationType; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'donationType'.toLowerCase(), - ); - }); - - it('gracefully rejects a payload that is contains the wrong recurring interval (not the enum)', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload, recurring: 'invalid' }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'recurring'.toLowerCase(), - ); - }); - - it('gracefully rejects a payload that is contains the wrong donation type (not the enum)', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payload: any = { ...oneTimePayload, donationType: 'invalid' }; - - const res = await request(app.getHttpServer()) - .post('/api/donations') - .send(payload) - .expect(400); - - expect(res.body).toHaveProperty('statusCode', 400); - expect(res.body).toHaveProperty('message'); - expect(String(res.body.message).toLowerCase()).toContain( - 'donationType'.toLowerCase(), - ); - }); - }); - - describe('GET /api/donations/public', () => { - it('returns only non-anonymous donations', async () => { - const now = new Date(); - inMemoryDonations.push({ - ...oneTimePayload, - email: 'public@example.com', - isAnonymous: false, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - id: nextId++, - } as TestDonation); - inMemoryDonations.push({ - ...oneTimePayload, - email: 'anon@example.com', - isAnonymous: true, - status: DonationStatus.SUCCEEDED, - createdAt: now, - updatedAt: now, - id: nextId++, - } as TestDonation); - - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(200); - - expect(Array.isArray(res.body)).toBe(true); - expect( - res.body.every( - (d: { isAnonymous: boolean }) => d.isAnonymous === false, - ), - ).toBe(true); - // Validate public DTO shape for the first item - if (res.body.length > 0) { - expectPublicDonationDtoShape(res.body[0], { - anonymous: false, - hasDedication: false, - }); - } - }); - - it('returns no donations if there are none in the database', async () => { - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(200); - - expect(Array.isArray(res.body)).toBe(true); - expect( - res.body.every( - (d: { isAnonymous: boolean }) => d.isAnonymous === false, - ), - ).toBe(true); - }); - - it('throws 500 server error if the database errors', async () => { - // Simulate DB find/query failures by making the mocked service throw - mockService.findPublic.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.findPublic.mockClear(); - } - }); - - it('Returns items with correct DTO (expected keys)', async () => { - inMemoryDonations.length = 0; - const now = new Date(); - inMemoryDonations.push( - buildTestDonation({ email: 'x@example.com', amount: 11 }, now, { - isAnonymous: false, - showDedicationPublicly: true, - dedicationMessage: 'Nice work', - donationType: DonationType.RECURRING, - recurringInterval: RecurringInterval.MONTHLY, - }), - ); - - const res = await request(app.getHttpServer()) - .get('/api/donations/public') - .expect(200); - - expect(Array.isArray(res.body)).toBe(true); - if (res.body.length > 0) { - const item = res.body[0]; - const keys = Object.keys(item); - const required = [ - 'id', - 'amount', - 'isAnonymous', - 'donationType', - 'status', - 'createdAt', - ]; - const optional = [ - 'donorName', - 'recurringInterval', - 'dedicationMessage', - ]; - const allowed = [...required, ...optional]; - - required.forEach((k) => expect(keys).toContain(k)); - keys.forEach((k) => expect(allowed).toContain(k)); - } - }); - }); - - describe('GET /api/donations/stats', () => { - it('successfully returns the correct total and count', async () => { - // Example: seed two donations and verify totals endpoint - inMemoryDonations.length = 0; // reset - const now = new Date(); - inMemoryDonations.push(buildTestDonation(donationASeed, now)); - inMemoryDonations.push(buildTestDonation(donationBSeed, now)); - - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(200); - - expect(res.body).toEqual({ total: 25, count: 2 }); - }); - - it('successfully returns the correct total and count even if the database is empty', async () => { - inMemoryDonations.length = 0; - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(200); - - expect(res.body).toEqual({ total: 0, count: 0 }); - }); - - it('throws 500 server error if the database errors', async () => { - mockService.getTotalDonations.mockRejectedValueOnce( - new Error('Simulated DB failure'), - ); - try { - const res = await request(app.getHttpServer()) - .get('/api/donations/stats') - .expect(500); - expect(res.body).toHaveProperty('statusCode', 500); - expect(res.body).toHaveProperty('message'); - } finally { - mockService.getTotalDonations.mockClear(); - } - }); - }); -}); diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index f99b195..4ed8155 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -93,7 +93,7 @@ describe('Donations (e2e) - expanded stubs', () => { if (app) { await app.close(); } - if (dataSource) { + if (dataSource && dataSource.isInitialized) { await dataSource.destroy(); } }); @@ -119,10 +119,16 @@ describe('Donations (e2e) - expanded stubs', () => { }; // ---------- DTO shape validators ---------- - const isISODateString = (value: unknown): boolean => { - if (typeof value !== 'string') return false; - const dt = new Date(value); - return !Number.isNaN(dt.getTime()); + const isValidDateValue = (value: unknown): boolean => { + // Accept either a Date object or a string that can be parsed as a date. + if (value instanceof Date) { + return !Number.isNaN(value.getTime()); + } + if (typeof value === 'string') { + const dt = new Date(value); + return !Number.isNaN(dt.getTime()); + } + return false; }; const expectDonationResponseDtoShape = ( @@ -171,8 +177,8 @@ describe('Donations (e2e) - expanded stubs', () => { expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( obj.status as string, ); - expect(isISODateString(String(obj.createdAt))).toBe(true); - expect(isISODateString(String(obj.updatedAt))).toBe(true); + expect(isValidDateValue(obj.createdAt)).toBe(true); + expect(isValidDateValue(obj.updatedAt)).toBe(true); if (expected.transactionIdPresent) { expect(typeof obj.transactionId).toBe('string'); } else { @@ -205,7 +211,7 @@ describe('Donations (e2e) - expanded stubs', () => { expect(['pending', 'succeeded', 'failed', 'cancelled']).toContain( obj.status as string, ); - expect(isISODateString(String(obj.createdAt))).toBe(true); + expect(isValidDateValue(obj.createdAt)).toBe(true); if (opts.anonymous) { expect(obj).not.toHaveProperty('donorName'); @@ -264,8 +270,8 @@ describe('Donations (e2e) - expanded stubs', () => { expect((created as Donation).dedicationMessage).toBeUndefined(); expect((created as Donation).showDedicationPublicly).toBe(false); expect((created as Donation).status).toBe('pending'); - expect(isISODateString((created as Donation).createdAt)).toBe(true); - expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect(isValidDateValue((created as Donation).createdAt)).toBe(true); + expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); expect((created as Donation).transactionId).toBeInstanceOf(Number); } }); @@ -302,10 +308,10 @@ describe('Donations (e2e) - expanded stubs', () => { payload.recurringInterval, ); expect((created as Donation).dedicationMessage).toBeUndefined(); - expect((created as Donation).showDedicationPublicly).toBe(false); + expect((created as Donation).showDedicationPublicly).toBe(true); expect((created as Donation).status).toBe('pending'); - expect(isISODateString((created as Donation).createdAt)).toBe(true); - expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect(isValidDateValue((created as Donation).createdAt)).toBe(true); + expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); expect((created as Donation).transactionId).toBeInstanceOf(Number); } }); @@ -486,8 +492,8 @@ describe('Donations (e2e) - expanded stubs', () => { expect((created as Donation).dedicationMessage).toBeUndefined(); expect((created as Donation).showDedicationPublicly).toBe(false); expect((created as Donation).status).toBe('pending'); - expect(isISODateString((created as Donation).createdAt)).toBe(true); - expect(isISODateString((created as Donation).updatedAt)).toBe(true); + expect(isValidDateValue((created as Donation).createdAt)).toBe(true); + expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); expect((created as Donation).transactionId).toBeInstanceOf(Number); } }); @@ -697,8 +703,6 @@ describe('Donations (e2e) - expanded stubs', () => { }); it('successfully returns the correct total and count even if the database is empty', async () => { - await donationRepository.clear(); - const res = await request(app.getHttpServer()) .get('/api/donations/stats') .expect(200); From 42a367f8f3dc218be42804697e3ca013a967998f Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:32:25 -0500 Subject: [PATCH 19/23] Fix for created at, may reduce performance(?) --- .../src/donations/donations.service.ts | 34 +++++++++++-------- apps/backend/test/donations.e2e-spec.ts | 4 --- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index e2fda9c..38fd3ad 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -62,33 +62,39 @@ export class DonationsService { showDedicationPublicly: createDonationRequest.showDedicationPublicly, }); + // Reload from database so any DB-side defaults are reflected const savedDonation = await this.donationRepository.save(donation); + const reloaded = await this.donationRepository.findOne({ + where: { id: savedDonation.id }, + }); + + const finalDonation = reloaded ?? savedDonation; return { - id: savedDonation.id, - firstName: savedDonation.firstName, - lastName: savedDonation.lastName, - email: savedDonation.email, - amount: savedDonation.amount, - isAnonymous: savedDonation.isAnonymous, - donationType: savedDonation.donationType as 'one_time' | 'recurring', - recurringInterval: savedDonation.recurringInterval as + id: finalDonation.id, + firstName: finalDonation.firstName, + lastName: finalDonation.lastName, + email: finalDonation.email, + amount: finalDonation.amount, + isAnonymous: finalDonation.isAnonymous, + donationType: finalDonation.donationType as 'one_time' | 'recurring', + recurringInterval: finalDonation.recurringInterval as | 'weekly' | 'monthly' | 'bimonthly' | 'quarterly' | 'annually' | undefined, - dedicationMessage: savedDonation.dedicationMessage || undefined, - showDedicationPublicly: savedDonation.showDedicationPublicly, - status: savedDonation.status as + dedicationMessage: finalDonation.dedicationMessage || undefined, + showDedicationPublicly: finalDonation.showDedicationPublicly, + status: finalDonation.status as | 'pending' | 'succeeded' | 'failed' | 'cancelled', - createdAt: savedDonation.createdAt, - updatedAt: savedDonation.updatedAt, - transactionId: savedDonation.transactionId || undefined, + createdAt: finalDonation.createdAt, + updatedAt: finalDonation.updatedAt, + transactionId: finalDonation.transactionId || undefined, }; } diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 4ed8155..a25eac1 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -121,9 +121,6 @@ describe('Donations (e2e) - expanded stubs', () => { // ---------- DTO shape validators ---------- const isValidDateValue = (value: unknown): boolean => { // Accept either a Date object or a string that can be parsed as a date. - if (value instanceof Date) { - return !Number.isNaN(value.getTime()); - } if (typeof value === 'string') { const dt = new Date(value); return !Number.isNaN(dt.getTime()); @@ -283,7 +280,6 @@ describe('Donations (e2e) - expanded stubs', () => { .post('/api/donations') .send(payload) .expect(201); - expectDonationResponseDtoShape(res.body, { donationType: DonationType.RECURRING, recurringInterval: RecurringInterval.MONTHLY, From a68e40005d1efa328803d5399dfe5bec67ae0e65 Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:04:27 -0500 Subject: [PATCH 20/23] Fix for returning non-anonymous donations --- apps/backend/src/donations/donations.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 38fd3ad..46d3860 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -1,7 +1,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { DonationResponseDto } from './dtos/donation-response-dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Donation, DonationType, RecurringInterval } from './donation.entity'; +import { + Donation, + DonationType, + RecurringInterval, + DonationStatus, +} from './donation.entity'; import { Repository } from 'typeorm'; import { CreateDonationRequest, Donation as DomainDonation } from './mappers'; @@ -126,7 +131,9 @@ export class DonationsService { } async findPublic(limit = 50): Promise { + // Return only non-anonymous, succeeded donations for public display const donations: Donation[] = await this.donationRepository.find({ + where: { isAnonymous: false, status: DonationStatus.SUCCEEDED }, take: limit, order: { createdAt: 'DESC' }, }); From 59590909632607ab37d216591bac055facac9afe Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Fri, 21 Nov 2025 19:03:55 -0500 Subject: [PATCH 21/23] New migration created for int amount --- apps/backend/src/donations/donation.entity.ts | 2 +- .../migrations/1759151447065-add_donations.ts | 31 ------------------- .../migrations/1763769154611-add_donations.ts | 15 +++++++++ 3 files changed, 16 insertions(+), 32 deletions(-) delete mode 100644 apps/backend/src/migrations/1759151447065-add_donations.ts create mode 100644 apps/backend/src/migrations/1763769154611-add_donations.ts diff --git a/apps/backend/src/donations/donation.entity.ts b/apps/backend/src/donations/donation.entity.ts index fc9a168..6ebca76 100644 --- a/apps/backend/src/donations/donation.entity.ts +++ b/apps/backend/src/donations/donation.entity.ts @@ -36,7 +36,7 @@ export class Donation { @Column() email: string; - @Column({ type: 'numeric', precision: 10, scale: 2 }) + @Column({ type: 'int' }) amount: number; @Column({ default: false }) diff --git a/apps/backend/src/migrations/1759151447065-add_donations.ts b/apps/backend/src/migrations/1759151447065-add_donations.ts deleted file mode 100644 index acc9503..0000000 --- a/apps/backend/src/migrations/1759151447065-add_donations.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddDonations1759151447065 implements MigrationInterface { - name = 'AddDonations1759151447065'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "donations" ( - "id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, - "firstName" character varying NOT NULL, - "lastName" character varying NOT NULL, - "email" character varying NOT NULL, - "amount" numeric(10,2) NOT NULL, - "isAnonymous" boolean NOT NULL DEFAULT false, - "donationType" character varying NOT NULL, - "recurringInterval" character varying, - "dedicationMessage" character varying, - "showDedicationPublicly" boolean NOT NULL DEFAULT false, - "status" character varying NOT NULL DEFAULT 'pending', - "transactionId" character varying, - "createdAt" TIMESTAMP NOT NULL DEFAULT now(), - "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), - CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id") - )`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "donations"`); - } -} diff --git a/apps/backend/src/migrations/1763769154611-add_donations.ts b/apps/backend/src/migrations/1763769154611-add_donations.ts new file mode 100644 index 0000000..017df2e --- /dev/null +++ b/apps/backend/src/migrations/1763769154611-add_donations.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDonations1763769154611 implements MigrationInterface { + name = 'AddDonations1763769154611'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "donations" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "email" character varying NOT NULL, "amount" integer NOT NULL, "isAnonymous" boolean NOT NULL DEFAULT false, "donationType" character varying NOT NULL, "recurringInterval" character varying, "dedicationMessage" character varying, "showDedicationPublicly" boolean NOT NULL DEFAULT false, "status" character varying NOT NULL DEFAULT 'pending', "transactionId" character varying, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "donations"`); + } +} From 8f12545b151146f4aae407d1946d118632b5a91f Mon Sep 17 00:00:00 2001 From: Sam Nie <147653722+SamNie2027@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:46:30 -0500 Subject: [PATCH 22/23] Defaulting createdAt and updatedAt , tests pass --- apps/backend/src/donations/donation.entity.ts | 12 +++++-- .../migrations/1763769154611-add_donations.ts | 2 +- apps/backend/test/donations.e2e-spec.ts | 36 ++++++++++++++----- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/donations/donation.entity.ts b/apps/backend/src/donations/donation.entity.ts index 6ebca76..906a013 100644 --- a/apps/backend/src/donations/donation.entity.ts +++ b/apps/backend/src/donations/donation.entity.ts @@ -1,4 +1,10 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; export enum DonationType { ONE_TIME = 'one_time', @@ -60,9 +66,9 @@ export class Donation { @Column({ nullable: true }) transactionId: string | null; - @Column() + @CreateDateColumn({ type: 'timestamp', default: () => 'now()' }) createdAt: Date; - @Column() + @UpdateDateColumn({ type: 'timestamp', default: () => 'now()' }) updatedAt: Date; } diff --git a/apps/backend/src/migrations/1763769154611-add_donations.ts b/apps/backend/src/migrations/1763769154611-add_donations.ts index 017df2e..ed6c1bf 100644 --- a/apps/backend/src/migrations/1763769154611-add_donations.ts +++ b/apps/backend/src/migrations/1763769154611-add_donations.ts @@ -5,7 +5,7 @@ export class AddDonations1763769154611 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "donations" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "email" character varying NOT NULL, "amount" integer NOT NULL, "isAnonymous" boolean NOT NULL DEFAULT false, "donationType" character varying NOT NULL, "recurringInterval" character varying, "dedicationMessage" character varying, "showDedicationPublicly" boolean NOT NULL DEFAULT false, "status" character varying NOT NULL DEFAULT 'pending', "transactionId" character varying, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id"))`, + `CREATE TABLE "donations" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "email" character varying NOT NULL, "amount" integer NOT NULL, "isAnonymous" boolean NOT NULL DEFAULT false, "donationType" character varying NOT NULL, "recurringInterval" character varying, "dedicationMessage" character varying, "showDedicationPublicly" boolean NOT NULL DEFAULT false, "status" character varying NOT NULL DEFAULT 'pending', "transactionId" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id"))`, ); } diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index a25eac1..9b4afd4 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -120,11 +120,29 @@ describe('Donations (e2e) - expanded stubs', () => { // ---------- DTO shape validators ---------- const isValidDateValue = (value: unknown): boolean => { + console.log('value:', value); + console.log(typeof value); // Accept either a Date object or a string that can be parsed as a date. + if (value instanceof Date) { + return !Number.isNaN(value.getTime()); + } if (typeof value === 'string') { const dt = new Date(value); return !Number.isNaN(dt.getTime()); } + // Some DB drivers may return date-like objects; try to stringify/parse as fallback + if (value && typeof value === 'object') { + try { + const asString = (value as any).toISOString + ? (value as any).toISOString() + : String(value); + const dt = new Date(asString); + return !Number.isNaN(dt.getTime()); + } catch { + return false; + } + } + return false; }; @@ -263,13 +281,13 @@ describe('Donations (e2e) - expanded stubs', () => { expect((created as Donation).amount).toBe(payload.amount); expect((created as Donation).isAnonymous).toBe(payload.isAnonymous); expect((created as Donation).donationType).toBe(payload.donationType); - expect((created as Donation).recurringInterval).toBeUndefined(); - expect((created as Donation).dedicationMessage).toBeUndefined(); + expect((created as Donation).recurringInterval).toBeNull(); + expect((created as Donation).dedicationMessage).toBeNull(); expect((created as Donation).showDedicationPublicly).toBe(false); expect((created as Donation).status).toBe('pending'); expect(isValidDateValue((created as Donation).createdAt)).toBe(true); expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); - expect((created as Donation).transactionId).toBeInstanceOf(Number); + expect((created as Donation).transactionId).toBeNull(); } }); @@ -303,12 +321,12 @@ describe('Donations (e2e) - expanded stubs', () => { expect((created as Donation).recurringInterval).toBe( payload.recurringInterval, ); - expect((created as Donation).dedicationMessage).toBeUndefined(); - expect((created as Donation).showDedicationPublicly).toBe(true); + expect((created as Donation).dedicationMessage).toBeNull(); + expect((created as Donation).showDedicationPublicly).toBe(false); expect((created as Donation).status).toBe('pending'); expect(isValidDateValue((created as Donation).createdAt)).toBe(true); expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); - expect((created as Donation).transactionId).toBeInstanceOf(Number); + expect((created as Donation).transactionId).toBeNull(); } }); @@ -484,13 +502,13 @@ describe('Donations (e2e) - expanded stubs', () => { expect((created as Donation).amount).toBe(payload.amount); expect((created as Donation).isAnonymous).toBe(false); expect((created as Donation).donationType).toBe(payload.donationType); - expect((created as Donation).recurringInterval).toBeUndefined(); - expect((created as Donation).dedicationMessage).toBeUndefined(); + expect((created as Donation).recurringInterval).toBeNull(); + expect((created as Donation).dedicationMessage).toBeNull(); expect((created as Donation).showDedicationPublicly).toBe(false); expect((created as Donation).status).toBe('pending'); expect(isValidDateValue((created as Donation).createdAt)).toBe(true); expect(isValidDateValue((created as Donation).updatedAt)).toBe(true); - expect((created as Donation).transactionId).toBeInstanceOf(Number); + expect((created as Donation).transactionId).toBeNull(); } }); From b4cc840f89906d0c7aa2bbcda079c4f8ec49b378 Mon Sep 17 00:00:00 2001 From: thaninbew Date: Sun, 23 Nov 2025 18:21:01 -0500 Subject: [PATCH 23/23] change test DB creds and add environment variables --- apps/backend/test/donations.e2e-spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/backend/test/donations.e2e-spec.ts b/apps/backend/test/donations.e2e-spec.ts index 9b4afd4..83426eb 100644 --- a/apps/backend/test/donations.e2e-spec.ts +++ b/apps/backend/test/donations.e2e-spec.ts @@ -10,6 +10,9 @@ import { DonationsModule } from '../src/donations/donations.module'; import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; import { Repository, DataSource, QueryRunner } from 'typeorm'; import { Donation } from '../src/donations/donation.entity'; // adjust path if needed +import * as dotenv from 'dotenv'; + +dotenv.config(); interface TestDonation { id: number; @@ -50,14 +53,15 @@ describe('Donations (e2e) - expanded stubs', () => { type: 'postgres', host: process.env.NX_DB_HOST || 'localhost', port: parseInt(process.env.NX_DB_PORT || '5432', 10), - username: 'postgres', - password: '12345678', - database: 'fcc_dev', + username: process.env.NX_DB_USERNAME || 'postgres', + password: process.env.NX_DB_PASSWORD || 'postgres', + database: process.env.NX_DB_DATABASE || 'fcc_dev', entities: [__dirname + '/../src/**/*.entity{.ts,.js}'], // Prefer running migrations in tests for parity; set migrationsRun to true // if you keep migrations up-to-date. If you want schema auto-sync for // a test DB, set `synchronize: true` manually here. synchronize: false, + dropSchema: true, migrationsRun: true, migrations: [__dirname + '/../src/migrations/*{.ts,.js}'], logging: false,