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
54 changes: 0 additions & 54 deletions backend/src/config/env.js

This file was deleted.

10 changes: 2 additions & 8 deletions backend/src/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import { ProjectRepository } from "../repositories/ProjectRepository.js";
import { ProjectService } from "../services/ProjectService.js";
import { ProjectController } from "../controllers/ProjectController.js";
import { providerRegistry } from "../services/payments/provider-registry.js";
import { SorobanPaymentProvider } from "../services/payments/providers/soroban.js";
import { EvmPaymentProvider } from "../services/payments/providers/evm.js";
import { FiatPaymentProvider } from "../services/payments/providers/fiat.js";
import { CreditPaymentProvider } from "../services/payments/providers/credit.js";
import { registerDefaultPaymentProviders } from "../services/payments/bootstrap.js";

export class DIContainer {
private static instance: DIContainer;
Expand Down Expand Up @@ -82,10 +79,7 @@ export class DIContainer {
const projectController = new ProjectController(projectService);
this.set("ProjectController", projectController);

providerRegistry.register(new SorobanPaymentProvider());
providerRegistry.register(new EvmPaymentProvider());
providerRegistry.register(new FiatPaymentProvider());
providerRegistry.register(new CreditPaymentProvider());
registerDefaultPaymentProviders(providerRegistry);
this.set("PaymentProviderRegistry", providerRegistry);
}

Expand Down
6 changes: 6 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import dataExportRouter from './routes/dataExport.js';
import securityRouter from './routes/security.js';
import commentsRouter from './routes/comments.js';
import collaborationRouter from './routes/collaboration.js';
import { paymentStrategiesRouter } from './routes/payment-strategies.js';
import { registerDefaultPaymentProviders } from './services/payments/bootstrap.js';

dotenv.config();

Expand Down Expand Up @@ -284,6 +286,8 @@ apiV1Router.use('/security', securityRouter);
apiV1Router.use('/comments', commentsRouter);
// Real-time collaboration: presence, field locks, edit history — Issue #714
apiV1Router.use('/collaboration', collaborationRouter);
// Multi-chain payment processing via the PaymentProvider strategy pattern — Issue #726
apiV1Router.use('/payment-strategies', paymentStrategiesRouter);

// Explicit URL-based mounting
app.use('/api/v1', apiV1Router);
Expand Down Expand Up @@ -319,6 +323,8 @@ if (config.queue.enabled) {
messageQueue.start();
}

registerDefaultPaymentProviders();

const server = app.listen(config.server.port, () => {
console.log(`AgenticPay backend running on port ${config.server.port} [${config.env}]`);
});
Expand Down
106 changes: 106 additions & 0 deletions backend/src/middleware/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* auth.test.ts — Issue #721
*/
import { describe, it, expect, vi } from 'vitest';
import type { Request, Response } from 'express';
import {
createAuthMiddleware,
sessionStrategy,
apiKeyStrategy,
type AuthStrategy,
} from '../auth.js';
import { createSession } from '../../services/session.js';
import { AppError } from '../errorHandler.js';

function makeReq(headers: Record<string, string> = {}): Request {
return { headers } as unknown as Request;
}

function makeRes(): Response {
return {} as Response;
}

describe('sessionStrategy', () => {
it('resolves a principal from a valid session', async () => {
const session = createSession('user-1', { deviceId: 'd1', browser: 'chrome', os: 'mac', ip: '127.0.0.1' });
const req = makeReq({ 'x-session-id': session.id });

expect(sessionStrategy.applies(req)).toBe(true);
const principal = await sessionStrategy.authenticate(req, makeRes());
expect(principal).toMatchObject({ id: 'user-1', method: 'session' });
});

it('rejects an unknown session id', async () => {
const req = makeReq({ 'x-session-id': 'does-not-exist' });
await expect(sessionStrategy.authenticate(req, makeRes())).rejects.toBeInstanceOf(AppError);
});

it('does not apply when no session header is present', () => {
expect(sessionStrategy.applies(makeReq())).toBe(false);
});
});

describe('apiKeyStrategy', () => {
it('resolves a principal for a known demo key', async () => {
const req = makeReq({ 'x-api-key': 'apk_free_demo_key_00000000000000000000000001' });
expect(apiKeyStrategy.applies(req)).toBe(true);
const principal = await apiKeyStrategy.authenticate(req, makeRes());
expect(principal).toMatchObject({ role: 'free', method: 'apiKey' });
});

it('rejects an unknown api key', async () => {
const req = makeReq({ 'x-api-key': 'not-a-real-key' });
await expect(apiKeyStrategy.authenticate(req, makeRes())).rejects.toBeInstanceOf(AppError);
});
});

describe('createAuthMiddleware (orchestration)', () => {
function makeStrategy(name: 'session' | 'apiKey', opts: { applies: boolean; succeed?: boolean }): AuthStrategy {
return {
name,
applies: () => opts.applies,
authenticate: vi.fn(async (req) => {
if (!opts.succeed) throw new AppError(401, `${name} failed`, 'FAIL');
req.user = { id: 'x', tenantId: 't', role: 'user', method: name };
return req.user;
}),
};
}

it('responds 401 when no strategy applies', async () => {
const middleware = createAuthMiddleware([makeStrategy('session', { applies: false })]);
const req = makeReq();
const next = vi.fn();

await middleware(req, makeRes(), next);

expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect((next.mock.calls[0][0] as AppError).statusCode).toBe(401);
});

it('authenticates with the first applicable strategy that succeeds', async () => {
const failing = makeStrategy('session', { applies: true, succeed: false });
const succeeding = makeStrategy('apiKey', { applies: true, succeed: true });
const middleware = createAuthMiddleware([failing, succeeding]);
const req = makeReq();
const next = vi.fn();

await middleware(req, makeRes(), next);

expect(req.user).toMatchObject({ method: 'apiKey' });
expect(next).toHaveBeenCalledWith();
});

it('propagates the last strategy failure when every applicable strategy fails', async () => {
const first = makeStrategy('session', { applies: true, succeed: false });
const second = makeStrategy('apiKey', { applies: true, succeed: false });
const middleware = createAuthMiddleware([first, second]);
const req = makeReq();
const next = vi.fn();

await middleware(req, makeRes(), next);

expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(req.user).toBeUndefined();
});
});
19 changes: 18 additions & 1 deletion backend/src/middleware/__tests__/errorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Request, Response } from 'express';
import { AppError, asyncHandler, notFoundHandler, errorHandler } from '../errorHandler.js';
import { AppError, PaymentError, asyncHandler, notFoundHandler, errorHandler } from '../errorHandler.js';

function makeReq(overrides: Partial<Request> = {}): Request {
return { method: 'GET', originalUrl: '/test', ...overrides } as unknown as Request;
Expand Down Expand Up @@ -158,4 +158,21 @@ describe('errorHandler', () => {
const callArg = (res.json as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(callArg.error.details).toEqual({ field: 'email' });
});

it('honors a domain error subclass statusCode/message instead of downgrading it to 500', () => {
// Regression test for Issue #719: PaymentError (and the other domain
// errors in types/errors.ts) used to extend `Error` directly, so
// `err instanceof AppError` was false and this handler discarded their
// statusCode/message in favor of a generic 500 "Internal server error".
const err = new PaymentError('Insufficient balance to cover this payment');
const req = makeReq();
const { res } = makeRes();
const next = vi.fn();

errorHandler(err, req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
const callArg = (res.json as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(callArg.error.message).toBe('Insufficient balance to cover this payment');
});
});
Loading