import assert from 'node:assert/strict'; import test from 'node:test'; import { createWorkspace, deleteWorkspaceMember, deleteWorkspaceIfEmpty, ensureDefaultWorkspaceForUser, ensurePersonalWorkspaceForUser, findAlternateWorkspaceForUser, getPlatformAdminSummary, listOwnedWorkspacesByOwnerEmail, updateWorkspace, updateWorkspaceMemberRole, } from './workspaceRepository.js'; import { mockDb } from '../test/mockDb.js'; import type { UserRow } from '../types.js'; const user: UserRow = { id: 'user-1', email: 'owner@example.com', password_hash: null, name: 'Owner', created_at: '2026-04-14T00:00:00.000Z', }; test('ensurePersonalWorkspaceForUser returns an existing workspace without creating one', async () => { const { calls } = mockDb({ rowCount: 1, rows: [{ workspace_id: 42 }], }); const workspaceId = await ensurePersonalWorkspaceForUser(user); assert.equal(workspaceId, 42); assert.equal(calls.length, 1); assert.match(calls[0].text, /FROM workspace_members/); }); test('ensurePersonalWorkspaceForUser creates a fresh workspace instead of claiming the legacy seed flock', async () => { const { calls } = mockDb( { rowCount: 0, rows: [], }, { rowCount: 1, rows: [{ next_id: 43 }], }, { rowCount: 1, rows: [], }, { rowCount: 1, rows: [], }, ); const workspaceId = await ensurePersonalWorkspaceForUser(user); assert.equal(workspaceId, 43); assert.equal(calls.length, 4); assert.match(calls[1].text, /SELECT COALESCE\(MAX\(id\), 0\) \+ 1 AS next_id FROM workspaces/); assert.match(calls[2].text, /INSERT INTO workspaces/); assert.match(calls[3].text, /INSERT INTO workspace_members/); assert.deepEqual(calls[2].params, [43, "Owner's Flock", 'owner@example.com']); }); test('ensureDefaultWorkspaceForUser reuses an existing rescue workspace without creating a household flock', async () => { const { calls } = mockDb({ rowCount: 1, rows: [{ workspace_id: 84 }], }); const workspaceId = await ensureDefaultWorkspaceForUser(user); assert.equal(workspaceId, 84); assert.equal(calls.length, 1); assert.match(calls[0].text, /FROM workspace_members/); assert.doesNotMatch(calls[0].text, /workspaces\.workspace_type = 'standard'/); }); test('ensureDefaultWorkspaceForUser creates a household flock when the user has no workspace', async () => { const { calls } = mockDb( { rowCount: 0, rows: [], }, { rowCount: 0, rows: [], }, { rowCount: 1, rows: [{ next_id: 43 }], }, { rowCount: 1, rows: [], }, { rowCount: 1, rows: [], }, ); const workspaceId = await ensureDefaultWorkspaceForUser(user); assert.equal(workspaceId, 43); assert.equal(calls.length, 5); assert.match(calls[0].text, /FROM workspace_members/); assert.match(calls[1].text, /workspaces\.workspace_type = 'standard'/); assert.match(calls[3].text, /INSERT INTO workspaces/); }); test('createWorkspace inserts owner membership and returns the created workspace', async () => { const { calls } = mockDb( { rowCount: 1, rows: [] }, { rowCount: 1, rows: [] }, { rowCount: 1, rows: [ { id: 9, name: 'My Rescue', workspace_type: 'rescue', billing_email: 'billing@example.com', billing_plan: 'rescue_free', billing_interval: 'monthly', created_at: '2026-04-14T00:00:00.000Z', updated_at: '2026-04-14T00:00:00.000Z', }, ], }, ); const workspace = await createWorkspace({ id: 9, name: 'My Rescue', workspaceType: 'rescue', billingEmail: 'billing@example.com', billingPlan: 'rescue_free', billingInterval: 'monthly', owner: user, }); assert.equal(workspace?.id, 9); assert.equal(calls.length, 3); assert.match(calls[0].text, /INSERT INTO workspaces/); assert.match(calls[1].text, /INSERT INTO workspace_members/); assert.match(calls[2].text, /SELECT id, name, workspace_type/); }); test('updateWorkspace converts an existing household flock to rescue without inserting a new flock', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 42, name: 'Converted Rescue', workspace_type: 'rescue', billing_email: 'billing@example.com', billing_plan: 'rescue_free', billing_interval: 'monthly', subscription_status: 'active', rescue_verification_status: 'pending', created_at: '2026-04-14T00:00:00.000Z', updated_at: '2026-04-15T00:00:00.000Z', }, ], }); const workspace = await updateWorkspace({ workspaceId: 42, name: 'Converted Rescue', workspaceType: 'rescue', billingEmail: 'billing@example.com', billingPlan: 'rescue_free', billingInterval: 'monthly', }); assert.equal(workspace?.id, 42); assert.equal(workspace?.workspace_type, 'rescue'); assert.equal(calls.length, 1); assert.match(calls[0].text, /UPDATE workspaces/); assert.doesNotMatch(calls[0].text, /INSERT INTO workspaces/); assert.deepEqual(calls[0].params, [42, 'Converted Rescue', 'rescue', 'billing@example.com', 'rescue_free', 'monthly']); }); test('deleteWorkspaceIfEmpty blocks deletion when birds are still assigned', async () => { const { calls } = mockDb( { rowCount: 1, rows: [{ count: '2' }], }, ); const result = await deleteWorkspaceIfEmpty(42); assert.deepEqual(result, { deleted: false, reason: 'birds_present' }); assert.equal(calls.length, 1); assert.match(calls[0].text, /FROM birds/); }); test('deleteWorkspaceIfEmpty deletes an empty workspace', async () => { const { calls } = mockDb( { rowCount: 1, rows: [{ count: '0' }], }, { rowCount: 1, rows: [{ id: 42 }], }, ); const result = await deleteWorkspaceIfEmpty(42); assert.deepEqual(result, { deleted: true }); assert.equal(calls.length, 2); assert.match(calls[1].text, /DELETE FROM workspaces/); }); test('findAlternateWorkspaceForUser returns another workspace when available', async () => { const { calls } = mockDb({ rowCount: 1, rows: [{ workspace_id: 84 }], }); const workspaceId = await findAlternateWorkspaceForUser('user-1', 42); assert.equal(workspaceId, 84); assert.deepEqual(calls[0].params, ['user-1', 42]); }); test('listOwnedWorkspacesByOwnerEmail resolves accepted owner flocks by email', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 84, name: 'Receiving Flock', workspace_type: 'standard', billing_email: 'receiver@example.com', billing_plan: 'household_basic', subscription_status: 'active', rescue_verification_status: 'not_required', created_at: '2026-04-14T00:00:00.000Z', updated_at: '2026-04-14T00:00:00.000Z', }, ], }); const workspaces = await listOwnedWorkspacesByOwnerEmail('Receiver@Example.com', 42); assert.equal(workspaces[0]?.id, 84); assert.deepEqual(calls[0].params, ['Receiver@Example.com', 42]); assert.match(calls[0].text, /workspace_members\.role = 'owner'/); assert.match(calls[0].text, /accepted_at IS NOT NULL/); assert.match(calls[0].text, /workspaces\.id <> \$2/); }); test('updateWorkspaceMemberRole changes a non-owner member role', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 'member-1', workspace_id: 42, user_id: 'user-2', invite_email: 'helper@example.com', name: 'Helper', role: 'viewer', accepted_at: '2026-04-14T00:00:00.000Z', created_at: '2026-04-14T00:00:00.000Z', }, ], }); const member = await updateWorkspaceMemberRole({ memberId: 'member-1', workspaceId: 42, role: 'viewer', requesterMemberId: 'owner-member', requesterIsBillingOwner: false, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member?.role, 'viewer'); assert.deepEqual(calls[0].params, ['member-1', 42, 'viewer', false, 'owner-member', 'billing@example.com', 'owner']); assert.match(calls[0].text, /UPDATE workspace_members/); assert.match(calls[0].text, /role <> 'owner'/); }); test('updateWorkspaceMemberRole returns null when no non-owner member matches', async () => { mockDb({ rowCount: 0, rows: [], }); const member = await updateWorkspaceMemberRole({ memberId: 'owner-member', workspaceId: 42, role: 'viewer', requesterMemberId: 'owner-member', requesterIsBillingOwner: false, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member, null); }); test('updateWorkspaceMemberRole lets the billing owner change another owner role', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 'other-owner', workspace_id: 42, user_id: 'user-2', invite_email: 'other@example.com', name: 'Other Owner', role: 'assistant', accepted_at: '2026-04-14T00:00:00.000Z', created_at: '2026-04-14T00:00:00.000Z', }, ], }); const member = await updateWorkspaceMemberRole({ memberId: 'other-owner', workspaceId: 42, role: 'assistant', requesterMemberId: 'billing-owner', requesterIsBillingOwner: true, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member?.role, 'assistant'); assert.deepEqual(calls[0].params, ['other-owner', 42, 'assistant', true, 'billing-owner', 'billing@example.com', 'owner']); assert.match(calls[0].text, /id <> \$5/); }); test('updateWorkspaceMemberRole does not let the billing owner change their own owner role', async () => { mockDb({ rowCount: 0, rows: [], }); const member = await updateWorkspaceMemberRole({ memberId: 'billing-owner', workspaceId: 42, role: 'assistant', requesterMemberId: 'billing-owner', requesterIsBillingOwner: true, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member, null); }); test('updateWorkspaceMemberRole lets a non-billing owner change another non-billing owner role', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 'other-owner', workspace_id: 42, user_id: 'user-2', invite_email: 'other@example.com', name: 'Other Owner', role: 'assistant', accepted_at: '2026-04-14T00:00:00.000Z', created_at: '2026-04-14T00:00:00.000Z', }, ], }); const member = await updateWorkspaceMemberRole({ memberId: 'other-owner', workspaceId: 42, role: 'assistant', requesterMemberId: 'non-billing-owner', requesterIsBillingOwner: false, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member?.role, 'assistant'); assert.deepEqual(calls[0].params, ['other-owner', 42, 'assistant', false, 'non-billing-owner', 'billing@example.com', 'owner']); assert.match(calls[0].text, /LOWER\(BTRIM\(COALESCE\(invite_email, email\)\)\) <> LOWER\(BTRIM\(\$6\)\)/); }); test('updateWorkspaceMemberRole does not let a non-billing owner change the billing owner role', async () => { mockDb({ rowCount: 0, rows: [], }); const member = await updateWorkspaceMemberRole({ memberId: 'billing-owner', workspaceId: 42, role: 'assistant', requesterMemberId: 'non-billing-owner', requesterIsBillingOwner: false, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member, null); }); test('updateWorkspaceMemberRole lets the billing owner promote a non-owner to owner', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { id: 'member-1', workspace_id: 42, user_id: 'user-2', invite_email: 'helper@example.com', name: 'Helper', role: 'owner', accepted_at: '2026-04-14T00:00:00.000Z', created_at: '2026-04-14T00:00:00.000Z', }, ], }); const member = await updateWorkspaceMemberRole({ memberId: 'member-1', workspaceId: 42, role: 'owner', requesterMemberId: 'billing-owner', requesterIsBillingOwner: true, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member?.role, 'owner'); assert.deepEqual(calls[0].params, ['member-1', 42, 'owner', true, 'billing-owner', 'billing@example.com', 'owner']); assert.match(calls[0].text, /\$3 <> 'owner'/); }); test('updateWorkspaceMemberRole does not let a non-billing owner promote a member to owner', async () => { mockDb({ rowCount: 0, rows: [], }); const member = await updateWorkspaceMemberRole({ memberId: 'member-1', workspaceId: 42, role: 'owner', requesterMemberId: 'non-billing-owner', requesterIsBillingOwner: false, requesterRole: 'owner', billingEmail: 'billing@example.com', }); assert.equal(member, null); }); test('deleteWorkspaceMember removes non-owner members without billing owner access', async () => { const { calls } = mockDb({ rowCount: 1, rows: [{ id: 'member-1' }], }); const deleted = await deleteWorkspaceMember({ memberId: 'member-1', workspaceId: 42, requesterMemberId: 'owner-member', requesterIsBillingOwner: false, }); assert.equal(deleted, true); assert.deepEqual(calls[0].params, ['member-1', 42, false, 'owner-member']); assert.match(calls[0].text, /role <> 'owner'/); }); test('deleteWorkspaceMember lets the billing owner remove another owner', async () => { const { calls } = mockDb({ rowCount: 1, rows: [{ id: 'other-owner' }], }); const deleted = await deleteWorkspaceMember({ memberId: 'other-owner', workspaceId: 42, requesterMemberId: 'billing-owner', requesterIsBillingOwner: true, }); assert.equal(deleted, true); assert.deepEqual(calls[0].params, ['other-owner', 42, true, 'billing-owner']); assert.match(calls[0].text, /id <> \$4/); }); test('deleteWorkspaceMember does not let the billing owner remove their own owner membership', async () => { mockDb({ rowCount: 0, rows: [], }); const deleted = await deleteWorkspaceMember({ memberId: 'billing-owner', workspaceId: 42, requesterMemberId: 'billing-owner', requesterIsBillingOwner: true, }); assert.equal(deleted, false); }); test('getPlatformAdminSummary counts memorialized birds separately', async () => { const { calls } = mockDb({ rowCount: 1, rows: [ { total_birds: 8, memorialized_birds: 3, total_users: 4, total_workspaces: 2, rescue_workspaces: 1, rescue_birds: 5, pending_rescues: 1, daily_users: 2, }, ], }); const summary = await getPlatformAdminSummary(); assert.equal(summary?.memorialized_birds, 3); assert.equal(calls.length, 1); assert.match(calls[0].text, /memorialized_birds/); assert.match(calls[0].text, /memorialized_at IS NOT NULL/); assert.match(calls[0].text, /rescue_birds/); assert.match(calls[0].text, /workspaces\.workspace_type = 'rescue'/); });