Skip to content
Open
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
82 changes: 82 additions & 0 deletions backend/services/billing/__tests__/billingEngine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { BillingEngine } from '../billingEngine';
import { PricingStrategyFactory } from '../strategyFactory';
import { PricingContext } from '../pricingStrategy';

describe('BillingEngine Strategy Pattern Integration', () => {
beforeEach(() => {
PricingStrategyFactory.reset();
});

const sampleContext: PricingContext = {
planId: 'plan-pro-01',
subscriberAddress: '0x1234567890abcdef',
currentPrice: 49.99,
currency: 'USD',
usageData: {
sessionsPerWeek: 12,
retentionRate: 0.95,
apiCallsThisPeriod: 5500,
storageUsedMB: 1024,
seatsActive: 5,
},
};

it('should initialize with default config', () => {
const engine = new BillingEngine();
expect(engine.getAvailableStrategies()).toEqual([
'flat_rate',
'usage_based',
'tiered',
'dynamic',
]);
});

it('should calculate price using default flat_rate for basic plan', () => {
const engine = new BillingEngine();
const result = engine.calculatePrice('basic', sampleContext);

expect(result.strategyName).toBe('flat_rate');
expect(result.price).toBe(49.99);
expect(result.breakdown.finalPrice).toBe(49.99);
});

it('should calculate price using tiered strategy for premium plan', () => {
const engine = new BillingEngine();
const result = engine.calculatePrice('premium', sampleContext);

expect(result.strategyName).toBe('tiered');
expect(result.price).toBeGreaterThan(0);
});

it('should allow runtime strategy override', () => {
const engine = new BillingEngine();
const result = engine.calculatePrice('basic', sampleContext, 'usage_based');

expect(result.strategyName).toBe('usage_based');
});

it('should process billing charge and record history', () => {
const engine = new BillingEngine();
const record = engine.processCharge('enterprise', sampleContext);

expect(record.subscriptionId).toBe('plan-pro-01');
expect(record.amount).toBeGreaterThan(0);
expect(record.currency).toBe('USD');

const history = engine.getBillingHistory('0x1234567890abcdef');
expect(history.length).toBe(1);
expect(history[0].subscriptionId).toBe('plan-pro-01');
});

it('should generate A/B test variants and analytics', () => {
const engine = new BillingEngine();
engine.calculatePrice('basic', sampleContext);

const variants = engine.getABTestVariants('basic', 49.99);
expect(variants.length).toBeGreaterThan(0);

const analytics = engine.getAnalytics('flat_rate');
expect(analytics.length).toBe(1);
expect(analytics[0].totalCalculations).toBe(2); // calculatePrice + getABTestVariants
});
});
6 changes: 1 addition & 5 deletions backend/services/notification/alerting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
* Channels are pluggable; add as many as needed.
*/

<<<<<<< HEAD:backend/services/alerting.ts
import { logger } from './logging';
import type { Alert, AlertChannelConfig } from './types';
=======
import { logger } from '../shared/logging';
import type { Alert, AlertChannelConfig } from '../shared/types';
>>>>>>> main:backend/services/notification/alerting.ts

export interface AlertDispatcher {
send(alert: Alert): Promise<void>;
Expand Down
105 changes: 105 additions & 0 deletions backend/services/shared/__tests__/authStrategies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Request } from 'express';
import {
JwtAuthStrategy,
ApiKeyAuthStrategy,
WalletAuthStrategy,
OAuthSessionAuthStrategy,
CompositeAuthStrategyManager,
createRequireRoleMiddleware,
createRequireStrategyMiddleware,
} from '../authStrategies';
import { UnauthorizedError, ForbiddenError } from '../errors';

