Skip to content

Commit 9867799

Browse files
authored
Merge pull request #1305 from Tukura11/main
This PR adds comprehensive test coverage and enhances validation for the creators module, addressing four key issues
2 parents f9d68b0 + 5b84dc6 commit 9867799

8 files changed

Lines changed: 1562 additions & 372 deletions

File tree

CHANGELOG.md

Lines changed: 360 additions & 359 deletions
Large diffs are not rendered by default.

backend/src/creators/creators.controller.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { PlanDto } from './dto/plan.dto';
77
import { SearchCreatorsDto } from './dto/search-creators.dto';
88
import { PublicCreatorDto } from './dto/public-creator.dto';
99
import { DashboardQueryDto } from './dto/creator-dashboard.dto';
10+
import { CreatePlanDto } from './dto/create-plan.dto';
1011
import { JwtAuthGuard } from '../auth-module/guards/jwt-auth.guard';
1112

1213
@ApiTags('creators')
@@ -60,20 +61,12 @@ export class CreatorsController {
6061
@Post('plans')
6162
@ApiOperation({ summary: 'Create a new subscription plan' })
6263
@ApiResponse({ status: 201, description: 'Plan created successfully' })
63-
createPlan(
64-
@Body()
65-
body: {
66-
creator: string;
67-
asset: string;
68-
amount: string;
69-
intervalDays: number;
70-
},
71-
) {
64+
createPlan(@Body() planDto: CreatePlanDto) {
7265
return this.creatorsService.createPlan(
73-
body.creator,
74-
body.asset,
75-
body.amount,
76-
body.intervalDays,
66+
planDto.creator,
67+
planDto.asset,
68+
planDto.amount,
69+
planDto.intervalDays,
7770
);
7871
}
7972

backend/src/creators/creators.service.spec.ts

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,289 @@ describe('CreatorsService', () => {
468468
});
469469
});
470470

