Files
motovaultpro/backend/src/core/plugins/tests/tier-guard.plugin.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

206 lines
6.3 KiB
TypeScript

import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import tierGuardPlugin from '../tier-guard.plugin';
const createReply = (): Partial<FastifyReply> & { payload?: unknown; statusCode?: number } => {
return {
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;
}),
};
};
describe('tier guard plugin', () => {
let fastify: FastifyInstance;
let authenticateMock: jest.Mock;
beforeEach(async () => {
fastify = Fastify();
// Mock authenticate to set userContext
authenticateMock = jest.fn(async (request: FastifyRequest) => {
request.userContext = {
userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com',
emailVerified: true,
onboardingCompleted: true,
isAdmin: false,
subscriptionTier: 'free',
};
});
fastify.decorate('authenticate', authenticateMock);
await fastify.register(tierGuardPlugin);
});
afterEach(async () => {
await fastify.close();
jest.clearAllMocks();
});
describe('requireTier with minTier', () => {
it('allows access when user tier meets minimum', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = {
userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com',
emailVerified: true,
onboardingCompleted: true,
isAdmin: false,
subscriptionTier: 'pro',
};
});
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ minTier: 'pro' });
await handler(request, reply as FastifyReply);
expect(authenticateMock).toHaveBeenCalledTimes(1);
expect(reply.code).not.toHaveBeenCalled();
expect(reply.send).not.toHaveBeenCalled();
});
it('allows access when user tier exceeds minimum', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = {
userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com',
emailVerified: true,
onboardingCompleted: true,
isAdmin: false,
subscriptionTier: 'enterprise',
};
});
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ minTier: 'pro' });
await handler(request, reply as FastifyReply);
expect(reply.code).not.toHaveBeenCalled();
});
it('denies access when user tier is below minimum', async () => {
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ minTier: 'pro' });
await handler(request, reply as FastifyReply);
expect(reply.code).toHaveBeenCalledWith(403);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
error: 'TIER_REQUIRED',
requiredTier: 'pro',
currentTier: 'free',
})
);
});
});
describe('requireTier with featureKey', () => {
it('denies free tier access to pro feature', async () => {
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ featureKey: 'document.scanMaintenanceSchedule' });
await handler(request, reply as FastifyReply);
expect(reply.code).toHaveBeenCalledWith(403);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
error: 'TIER_REQUIRED',
requiredTier: 'pro',
currentTier: 'free',
feature: 'document.scanMaintenanceSchedule',
featureName: 'Scan for Maintenance Schedule',
})
);
});
it('allows pro tier access to pro feature', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = {
userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com',
emailVerified: true,
onboardingCompleted: true,
isAdmin: false,
subscriptionTier: 'pro',
};
});
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ featureKey: 'document.scanMaintenanceSchedule' });
await handler(request, reply as FastifyReply);
expect(reply.code).not.toHaveBeenCalled();
});
it('allows access for unknown feature (fail open)', async () => {
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ featureKey: 'unknown.feature' });
await handler(request, reply as FastifyReply);
expect(reply.code).not.toHaveBeenCalled();
});
});
describe('error handling', () => {
it('returns 500 when authenticate handler is not a function', async () => {
const brokenFastify = Fastify();
// Decorate with a non-function value to simulate missing handler
brokenFastify.decorate('authenticate', 'not-a-function' as any);
await brokenFastify.register(tierGuardPlugin);
const request = {} as FastifyRequest;
const reply = createReply();
const handler = brokenFastify.requireTier({ minTier: 'pro' });
await handler(request, reply as FastifyReply);
expect(reply.code).toHaveBeenCalledWith(500);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
error: 'Internal server error',
message: 'Authentication handler missing',
})
);
await brokenFastify.close();
});
it('defaults to free tier when userContext is missing', async () => {
authenticateMock.mockImplementation(async () => {
// Don't set userContext
});
const request = {} as FastifyRequest;
const reply = createReply();
const handler = fastify.requireTier({ minTier: 'pro' });
await handler(request, reply as FastifyReply);
expect(reply.code).toHaveBeenCalledWith(403);
expect(reply.send).toHaveBeenCalledWith(
expect.objectContaining({
currentTier: 'free',
})
);
});
});
});