Packages added: - @bytelyst/referral-client — referral API client + share helpers - @bytelyst/subscription-client — subscription/plan API client + cache - @bytelyst/celebrations — milestone triggers, confetti, positive messages - @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases - @bytelyst/accessibility — VoiceOver/TalkBack label generators - @bytelyst/quick-actions — progressive disclosure, smart defaults - @bytelyst/time-references — familiar duration references - @bytelyst/org-client — org/workspace/membership/license API client - @bytelyst/marketplace-client — listing/review/install API client All packages: pure TS, ESM, globalThis.fetch, no Node.js deps. 99 Vitest tests across 9 packages, 79/79 public methods covered. Review fixes applied: - time-references: fix module-level mutable state leak + add clearCustomReferences() - accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords - subscription-client: fix flaky daysRemaining test (ms boundary race)
290 lines
7.9 KiB
TypeScript
290 lines
7.9 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { createOrgClient } from './client.js';
|
|
import type { OrganizationDoc, WorkspaceDoc, MembershipDoc, LicenseDoc } from './types.js';
|
|
|
|
const baseConfig = {
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAccessToken: () => 'admin-token',
|
|
};
|
|
|
|
function mockOrg(overrides?: Partial<OrganizationDoc>): OrganizationDoc {
|
|
return {
|
|
id: 'org_1',
|
|
productId: 'testapp',
|
|
name: 'Test Org',
|
|
slug: 'test-org',
|
|
status: 'active',
|
|
ownerUserId: 'user-1',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockWorkspace(overrides?: Partial<WorkspaceDoc>): WorkspaceDoc {
|
|
return {
|
|
id: 'ws_1',
|
|
orgId: 'org_1',
|
|
productId: 'testapp',
|
|
name: 'Engineering',
|
|
slug: 'engineering',
|
|
status: 'active',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockMembership(overrides?: Partial<MembershipDoc>): MembershipDoc {
|
|
return {
|
|
id: 'mbr_org1_user1_org',
|
|
orgId: 'org_1',
|
|
productId: 'testapp',
|
|
scope: 'org',
|
|
userId: 'user-1',
|
|
role: 'admin',
|
|
status: 'active',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockLicense(overrides?: Partial<LicenseDoc>): LicenseDoc {
|
|
return {
|
|
id: 'lic_1',
|
|
productId: 'testapp',
|
|
key: 'LYSNR-XXXX-YYYY-ZZZZ',
|
|
userId: 'user-1',
|
|
plan: 'pro',
|
|
status: 'active',
|
|
activatedAt: '2026-01-01T00:00:00Z',
|
|
expiresAt: null,
|
|
deviceIds: ['device-1'],
|
|
maxDevices: 3,
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('createOrgClient', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should list orgs', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([mockOrg()]),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.listOrgs();
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('Test Org');
|
|
});
|
|
|
|
it('should create an org', async () => {
|
|
const org = mockOrg();
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(org),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.createOrg({ name: 'Test Org', slug: 'test-org' });
|
|
expect(result.id).toBe('org_1');
|
|
});
|
|
|
|
it('should get an org by id', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockOrg()),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.getOrg('org_1');
|
|
expect(result.slug).toBe('test-org');
|
|
});
|
|
|
|
it('should list workspaces for an org', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([mockWorkspace()]),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.listWorkspaces('org_1');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('Engineering');
|
|
});
|
|
|
|
it('should create a workspace', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockWorkspace()),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.createWorkspace('org_1', {
|
|
name: 'Engineering',
|
|
slug: 'engineering',
|
|
});
|
|
expect(result.orgId).toBe('org_1');
|
|
});
|
|
|
|
it('should list memberships', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([mockMembership()]),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.listMemberships('org_1');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].role).toBe('admin');
|
|
});
|
|
|
|
it('should add a member', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockMembership()),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.addMember('org_1', { userId: 'user-1', role: 'admin' });
|
|
expect(result.userId).toBe('user-1');
|
|
});
|
|
|
|
it('should generate a license', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockLicense()),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.generateLicense({ userId: 'user-1', plan: 'pro' });
|
|
expect(result.key).toContain('LYSNR');
|
|
});
|
|
|
|
it('should activate a license', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockLicense()),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.activateLicense({ key: 'LYSNR-XXXX', deviceId: 'device-1' });
|
|
expect(result.status).toBe('active');
|
|
});
|
|
|
|
it('should update an org', async () => {
|
|
const org = mockOrg({ name: 'Updated Org' });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(org),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.updateOrg('org_1', { name: 'Updated Org' });
|
|
expect(result.name).toBe('Updated Org');
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/orgs/org_1',
|
|
expect.objectContaining({ method: 'PATCH' })
|
|
);
|
|
});
|
|
|
|
it('should update a workspace', async () => {
|
|
const ws = mockWorkspace({ name: 'Renamed' });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(ws),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
const result = await client.updateWorkspace('org_1', 'ws_1', { name: 'Renamed' });
|
|
expect(result.name).toBe('Renamed');
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/orgs/org_1/workspaces/ws_1',
|
|
expect.objectContaining({ method: 'PATCH' })
|
|
);
|
|
});
|
|
|
|
it('should deactivate a license', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
await expect(
|
|
client.deactivateLicense({ key: 'LYSNR-XXXX', deviceId: 'device-1' })
|
|
).resolves.toBeUndefined();
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/licenses/deactivate',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
});
|
|
|
|
it('should throw 403 for non-admin', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 403,
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
await expect(client.listOrgs()).rejects.toThrow('listOrgs failed: 403');
|
|
});
|
|
|
|
it('should send correct headers', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([]),
|
|
})
|
|
);
|
|
const client = createOrgClient(baseConfig);
|
|
await client.listOrgs();
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
const callHeaders = fetchMock.mock.calls[0][1].headers as Record<string, string>;
|
|
expect(callHeaders['x-product-id']).toBe('testapp');
|
|
expect(callHeaders['Authorization']).toBe('Bearer admin-token');
|
|
expect(callHeaders['x-request-id']).toBeDefined();
|
|
});
|
|
});
|