Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s
Backend test fixtures: - Replace auth0|xxx format with UUID in all test userId values - Update admin tests for new id/userProfileId schema - Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks - Fix admin integration test supertest usage (app.server) Frontend: - AdminUser type: auth0Sub -> id + userProfileId - admin.api.ts: all user management methods use userId (UUID) params - useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations - AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id - Remove encodeURIComponent (UUIDs don't need encoding) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
import { requireTier } from './require-tier';
|
|
|
|
// Mock logger to suppress output during tests
|
|
jest.mock('../logging/logger', () => ({
|
|
logger: {
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
debug: jest.fn(),
|
|
info: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
const createRequest = (subscriptionTier?: string): Partial<FastifyRequest> => {
|
|
if (subscriptionTier === undefined) {
|
|
return { userContext: undefined };
|
|
}
|
|
return {
|
|
userContext: {
|
|
userId: '550e8400-e29b-41d4-a716-446655440000',
|
|
email: 'user@example.com',
|
|
emailVerified: true,
|
|
onboardingCompleted: true,
|
|
isAdmin: false,
|
|
subscriptionTier: subscriptionTier as any,
|
|
},
|
|
};
|
|
};
|
|
|
|
const createReply = (): Partial<FastifyReply> & { statusCode?: number; payload?: unknown } => {
|
|
const reply: any = {
|
|
sent: false,
|
|
code: jest.fn(function (this: any, status: number) {
|
|
this.statusCode = status;
|
|
return this;
|
|
}),
|
|
send: jest.fn(function (this: any, payload: unknown) {
|
|
this.payload = payload;
|
|
this.sent = true;
|
|
return this;
|
|
}),
|
|
};
|
|
return reply;
|
|
};
|
|
|
|
describe('requireTier middleware', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('pro user passes fuelLog.receiptScan check', () => {
|
|
it('allows pro user through without sending a response', async () => {
|
|
const handler = requireTier('fuelLog.receiptScan');
|
|
const request = createRequest('pro');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).not.toHaveBeenCalled();
|
|
expect(reply.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('enterprise user passes all checks (tier inheritance)', () => {
|
|
it('allows enterprise user access to pro-gated features', async () => {
|
|
const handler = requireTier('fuelLog.receiptScan');
|
|
const request = createRequest('enterprise');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).not.toHaveBeenCalled();
|
|
expect(reply.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows enterprise user access to document.scanMaintenanceSchedule', async () => {
|
|
const handler = requireTier('document.scanMaintenanceSchedule');
|
|
const request = createRequest('enterprise');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).not.toHaveBeenCalled();
|
|
expect(reply.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows enterprise user access to vehicle.vinDecode', async () => {
|
|
const handler = requireTier('vehicle.vinDecode');
|
|
const request = createRequest('enterprise');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).not.toHaveBeenCalled();
|
|
expect(reply.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('free user blocked with 403 and correct response body', () => {
|
|
it('blocks free user from fuelLog.receiptScan', async () => {
|
|
const handler = requireTier('fuelLog.receiptScan');
|
|
const request = createRequest('free');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).toHaveBeenCalledWith(403);
|
|
expect(reply.send).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
error: 'TIER_REQUIRED',
|
|
requiredTier: 'pro',
|
|
currentTier: 'free',
|
|
featureName: 'Receipt Scan',
|
|
upgradePrompt: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('blocks free user from document.scanMaintenanceSchedule', async () => {
|
|
const handler = requireTier('document.scanMaintenanceSchedule');
|
|
const request = createRequest('free');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).toHaveBeenCalledWith(403);
|
|
expect(reply.send).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
error: 'TIER_REQUIRED',
|
|
requiredTier: 'pro',
|
|
currentTier: 'free',
|
|
featureName: 'Scan for Maintenance Schedule',
|
|
upgradePrompt: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('response body includes all required fields', async () => {
|
|
const handler = requireTier('fuelLog.receiptScan');
|
|
const request = createRequest('free');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
const body = (reply.send as jest.Mock).mock.calls[0][0];
|
|
expect(body).toHaveProperty('requiredTier', 'pro');
|
|
expect(body).toHaveProperty('currentTier', 'free');
|
|
expect(body).toHaveProperty('featureName', 'Receipt Scan');
|
|
expect(body).toHaveProperty('upgradePrompt');
|
|
expect(typeof body.upgradePrompt).toBe('string');
|
|
expect(body.upgradePrompt.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('unknown feature key returns 500', () => {
|
|
it('returns 500 INTERNAL_ERROR for unregistered feature', async () => {
|
|
const handler = requireTier('unknown.nonexistent.feature');
|
|
const request = createRequest('pro');
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).toHaveBeenCalledWith(500);
|
|
expect(reply.send).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Unknown feature configuration',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('missing user.tier on request returns 403', () => {
|
|
it('defaults to free tier when userContext is undefined', async () => {
|
|
const handler = requireTier('fuelLog.receiptScan');
|
|
const request = createRequest(); // no tier = undefined userContext
|
|
const reply = createReply();
|
|
|
|
await handler(request as FastifyRequest, reply as FastifyReply);
|
|
|
|
expect(reply.code).toHaveBeenCalledWith(403);
|
|
expect(reply.send).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
error: 'TIER_REQUIRED',
|
|
currentTier: 'free',
|
|
requiredTier: 'pro',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|