describe('Auth Strategies', () => {
it('validates JWT token strategy', async () => {
const strategy = new JwtAuthStrategy();
const mockReq = { headers: { authorization: 'Bearer valid.jwt.token' } } as Request;
const user = await strategy.validate(mockReq);

expect(user).not.toBeNull();
expect(user?.strategy).toBe('jwt');
expect(user?.roles).toContain('user');
});

it('rejects invalid JWT token', async () => {
const strategy = new JwtAuthStrategy();
const mockReq = { headers: { authorization: 'Bearer invalid-token' } } as Request;
const user = await strategy.validate(mockReq);

expect(user).toBeNull();
});

it('validates API key strategy', async () => {
const strategy = new ApiKeyAuthStrategy();
const mockReq = { headers: { 'x-api-key': 'valid-api-key-12345' } } as Request;
const user = await strategy.validate(mockReq);

expect(user).not.toBeNull();
expect(user?.strategy).toBe('api-key');
expect(user?.roles).toContain('api_client');
});

it('validates OAuth session strategy', async () => {
const strategy = new OAuthSessionAuthStrategy();
const mockReq = { headers: { 'x-session-id': 'sess_abc123' } } as Request;
const user = await strategy.validate(mockReq);

expect(user).not.toBeNull();
expect(user?.strategy).toBe('oauth-session');
expect(user?.roles).toContain('oauth_user');
});

it('runs composite authentication manager across multiple strategies', async () => {
const manager = new CompositeAuthStrategyManager([
new JwtAuthStrategy(),
new ApiKeyAuthStrategy(),
new WalletAuthStrategy(),
new OAuthSessionAuthStrategy(),
]);

const reqWithApiKey = { headers: { 'x-api-key': 'valid-api-key-123' }, query: {} } as Request;
const user = await manager.authenticate(reqWithApiKey);

expect(user.strategy).toBe('api-key');
});

it('throws UnauthorizedError when all strategies fail', async () => {
const manager = new CompositeAuthStrategyManager([
new JwtAuthStrategy(),
new ApiKeyAuthStrategy(),
]);

const emptyReq = { headers: {}, query: {} } as Request;
await expect(manager.authenticate(emptyReq)).rejects.toThrow(UnauthorizedError);
});

it('enforces role authorization middleware', () => {
const middleware = createRequireRoleMiddleware(['admin', 'api_client']);

const reqWithRole = { user: { id: '123', roles: ['api_client'], strategy: 'api-key' } } as any;
const reqWithoutRole = { user: { id: '456', roles: ['user'], strategy: 'jwt' } } as any;
const next = jest.fn();

middleware(reqWithRole, {} as any, next);
expect(next).toHaveBeenCalledWith();

next.mockClear();
middleware(reqWithoutRole, {} as any, next);
expect(next).toHaveBeenCalledWith(expect.any(ForbiddenError));
});

it('enforces strategy requirement middleware', () => {
const middleware = createRequireStrategyMiddleware(['api-key', 'jwt']);

const validReq = { user: { id: '123', roles: [], strategy: 'api-key' } } as any;
const invalidReq = { user: { id: '456', roles: [], strategy: 'wallet' } } as any;
const next = jest.fn();

middleware(validReq, {} as any, next);
expect(next).toHaveBeenCalledWith();

next.mockClear();
middleware(invalidReq, {} as any, next);
expect(next).toHaveBeenCalledWith(expect.any(ForbiddenError));
});
});
62 changes: 62 additions & 0 deletions backend/services/shared/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
DomainError,
UnprocessableEntityError,
BadGatewayError,
isDomainError,
fromUnknownError,
} from '../errors';

describe('Structured Error Types', () => {
it('creates DomainError with structured fields', () => {
const err = new DomainError('Test error', 'TEST_CODE', 400, {
userMessage: 'Friendly user message',
recovery: 'Try again later',
details: { foo: 'bar' },
requestId: 'req_123',
});

expect(err.message).toBe('Test error');
expect(err.code).toBe('TEST_CODE');
expect(err.statusCode).toBe(400);
expect(err.userMessage).toBe('Friendly user message');
expect(err.recovery).toBe('Try again later');
expect(err.details).toEqual({ foo: 'bar' });
expect(err.requestId).toBe('req_123');

const apiRes = err.toApiResponse();
expect(apiRes.error.code).toBe('TEST_CODE');
expect(apiRes.error.userMessage).toBe('Friendly user message');
});

it('creates UnprocessableEntityError with HTTP 422', () => {
const err = new UnprocessableEntityError('Invalid payload', { field: 'email' });
expect(err.statusCode).toBe(422);
expect(err.code).toBe('UNPROCESSABLE_ENTITY');
expect(err.details).toEqual({ field: 'email' });
});

it('creates BadGatewayError with HTTP 502', () => {
const err = new BadGatewayError('Upstream service failed');
expect(err.statusCode).toBe(502);
expect(err.code).toBe('BAD_GATEWAY');
});

it('identifies DomainError using isDomainError type guard', () => {
const domainErr = new DomainError('Domain error');
const standardErr = new Error('Standard error');

expect(isDomainError(domainErr)).toBe(true);
expect(isDomainError(standardErr)).toBe(false);
expect(isDomainError(null)).toBe(false);
});

it('converts unknown error using fromUnknownError helper', () => {
const nativeErr = new Error('Something broke');
const wrapped = fromUnknownError(nativeErr, 'WRAPPED_CODE', 500, 'req_999');

expect(wrapped).toBeInstanceOf(DomainError);
expect(wrapped.message).toBe('Something broke');
expect(wrapped.code).toBe('WRAPPED_CODE');
expect(wrapped.requestId).toBe('req_999');
});
});
Loading
Loading