From a9d1b79ff0b60236df5455b7bbf9116ffb8cb026 Mon Sep 17 00:00:00 2001 From: moxiaoshen567 Date: Tue, 25 Aug 2026 05:29:54 +0000 Subject: [PATCH] fix(bounties): throw BadRequestException when releasing team bounty with empty splits --- src/bounties/bounties.service.spec.ts | 83 +++++++++++++++++++++++++++ src/bounties/bounties.service.ts | 31 +++++----- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/bounties/bounties.service.spec.ts b/src/bounties/bounties.service.spec.ts index fd5a497..6f94f62 100644 --- a/src/bounties/bounties.service.spec.ts +++ b/src/bounties/bounties.service.spec.ts @@ -1,3 +1,4 @@ +import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BountiesService } from './bounties.service'; @@ -143,4 +144,86 @@ describe('BountiesService', () => { ); expect(bounty.status).toBe(BountyStatus.PAID); }); + + it('markMergedAndRelease throws BadRequestException if team has no member splits', async () => { + bountyRepo.findOne.mockResolvedValue({ + id: 'b-team', + status: BountyStatus.IN_REVIEW, + escrowId: 'escrow-team', + claimedById: null, + teamId: 'team-empty', + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BountiesService, + { provide: getRepositoryToken(Bounty), useValue: bountyRepo }, + { provide: getRepositoryToken(User), useValue: { findOne: jest.fn() } }, + { + provide: getRepositoryToken(Team), + useValue: { + findOne: jest.fn().mockResolvedValue({ + id: 'team-empty', + splits: [], + }), + }, + }, + { provide: EscrowService, useValue: escrowService }, + ], + }).compile(); + service = module.get(BountiesService); + + await expect(service.markMergedAndRelease('b-team')).rejects.toThrow( + BadRequestException, + ); + expect(escrowService.splitRelease).not.toHaveBeenCalled(); + }); + + it('markMergedAndRelease releases split to team members when splits exist', async () => { + bountyRepo.findOne.mockResolvedValue({ + id: 'b-team-valid', + status: BountyStatus.IN_REVIEW, + escrowId: 'escrow-team-valid', + claimedById: null, + teamId: 'team-1', + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BountiesService, + { provide: getRepositoryToken(Bounty), useValue: bountyRepo }, + { + provide: getRepositoryToken(User), + useValue: { + findOne: jest.fn().mockResolvedValue({ + id: 'u1', + stellarAddress: 'GSTELLAR1', + }), + }, + }, + { + provide: getRepositoryToken(Team), + useValue: { + findOne: jest.fn().mockResolvedValue({ + id: 'team-1', + splits: [{ userId: 'u1', percentage: 100 }], + }), + }, + }, + { provide: EscrowService, useValue: escrowService }, + ], + }).compile(); + service = module.get(BountiesService); + + const bounty = await service.markMergedAndRelease('b-team-valid'); + + expect(escrowService.splitRelease).toHaveBeenCalledWith('escrow-team-valid', [ + { + recipientId: 'u1', + recipientAddress: 'GSTELLAR1', + percentage: 100, + }, + ]); + expect(bounty.status).toBe(BountyStatus.PAID); + }); }); diff --git a/src/bounties/bounties.service.ts b/src/bounties/bounties.service.ts index 7bf75f8..ac4e844 100644 --- a/src/bounties/bounties.service.ts +++ b/src/bounties/bounties.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Bounty, Team, User } from '../common/entities'; @@ -103,21 +103,24 @@ export class BountiesService { where: { id: bounty.teamId }, relations: { splits: true }, }); - if (team && team.splits.length > 0) { - const recipients = await Promise.all( - team.splits.map(async (split) => { - const user = await this.userRepo.findOne({ - where: { id: split.userId }, - }); - return { - recipientId: split.userId, - recipientAddress: user?.stellarAddress ?? '', - percentage: Number(split.percentage), - }; - }), + if (!team || !team.splits || team.splits.length === 0) { + throw new BadRequestException( + `Cannot release bounty ${id}: assigned team ${bounty.teamId} has no member splits`, ); - await this.escrowService.splitRelease(bounty.escrowId, recipients); } + const recipients = await Promise.all( + team.splits.map(async (split) => { + const user = await this.userRepo.findOne({ + where: { id: split.userId }, + }); + return { + recipientId: split.userId, + recipientAddress: user?.stellarAddress ?? '', + percentage: Number(split.percentage), + }; + }), + ); + await this.escrowService.splitRelease(bounty.escrowId, recipients); } else if (bounty.claimedById) { const contributor = await this.userRepo.findOne({ where: { id: bounty.claimedById },