Skip to content
Merged
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
36 changes: 35 additions & 1 deletion backend/src/config/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JSON Web Token issued by /v1/auth/verify after completing the SEP-10 challenge flow.'
},
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Alias for BearerAuth — used by route-level security annotations.'
},
adminAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Admin JWT — the token subject must match ADMIN_PUBLIC_KEY.'
}
},
schemas: {
Expand Down Expand Up @@ -165,6 +177,28 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
description: 'Stream active status',
example: true,
},
isPaused: {
type: 'boolean',
description: 'Whether the stream is currently paused',
example: false,
},
pausedAt: {
type: 'integer',
nullable: true,
description: 'Ledger timestamp when the stream was last paused (Unix), null if not paused',
example: null,
},
totalPausedDuration: {
type: 'integer',
description: 'Cumulative seconds the stream has spent paused',
example: 0,
},
endTime: {
type: 'integer',
nullable: true,
description: 'Ledger timestamp when the stream ended (Unix), null if still active',
example: null,
},
createdAt: {
type: 'string',
format: 'date-time',
Expand All @@ -189,7 +223,7 @@ See [Sandbox Mode Documentation](../docs/SANDBOX_MODE.md) for details.`,
},
eventType: {
type: 'string',
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED'],
enum: ['CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED', 'PAUSED', 'RESUMED', 'FEE_COLLECTED'],
description: 'Type of stream event',
example: 'TOPPED_UP',
},
Expand Down
47 changes: 45 additions & 2 deletions backend/src/routes/v1/stream.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,56 @@ router.post('/:streamId/withdraw', requireAuth, withdrawHandler as any);

/**
* @openapi
* /v1/streams/{streamId}/cancel:
* /v1/streams/{streamId}/top-up:
* post:
* tags:
* - Streams
* summary: Cancel an active payment stream
* summary: Top up a payment stream
* description: Adds additional funds to an existing active stream. Only the original sender can top up.
* parameters:
* - in: path
* name: streamId
* required: true
* schema:
* type: integer
* description: On-chain stream ID
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - amount
* properties:
* amount:
* type: string
* description: Amount to add to the stream deposit (i128 as string)
* example: '5000'
* responses:
* 200:
* description: Stream topped up successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* txHash:
* type: string
* streamId:
* type: integer
* newDepositedAmount:
* type: string
* 400:
* description: Invalid request — amount missing or not a positive integer string
* 401:
* description: Unauthorized - missing or invalid authentication token
* 403:
* description: Forbidden - caller is not the stream sender
* 404:
* description: Stream not found
*/
router.post('/:streamId/top-up', requireAuth, topUpStreamHandler);
router.post('/:streamId/cancel', requireAuth, cancelStreamHandler as any);
Expand Down
20 changes: 10 additions & 10 deletions backend/tests/integration/streams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import request from 'supertest';
// Bypass Stellar signature verification on POST /v1/streams. The route is
// exercised here as a stand-in for the indexer worker, so we replace the auth
// middleware with a stub that injects a deterministic wallet.
vi.mock('../../src/middleware/auth.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../src/middleware/auth.js')>();
return {
...actual,
requireAuth: (req: any, _res: any, next: any) => {
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
next();
},
};
});
// Simple factory — no importOriginal — reliable with pool:forks.
vi.mock('../../src/middleware/auth.js', () => ({
requireAuth: (req: any, _res: any, next: any) => {
req.user = { publicKey: 'GTEST_USER_PUBLIC_KEY' };
next();
},
requireAdmin: (_req: any, res: any, _next: any) => {
res.status(403).json({ error: 'Forbidden' });
},
}));

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

Expand Down
22 changes: 11 additions & 11 deletions backend/tests/integration/streams/cancel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ vi.mock('../../../src/lib/prisma.js', () => {
};
});

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

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

Expand Down
20 changes: 10 additions & 10 deletions backend/tests/integration/streams/withdraw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ vi.mock('../../../src/services/sorobanService.js', () => ({
isStale: vi.fn().mockReturnValue(false),
}));

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

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

Expand Down
20 changes: 10 additions & 10 deletions backend/tests/integration/top-up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ vi.mock('../../src/services/sorobanService.js', () => ({
cancelStream: vi.fn(),
}));

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

// App import after mocks
import app from '../../src/app.js';
Expand Down
20 changes: 10 additions & 10 deletions backend/tests/stream.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';

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

vi.mock('../src/middleware/stream-rate-limiter.middleware.js', () => ({
streamCreationRateLimiter: (_req: any, _res: any, next: any) => next(),
Expand Down
6 changes: 6 additions & 0 deletions backend/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export default defineConfig({
environment: 'node',
globals: true,
setupFiles: [],
// Provide a stable JWT_SECRET so verifyJwt is deterministic in tests.
// The integration test mocks replace requireAuth entirely, but a known
// secret means the real middleware also works if a mock is not applied.
env: {
JWT_SECRET: 'flowfi-test-secret-do-not-use-in-production',
},
include: ['tests/**/*.{test,spec}.ts', 'src/__tests__/**/*.{test,spec}.ts'],
coverage: {
enabled: true,
Expand Down
Loading