Skip to content

Commit 6b1fd3c

Browse files
authored
Merge pull request #703 from oluebubejoy/fix/swagger-docs-auth-tests
fix: swagger doc/route mismatch, schema drift, security schemes, and 401 auth test failures
2 parents f02a85d + 492f6a5 commit 6b1fd3c

8 files changed

Lines changed: 137 additions & 54 deletions

File tree

backend/src/config/swagger.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
7676
scheme: 'bearer',
7777
bearerFormat: 'JWT',
7878
description: 'JSON Web Token issued by /v1/auth/verify after completing the SEP-10 challenge flow.'
79+
},
80+
bearerAuth: {
81+
type: 'http',
82+
scheme: 'bearer',
83+
bearerFormat: 'JWT',
84+
description: 'Alias for BearerAuth — used by route-level security annotations.'
85+
},
86+
adminAuth: {
87+
type: 'http',
88+
scheme: 'bearer',
89+
bearerFormat: 'JWT',
90+
description: 'Admin JWT — the token subject must match ADMIN_PUBLIC_KEY.'
7991
}
8092
},
8193
schemas: {
@@ -165,6 +177,28 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
165177
description: 'Stream active status',
166178
example: true,
167179
},
180+
isPaused: {
181+
type: 'boolean',
182+
description: 'Whether the stream is currently paused',
183+
example: false,
184+
},
185+
pausedAt: {
186+
type: 'integer',
187+
nullable: true,
188+
description: 'Ledger timestamp when the stream was last paused (Unix), null if not paused',
189+
example: null,
190+
},
191+
totalPausedDuration: {
192+
type: 'integer',
193+
description: 'Cumulative seconds the stream has spent paused',
194+
example: 0,
195+
},
196+
endTime: {
197+
type: 'integer',
198+
nullable: true,
199+
description: 'Ledger timestamp when the stream ended (Unix), null if still active',
200+
example: null,
201+
},
168202
createdAt: {
169203
type: 'string',
170204
format: 'date-time',
@@ -189,7 +223,7 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
189223
},
190224
eventType: {
191225
type: 'string',
192-
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED'],
226+
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED', 'PAUSED', 'RESUMED', 'FEE_COLLECTED'],
193227
description: 'Type of stream event',
194228
example: 'TOPPED_UP',
195229
},

backend/src/routes/v1/stream.routes.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,56 @@ router.post('/:streamId/withdraw', requireAuth, withdrawHandler as any);
263263

264264
/**
265265
* @openapi
266-
* /v1/streams/{streamId}/cancel:
266+
* /v1/streams/{streamId}/top-up:
267267
* post:
268268
* tags:
269269
* - Streams
270-
* summary: Cancel an active payment stream
270+
* summary: Top up a payment stream
271+
* description: Adds additional funds to an existing active stream. Only the original sender can top up.
272+
* parameters:
273+
* - in: path
274+
* name: streamId
275+
* required: true
276+
* schema:
277+
* type: integer
278+
* description: On-chain stream ID
271279
* security:
272280
* - bearerAuth: []
281+
* requestBody:
282+
* required: true
283+
* content:
284+
* application/json:
285+
* schema:
286+
* type: object
287+
* required:
288+
* - amount
289+
* properties:
290+
* amount:
291+
* type: string
292+
* description: Amount to add to the stream deposit (i128 as string)
293+
* example: '5000'
294+
* responses:
295+
* 200:
296+
* description: Stream topped up successfully
297+
* content:
298+
* application/json:
299+
* schema:
300+
* type: object
301+
* properties:
302+
* txHash:
303+
* type: string
304+
* streamId:
305+
* type: integer
306+
* newDepositedAmount:
307+
* type: string
308+
* 400:
309+
* description: Invalid request — amount missing or not a positive integer string
310+
* 401:
311+
* description: Unauthorized - missing or invalid authentication token
312+
* 403:
313+
* description: Forbidden - caller is not the stream sender
314+
* 404:
315+
* description: Stream not found
273316
*/
274317
router.post('/:streamId/top-up', requireAuth, topUpStreamHandler);
275318
router.post('/:streamId/cancel', requireAuth, cancelStreamHandler as any);

backend/tests/integration/streams.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ import request from 'supertest';
1111
// Bypass Stellar signature verification on POST /v1/streams. The route is
1212
// exercised here as a stand-in for the indexer worker, so we replace the auth
1313
// middleware with a stub that injects a deterministic wallet.
14-
vi.mock('../../src/middleware/auth.js', async (importOriginal) => {
15-
const actual = await importOriginal<typeof import('../../src/middleware/auth.js')>();
16-
return {
17-
...actual,
18-
requireAuth: (req: any, _res: any, next: any) => {
19-
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
20-
next();
21-
},
22-
};
23-
});
14+
// Simple factory — no importOriginal — reliable with pool:forks.
15+
vi.mock('../../src/middleware/auth.js', () => ({
16+
requireAuth: (req: any, _res: any, next: any) => {
17+
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
18+
next();
19+
},
20+
requireAdmin: (_req: any, res: any, _next: any) => {
21+
res.status(403).json({ error: 'Forbidden' });
22+
},
23+
}));
2424

