import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { buildServer } from '../../src/index.js'; import { users, teams, teamMembers, teamResources } from '../../src/db/schema.js'; import { eq, sql } from 'drizzle-orm'; describe('Team Resources API', () => { let server: Awaited>; let ownerId: string; let memberId: string; let outsiderId: string; let teamSlug: string; let teamId: string; beforeAll(async () => { server = await buildServer(); // Clean up any leftover test data await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'restest-%'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'restest_%'`); // Create test users const [owner] = await server.db.insert(users).values({ clerkId: 'restest_owner', displayName: 'Res Owner', email: 'restest_owner@test.com', }).returning(); ownerId = owner.id; const [member] = await server.db.insert(users).values({ clerkId: 'restest_member', displayName: 'Res Member', email: 'restest_member@test.com', }).returning(); memberId = member.id; const [outsider] = await server.db.insert(users).values({ clerkId: 'restest_outsider', displayName: 'Res Outsider', email: 'restest_outsider@test.com', }).returning(); outsiderId = outsider.id; // Create a team with owner + member const [team] = await server.db.insert(teams).values({ name: 'Res Test Team', slug: 'restest-resources', ownerId, }).returning(); teamId = team.id; teamSlug = team.slug; await server.db.insert(teamMembers).values([ { teamId, userId: ownerId, role: 'owner' }, { teamId, userId: memberId, role: 'member' }, ]); // Override auth handler server._authHandler.fn = async function (request, reply) { const testUserId = request.headers['x-test-user-id'] as string; if (!testUserId) { return reply.code(401).send({ error: 'Missing x-test-user-id header' }); } request.userId = testUserId; request.clerkId = 'test'; }; }); afterAll(async () => { await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'restest-%')`); await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'restest-%'`); await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'restest_%'`); await server.close(); }); it('shares a model_recipe resource', async () => { const response = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': ownerId }, payload: { resourceType: 'model_recipe', name: 'GPT-4 Coding Recipe', description: 'Optimized for code generation', config: { model: 'gpt-4', temperature: 0.2 }, }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.resourceType).toBe('model_recipe'); expect(body.name).toBe('GPT-4 Coding Recipe'); expect(body.description).toBe('Optimized for code generation'); expect(body.config).toEqual({ model: 'gpt-4', temperature: 0.2 }); expect(body.sharedBy).toBe(ownerId); expect(body.rating).toBe(0); expect(body.useCount).toBe(0); }); it('shares a skill resource', async () => { const response = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': memberId }, payload: { resourceType: 'skill', name: 'Code Review Skill', description: 'Automated code review with best practices', config: { language: 'typescript', rules: ['no-any', 'strict-null'] }, }, }); expect(response.statusCode).toBe(201); const body = JSON.parse(response.body); expect(body.resourceType).toBe('skill'); expect(body.name).toBe('Code Review Skill'); expect(body.sharedBy).toBe(memberId); }); it('rates a resource and updates running average', async () => { // Create a resource first const createRes = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': ownerId }, payload: { resourceType: 'prompt_template', name: 'Summarizer Prompt', config: { template: 'Summarize: {{input}}' }, }, }); const resource = JSON.parse(createRes.body); // First rating: should be the rating itself (useCount was 0) const rate1 = await server.inject({ method: 'PATCH', url: `/api/teams/${teamSlug}/resources/${resource.id}`, headers: { 'x-test-user-id': ownerId }, payload: { rating: 4 }, }); expect(rate1.statusCode).toBe(200); const body1 = JSON.parse(rate1.body); // After first rate: rating was set to 4 (useCount was 0), then useCount incremented to 1 expect(body1.useCount).toBe(1); // Second rating: running average = (4 * 1 + 2) / 2 = 3 const rate2 = await server.inject({ method: 'PATCH', url: `/api/teams/${teamSlug}/resources/${resource.id}`, headers: { 'x-test-user-id': memberId }, payload: { rating: 2 }, }); expect(rate2.statusCode).toBe(200); const body2 = JSON.parse(rate2.body); expect(body2.useCount).toBe(2); // rating = (4 * 1 + 2) / 2 = 3 expect(body2.rating).toBeCloseTo(3, 1); }); it('increments use_count on rate', async () => { const createRes = await server.inject({ method: 'POST', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': ownerId }, payload: { resourceType: 'tool_config', name: 'Linter Config', config: { tool: 'eslint', extends: 'recommended' }, }, }); const resource = JSON.parse(createRes.body); expect(resource.useCount).toBe(0); // Rate it const rateRes = await server.inject({ method: 'PATCH', url: `/api/teams/${teamSlug}/resources/${resource.id}`, headers: { 'x-test-user-id': memberId }, payload: { rating: 5 }, }); const rated = JSON.parse(rateRes.body); expect(rated.useCount).toBe(1); // Rate again const rateRes2 = await server.inject({ method: 'PATCH', url: `/api/teams/${teamSlug}/resources/${resource.id}`, headers: { 'x-test-user-id': ownerId }, payload: { rating: 3 }, }); const rated2 = JSON.parse(rateRes2.body); expect(rated2.useCount).toBe(2); }); it('does not rate a resource owned by another team through this team route', async () => { const [foreignTeam] = await server.db.insert(teams).values({ name: 'Foreign Resource Team', slug: 'restest-foreign-resources', ownerId: outsiderId, }).returning(); await server.db.insert(teamMembers).values({ teamId: foreignTeam.id, userId: outsiderId, role: 'owner', }); const [foreignResource] = await server.db.insert(teamResources).values({ teamId: foreignTeam.id, resourceType: 'model_recipe', name: 'Foreign Recipe', config: { model: 'private-model' }, sharedBy: outsiderId, rating: 1, useCount: 1, }).returning(); const response = await server.inject({ method: 'PATCH', url: `/api/teams/${teamSlug}/resources/${foreignResource.id}`, headers: { 'x-test-user-id': ownerId }, payload: { rating: 5 }, }); expect(response.statusCode).toBe(404); expect(response.json()).toEqual({ error: 'Resource not found' }); const [unchanged] = await server.db.select().from(teamResources) .where(eq(teamResources.id, foreignResource.id)); expect(unchanged.rating).toBe(1); expect(unchanged.useCount).toBe(1); }); it('filters resources by resource_type', async () => { const response = await server.inject({ method: 'GET', url: `/api/teams/${teamSlug}/resources?type=skill`, headers: { 'x-test-user-id': ownerId }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThanOrEqual(1); expect(body.every((r: { resourceType: string }) => r.resourceType === 'skill')).toBe(true); }); it('non-member gets 403', async () => { const response = await server.inject({ method: 'GET', url: `/api/teams/${teamSlug}/resources`, headers: { 'x-test-user-id': outsiderId }, }); expect(response.statusCode).toBe(403); }); });