Skip to content

Commit ef00c0d

Browse files
authored
Merge branch 'main' into feat/subscriptions-tests-and-pagination
2 parents a9c55f7 + ce06ac6 commit ef00c0d

35 files changed

Lines changed: 2923 additions & 149 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 0.1.0 (2026-06-27)
1+
# 0.1.0 (2026-06-28)
22

33

44
### Bug Fixes
@@ -94,6 +94,7 @@
9494
* return consistent error shape on creators and posts module failures ([#995](https://github.com/MyFanss/MyFans/issues/995)) ([2147319](https://github.com/MyFanss/MyFans/commit/2147319977f6a48dc204ac8e122d663b253c5528))
9595
* **security:** fix CORS duplicate origin key bug and add per-environment host allowlist tests ([d6feee9](https://github.com/MyFanss/MyFans/commit/d6feee93af56cd1a9fe173491d10ccdf9062588f))
9696
* **security:** remediate frontend high-severity npm audit vulnerabilities ([93a35a1](https://github.com/MyFanss/MyFans/commit/93a35a1ab77c679b0cd5976d86eb90e60df66719)), closes [hi#severity](https://github.com/hi/issues/severity)
97+
* **subscriptions:** return consistent error shape on failure ([74ba625](https://github.com/MyFanss/MyFans/commit/74ba6259f04da649de791c70be6ddb4a893c2ece))
9798
* **subscription:** validate error codes, optimize gas, add integration tests, verify wasm CI ([#895](https://github.com/MyFanss/MyFans/issues/895) [#896](https://github.com/MyFanss/MyFans/issues/896) [#897](https://github.com/MyFanss/MyFans/issues/897) [#898](https://github.com/MyFanss/MyFans/issues/898)) ([0ab1336](https://github.com/MyFanss/MyFans/commit/0ab13369ee87aa193d3ea8e7fca69f56bec28119))
9899
* test errors ([d48694f](https://github.com/MyFanss/MyFans/commit/d48694f2a32b5021c29fdd817add9a17a6331df0))
99100
* **test:** add CreatorDashboardService mock to creators controller spec ([70db6a7](https://github.com/MyFanss/MyFans/commit/70db6a7694ad813fcd10fd0041ee62cbe7134e0c))
@@ -179,6 +180,7 @@
179180
* **analytics:** payment analytics endpoints ([0a31511](https://github.com/MyFanss/MyFans/commit/0a31511c45b7bbf2fba829e92a5a2c6e17bcadb9)), closes [#payment-analytics](https://github.com/MyFanss/MyFans/issues/payment-analytics)
180181
* API versioning ([c62bb64](https://github.com/MyFanss/MyFans/commit/c62bb648e1fc7745ca67b945fbfb991718152117))
181182
* **api:** standardize pagination on subscriptions, creators, and posts ([#861](https://github.com/MyFanss/MyFans/issues/861)) ([ad25d72](https://github.com/MyFanss/MyFans/commit/ad25d7280d658e48c73b4277506a45712163bd41))
183+
* **auth:** add pagination and limit query support ([ca9bfea](https://github.com/MyFanss/MyFans/commit/ca9bfeabc33f65e4f4ad4d61c29d1356c1fd7677))
182184
* **auth:** wallet signature challenge endpoints ([561d65d](https://github.com/MyFanss/MyFans/commit/561d65d498549206d65d72ce9ca3a6105f6826d7)), closes [#wallet-challenge-auth](https://github.com/MyFanss/MyFans/issues/wallet-challenge-auth)
183185
* backend contract health checks against CI deployment outputs ([5282a06](https://github.com/MyFanss/MyFans/commit/5282a06a8c02a7395a65e8f6e3b591b5c8f45e61))
184186
* **backend:** add getLatestLedgerSequence & getNetworkEvents to SorobanRpcService; guard poller behind feature flag ([bad9786](https://github.com/MyFanss/MyFans/commit/bad9786ed42e28652b91be8576c59c09ec3e473e))
@@ -192,6 +194,7 @@
192194
* **backend:** creator dashboard endpoint for revenue and subscriber metrics ([d1a39ea](https://github.com/MyFanss/MyFans/commit/d1a39eab6d25c3d77ae84a590a73545515d2e1ed))
193195
* **backend:** harden CreatorsService with Logger and resilient edges ([d4418ed](https://github.com/MyFanss/MyFans/commit/d4418edfe825c4d03bf7cf0363ab8a2edcdcae9c))
194196
* **backend:** IPFS metadata upload flow ([c46de68](https://github.com/MyFanss/MyFans/commit/c46de682411b3f69f24ccd4e450ae743c4d41629))
197+
* **backend:** social-links service tests, DTO validation, e2e coverage, pagination ([2f1db47](https://github.com/MyFanss/MyFans/commit/2f1db47e7ae4491bf0cae7789cd1fe7e661b5e08))
195198
* **backend:** subscription reconciler job with dry-run and audit logging ([0a145a1](https://github.com/MyFanss/MyFans/commit/0a145a18aea622f9797ea79828030a2eeb06fb41))
196199
* **backend:** validate Soroban env at startup with tests ([a10971a](https://github.com/MyFanss/MyFans/commit/a10971ad7be01e549a0526df2bd114af7d68a179))
197200
* bootstrap runtime contract config ([676e56a](https://github.com/MyFanss/MyFans/commit/676e56a2c195ca3371754479de6406555595dcfb))
@@ -260,6 +263,7 @@
260263
* **frontend:** persist wallet/subscription state for e2e critical flow ([ad944bd](https://github.com/MyFanss/MyFans/commit/ad944bd992753f3a00f0e82cad13ee072b92dff4))
261264
* **frontend:** show offline banner when RPC or network is unavailable ([11a4a2c](https://github.com/MyFanss/MyFans/commit/11a4a2cb54b8fbd36d089358a3b36744cd9eaba6))
262265
* **frontend:** standardize error copy with actionable recovery steps ([53e8b4f](https://github.com/MyFanss/MyFans/commit/53e8b4fbb0721d7df0eea13702de2b1c878d81ed))
266+
* **games:** add pagination and limit query support ([4d06881](https://github.com/MyFanss/MyFans/commit/4d06881649619b64065e0afd40cd1c3ab7409aa1))
263267
* harden backend secret management ([#350](https://github.com/MyFanss/MyFans/issues/350)) ([4cc8a96](https://github.com/MyFanss/MyFans/commit/4cc8a9662faa27a06a9c3ead0a9b4f6e5d437c26))
264268
* Health check for Soroban RPC ([5f189be](https://github.com/MyFanss/MyFans/commit/5f189be70eec41873edeb0fcb666bec2fd10d283))
265269
* **idempotency:** document TTL & harden collision behavior ([770024e](https://github.com/MyFanss/MyFans/commit/770024e7281b58bbf4f3ebb9884fd4ef3f7017c2))
@@ -320,6 +324,7 @@
320324
* **onboarding:** creator onboarding progress UI — stale state handling + tests ([3ca82f8](https://github.com/MyFanss/MyFans/commit/3ca82f84f0fd8ecf3f3068333845374574adb6f7))
321325
* optimistic updates for content metadata actions ([1dba8a5](https://github.com/MyFanss/MyFans/commit/1dba8a50337619e76c346f76ef2ae813a9ea31bf))
322326
* Posts: soft delete and audit trail ([36561ec](https://github.com/MyFanss/MyFans/commit/36561ecd34e5f6e2eb3b706947f40102b5b6ee5a))
327+
* **posts:** DTO validation, pagination enforcement, Swagger docs, e2e tests ([ffb605c](https://github.com/MyFanss/MyFans/commit/ffb605c9c52534c75bd6c1bd1a1c7bb29fce2904)), closes [#1000](https://github.com/MyFanss/MyFans/issues/1000) [#1001](https://github.com/MyFanss/MyFans/issues/1001) [#1002](https://github.com/MyFanss/MyFans/issues/1002) [#1004](https://github.com/MyFanss/MyFans/issues/1004) [#1000](https://github.com/MyFanss/MyFans/issues/1000) [#1001](https://github.com/MyFanss/MyFans/issues/1001) [#1002](https://github.com/MyFanss/MyFans/issues/1002) [#1004](https://github.com/MyFanss/MyFans/issues/1004)
323328
* **posts:** soft delete with audit trail ([d3741fb](https://github.com/MyFanss/MyFans/commit/d3741fbadd72af907b5377abaaf0089801f1d178)), closes [#732](https://github.com/MyFanss/MyFans/issues/732)
324329
* PR template with test plan ([#723](https://github.com/MyFanss/MyFans/issues/723)) ([ac66a6d](https://github.com/MyFanss/MyFans/commit/ac66a6d5d9825781ab5de2a9297f45edd8b742aa))
325330
* profile settings for fans and creators with API + validation ([165a7b5](https://github.com/MyFanss/MyFans/commit/165a7b56c48da00a92df7f0e70e0dc86e03661e3))
@@ -352,6 +357,7 @@
352357
* **subscription:** add unit tests for initialize and admin paths ([#890](https://github.com/MyFanss/MyFans/issues/890)) ([9b3d0b9](https://github.com/MyFanss/MyFans/commit/9b3d0b944192676a42bca6b4ccbfccba8e7e08a9))
353358
* **subscription:** admin set_fee_bps with bounds and fee_updated event ([5a7c7e2](https://github.com/MyFanss/MyFans/commit/5a7c7e26f5546b4b14ea65e71644765be2346475))
354359
* **subscription:** admin set_fee_recipient with validation and event ([c372cab](https://github.com/MyFanss/MyFans/commit/c372caba4c9716bcdc3fa1e4825f70af8158cd20))
360+
* **subscriptions:** add rate limiting on write endpoints ([5f47524](https://github.com/MyFanss/MyFans/commit/5f4752492db317bdfa90b138c180d3d6644dc22e))
355361
* **subscriptions:** add status filter and sort to subscription list endpoints ([ddaf69a](https://github.com/MyFanss/MyFans/commit/ddaf69a0f5b8da995d52d0505a8fc0204325f879))
356362
* **subscriptions:** gated content access guard ([da2d891](https://github.com/MyFanss/MyFans/commit/da2d891ae5062ab9ded3994d67c28521ce42b318)), closes [#gated-content-access](https://github.com/MyFanss/MyFans/issues/gated-content-access)
357363
* **subscriptions:** GET fan–creator subscription state with auth ([2fec38f](https://github.com/MyFanss/MyFans/commit/2fec38f7d73e046ce497399ec412220b12fd8044))
Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthController } from './auth.controller';
33
import { AuthService } from './auth.service';
4+
import { PaginatedResponseDto } from '../common/dto/paginated-response.dto';
45

56
describe('AuthController', () => {
67
let controller: AuthController;
78

8-
const mockAuthService = {};
9+
const mockAuthService = {
10+
findById: jest.fn(),
11+
findAllUsers: jest.fn(),
12+
};
913

1014
beforeEach(async () => {
1115
const module: TestingModule = await Test.createTestingModule({
@@ -14,9 +18,43 @@ describe('AuthController', () => {
1418
}).compile();
1519

1620
controller = module.get<AuthController>(AuthController);
21+
jest.clearAllMocks();
1722
});
1823

1924
it('should be defined', () => {
2025
expect(controller).toBeDefined();
2126
});
27+
28+
describe('getProfile', () => {
29+
it('returns user profile for authenticated user', async () => {
30+
const user = { id: 'u1', email: 'a@b.com' };
31+
mockAuthService.findById.mockResolvedValue(user);
32+
33+
const result = await controller.getProfile({ user: { userId: 'u1' } });
34+
35+
expect(result).toEqual(user);
36+
expect(mockAuthService.findById).toHaveBeenCalledWith('u1');
37+
});
38+
});
39+
40+
describe('getUsers', () => {
41+
it('returns paginated users', async () => {
42+
const response = new PaginatedResponseDto(
43+
[{ id: 'u1' }],
44+
20,
45+
null,
46+
false,
47+
1,
48+
);
49+
mockAuthService.findAllUsers.mockResolvedValue(response);
50+
51+
const result = await controller.getUsers({ limit: 20, page: 1 });
52+
53+
expect(result).toEqual(response);
54+
expect(mockAuthService.findAllUsers).toHaveBeenCalledWith({
55+
limit: 20,
56+
page: 1,
57+
});
58+
});
59+
});
2260
});
Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,50 @@
1-
import { Controller } from '@nestjs/common';
1+
import { Controller, Get, Query, UseGuards, Request } from '@nestjs/common';
2+
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
3+
import { AuthService } from './auth.service';
4+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
5+
import { PaginationDto, PaginatedResponseDto } from '../common/dto';
26

7+
@ApiTags('auth')
38
@Controller({ path: 'auth', version: '1' })
4-
export class AuthController {}
9+
export class AuthController {
10+
constructor(private readonly authService: AuthService) {}
11+
12+
@Get('profile')
13+
@UseGuards(JwtAuthGuard)
14+
@ApiOperation({ summary: 'Get current user profile' })
15+
@ApiResponse({ status: 200, description: 'Current user profile' })
16+
@ApiResponse({ status: 401, description: 'Unauthorized' })
17+
async getProfile(@Request() req: any) {
18+
return this.authService.findById(req.user.userId);
19+
}
20+
21+
@Get('users')
22+
@UseGuards(JwtAuthGuard)
23+
@ApiOperation({ summary: 'List users (paginated)' })
24+
@ApiQuery({
25+
name: 'cursor',
26+
required: false,
27+
description: 'Pagination cursor (nextCursor from previous page)',
28+
})
29+
@ApiQuery({
30+
name: 'limit',
31+
required: false,
32+
description: 'Number of items per page (default 20, max 100)',
33+
})
34+
@ApiQuery({
35+
name: 'page',
36+
required: false,
37+
description: 'Page number (1-based, default 1)',
38+
})
39+
@ApiResponse({
40+
status: 200,
41+
description: 'Paginated list of users',
42+
type: PaginatedResponseDto,
43+
})
44+
@ApiResponse({ status: 401, description: 'Unauthorized' })
45+
async getUsers(
46+
@Query() pagination: PaginationDto,
47+
): Promise<PaginatedResponseDto<any>> {
48+
return this.authService.findAllUsers(pagination);
49+
}
50+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { INestApplication, ValidationPipe } from '@nestjs/common';
3+
import { ConfigModule } from '@nestjs/config';
4+
import { JwtModule, JwtService } from '@nestjs/jwt';
5+
import { PassportModule } from '@nestjs/passport';
6+
import * as request from 'supertest';
7+
8+
import { AuthController } from './auth.controller';
9+
import { AuthService } from './auth.service';
10+
import { JwtStrategy } from './strategies/jwt.strategy';
11+
import { UsersService } from '../users/users.service';
12+
13+
const TEST_SECRET = 'e2e-test-secret';
14+
15+
const mockUser = {
16+
id: 'user-1',
17+
email: 'test@example.com',
18+
username: 'testuser',
19+
role: 'USER',
20+
};
21+
22+
describe('Auth Module (e2e)', () => {
23+
let app: INestApplication;
24+
let jwtService: JwtService;
25+
26+
const mockUsersService = {
27+
findOne: jest.fn(),
28+
findAll: jest.fn(),
29+
};
30+
31+
beforeAll(async () => {
32+
const module: TestingModule = await Test.createTestingModule({
33+
imports: [
34+
ConfigModule.forRoot({
35+
isGlobal: true,
36+
load: [() => ({ JWT_SECRET: TEST_SECRET })],
37+
}),
38+
PassportModule.register({ defaultStrategy: 'jwt' }),
39+
JwtModule.register({
40+
secret: TEST_SECRET,
41+
signOptions: { expiresIn: '1h' },
42+
}),
43+
],
44+
controllers: [AuthController],
45+
providers: [
46+
AuthService,
47+
JwtStrategy,
48+
{ provide: UsersService, useValue: mockUsersService },
49+
],
50+
}).compile();
51+
52+
app = module.createNestApplication();
53+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
54+
await app.init();
55+
56+
jwtService = module.get(JwtService);
57+
});
58+
59+
afterAll(() => app.close());
60+
61+
afterEach(() => jest.clearAllMocks());
62+
63+
function getAccessToken(userId: string) {
64+
return jwtService.sign({ sub: userId, email: mockUser.email });
65+
}
66+
67+
describe('GET /v1/auth/users', () => {
68+
it('returns 401 without a bearer token', async () => {
69+
const { status } = await request(app.getHttpServer()).get('/v1/auth/users');
70+
expect(status).toBe(401);
71+
});
72+
73+
it('returns paginated users with valid token', async () => {
74+
mockUsersService.findOne.mockResolvedValue(mockUser);
75+
mockUsersService.findAll.mockResolvedValue({
76+
data: [mockUser],
77+
total: 1,
78+
});
79+
80+
const token = getAccessToken(mockUser.id);
81+
82+
const { status, body } = await request(app.getHttpServer())
83+
.get('/v1/auth/users?limit=10&page=1')
84+
.set('Authorization', `Bearer ${token}`);
85+
86+
expect(status).toBe(200);
87+
expect(body).toHaveProperty('data');
88+
expect(body).toHaveProperty('limit');
89+
expect(body).toHaveProperty('hasMore');
90+
});
91+
92+
it('returns 401 with an invalid token', async () => {
93+
const { status } = await request(app.getHttpServer())
94+
.get('/v1/auth/users')
95+
.set('Authorization', 'Bearer invalid.token.here');
96+
97+
expect(status).toBe(401);
98+
});
99+
});
100+
101+
describe('GET /v1/auth/profile', () => {
102+
it('returns 401 without a bearer token', async () => {
103+
const { status } = await request(app.getHttpServer()).get(
104+
'/v1/auth/profile',
105+
);
106+
expect(status).toBe(401);
107+
});
108+
109+
it('returns the current user profile with valid token', async () => {
110+
mockUsersService.findOne.mockResolvedValue(mockUser);
111+
112+
const token = getAccessToken(mockUser.id);
113+
114+
const { status, body } = await request(app.getHttpServer())
115+
.get('/v1/auth/profile')
116+
.set('Authorization', `Bearer ${token}`);
117+
118+
expect(status).toBe(200);
119+
expect(body).toHaveProperty('id', mockUser.id);
120+
expect(body).toHaveProperty('email', mockUser.email);
121+
});
122+
});
123+
});

backend/src/auth-module/auth.service.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Test, TestingModule } from '@nestjs/testing';
2+
import { NotFoundException } from '@nestjs/common';
23
import { AuthService } from './auth.service';
34
import { UsersService } from '../users/users.service';
45

@@ -24,4 +25,67 @@ describe('AuthService', () => {
2425
it('should be defined', () => {
2526
expect(service).toBeDefined();
2627
});
28+
29+
describe('validateUser', () => {
30+
it('returns the user when found', async () => {
31+
const user = { id: 'u1', email: 'a@b.com', role: 'USER' };
32+
mockUsersService.findOne.mockResolvedValue(user);
33+
34+
const result = await service.validateUser('u1');
35+
36+
expect(result).toEqual(user);
37+
expect(mockUsersService.findOne).toHaveBeenCalledWith('u1');
38+
});
39+
40+
it('returns null when user is not found', async () => {
41+
mockUsersService.findOne.mockRejectedValue(
42+
new NotFoundException('User not found'),
43+
);
44+
45+
const result = await service.validateUser('missing');
46+
47+
expect(result).toBeNull();
48+
});
49+
50+
it('re-throws non-NotFoundException errors', async () => {
51+
mockUsersService.findOne.mockRejectedValue(new Error('DB down'));
52+
53+
await expect(service.validateUser('u1')).rejects.toThrow('DB down');
54+
});
55+
});
56+
57+
describe('findById', () => {
58+
it('returns the user by id', async () => {
59+
const user = { id: 'u2', email: 'b@c.com', role: 'CREATOR' };
60+
mockUsersService.findOne.mockResolvedValue(user);
61+
62+
const result = await service.findById('u2');
63+
64+
expect(result).toEqual(user);
65+
expect(mockUsersService.findOne).toHaveBeenCalledWith('u2');
66+
});
67+
68+
it('propagates NotFoundException from UsersService', async () => {
69+
mockUsersService.findOne.mockRejectedValue(
70+
new NotFoundException('User not found'),
71+
);
72+
73+
await expect(service.findById('missing')).rejects.toThrow(
74+
NotFoundException,
75+
);
76+
});
77+
});
78+
79+
describe('register', () => {
80+
it('throws Method not implemented', () => {
81+
expect(() =>
82+
service.register({
83+
email: 'new@user.com',
84+
password: 'securePass1',
85+
firstName: 'Test',
86+
lastName: 'User',
87+
}),
88+
).toThrow('Method not implemented.');
89+
});
90+
});
2791
});

backend/src/auth-module/auth.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Injectable, NotFoundException } from '@nestjs/common';
22
import { RegisterDto } from './dto/register.dto';
33
import { UsersService } from '../users/users.service';
4+
import { PaginationDto } from '../common/dto/pagination.dto';
5+
import { PaginatedResponseDto } from '../common/dto/paginated-response.dto';
46

57
@Injectable()
68
export class AuthService {
@@ -22,4 +24,15 @@ export class AuthService {
2224
async findById(id: string) {
2325
return this.usersService.findOne(id);
2426
}
27+
28+
async findAllUsers(
29+
pagination: PaginationDto,
30+
): Promise<PaginatedResponseDto<any>> {
31+
const { data, total } = await this.usersService.findAll(pagination);
32+
const limit = pagination.limit ?? 20;
33+
const page = pagination.page ?? 1;
34+
const hasMore = page * limit < total;
35+
36+
return new PaginatedResponseDto(data, limit, null, hasMore, page);
37+
}
2538
}

0 commit comments

Comments
 (0)