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
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,32 @@ jobs:
- name: Type Check
run: pnpm check-types

unit-test:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.12.4

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run API route tests
run: pnpm --filter @governs-ai/platform test:ci

build:
name: Build
runs-on: ubuntu-latest
needs: lint-and-typecheck
needs: unit-test

services:
postgres:
Expand Down
53 changes: 53 additions & 0 deletions apps/platform/__mocks__/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Manual mock for @governs-ai/db — replaces every Prisma model method
* with a jest.fn() so tests can configure return values without a real DB.
*/

export const prisma = {
aPIKey: {
findMany: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
orgMembership: {
findFirst: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
},
user: {
findUnique: jest.fn(),
create: jest.fn(),
},
budgetLimit: {
findFirst: jest.fn(),
},
usageRecord: {
aggregate: jest.fn(),
},
purchaseRecord: {
aggregate: jest.fn(),
},
decision: {
create: jest.fn(),
},
usageRecord_: jest.fn(),
mfaTotp: {
findUnique: jest.fn(),
},
passkey: {
count: jest.fn(),
},
webhookIdempotencyKey: {
findUnique: jest.fn(),
create: jest.fn(),
},
policy: {
create: jest.fn(),
},
contextMemory: {
findFirst: jest.fn(),
},
};
154 changes: 154 additions & 0 deletions apps/platform/__tests__/api/budget-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* TEST-3.7c — Console API: Budget context route integration tests.
*
* Covers:
* - GET /api/v1/budget/context
* - Missing API key → 401
* - Invalid/inactive key → 401
* - Valid key, no budget → returns zeros
* - User-level budget → scoped to userId
* - Org-level budget → scoped to orgId only
* - Remaining budget correctly clamped to 0 when overspent
*/

import { NextRequest } from 'next/server';
import { prisma } from '@governs-ai/db';
import { GET } from '@/app/api/v1/budget/context/route';

const mockPrisma = prisma as any;

function makeReq(apiKey?: string) {
return new NextRequest('http://localhost/api/v1/budget/context', {
method: 'GET',
headers: apiKey ? { 'X-Governs-Key': apiKey } : {},
});
}

beforeEach(() => jest.clearAllMocks());

// ---------------------------------------------------------------------------
// Authentication
// ---------------------------------------------------------------------------

describe('authentication', () => {
it('returns 401 when X-Governs-Key header is absent', async () => {
const res = await GET(makeReq());
expect(res.status).toBe(401);
});

it('returns 401 when key is inactive', async () => {
mockPrisma.aPIKey.findUnique.mockResolvedValue({ isActive: false, user: {}, org: {} });
const res = await GET(makeReq('gai_bad'));
expect(res.status).toBe(401);
});

it('returns 401 when key is not found in DB', async () => {
mockPrisma.aPIKey.findUnique.mockResolvedValue(null);
const res = await GET(makeReq('gai_unknown'));
expect(res.status).toBe(401);
});
});

// ---------------------------------------------------------------------------
// Budget resolution — no budget configured
// ---------------------------------------------------------------------------

describe('no budget configured', () => {
beforeEach(() => {
mockPrisma.aPIKey.findUnique.mockResolvedValue({
isActive: true,
user: { id: 'u1' },
org: { id: 'org-1' },
});
mockPrisma.budgetLimit.findFirst.mockResolvedValue(null);
mockPrisma.usageRecord.aggregate.mockResolvedValue({ _sum: { cost: null } });
mockPrisma.purchaseRecord.aggregate.mockResolvedValue({ _sum: { amount: null } });
});

it('returns zeros when no budget or spend exists', async () => {
const res = await GET(makeReq('gai_valid'));
expect(res.status).toBe(200);
const body = await res.json();
expect(body.monthly_limit).toBe(0);
expect(body.current_spend).toBe(0);
expect(body.llm_spend).toBe(0);
expect(body.purchase_spend).toBe(0);
expect(body.remaining_budget).toBe(0);
});

it('response includes budget_type field', async () => {
const res = await GET(makeReq('gai_valid'));
const body = await res.json();
expect(body).toHaveProperty('budget_type');
});
});

// ---------------------------------------------------------------------------
// User-level budget
// ---------------------------------------------------------------------------

describe('user-level budget', () => {
beforeEach(() => {
mockPrisma.aPIKey.findUnique.mockResolvedValue({
isActive: true,
user: { id: 'u1' },
org: { id: 'org-1' },
});
// User budget takes precedence over org budget
mockPrisma.budgetLimit.findFirst
.mockResolvedValueOnce({ monthlyLimit: 50 }) // user budget found
.mockResolvedValueOnce(null); // org budget (not checked if user exists)
mockPrisma.usageRecord.aggregate.mockResolvedValue({ _sum: { cost: 10 } });
mockPrisma.purchaseRecord.aggregate.mockResolvedValue({ _sum: { amount: 5 } });
});

it('budget_type is user', async () => {
const res = await GET(makeReq('gai_valid'));
const body = await res.json();
expect(body.budget_type).toBe('user');
});

it('computes remaining correctly', async () => {
const res = await GET(makeReq('gai_valid'));
const body = await res.json();
expect(body.monthly_limit).toBe(50);
expect(body.llm_spend).toBe(10);
expect(body.purchase_spend).toBe(5);
expect(body.current_spend).toBe(15);
expect(body.remaining_budget).toBeCloseTo(35);
});
});

// ---------------------------------------------------------------------------
// Org-level budget
// ---------------------------------------------------------------------------

describe('org-level budget', () => {
beforeEach(() => {
mockPrisma.aPIKey.findUnique.mockResolvedValue({
isActive: true,
user: { id: 'u1' },
org: { id: 'org-1' },
});
// No user-level budget → falls back to org budget
mockPrisma.budgetLimit.findFirst
.mockResolvedValueOnce(null) // no user budget
.mockResolvedValueOnce({ monthlyLimit: 100 }); // org budget
mockPrisma.usageRecord.aggregate.mockResolvedValue({ _sum: { cost: 30 } });
mockPrisma.purchaseRecord.aggregate.mockResolvedValue({ _sum: { amount: 0 } });
});

it('budget_type is organization', async () => {
const res = await GET(makeReq('gai_valid'));
const body = await res.json();
expect(body.budget_type).toBe('organization');
});

it('remaining is clamped to 0 when spend exceeds limit', async () => {
// Override: spend > limit
mockPrisma.usageRecord.aggregate.mockResolvedValue({ _sum: { cost: 110 } });
const res = await GET(makeReq('gai_valid'));
const body = await res.json();
expect(body.remaining_budget).toBe(0);
});
});
Loading
Loading