2525
// ─── Mocks (using vi.hoisted to ensure they are available to vi.mock) ─────────
2626

backend/tests/integration/streams/cancel.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ vi.mock('../../../src/lib/prisma.js', () => {
3737
};
3838
});
3939

40-
// Mock auth middleware to bypass real Stellar signature verification
41-
vi.mock('../../../src/middleware/auth.js', async (importOriginal) => {
42-
const actual = await importOriginal<typeof import('../../../src/middleware/auth.js')>();
43-
return {
44-
...actual,
45-
requireAuth: (req: any, _res: any, next: any) => {
46-
req.user = { publicKey: 'G_SENDER_123' };
47-
next();
48-
},
49-
};
50-
});
40+
// Mock auth middleware to bypass real Stellar signature verification.
41+
// Uses a simple factory (no importOriginal) so it is reliable with pool:forks.
42+
vi.mock('../../../src/middleware/auth.js', () => ({
43+
requireAuth: (req: any, _res: any, next: any) => {
44+
req.user = { publicKey: 'G_SENDER_123' };
45+
next();
46+
},
47+
requireAdmin: (_req: any, res: any, _next: any) => {
48+
res.status(403).json({ error: 'Forbidden' });
49+
},
50+
}));
5151

5252
// ─── App import (after mocks) ───────────────────────────────────────────────
5353

backend/tests/integration/streams/withdraw.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ vi.mock('../../../src/services/sorobanService.js', () => ({
3232
isStale: vi.fn().mockReturnValue(false),
3333
}));
3434

35-
vi.mock('../../../src/middleware/auth.js', async (importOriginal) => {
36-
const actual = await importOriginal<typeof import('../../../src/middleware/auth.js')>();
37-
return {
38-
...actual,
39-
requireAuth: (req: any, _res: any, next: any) => {
40-
req.user = { publicKey: currentUser.publicKey };
41-
next();
42-
},
43-
};
44-
});
35+
// Simple factory — no importOriginal — reliable with pool:forks.
36+
vi.mock('../../../src/middleware/auth.js', () => ({
37+
requireAuth: (req: any, _res: any, next: any) => {
38+
req.user = { publicKey: currentUser.publicKey };
39+
next();
40+
},
41+
requireAdmin: (_req: any, res: any, _next: any) => {
42+
res.status(403).json({ error: 'Forbidden' });
43+
},
44+
}));
4545

4646
import app from '../../../src/app.js';
4747

backend/tests/integration/top-up.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ vi.mock('../../src/services/sorobanService.js', () => ({
5858
cancelStream: vi.fn(),
5959
}));
6060

61-
vi.mock('../../src/middleware/auth.js', async (importOriginal) => {
62-
const actual = await importOriginal<typeof import('../../src/middleware/auth.js')>();
63-
return {
64-
...actual,
65-
requireAuth: vi.fn((req: any, _res: any, next: any) => {
66-
req.user = { publicKey: req.headers['x-test-caller'] ?? SENDER };
67-
next();
68-
}),
69-
};
70-
});
61+
// Simple factory — no importOriginal — reliable with pool:forks.
62+
vi.mock('../../src/middleware/auth.js', () => ({
63+
requireAuth: vi.fn((req: any, _res: any, next: any) => {
64+
req.user = { publicKey: req.headers['x-test-caller'] ?? SENDER };
65+
next();
66+
}),
67+
requireAdmin: vi.fn((_req: any, res: any, _next: any) => {
68+
res.status(403).json({ error: 'Forbidden' });
69+
}),
70+
}));
7171

7272
// App import after mocks
7373
import app from '../../src/app.js';

backend/tests/stream.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import request from 'supertest';
33

4-
vi.mock('../src/middleware/auth.js', async (importOriginal) => {
5-
const actual = await importOriginal<typeof import('../src/middleware/auth.js')>();
6-
return {
7-
...actual,
8-
requireAuth: (req: any, _res: any, next: any) => {
9-
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
10-
next();
11-
},
12-
};
13-
});
4+
// Simple factory — no importOriginal — reliable with pool:forks.
5+
vi.mock('../src/middleware/auth.js', () => ({
6+
requireAuth: (req: any, _res: any, next: any) => {
7+
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
8+
next();
9+
},
10+
requireAdmin: (_req: any, res: any, _next: any) => {
11+
res.status(403).json({ error: 'Forbidden' });
12+
},
13+
}));
1414

1515
vi.mock('../src/middleware/stream-rate-limiter.middleware.js', () => ({
1616
streamCreationRateLimiter: (_req: any, _res: any, next: any) => next(),

backend/vitest.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export default defineConfig({
55
environment: 'node',
66
globals: true,
77
setupFiles: [],
8+
// Provide a stable JWT_SECRET so verifyJwt is deterministic in tests.
9+
// The integration test mocks replace requireAuth entirely, but a known
10+
// secret means the real middleware also works if a mock is not applied.
11+
env: {
12+
JWT_SECRET: 'flowfi-test-secret-do-not-use-in-production',
13+
},
814
include: ['tests/**/*.{test,spec}.ts', 'src/__tests__/**/*.{test,spec}.ts'],
915
coverage: {
1016
enabled: true,

0 commit comments

Comments
 (0)