File size: 2,673 Bytes
34c79d1
 
6dd9bad
 
 
 
 
 
 
 
 
 
 
 
 
 
34c79d1
6dd9bad
 
34c79d1
 
6dd9bad
34c79d1
6dd9bad
34c79d1
 
 
 
 
 
6dd9bad
34c79d1
 
 
6dd9bad
34c79d1
 
 
 
6dd9bad
 
34c79d1
6dd9bad
 
 
34c79d1
 
6dd9bad
 
34c79d1
6dd9bad
 
 
 
 
 
34c79d1
6dd9bad
 
 
 
 
34c79d1
6dd9bad
 
34c79d1
6dd9bad
 
34c79d1
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock infrastructure before importing aiService
vi.mock('../src/services/queue', () => ({
    redis: { get: vi.fn(), set: vi.fn() },
    whatsappQueue: { add: vi.fn() },
    notificationQueue: { add: vi.fn() },
    scheduleEmail: vi.fn(),
    scheduleBroadcast: vi.fn(),
    scheduleCampaign: vi.fn(),
}));

vi.mock('../src/services/prisma', () => ({
    prisma: { organization: { findUnique: vi.fn() } }
}));

vi.mock('../src/services/organization', async () => {
    const actual = await vi.importActual('../src/services/organization') as object;
    return { ...actual, getTenantSecrets: vi.fn(), getOrganizationId: vi.fn() };
});

import { aiService } from '../src/services/ai';
import { getTenantSecrets } from '../src/services/organization';
import { ProviderCapability } from '@repo/ai-sdk';

describe('AIService - Multi-Tenant Isolation', () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });

    it('calls getTenantSecrets with the org ID and builds a tenant registry', async () => {
        const orgId = 'org-premium-123';
        const customKey = 'sk-custom-org-key';

        (getTenantSecrets as ReturnType<typeof vi.fn>).mockResolvedValue({
            openAiApiKey: customKey,
            googleAiApiKey: null
        });

        const providers = await (aiService as any).getProvidersForTenant(ProviderCapability.TEXT, orgId);

        expect(getTenantSecrets).toHaveBeenCalledWith(orgId);
        // Tenant provider should be registered and sorted first (priority 500)
        const tenantProvider = providers.find((p: any) => p.name === 'OPENAI_TENANT');
        expect(tenantProvider).toBeDefined();
    });

    it('returns global providers when org has no custom keys', async () => {
        const orgId = 'org-free-456';

        (getTenantSecrets as ReturnType<typeof vi.fn>).mockResolvedValue({
            openAiApiKey: null,
            googleAiApiKey: null
        });

        const providers = await (aiService as any).getProvidersForTenant(ProviderCapability.TEXT, orgId);

        expect(getTenantSecrets).toHaveBeenCalledWith(orgId);
        // No tenant-specific providers — all names are global
        const tenantProvider = providers.find((p: any) => p.name.endsWith('_TENANT'));
        expect(tenantProvider).toBeUndefined();
    });

    it('skips getTenantSecrets when no organizationId is provided', async () => {
        const providers = await (aiService as any).getProvidersForTenant(ProviderCapability.TEXT, undefined);

        expect(getTenantSecrets).not.toHaveBeenCalled();
        expect(Array.isArray(providers)).toBe(true);
    });
});