Initial commit: breakpilot-compliance - Compliance SDK Platform
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>
This commit is contained in:
312
consent-sdk/src/core/ConsentAPI.test.ts
Normal file
312
consent-sdk/src/core/ConsentAPI.test.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user