Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/teams/teams.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { BadRequestException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { TeamsService } from './teams.service';
import { Bounty, Team, TeamMemberSplit } from '../common/entities';
import { BountyStatus } from '../common/enums';

describe('TeamsService', () => {
let service: TeamsService;
let teamRepo: { findOne: jest.Mock; save: jest.Mock; create: jest.Mock };
let splitRepo: { save: jest.Mock; create: jest.Mock };
let bountyRepo: { findOne: jest.Mock; save: jest.Mock };

beforeEach(async () => {
teamRepo = {
findOne: jest.fn(),
save: jest.fn((t: Partial<Team>) =>
Promise.resolve({ id: 'team-1', ...t }),
),
create: jest.fn((data: Partial<Team>) => ({ id: 'team-1', ...data })),
};
splitRepo = {
save: jest.fn((s: Partial<TeamMemberSplit>) =>
Promise.resolve({ id: 'split-1', ...s }),
),
create: jest.fn((data: Partial<TeamMemberSplit>) => ({
id: 'split-1',
...data,
})),
};
bountyRepo = {
findOne: jest.fn(),
save: jest.fn((b: Partial<Bounty>) => Promise.resolve(b)),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
TeamsService,
{ provide: getRepositoryToken(Team), useValue: teamRepo },
{ provide: getRepositoryToken(TeamMemberSplit), useValue: splitRepo },
{ provide: getRepositoryToken(Bounty), useValue: bountyRepo },
],
}).compile();

service = module.get(TeamsService);
});

describe('assignToBounty', () => {
it('assigns a team to an OPEN bounty', async () => {
teamRepo.findOne.mockResolvedValue({ id: 'team-1', splits: [] });
bountyRepo.findOne.mockResolvedValue({
id: 'bounty-1',
status: BountyStatus.OPEN,
claimedById: null,
});

const updated = await service.assignToBounty('team-1', 'bounty-1');
expect(updated.teamId).toBe('team-1');
expect(bountyRepo.save).toHaveBeenCalled();
});

it('assigns a team to a FUNDED bounty', async () => {
teamRepo.findOne.mockResolvedValue({ id: 'team-1', splits: [] });
bountyRepo.findOne.mockResolvedValue({
id: 'bounty-1',
status: BountyStatus.FUNDED,
claimedById: null,
});

const updated = await service.assignToBounty('team-1', 'bounty-1');
expect(updated.teamId).toBe('team-1');
expect(bountyRepo.save).toHaveBeenCalled();
});

it('rejects assigning a team to a CLAIMED bounty', async () => {
teamRepo.findOne.mockResolvedValue({ id: 'team-1', splits: [] });
bountyRepo.findOne.mockResolvedValue({
id: 'bounty-1',
status: BountyStatus.CLAIMED,
claimedById: 'user-1',
});

await expect(
service.assignToBounty('team-1', 'bounty-1'),
).rejects.toThrow(BadRequestException);
});

it('rejects assigning a team to an IN_REVIEW bounty', async () => {
teamRepo.findOne.mockResolvedValue({ id: 'team-1', splits: [] });
bountyRepo.findOne.mockResolvedValue({
id: 'bounty-1',
status: BountyStatus.IN_REVIEW,
claimedById: 'user-1',
});

await expect(
service.assignToBounty('team-1', 'bounty-1'),
).rejects.toThrow(BadRequestException);
});

it('rejects assigning a team when bounty already has a claimedById', async () => {
teamRepo.findOne.mockResolvedValue({ id: 'team-1', splits: [] });
bountyRepo.findOne.mockResolvedValue({
id: 'bounty-1',
status: BountyStatus.FUNDED,
claimedById: 'user-1',
});

await expect(
service.assignToBounty('team-1', 'bounty-1'),
).rejects.toThrow(BadRequestException);
});
});
});
23 changes: 22 additions & 1 deletion src/teams/teams.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
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, TeamMemberSplit } from '../common/entities';
import { BountyStatus } from '../common/enums';
import { CreateTeamDto } from './dto/create-team.dto';
import { validateSplitPercentages } from './team-split.util';

Expand Down Expand Up @@ -54,6 +59,22 @@ export class TeamsService {
await this.findOne(teamId); // ensures team exists
const bounty = await this.bountyRepo.findOne({ where: { id: bountyId } });
if (!bounty) throw new NotFoundException(`Bounty ${bountyId} not found`);

if (
bounty.status !== BountyStatus.OPEN &&
bounty.status !== BountyStatus.FUNDED
) {
throw new BadRequestException(
`Cannot assign a team to a bounty in ${bounty.status} status. Assignment is only allowed in OPEN or FUNDED state before claiming.`,
);
}

if (bounty.claimedById) {
throw new BadRequestException(
`Cannot assign a team to a bounty that has already been claimed by contributor ${bounty.claimedById}.`,
);
}

bounty.teamId = teamId;
return this.bountyRepo.save(bounty);
}
Expand Down