Files
motovaultpro/backend/src/core/middleware/require-tier.test.ts
Eric Gullickson 754639c86d
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
chore: update test fixtures and frontend for UUID identity (refs #217)
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>
2026-02-16 10:21:18 -06:00

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',
}),
);
});
});
});