/** @jest-environment node */ import { makeZodVerifiedAPICall, makeAPICall, } from "@/src/__tests__/test-utils"; import { prisma } from "@langfuse/shared/src/db"; import { z } from "zod/v4"; import { randomUUID } from "crypto"; import { Role } from "@langfuse/shared"; import { createAndAddApiKeysToDb, createBasicAuthHeader, } from "@langfuse/shared/src/server"; // Schema for membership response const MembershipResponseSchema = z.object({ userId: z.string(), role: z.enum(Role), email: z.string().email(), name: z.string().nullable(), }); // Schema for memberships list response const MembershipsListSchema = z.object({ memberships: z.array(MembershipResponseSchema), }); describe("Memberships APIs", () => { // Create test data let testOrgId: string; let testProjectId: string; let testUserId: string; let testApiKey: string; let testApiSecretKey: string; beforeAll(async () => { // Create a test organization const uniqueOrgName = `Test Org ${randomUUID().substring(0, 8)}`; const org = await prisma.organization.create({ data: { name: uniqueOrgName, cloudConfig: { plan: "Team" } }, }); testOrgId = org.id; // Create a test project const uniqueProjectName = `Test Project ${randomUUID().substring(0, 8)}`; const project = await prisma.project.create({ data: { name: uniqueProjectName, orgId: testOrgId, }, }); testProjectId = project.id; // Create a test user const uniqueUserEmail = `test-user-${randomUUID().substring(0, 8)}@example.com`; const user = await prisma.user.create({ data: { email: uniqueUserEmail, name: `Test User ${randomUUID().substring(0, 8)}`, }, }); testUserId = user.id; // Create an organization API key const apiKey = await createAndAddApiKeysToDb({ prisma, entityId: testOrgId, scope: "ORGANIZATION", note: "Test API Key for Memberships API", predefinedKeys: { publicKey: `pk-lf-org-${randomUUID().substring(0, 8)}`, secretKey: `sk-lf-org-${randomUUID().substring(0, 8)}`, }, }); testApiKey = apiKey.publicKey; testApiSecretKey = apiKey.secretKey; }); afterAll(async () => { // Clean up test data await prisma.user.delete({ where: { id: testUserId, }, }); await prisma.organization.delete({ where: { id: testOrgId, }, }); }); describe("Project Memberships", () => { describe("GET /api/public/projects/[projectId]/memberships", () => { it("should get all project memberships with valid API key", async () => { // First create an organization membership for the test user const orgMembership = await prisma.organizationMembership.create({ data: { userId: testUserId, orgId: testOrgId, role: Role.MEMBER, }, }); // Then create a project membership await prisma.projectMembership.create({ data: { userId: testUserId, projectId: testProjectId, role: Role.VIEWER, orgMembershipId: orgMembership.id, }, }); const response = await makeZodVerifiedAPICall( MembershipsListSchema, "GET", `/api/public/projects/${testProjectId}/memberships`, undefined, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(Array.isArray(response.body.memberships)).toBe(true); expect(response.body.memberships.length).toBeGreaterThan(0); expect( response.body.memberships.some( (membership) => membership.userId === testUserId, ), ).toBe(true); const membership = response.body.memberships.find( (m) => m.userId === testUserId, ); expect(membership?.role).toBe(Role.VIEWER); }); it("should return 403 when using a non-organization API key", async () => { // Create a project API key const projectApiKey = await createAndAddApiKeysToDb({ prisma, entityId: testProjectId, scope: "PROJECT", note: "Test API Key for Memberships API", predefinedKeys: { publicKey: `pk-lf-project-${randomUUID().substring(0, 8)}`, secretKey: `sk-lf-project-${randomUUID().substring(0, 8)}`, }, }); const result = await makeAPICall( "GET", `/api/public/projects/${testProjectId}/memberships`, undefined, createBasicAuthHeader( projectApiKey.publicKey, projectApiKey.secretKey, ), ); expect(result.status).toBe(403); // Clean up await prisma.apiKey.delete({ where: { id: projectApiKey.id, }, }); }); }); describe("PUT /api/public/projects/[projectId]/memberships", () => { it("should create a new project membership with valid API key", async () => { // First ensure the user has an organization membership await prisma.organizationMembership.upsert({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, update: {}, create: { userId: testUserId, orgId: testOrgId, role: Role.MEMBER, }, }); // Delete any existing project membership await prisma.projectMembership.deleteMany({ where: { userId: testUserId, projectId: testProjectId, }, }); const response = await makeZodVerifiedAPICall( MembershipResponseSchema, "PUT", `/api/public/projects/${testProjectId}/memberships`, { userId: testUserId, role: Role.ADMIN, }, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(response.body.userId).toBe(testUserId); expect(response.body.role).toBe(Role.ADMIN); // Verify the membership was created in the database const membership = await prisma.projectMembership.findUnique({ where: { projectId_userId: { userId: testUserId, projectId: testProjectId, }, }, }); expect(membership?.role).toBe(Role.ADMIN); }); it("should update an existing project membership with valid API key", async () => { const response = await makeZodVerifiedAPICall( MembershipResponseSchema, "PUT", `/api/public/projects/${testProjectId}/memberships`, { userId: testUserId, role: Role.OWNER, }, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(response.body.userId).toBe(testUserId); expect(response.body.role).toBe(Role.OWNER); // Verify the role was updated in the database const membership = await prisma.projectMembership.findUnique({ where: { projectId_userId: { userId: testUserId, projectId: testProjectId, }, }, }); expect(membership?.role).toBe(Role.OWNER); }); it("should return 404 when user is not a member of the organization", async () => { // Create a new user that is not a member of the organization const newUser = await prisma.user.create({ data: { email: `test-user-${randomUUID().substring(0, 8)}@example.com`, name: `New Test User`, }, }); const result = await makeAPICall( "PUT", `/api/public/projects/${testProjectId}/memberships`, { userId: newUser.id, role: Role.VIEWER, }, createBasicAuthHeader(testApiKey, testApiSecretKey), ); expect(result.status).toBe(404); // Clean up await prisma.user.delete({ where: { id: newUser.id, }, }); }); }); describe("DELETE /api/public/projects/[projectId]/memberships", () => { it("should delete an existing project membership with valid API key", async () => { // First ensure the user has an organization membership const orgMembership = await prisma.organizationMembership.upsert({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, update: {}, create: { userId: testUserId, orgId: testOrgId, role: Role.MEMBER, }, }); // Create a project membership await prisma.projectMembership.upsert({ where: { projectId_userId: { userId: testUserId, projectId: testProjectId, }, }, update: {}, create: { userId: testUserId, projectId: testProjectId, role: Role.VIEWER, orgMembershipId: orgMembership.id, }, }); // Verify membership exists before deletion const membershipBefore = await prisma.projectMembership.findUnique({ where: { projectId_userId: { userId: testUserId, projectId: testProjectId, }, }, }); expect(membershipBefore).not.toBeNull(); // Delete the membership const response = await makeAPICall( "DELETE", `/api/public/projects/${testProjectId}/memberships`, { userId: testUserId, }, createBasicAuthHeader(testApiKey, testApiSecretKey), ); expect(response.status).toBe(200); expect(response.body.message).toBe( "Project membership deleted successfully", ); expect(response.body.userId).toBe(testUserId); // Verify the membership was deleted from the database const membershipAfter = await prisma.projectMembership.findUnique({ where: { projectId_userId: { userId: testUserId, projectId: testProjectId, }, }, }); expect(membershipAfter).toBeNull(); }); }); }); describe("Organization Memberships", () => { describe("GET /api/public/organizations/memberships", () => { it("should get all organization memberships with valid API key", async () => { // First ensure the membership exists await prisma.organizationMembership.upsert({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, update: {}, create: { userId: testUserId, orgId: testOrgId, role: Role.MEMBER, }, }); const response = await makeZodVerifiedAPICall( MembershipsListSchema, "GET", `/api/public/organizations/memberships`, undefined, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(Array.isArray(response.body.memberships)).toBe(true); expect(response.body.memberships.length).toBeGreaterThan(0); expect( response.body.memberships.some( (membership) => membership.userId === testUserId, ), ).toBe(true); }); }); describe("PUT /api/public/organizations/memberships", () => { it("should create a new organization membership with valid API key", async () => { // First delete any existing membership await prisma.organizationMembership.deleteMany({ where: { userId: testUserId, orgId: testOrgId, }, }); const response = await makeZodVerifiedAPICall( MembershipResponseSchema, "PUT", `/api/public/organizations/memberships`, { userId: testUserId, role: Role.ADMIN, }, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(response.body.userId).toBe(testUserId); expect(response.body.role).toBe(Role.ADMIN); // Verify the membership was created in the database const membership = await prisma.organizationMembership.findUnique({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, }); expect(membership?.role).toBe(Role.ADMIN); }); it("should update an existing organization membership with valid API key", async () => { const response = await makeZodVerifiedAPICall( MembershipResponseSchema, "PUT", `/api/public/organizations/memberships`, { userId: testUserId, role: Role.OWNER, }, createBasicAuthHeader(testApiKey, testApiSecretKey), 200, ); expect(response.status).toBe(200); expect(response.body.userId).toBe(testUserId); expect(response.body.role).toBe(Role.OWNER); // Verify the role was updated in the database const membership = await prisma.organizationMembership.findUnique({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, }); expect(membership?.role).toBe(Role.OWNER); }); it("should return 404 when user does not exist", async () => { const nonExistentUserId = `user-${randomUUID()}`; const result = await makeAPICall( "PUT", `/api/public/organizations/memberships`, { userId: nonExistentUserId, role: Role.VIEWER, }, createBasicAuthHeader(testApiKey, testApiSecretKey), ); expect(result.status).toBe(404); }); }); describe("DELETE /api/public/organizations/memberships", () => { it("should delete an existing organization membership with valid API key", async () => { // First ensure the membership exists await prisma.organizationMembership.upsert({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, update: {}, create: { userId: testUserId, orgId: testOrgId, role: Role.MEMBER, }, }); // Verify membership exists before deletion const membershipBefore = await prisma.organizationMembership.findUnique( { where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, }, ); expect(membershipBefore).not.toBeNull(); // Delete the membership const response = await makeAPICall( "DELETE", `/api/public/organizations/memberships`, { userId: testUserId, }, createBasicAuthHeader(testApiKey, testApiSecretKey), ); expect(response.status).toBe(200); expect(response.body.message).toBe("Membership deleted successfully"); expect(response.body.userId).toBe(testUserId); // Verify the membership was deleted from the database const membershipAfter = await prisma.organizationMembership.findUnique({ where: { orgId_userId: { userId: testUserId, orgId: testOrgId, }, }, }); expect(membershipAfter).toBeNull(); }); }); }); });