471+
describe('happy path - createPlan', () => {
472+
it('should create a plan with valid inputs and return plan with id, creator, asset, amount, intervalDays', () => {
473+
// Act
474+
const plan = service.createPlan('creator1', 'USDC', '100.50', 30);
475+
476+
// Assert
477+
expect(plan).toBeDefined();
478+
expect(plan.id).toBeGreaterThan(0);
479+
expect(plan.creator).toBe('creator1');
480+
expect(plan.asset).toBe('USDC');
481+
expect(plan.amount).toBe('100.50');
482+
expect(plan.intervalDays).toBe(30);
483+
});
484+
485+
it('should publish PlanCreatedEvent when EventBus is wired', () => {
486+
const publish = jest.fn();
487+
const module = Test.createTestingModule({
488+
providers: [
489+
CreatorsService,
490+
{ provide: EventBus, useValue: { publish } },
491+
{
492+
provide: getRepositoryToken(User),
493+
useValue: {
494+
createQueryBuilder: jest.fn(() => mockQueryBuilder),
495+
},
496+
},
497+
],
498+
});
499+
500+
return module.compile().then((m) => {
501+
const svc = m.get<CreatorsService>(CreatorsService);
502+
svc.createPlan('creator1', 'XLM', '50', 7);
503+
504+
expect(publish).toHaveBeenCalledTimes(1);
505+
expect(publish.mock.calls[0][0].constructor.name).toBe('PlanCreatedEvent');
506+
});
507+
});
508+
});
509+
510+
describe('happy path - getPlan', () => {
511+
it('should retrieve a created plan by id', () => {
512+
// Arrange
513+
const plan1 = service.createPlan('creator1', 'USDC', '100', 30);
514+
515+
// Act
516+
const retrieved = service.getPlan(plan1.id);
517+
518+
// Assert
519+
expect(retrieved).toBeDefined();
520+
expect(retrieved?.id).toBe(plan1.id);
521+
expect(retrieved?.creator).toBe('creator1');
522+
});
523+
524+
it('should return undefined for non-existent plan id', () => {
525+
// Act
526+
const retrieved = service.getPlan(99999);
527+
528+
// Assert
529+
expect(retrieved).toBeUndefined();
530+
});
531+
});
532+
533+
describe('happy path - getCreatorPlans', () => {
534+
it('should return all plans for a creator', () => {
535+
// Arrange
536+
service.createPlan('alice', 'USDC', '100', 30);
537+
service.createPlan('alice', 'XLM', '50', 7);
538+
service.createPlan('bob', 'USDC', '75', 14);
539+
540+
// Act
541+
const alicePlans = service.getCreatorPlans('alice');
542+
543+
// Assert
544+
expect(alicePlans).toHaveLength(2);
545+
expect(alicePlans.every((p) => p.creator === 'alice')).toBe(true);
546+
});
547+
548+
it('should return empty array for creator with no plans', () => {
549+
// Act
550+
const plans = service.getCreatorPlans('nonexistent');
551+
552+
// Assert
553+
expect(plans).toHaveLength(0);
554+
});
555+
});
556+
557+
describe('happy path - findAllPlans', () => {
558+
it('should return all plans paginated with default limit', () => {
559+
// Arrange
560+
service.createPlan('alice', 'USDC', '100', 30);
561+
service.createPlan('bob', 'XLM', '50', 7);
562+
563+
// Act
564+
const result = service.findAllPlans({ limit: 20 });
565+
566+
// Assert
567+
expect(result.data).toHaveLength(2);
568+
expect(result.limit).toBe(20);
569+
expect(result.hasMore).toBe(false);
570+
// nextCursor is the last plan's ID when there are results
571+
expect(result.nextCursor).toBe('2');
572+
});
573+
574+
it('should paginate correctly with cursor and limit', () => {
575+
// Arrange
576+
service.createPlan('a', 'USDC', '1', 1);
577+
service.createPlan('b', 'USDC', '2', 2);
578+
service.createPlan('c', 'USDC', '3', 3);
579+
580+
// Act - first page
581+
const page1 = service.findAllPlans({ limit: 1 });
582+
583+
// Assert first page
584+
expect(page1.data).toHaveLength(1);
585+
expect(page1.hasMore).toBe(true);
586+
expect(page1.nextCursor).toBe('1');
587+
588+
// Act - second page
589+
const page2 = service.findAllPlans({
590+
cursor: page1.nextCursor ?? undefined,
591+
limit: 1,
592+
});
593+
594+
// Assert second page
595+
expect(page2.data).toHaveLength(1);
596+
expect(page2.data[0].id).toBeGreaterThan(page1.data[0].id);
597+
});
598+
599+
it('should return data with correct shape', () => {
600+
// Arrange
601+
service.createPlan('creator1', 'USDC', '100.50', 30);
602+
603+
// Act
604+
const result = service.findAllPlans({ limit: 20 });
605+
606+
// Assert
607+
expect(result.data[0]).toHaveProperty('id');
608+
expect(result.data[0]).toHaveProperty('creator');
609+
expect(result.data[0]).toHaveProperty('asset');
610+
expect(result.data[0]).toHaveProperty('amount');
611+
expect(result.data[0]).toHaveProperty('intervalDays');
612+
});
613+
});
614+
615+
describe('happy path - findCreatorPlans', () => {
616+
it('should return paginated plans for a specific creator', () => {
617+
// Arrange
618+
service.createPlan('alice', 'USDC', '100', 30);
619+
service.createPlan('alice', 'XLM', '50', 7);
620+
service.createPlan('bob', 'USDC', '75', 14);
621+
622+
// Act
623+
const result = service.findCreatorPlans('alice', { limit: 20 });
624+
625+
// Assert
626+
expect(result.data).toHaveLength(2);
627+
expect(result.data.every((p) => p.creator === 'alice')).toBe(true);
628+
expect(result.hasMore).toBe(false);
629+
});
630+
631+
it('should handle pagination with cursor for creator plans', () => {
632+
// Arrange
633+
service.createPlan('alice', 'USDC', '1', 1);
634+
service.createPlan('alice', 'XLM', '2', 2);
635+
service.createPlan('alice', 'ETH', '3', 3);
636+
637+
// Act
638+
const page1 = service.findCreatorPlans('alice', { limit: 1 });
639+
const page2 = service.findCreatorPlans('alice', {
640+
cursor: page1.nextCursor ?? undefined,
641+
limit: 1,
642+
});
643+
644+
// Assert
645+
expect(page1.data).toHaveLength(1);
646+
expect(page2.data).toHaveLength(1);
647+
expect(page1.data[0].id).not.toBe(page2.data[0].id);
648+
});
649+
650+
it('should return empty pagination for creator with no plans', () => {
651+
// Act
652+
const result = service.findCreatorPlans('nonexistent', { limit: 20 });
653+
654+
// Assert
655+
expect(result.data).toHaveLength(0);
656+
expect(result.hasMore).toBe(false);
657+
expect(result.nextCursor).toBeNull();
658+
});
659+
});
660+
661+
describe('happy path - listCreators', () => {
662+
it('should list all plans without chain merge', async () => {
663+
// Arrange
664+
service.createPlan('alice', 'USDC', '100', 30);
665+
service.createPlan('bob', 'XLM', '50', 7);
666+
667+
// Act
668+
const result = await service.listCreators(false);
669+
670+
// Assert
671+
expect(result).toHaveLength(2);
672+
expect(result[0]).toHaveProperty('id');
673+
expect(result[0]).toHaveProperty('creator');
674+
expect(result[0]).toHaveProperty('asset');
675+
});
676+
677+
it('should return sorted plans by id', async () => {
678+
// Arrange
679+
const plan1 = service.createPlan('alice', 'USDC', '100', 30);
680+
const plan2 = service.createPlan('bob', 'XLM', '50', 7);
681+
682+
// Act
683+
const result = await service.listCreators(false);
684+
685+
// Assert
686+
expect(result[0].id).toBe(plan1.id);
687+
expect(result[1].id).toBe(plan2.id);
688+
});
689+
690+
it('should mark syncStatus as unknown when chain reader is not configured', async () => {
691+
// Arrange
692+
service.createPlan('alice', 'USDC', '100', 30);
693+
694+
// Act
695+
const result = await service.listCreators(true);
696+
697+
// Assert
698+
expect(result[0]).toHaveProperty('syncStatus');
699+
expect(result[0].syncStatus).toBe('unknown');
700+
});
701+
});
702+
703+
describe('happy path - searchCreators', () => {
704+
it('should search creators and return paginated results', async () => {
705+
// Arrange
706+
const mockUsers: User[] = [
707+
createMockUser('1', 'alice', 'Alice Smith'),
708+
createMockUser('2', 'bob', 'Bob Jones'),
709+
];
710+
711+
(mockQueryBuilder.getRawAndEntities as jest.Mock).mockResolvedValue({
712+
entities: mockUsers,
713+
raw: [{ creator_bio: 'Alice bio' }, { creator_bio: 'Bob bio' }],
714+
});
715+
716+
// Act
717+
const result = await service.searchCreators({
718+
q: 'alice',
719+
limit: 20,
720+
});
721+
722+
// Assert
723+
expect(result.data).toHaveLength(2);
724+
expect(result.limit).toBe(20);
725+
expect(result.hasMore).toBe(false);
726+
expect(result.data[0]).toHaveProperty('id');
727+
expect(result.data[0]).toHaveProperty('username');
728+
expect(result.data[0]).toHaveProperty('display_name');
729+
expect(result.data[0]).toHaveProperty('avatar_url');
730+
expect(result.data[0]).toHaveProperty('bio');
731+
});
732+
733+
it('should return correct pagination metadata', async () => {
734+
// Arrange
735+
const mockUsers: User[] = Array.from({ length: 6 }, (_, i) =>
736+
createMockUser(`${i}`, `user${i}`, `User ${i}`),
737+
);
738+
739+
(mockQueryBuilder.getRawAndEntities as jest.Mock).mockResolvedValue({
740+
entities: mockUsers,
741+
raw: mockUsers.map(() => ({ creator_bio: 'Bio' })),
742+
});
743+
744+
// Act
745+
const result = await service.searchCreators({ limit: 5 });
746+
747+
// Assert
748+
expect(result.data).toHaveLength(5);
749+
expect(result.hasMore).toBe(true);
750+
expect(result.nextCursor).toBe('user4');
751+
});
752+
});
753+
471754
describe('logging and resilience', () => {
472755
it('createPlan logs debug when EventBus is not wired', async () => {
473756
const module: TestingModule = await Test.createTestingModule({

0 commit comments

Comments
 (0)