@tantainnovative/ndpr-toolkit 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +10 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/nextjs.yml +104 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.lintstagedrc.js +4 -0
- package/.nvmrc +1 -0
- package/.versionrc +17 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +90 -0
- package/CNAME +1 -0
- package/CONTRIBUTING.md +87 -0
- package/README.md +84 -431
- package/RELEASE-NOTES-v1.0.0.md +140 -0
- package/RELEASE-NOTES-v1.0.1.md +69 -0
- package/SECURITY.md +21 -0
- package/commitlint.config.js +36 -0
- package/components.json +21 -0
- package/eslint.config.mjs +16 -0
- package/jest.config.js +31 -0
- package/jest.setup.js +15 -0
- package/next.config.js +15 -0
- package/next.config.ts +62 -0
- package/package.json +70 -52
- package/packages/ndpr-toolkit/README.md +467 -0
- package/packages/ndpr-toolkit/jest.config.js +23 -0
- package/packages/ndpr-toolkit/package-lock.json +8197 -0
- package/packages/ndpr-toolkit/package.json +71 -0
- package/packages/ndpr-toolkit/rollup.config.js +34 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
- package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
- package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
- package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
- package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
- package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
- package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
- package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
- package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +14 -1
- package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
- package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
- package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
- package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
- package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
- package/packages/ndpr-toolkit/src/types/index.ts +42 -0
- package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
- package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
- package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
- package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
- package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
- package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
- package/packages/ndpr-toolkit/tsconfig.json +23 -0
- package/postcss.config.mjs +5 -0
- package/public/NDPR TOOLKIT.svg +1 -0
- package/public/favicon/android-chrome-192x192.png +0 -0
- package/public/favicon/android-chrome-512x512.png +0 -0
- package/public/favicon/apple-touch-icon.png +0 -0
- package/public/favicon/favicon-16x16.png +0 -0
- package/public/favicon/favicon-32x32.png +0 -0
- package/public/favicon/site.webmanifest +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/ndpr-toolkit-logo.svg +108 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/example.test.ts +13 -0
- package/src/__tests__/requestService.test.ts +57 -0
- package/src/app/accessibility.css +70 -0
- package/src/app/docs/components/DocLayout.tsx +267 -0
- package/src/app/docs/components/breach-notification/page.tsx +797 -0
- package/src/app/docs/components/consent-management/page.tsx +576 -0
- package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
- package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
- package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
- package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
- package/src/app/docs/components/hooks/page.tsx +305 -0
- package/src/app/docs/components/page.tsx +84 -0
- package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
- package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
- package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
- package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
- package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
- package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
- package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
- package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
- package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
- package/src/app/docs/guides/managing-consent/page.tsx +738 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
- package/src/app/docs/guides/page.tsx +67 -0
- package/src/app/docs/layout.tsx +15 -0
- package/src/app/docs/metadata.ts +31 -0
- package/src/app/docs/page.tsx +572 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/ndpr-demos/breach/page.tsx +354 -0
- package/src/app/ndpr-demos/consent/page.tsx +366 -0
- package/src/app/ndpr-demos/dpia/page.tsx +495 -0
- package/src/app/ndpr-demos/dsr/page.tsx +280 -0
- package/src/app/ndpr-demos/page.tsx +73 -0
- package/src/app/ndpr-demos/policy/page.tsx +771 -0
- package/src/app/page.tsx +452 -0
- package/src/components/ErrorBoundary.tsx +90 -0
- package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
- package/src/components/consent/ConsentBanner.tsx +159 -0
- package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
- package/src/components/docs/DocLayout.tsx +289 -0
- package/src/components/docs/index.ts +2 -0
- package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
- package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
- package/src/components/privacy-policy/data.ts +98 -0
- package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
- package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
- package/src/components/privacy-policy/shared/FormField.tsx +79 -0
- package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
- package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
- package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
- package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
- package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
- package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
- package/src/components/ui/Badge.tsx +46 -0
- package/src/components/ui/Button.tsx +59 -0
- package/src/components/ui/Card.tsx +92 -0
- package/src/components/ui/Checkbox.tsx +57 -0
- package/src/components/ui/FormField.tsx +50 -0
- package/src/components/ui/Input.tsx +38 -0
- package/src/components/ui/Loading.tsx +201 -0
- package/src/components/ui/Select.tsx +42 -0
- package/src/components/ui/TextArea.tsx +38 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/hooks/useConsent.ts +64 -0
- package/src/hooks/useLoadingState.ts +85 -0
- package/src/lib/consentService.ts +137 -0
- package/src/lib/dpiaQuestions.ts +148 -0
- package/src/lib/requestService.ts +75 -0
- package/src/lib/sanitize.ts +108 -0
- package/src/lib/storage.ts +222 -0
- package/src/lib/utils.ts +6 -0
- package/src/types/html-to-docx.d.ts +30 -0
- package/src/types/index.ts +72 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +41 -0
- package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
- package/dist/components/breach/BreachReportForm.d.ts +0 -66
- package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
- package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
- package/dist/components/consent/ConsentBanner.d.ts +0 -79
- package/dist/components/consent/ConsentManager.d.ts +0 -73
- package/dist/components/consent/ConsentStorage.d.ts +0 -41
- package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
- package/dist/components/dpia/DPIAReport.d.ts +0 -40
- package/dist/components/dpia/StepIndicator.d.ts +0 -64
- package/dist/components/dsr/DSRDashboard.d.ts +0 -58
- package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
- package/dist/components/dsr/DSRTracker.d.ts +0 -56
- package/dist/components/policy/PolicyExporter.d.ts +0 -65
- package/dist/components/policy/PolicyGenerator.d.ts +0 -54
- package/dist/components/policy/PolicyPreview.d.ts +0 -71
- package/dist/hooks/useBreach.d.ts +0 -97
- package/dist/hooks/useConsent.d.ts +0 -63
- package/dist/hooks/useDPIA.d.ts +0 -92
- package/dist/hooks/useDSR.d.ts +0 -72
- package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
- package/dist/index.esm.js +0 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/setupTests.d.ts +0 -2
- package/dist/types/breach.d.ts +0 -239
- package/dist/types/consent.d.ts +0 -95
- package/dist/types/dpia.d.ts +0 -196
- package/dist/types/dsr.d.ts +0 -162
- package/dist/types/privacy.d.ts +0 -204
- package/dist/utils/breach.d.ts +0 -14
- package/dist/utils/consent.d.ts +0 -10
- package/dist/utils/dpia.d.ts +0 -12
- package/dist/utils/dsr.d.ts +0 -11
- package/dist/utils/privacy.d.ts +0 -12
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react';
|
|
2
|
+
import { useDSR } from '../../hooks/useDSR';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { DSRRequest, RequestType } from '../../types/dsr';
|
|
5
|
+
|
|
6
|
+
// Mock localStorage
|
|
7
|
+
const mockLocalStorage = (() => {
|
|
8
|
+
let store: Record<string, string> = {};
|
|
9
|
+
return {
|
|
10
|
+
getItem: jest.fn((key: string) => store[key] || null),
|
|
11
|
+
setItem: jest.fn((key: string, value: string) => {
|
|
12
|
+
store[key] = value.toString();
|
|
13
|
+
}),
|
|
14
|
+
removeItem: jest.fn((key: string) => {
|
|
15
|
+
delete store[key];
|
|
16
|
+
}),
|
|
17
|
+
clear: jest.fn(() => {
|
|
18
|
+
store = {};
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
Object.defineProperty(window, 'localStorage', {
|
|
24
|
+
value: mockLocalStorage,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('useDSR', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mockLocalStorage.clear();
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const mockRequestTypes: RequestType[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'access',
|
|
36
|
+
name: 'Access Request',
|
|
37
|
+
description: 'Request to access your personal data',
|
|
38
|
+
estimatedCompletionTime: 30,
|
|
39
|
+
requiresAdditionalInfo: false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'erasure',
|
|
43
|
+
name: 'Erasure Request',
|
|
44
|
+
description: 'Request to delete your personal data',
|
|
45
|
+
estimatedCompletionTime: 45,
|
|
46
|
+
requiresAdditionalInfo: false
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'rectification',
|
|
50
|
+
name: 'Rectification Request',
|
|
51
|
+
description: 'Request to correct your personal data',
|
|
52
|
+
estimatedCompletionTime: 15,
|
|
53
|
+
requiresAdditionalInfo: true
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
it('should initialize with empty requests array', () => {
|
|
58
|
+
const { result } = renderHook(() => useDSR({
|
|
59
|
+
requestTypes: mockRequestTypes,
|
|
60
|
+
storageKey: 'test-dsr',
|
|
61
|
+
useLocalStorage: true
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
expect(result.current.requests).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should submit a new DSR request', () => {
|
|
68
|
+
const { result } = renderHook(() => useDSR({
|
|
69
|
+
requestTypes: mockRequestTypes,
|
|
70
|
+
storageKey: 'test-dsr',
|
|
71
|
+
useLocalStorage: true
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
act(() => {
|
|
75
|
+
result.current.submitRequest({
|
|
76
|
+
type: 'access',
|
|
77
|
+
subject: {
|
|
78
|
+
name: 'John Doe',
|
|
79
|
+
email: 'john@example.com',
|
|
80
|
+
},
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
description: 'I want to access my data',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.current.requests.length).toBe(1);
|
|
87
|
+
expect(result.current.requests[0].type).toBe('access');
|
|
88
|
+
expect(result.current.requests[0].status).toBe('pending');
|
|
89
|
+
expect(result.current.requests[0].subject.name).toBe('John Doe');
|
|
90
|
+
expect(result.current.requests[0].id).toBeDefined();
|
|
91
|
+
expect(result.current.requests[0].createdAt).toBeDefined();
|
|
92
|
+
expect(result.current.requests[0].updatedAt).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should get a request by ID', () => {
|
|
96
|
+
const { result } = renderHook(() => useDSR({
|
|
97
|
+
requestTypes: mockRequestTypes,
|
|
98
|
+
storageKey: 'test-dsr',
|
|
99
|
+
useLocalStorage: true
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Initialize requestId with a default value
|
|
103
|
+
let requestId: string = '';
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
const request = result.current.submitRequest({
|
|
107
|
+
type: 'access',
|
|
108
|
+
subject: {
|
|
109
|
+
name: 'John Doe',
|
|
110
|
+
email: 'john@example.com',
|
|
111
|
+
},
|
|
112
|
+
createdAt: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
requestId = request.id;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const retrievedRequest = result.current.getRequest(requestId);
|
|
118
|
+
expect(retrievedRequest).toBeDefined();
|
|
119
|
+
expect(retrievedRequest?.id).toBe(requestId);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should update a request', () => {
|
|
123
|
+
const { result } = renderHook(() => useDSR({
|
|
124
|
+
requestTypes: mockRequestTypes,
|
|
125
|
+
storageKey: 'test-dsr',
|
|
126
|
+
useLocalStorage: true
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
let requestId = '';
|
|
130
|
+
|
|
131
|
+
act(() => {
|
|
132
|
+
const request = result.current.submitRequest({
|
|
133
|
+
type: 'access',
|
|
134
|
+
subject: {
|
|
135
|
+
name: 'John Doe',
|
|
136
|
+
email: 'john@example.com',
|
|
137
|
+
},
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
});
|
|
140
|
+
requestId = request.id;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.updateRequest(requestId!, {
|
|
145
|
+
status: 'inProgress',
|
|
146
|
+
internalNotes: [
|
|
147
|
+
{
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
author: 'Admin',
|
|
150
|
+
note: 'Working on this request',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const updatedRequest = result.current.getRequest(requestId!);
|
|
157
|
+
expect(updatedRequest?.status).toBe('inProgress');
|
|
158
|
+
expect(updatedRequest?.internalNotes?.length).toBe(1);
|
|
159
|
+
expect(updatedRequest?.internalNotes?.[0].author).toBe('Admin');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should clear all requests', () => {
|
|
163
|
+
const { result } = renderHook(() => useDSR({
|
|
164
|
+
requestTypes: mockRequestTypes,
|
|
165
|
+
storageKey: 'test-dsr',
|
|
166
|
+
useLocalStorage: true
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
act(() => {
|
|
170
|
+
result.current.submitRequest({
|
|
171
|
+
type: 'access',
|
|
172
|
+
subject: {
|
|
173
|
+
name: 'John Doe',
|
|
174
|
+
email: 'john@example.com',
|
|
175
|
+
},
|
|
176
|
+
createdAt: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.current.requests.length).toBe(1);
|
|
181
|
+
|
|
182
|
+
act(() => {
|
|
183
|
+
result.current.clearRequests();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.current.requests.length).toBe(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should filter requests by type', () => {
|
|
190
|
+
const { result } = renderHook(() => useDSR({
|
|
191
|
+
requestTypes: mockRequestTypes,
|
|
192
|
+
storageKey: 'test-dsr',
|
|
193
|
+
useLocalStorage: true
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
act(() => {
|
|
197
|
+
result.current.submitRequest({
|
|
198
|
+
type: 'access',
|
|
199
|
+
subject: {
|
|
200
|
+
name: 'John Doe',
|
|
201
|
+
email: 'john@example.com',
|
|
202
|
+
},
|
|
203
|
+
createdAt: Date.now(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
result.current.submitRequest({
|
|
207
|
+
type: 'erasure',
|
|
208
|
+
subject: {
|
|
209
|
+
name: 'Jane Smith',
|
|
210
|
+
email: 'jane@example.com',
|
|
211
|
+
},
|
|
212
|
+
createdAt: Date.now(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
result.current.submitRequest({
|
|
216
|
+
type: 'access',
|
|
217
|
+
subject: {
|
|
218
|
+
name: 'Bob Johnson',
|
|
219
|
+
email: 'bob@example.com',
|
|
220
|
+
},
|
|
221
|
+
createdAt: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const accessRequests = result.current.getRequestsByType('access');
|
|
226
|
+
expect(accessRequests.length).toBe(2);
|
|
227
|
+
expect(accessRequests[0].subject.name).toBe('John Doe');
|
|
228
|
+
expect(accessRequests[1].subject.name).toBe('Bob Johnson');
|
|
229
|
+
|
|
230
|
+
const erasureRequests = result.current.getRequestsByType('erasure');
|
|
231
|
+
expect(erasureRequests.length).toBe(1);
|
|
232
|
+
expect(erasureRequests[0].subject.name).toBe('Jane Smith');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should filter requests by status', () => {
|
|
236
|
+
const { result } = renderHook(() => useDSR({
|
|
237
|
+
requestTypes: mockRequestTypes,
|
|
238
|
+
storageKey: 'test-dsr',
|
|
239
|
+
useLocalStorage: true
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
let requestId: string = '';
|
|
243
|
+
|
|
244
|
+
act(() => {
|
|
245
|
+
result.current.submitRequest({
|
|
246
|
+
type: 'access',
|
|
247
|
+
subject: {
|
|
248
|
+
name: 'John Doe',
|
|
249
|
+
email: 'john@example.com',
|
|
250
|
+
},
|
|
251
|
+
createdAt: Date.now(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const request = result.current.submitRequest({
|
|
255
|
+
type: 'erasure',
|
|
256
|
+
subject: {
|
|
257
|
+
name: 'Jane Smith',
|
|
258
|
+
email: 'jane@example.com',
|
|
259
|
+
},
|
|
260
|
+
createdAt: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
requestId = request.id;
|
|
263
|
+
|
|
264
|
+
result.current.submitRequest({
|
|
265
|
+
type: 'rectification',
|
|
266
|
+
subject: {
|
|
267
|
+
name: 'Bob Johnson',
|
|
268
|
+
email: 'bob@example.com',
|
|
269
|
+
},
|
|
270
|
+
createdAt: Date.now(),
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
result.current.updateRequest(requestId!, {
|
|
276
|
+
status: 'completed',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const pendingRequests = result.current.getRequestsByStatus('pending');
|
|
281
|
+
expect(pendingRequests.length).toBe(2);
|
|
282
|
+
|
|
283
|
+
const completedRequests = result.current.getRequestsByStatus('completed');
|
|
284
|
+
expect(completedRequests.length).toBe(1);
|
|
285
|
+
expect(completedRequests[0].subject.name).toBe('Jane Smith');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should format a request', () => {
|
|
289
|
+
const { result } = renderHook(() => useDSR({
|
|
290
|
+
requestTypes: mockRequestTypes,
|
|
291
|
+
storageKey: 'test-dsr',
|
|
292
|
+
useLocalStorage: true
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
let request: DSRRequest | null = null;
|
|
296
|
+
|
|
297
|
+
act(() => {
|
|
298
|
+
request = result.current.submitRequest({
|
|
299
|
+
type: 'access',
|
|
300
|
+
subject: {
|
|
301
|
+
name: 'John Doe',
|
|
302
|
+
email: 'john@example.com',
|
|
303
|
+
},
|
|
304
|
+
createdAt: Date.now(),
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(request).not.toBeNull();
|
|
309
|
+
|
|
310
|
+
if (request) {
|
|
311
|
+
const formattedRequest = result.current.formatRequest(request);
|
|
312
|
+
expect(formattedRequest).toBeDefined();
|
|
313
|
+
expect(formattedRequest.requestType).toBe('access');
|
|
314
|
+
expect(formattedRequest.dataSubject.name).toBe('John Doe');
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should get request type by ID', () => {
|
|
319
|
+
const { result } = renderHook(() => useDSR({
|
|
320
|
+
requestTypes: mockRequestTypes,
|
|
321
|
+
storageKey: 'test-dsr',
|
|
322
|
+
useLocalStorage: true
|
|
323
|
+
}));
|
|
324
|
+
|
|
325
|
+
const requestType = result.current.getRequestType('access');
|
|
326
|
+
expect(requestType).toBeDefined();
|
|
327
|
+
expect(requestType?.name).toBe('Access Request');
|
|
328
|
+
expect(requestType?.estimatedCompletionTime).toBe(30);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { calculateBreachSeverity } from '../../utils/breach';
|
|
2
|
+
import { BreachReport, RiskAssessment } from '../../types/breach';
|
|
3
|
+
|
|
4
|
+
describe('calculateBreachSeverity', () => {
|
|
5
|
+
const breachReport: BreachReport = {
|
|
6
|
+
id: 'breach-123',
|
|
7
|
+
title: 'Database Breach',
|
|
8
|
+
description: 'Unauthorized access to customer database',
|
|
9
|
+
category: 'unauthorized_access',
|
|
10
|
+
discoveredAt: 1620000000000,
|
|
11
|
+
occurredAt: 1619900000000,
|
|
12
|
+
reportedAt: 1620010000000,
|
|
13
|
+
reporter: {
|
|
14
|
+
name: 'John Smith',
|
|
15
|
+
email: 'john@example.com',
|
|
16
|
+
department: 'IT Security',
|
|
17
|
+
phone: '1234567890'
|
|
18
|
+
},
|
|
19
|
+
affectedSystems: ['customer-db', 'payment-system'],
|
|
20
|
+
dataTypes: ['personal', 'financial'],
|
|
21
|
+
estimatedAffectedSubjects: 1000,
|
|
22
|
+
status: 'contained'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it('should calculate high severity for sensitive data and large number of affected subjects', () => {
|
|
26
|
+
const assessment: RiskAssessment = {
|
|
27
|
+
id: 'risk-123',
|
|
28
|
+
breachId: 'breach-123',
|
|
29
|
+
assessor: {
|
|
30
|
+
name: 'John Doe',
|
|
31
|
+
role: 'DPO',
|
|
32
|
+
email: 'dpo@example.com'
|
|
33
|
+
},
|
|
34
|
+
assessedAt: 1620000000000,
|
|
35
|
+
confidentialityImpact: 4,
|
|
36
|
+
integrityImpact: 3,
|
|
37
|
+
availabilityImpact: 3,
|
|
38
|
+
harmLikelihood: 4,
|
|
39
|
+
harmSeverity: 4,
|
|
40
|
+
overallRiskScore: 16,
|
|
41
|
+
riskLevel: 'high',
|
|
42
|
+
risksToRightsAndFreedoms: true,
|
|
43
|
+
highRisksToRightsAndFreedoms: true,
|
|
44
|
+
justification: 'High risk due to sensitive financial data'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const result = calculateBreachSeverity(breachReport, assessment);
|
|
48
|
+
|
|
49
|
+
expect(result.severityLevel).toBe('high');
|
|
50
|
+
expect(result.notificationRequired).toBe(true);
|
|
51
|
+
expect(result.urgentNotificationRequired).toBe(true);
|
|
52
|
+
expect(result.timeframeHours).toBe(72);
|
|
53
|
+
expect(result.justification).toBe('High risk due to sensitive financial data');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should calculate medium severity for non-sensitive data with medium impact', () => {
|
|
57
|
+
const assessment: RiskAssessment = {
|
|
58
|
+
id: 'risk-456',
|
|
59
|
+
breachId: 'breach-456',
|
|
60
|
+
assessor: {
|
|
61
|
+
name: 'Jane Smith',
|
|
62
|
+
role: 'Security Officer',
|
|
63
|
+
email: 'security@example.com'
|
|
64
|
+
},
|
|
65
|
+
assessedAt: 1620100000000,
|
|
66
|
+
confidentialityImpact: 3,
|
|
67
|
+
integrityImpact: 2,
|
|
68
|
+
availabilityImpact: 2,
|
|
69
|
+
harmLikelihood: 3,
|
|
70
|
+
harmSeverity: 3,
|
|
71
|
+
overallRiskScore: 9,
|
|
72
|
+
riskLevel: 'medium',
|
|
73
|
+
risksToRightsAndFreedoms: true,
|
|
74
|
+
highRisksToRightsAndFreedoms: false,
|
|
75
|
+
justification: 'Medium risk due to personal data exposure'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = calculateBreachSeverity(breachReport, assessment);
|
|
79
|
+
|
|
80
|
+
expect(result.severityLevel).toBe('medium');
|
|
81
|
+
expect(result.notificationRequired).toBe(true);
|
|
82
|
+
expect(result.urgentNotificationRequired).toBe(false);
|
|
83
|
+
expect(result.timeframeHours).toBe(72);
|
|
84
|
+
expect(result.justification).toBe('Medium risk due to personal data exposure');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should calculate low severity for contained breach with low impact', () => {
|
|
88
|
+
const assessment: RiskAssessment = {
|
|
89
|
+
id: 'risk-789',
|
|
90
|
+
breachId: 'breach-789',
|
|
91
|
+
assessor: {
|
|
92
|
+
name: 'Alex Johnson',
|
|
93
|
+
role: 'Compliance Manager',
|
|
94
|
+
email: 'compliance@example.com'
|
|
95
|
+
},
|
|
96
|
+
assessedAt: 1620200000000,
|
|
97
|
+
confidentialityImpact: 1,
|
|
98
|
+
integrityImpact: 1,
|
|
99
|
+
availabilityImpact: 2,
|
|
100
|
+
harmLikelihood: 2,
|
|
101
|
+
harmSeverity: 1,
|
|
102
|
+
overallRiskScore: 2,
|
|
103
|
+
riskLevel: 'low',
|
|
104
|
+
risksToRightsAndFreedoms: false,
|
|
105
|
+
highRisksToRightsAndFreedoms: false,
|
|
106
|
+
justification: 'Low risk due to minimal data exposure'
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = calculateBreachSeverity(breachReport, assessment);
|
|
110
|
+
|
|
111
|
+
expect(result.severityLevel).toBe('low');
|
|
112
|
+
expect(result.notificationRequired).toBe(false);
|
|
113
|
+
expect(result.urgentNotificationRequired).toBe(false);
|
|
114
|
+
expect(result.timeframeHours).toBe(72);
|
|
115
|
+
expect(result.justification).toBe('Low risk due to minimal data exposure');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should calculate severity based on breach report when no assessment is provided', () => {
|
|
119
|
+
// Create a breach report with sensitive data and large scale
|
|
120
|
+
const sensitiveBreachReport: BreachReport = {
|
|
121
|
+
...breachReport,
|
|
122
|
+
dataTypes: ['personal', 'financial', 'health'],
|
|
123
|
+
estimatedAffectedSubjects: 5000,
|
|
124
|
+
status: 'ongoing'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = calculateBreachSeverity(sensitiveBreachReport);
|
|
128
|
+
|
|
129
|
+
// With 3 severity factors (sensitive data, large scale, ongoing), it should be critical
|
|
130
|
+
expect(result.severityLevel).toBe('critical');
|
|
131
|
+
expect(result.notificationRequired).toBe(true);
|
|
132
|
+
expect(result.urgentNotificationRequired).toBe(true);
|
|
133
|
+
expect(result.timeframeHours).toBe(72);
|
|
134
|
+
expect(result.justification).toContain('Critical risk');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should always require notification for ongoing breaches', () => {
|
|
138
|
+
const ongoingBreachReport: BreachReport = {
|
|
139
|
+
...breachReport,
|
|
140
|
+
status: 'ongoing',
|
|
141
|
+
dataTypes: ['personal']
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const result = calculateBreachSeverity(ongoingBreachReport);
|
|
145
|
+
|
|
146
|
+
expect(result.notificationRequired).toBe(true);
|
|
147
|
+
expect(result.justification).toContain('Medium risk');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { validateConsent } from '../../utils/consent';
|
|
2
|
+
import { ConsentSettings } from '../../types/consent';
|
|
3
|
+
|
|
4
|
+
describe('validateConsent', () => {
|
|
5
|
+
it('should validate valid consent settings', () => {
|
|
6
|
+
const settings: ConsentSettings = {
|
|
7
|
+
consents: {
|
|
8
|
+
necessary: true,
|
|
9
|
+
analytics: false,
|
|
10
|
+
marketing: true
|
|
11
|
+
},
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
version: '1.0',
|
|
14
|
+
method: 'banner',
|
|
15
|
+
hasInteracted: true
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const result = validateConsent(settings);
|
|
19
|
+
expect(result.valid).toBe(true);
|
|
20
|
+
expect(result.errors).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should invalidate settings with missing consents', () => {
|
|
24
|
+
const settings: ConsentSettings = {
|
|
25
|
+
consents: {},
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
version: '1.0',
|
|
28
|
+
method: 'banner',
|
|
29
|
+
hasInteracted: true
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = validateConsent(settings);
|
|
33
|
+
expect(result.valid).toBe(false);
|
|
34
|
+
expect(result.errors).toContain('Consent settings must include at least one consent option');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should invalidate settings with missing timestamp', () => {
|
|
38
|
+
const settings = {
|
|
39
|
+
consents: { necessary: true },
|
|
40
|
+
version: '1.0',
|
|
41
|
+
method: 'banner',
|
|
42
|
+
hasInteracted: true
|
|
43
|
+
} as unknown as ConsentSettings;
|
|
44
|
+
|
|
45
|
+
const result = validateConsent(settings);
|
|
46
|
+
expect(result.valid).toBe(false);
|
|
47
|
+
expect(result.errors).toContain('Consent timestamp is required');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should invalidate settings with missing version', () => {
|
|
51
|
+
const settings = {
|
|
52
|
+
consents: { necessary: true },
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
method: 'banner',
|
|
55
|
+
hasInteracted: true
|
|
56
|
+
} as unknown as ConsentSettings;
|
|
57
|
+
|
|
58
|
+
const result = validateConsent(settings);
|
|
59
|
+
expect(result.valid).toBe(false);
|
|
60
|
+
expect(result.errors).toContain('Consent version is required');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should invalidate settings with missing method', () => {
|
|
64
|
+
const settings = {
|
|
65
|
+
consents: { necessary: true },
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
version: '1.0',
|
|
68
|
+
hasInteracted: true
|
|
69
|
+
} as unknown as ConsentSettings;
|
|
70
|
+
|
|
71
|
+
const result = validateConsent(settings);
|
|
72
|
+
expect(result.valid).toBe(false);
|
|
73
|
+
expect(result.errors).toContain('Consent collection method is required');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should invalidate settings with missing hasInteracted', () => {
|
|
77
|
+
const settings = {
|
|
78
|
+
consents: { necessary: true },
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
version: '1.0',
|
|
81
|
+
method: 'banner'
|
|
82
|
+
} as unknown as ConsentSettings;
|
|
83
|
+
|
|
84
|
+
const result = validateConsent(settings);
|
|
85
|
+
expect(result.valid).toBe(false);
|
|
86
|
+
expect(result.errors).toContain('User interaction status is required');
|
|
87
|
+
});
|
|
88
|
+
});
|