Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
313 lines
8.1 KiB
TypeScript
313 lines
8.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ConsentAPI } from './ConsentAPI';
|
|
import type { ConsentConfig, ConsentState } from '../types';
|
|
|
|
describe('ConsentAPI', () => {
|
|
let api: ConsentAPI;
|
|
const mockConfig: ConsentConfig = {
|
|
apiEndpoint: 'https://api.example.com/',
|
|
siteId: 'test-site',
|
|
debug: false,
|
|
};
|
|
|
|
const mockConsent: ConsentState = {
|
|
categories: {
|
|
essential: true,
|
|
functional: true,
|
|
analytics: false,
|
|
marketing: false,
|
|
social: false,
|
|
},
|
|
vendors: {},
|
|
timestamp: '2024-01-15T10:00:00.000Z',
|
|
version: '1.0.0',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
api = new ConsentAPI(mockConfig);
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should strip trailing slash from apiEndpoint', () => {
|
|
expect(api).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('saveConsent', () => {
|
|
it('should POST consent to the API', async () => {
|
|
const mockResponse = {
|
|
consentId: 'consent-123',
|
|
timestamp: '2024-01-15T10:00:00.000Z',
|
|
expiresAt: '2025-01-15T10:00:00.000Z',
|
|
};
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(mockResponse),
|
|
} as Response);
|
|
|
|
const result = await api.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/consent',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
}),
|
|
credentials: 'include',
|
|
})
|
|
);
|
|
|
|
expect(result.consentId).toBe('consent-123');
|
|
});
|
|
|
|
it('should include metadata in the request', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({ consentId: '123' }),
|
|
} as Response);
|
|
|
|
await api.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
});
|
|
|
|
const call = vi.mocked(fetch).mock.calls[0];
|
|
const body = JSON.parse(call[1]?.body as string);
|
|
|
|
expect(body.metadata).toBeDefined();
|
|
expect(body.metadata.platform).toBe('web');
|
|
});
|
|
|
|
it('should throw on non-ok response', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
} as Response);
|
|
|
|
await expect(
|
|
api.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
})
|
|
).rejects.toThrow('Failed to save consent: 500');
|
|
});
|
|
|
|
it('should include signature headers', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({ consentId: '123' }),
|
|
} as Response);
|
|
|
|
await api.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
});
|
|
|
|
const call = vi.mocked(fetch).mock.calls[0];
|
|
const headers = call[1]?.headers as Record<string, string>;
|
|
|
|
expect(headers['X-Consent-Timestamp']).toBeDefined();
|
|
expect(headers['X-Consent-Signature']).toMatch(/^sha256=/);
|
|
});
|
|
});
|
|
|
|
describe('getConsent', () => {
|
|
it('should GET consent from the API', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({ consent: mockConsent }),
|
|
} as Response);
|
|
|
|
const result = await api.getConsent('test-site', 'fp_123');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/consent?siteId=test-site&deviceFingerprint=fp_123'),
|
|
expect.objectContaining({
|
|
headers: expect.any(Object),
|
|
credentials: 'include',
|
|
})
|
|
);
|
|
|
|
expect(result?.categories.essential).toBe(true);
|
|
});
|
|
|
|
it('should return null on 404', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
} as Response);
|
|
|
|
const result = await api.getConsent('test-site', 'fp_123');
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should throw on other errors', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
} as Response);
|
|
|
|
await expect(api.getConsent('test-site', 'fp_123')).rejects.toThrow(
|
|
'Failed to get consent: 500'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('revokeConsent', () => {
|
|
it('should DELETE consent from the API', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 204,
|
|
} as Response);
|
|
|
|
await api.revokeConsent('consent-123');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/consent/consent-123',
|
|
expect.objectContaining({
|
|
method: 'DELETE',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should throw on non-ok response', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
} as Response);
|
|
|
|
await expect(api.revokeConsent('consent-123')).rejects.toThrow(
|
|
'Failed to revoke consent: 404'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getSiteConfig', () => {
|
|
it('should GET site configuration', async () => {
|
|
const mockSiteConfig = {
|
|
siteId: 'test-site',
|
|
name: 'Test Site',
|
|
categories: ['essential', 'analytics'],
|
|
};
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(mockSiteConfig),
|
|
} as Response);
|
|
|
|
const result = await api.getSiteConfig('test-site');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
'https://api.example.com/config/test-site',
|
|
expect.any(Object)
|
|
);
|
|
|
|
expect(result.siteId).toBe('test-site');
|
|
});
|
|
|
|
it('should throw on error', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 404,
|
|
} as Response);
|
|
|
|
await expect(api.getSiteConfig('unknown-site')).rejects.toThrow(
|
|
'Failed to get site config: 404'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('exportConsent', () => {
|
|
it('should GET consent export for user', async () => {
|
|
const mockExport = {
|
|
userId: 'user-123',
|
|
consents: [mockConsent],
|
|
};
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(mockExport),
|
|
} as Response);
|
|
|
|
const result = await api.exportConsent('user-123');
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/consent/export?userId=user-123'),
|
|
expect.any(Object)
|
|
);
|
|
|
|
expect(result).toEqual(mockExport);
|
|
});
|
|
|
|
it('should throw on error', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 403,
|
|
} as Response);
|
|
|
|
await expect(api.exportConsent('user-123')).rejects.toThrow(
|
|
'Failed to export consent: 403'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('network errors', () => {
|
|
it('should propagate fetch errors', async () => {
|
|
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
await expect(
|
|
api.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
})
|
|
).rejects.toThrow('Network error');
|
|
});
|
|
});
|
|
|
|
describe('debug mode', () => {
|
|
it('should log when debug is enabled', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
const debugApi = new ConsentAPI({
|
|
...mockConfig,
|
|
debug: true,
|
|
});
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({ consentId: '123' }),
|
|
} as Response);
|
|
|
|
await debugApi.saveConsent({
|
|
siteId: 'test-site',
|
|
deviceFingerprint: 'fp_123',
|
|
consent: mockConsent,
|
|
});
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|