strapi-plugin-keycloak-realm-users 1.0.0
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/LICENSE +21 -0
- package/README.md +485 -0
- package/__tests__/constants.test.mjs +207 -0
- package/__tests__/mocks/strapi.mjs +182 -0
- package/__tests__/services/audit-log.test.mjs +283 -0
- package/__tests__/services/keycloak-client.test.mjs +651 -0
- package/__tests__/services/permission.test.mjs +374 -0
- package/__tests__/services/realm.test.mjs +415 -0
- package/__tests__/services/user.test.mjs +487 -0
- package/__tests__/utils/errors.test.mjs +109 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/RealmBadge.jsx +17 -0
- package/admin/src/constants.js +14 -0
- package/admin/src/hooks/useAuditLogs.js +142 -0
- package/admin/src/hooks/useKeycloakRoles.js +182 -0
- package/admin/src/hooks/useKeycloakUsers.js +477 -0
- package/admin/src/hooks/useRealmAdmins.js +249 -0
- package/admin/src/hooks/useRealms.js +269 -0
- package/admin/src/index.js +46 -0
- package/admin/src/pages/App.jsx +21 -0
- package/admin/src/pages/AuditPage/index.jsx +213 -0
- package/admin/src/pages/RealmsPage/RealmEditPage.jsx +791 -0
- package/admin/src/pages/RealmsPage/RealmListPage.jsx +231 -0
- package/admin/src/pages/RealmsPage/index.jsx +7 -0
- package/admin/src/pages/UsersPage/UserEditPage.jsx +313 -0
- package/admin/src/pages/UsersPage/UserListPage.jsx +437 -0
- package/admin/src/pages/UsersPage/index.jsx +7 -0
- package/admin/src/pluginId.js +2 -0
- package/admin/src/translations/en.json +77 -0
- package/admin/src/translations/fr.json +77 -0
- package/babel.config.cjs +17 -0
- package/coverage/clover.xml +422 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/bootstrap.js.html +346 -0
- package/coverage/lcov-report/src/config/index.html +116 -0
- package/coverage/lcov-report/src/config/index.js.html +106 -0
- package/coverage/lcov-report/src/constants.js.html +850 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.html +116 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/index.html +116 -0
- package/coverage/lcov-report/src/content-types/index.js.html +112 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.js.html +94 -0
- package/coverage/lcov-report/src/controllers/audit.js.html +517 -0
- package/coverage/lcov-report/src/controllers/index.html +161 -0
- package/coverage/lcov-report/src/controllers/index.js.html +112 -0
- package/coverage/lcov-report/src/controllers/realm.js.html +1057 -0
- package/coverage/lcov-report/src/controllers/user.js.html +1324 -0
- package/coverage/lcov-report/src/destroy.js.html +100 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/policies/can-access-realm.js.html +163 -0
- package/coverage/lcov-report/src/policies/index.html +146 -0
- package/coverage/lcov-report/src/policies/index.js.html +106 -0
- package/coverage/lcov-report/src/policies/is-authenticated.js.html +100 -0
- package/coverage/lcov-report/src/register.js.html +106 -0
- package/coverage/lcov-report/src/routes/admin.js.html +844 -0
- package/coverage/lcov-report/src/routes/index.html +131 -0
- package/coverage/lcov-report/src/routes/index.js.html +109 -0
- package/coverage/lcov-report/src/services/audit-log.js.html +673 -0
- package/coverage/lcov-report/src/services/index.html +176 -0
- package/coverage/lcov-report/src/services/index.js.html +124 -0
- package/coverage/lcov-report/src/services/keycloak-client.js.html +2359 -0
- package/coverage/lcov-report/src/services/permission.js.html +955 -0
- package/coverage/lcov-report/src/services/realm.js.html +1207 -0
- package/coverage/lcov-report/src/services/user.js.html +1924 -0
- package/coverage/lcov-report/src/utils/errors.js.html +274 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov-report/src/utils/index.js.html +103 -0
- package/coverage/lcov.info +804 -0
- package/dist/_chunks/App-BaKrvCeS.mjs +1975 -0
- package/dist/_chunks/App-DO6syS77.js +1975 -0
- package/dist/_chunks/en-Li-XBDe9.mjs +72 -0
- package/dist/_chunks/en-aCyfgNfr.js +72 -0
- package/dist/_chunks/fr-Cj33Q8jI.js +72 -0
- package/dist/_chunks/fr-vLrXph-Z.mjs +72 -0
- package/dist/_chunks/index-DwDO4-0C.js +69 -0
- package/dist/_chunks/index-jTVd7LdQ.mjs +70 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +3003 -0
- package/dist/server/index.mjs +3004 -0
- package/jest.config.cjs +50 -0
- package/package.json +55 -0
- package/server/src/bootstrap.js +87 -0
- package/server/src/config/index.js +7 -0
- package/server/src/constants.js +255 -0
- package/server/src/content-types/audit-log/index.js +3 -0
- package/server/src/content-types/audit-log/schema.json +61 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/realm-admin/index.js +3 -0
- package/server/src/content-types/realm-admin/schema.json +45 -0
- package/server/src/content-types/realm-config/index.js +3 -0
- package/server/src/content-types/realm-config/schema.json +56 -0
- package/server/src/controllers/audit.js +144 -0
- package/server/src/controllers/index.js +9 -0
- package/server/src/controllers/realm.js +324 -0
- package/server/src/controllers/user.js +413 -0
- package/server/src/destroy.js +5 -0
- package/server/src/index.js +21 -0
- package/server/src/policies/can-access-realm.js +26 -0
- package/server/src/policies/index.js +7 -0
- package/server/src/policies/is-authenticated.js +5 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/admin.js +253 -0
- package/server/src/routes/index.js +8 -0
- package/server/src/services/audit-log.js +196 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/keycloak-client.js +758 -0
- package/server/src/services/permission.js +290 -0
- package/server/src/services/realm.js +374 -0
- package/server/src/services/user.js +613 -0
- package/server/src/utils/errors.js +63 -0
- package/server/src/utils/index.js +6 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for plugin constants.
|
|
3
|
+
* Validates that constants have expected values and structures.
|
|
4
|
+
* @module __tests__/constants
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
PLUGIN_ID,
|
|
9
|
+
CONTENT_TYPES,
|
|
10
|
+
SERVICES,
|
|
11
|
+
AUDIT_ACTIONS,
|
|
12
|
+
ERROR_MESSAGES,
|
|
13
|
+
DEFAULT_REALM_PERMISSIONS,
|
|
14
|
+
STRAPI_SUPER_ADMIN_ROLE,
|
|
15
|
+
TOKEN_EXPIRY_BUFFER_MS,
|
|
16
|
+
EMAIL_ACTION_LIFESPAN_SECONDS,
|
|
17
|
+
DEFAULT_REALM_COLOR,
|
|
18
|
+
PAGINATION,
|
|
19
|
+
HTTP_STATUS,
|
|
20
|
+
HTTP_HEADERS,
|
|
21
|
+
KEYCLOAK_EMAIL_ACTIONS,
|
|
22
|
+
REALM_NAME_PATTERN,
|
|
23
|
+
UNKNOWN_USER_EMAIL,
|
|
24
|
+
} from '../server/src/constants.js';
|
|
25
|
+
|
|
26
|
+
describe('Plugin Constants', () => {
|
|
27
|
+
describe('PLUGIN_ID', () => {
|
|
28
|
+
it('should be the correct plugin identifier', () => {
|
|
29
|
+
expect(PLUGIN_ID).toBe('strapi-plugin-keycloak-realm-users');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('CONTENT_TYPES', () => {
|
|
34
|
+
it('should have all required content type UIDs', () => {
|
|
35
|
+
expect(CONTENT_TYPES.REALM_CONFIG).toBe(`plugin::${PLUGIN_ID}.realm-config`);
|
|
36
|
+
expect(CONTENT_TYPES.REALM_ADMIN).toBe(`plugin::${PLUGIN_ID}.realm-admin`);
|
|
37
|
+
expect(CONTENT_TYPES.AUDIT_LOG).toBe(`plugin::${PLUGIN_ID}.audit-log`);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('SERVICES', () => {
|
|
42
|
+
it('should have all service identifiers', () => {
|
|
43
|
+
expect(SERVICES.REALM).toBe('realm');
|
|
44
|
+
expect(SERVICES.KEYCLOAK_CLIENT).toBe('keycloak-client');
|
|
45
|
+
expect(SERVICES.USER).toBe('user');
|
|
46
|
+
expect(SERVICES.PERMISSION).toBe('permission');
|
|
47
|
+
expect(SERVICES.AUDIT_LOG).toBe('audit-log');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('AUDIT_ACTIONS', () => {
|
|
52
|
+
it('should have all audit action types', () => {
|
|
53
|
+
const expectedActions = [
|
|
54
|
+
'CREATE_USER',
|
|
55
|
+
'UPDATE_USER',
|
|
56
|
+
'DELETE_USER',
|
|
57
|
+
'RESET_PASSWORD',
|
|
58
|
+
'ASSIGN_ROLE',
|
|
59
|
+
'REMOVE_ROLE',
|
|
60
|
+
'ENABLE_USER',
|
|
61
|
+
'DISABLE_USER',
|
|
62
|
+
'SEND_VERIFY_EMAIL',
|
|
63
|
+
'SEND_RESET_PASSWORD_EMAIL',
|
|
64
|
+
'BULK_IMPORT',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
expectedActions.forEach((action) => {
|
|
68
|
+
expect(AUDIT_ACTIONS[action]).toBe(action);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('ERROR_MESSAGES', () => {
|
|
74
|
+
it('should have sanitized error messages', () => {
|
|
75
|
+
expect(ERROR_MESSAGES.REALM_NOT_FOUND).toBe('Realm configuration not found.');
|
|
76
|
+
expect(ERROR_MESSAGES.REALM_DISABLED).toBe('This realm is currently disabled.');
|
|
77
|
+
expect(ERROR_MESSAGES.USER_NOT_FOUND).toBe('Keycloak user not found.');
|
|
78
|
+
expect(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS).toBe('You do not have permission to perform this action.');
|
|
79
|
+
expect(ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED).toBe('Failed to connect to Keycloak server.');
|
|
80
|
+
expect(ERROR_MESSAGES.KEYCLOAK_AUTH_FAILED).toBe('Failed to authenticate with Keycloak.');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should not contain sensitive information patterns', () => {
|
|
84
|
+
Object.values(ERROR_MESSAGES).forEach((message) => {
|
|
85
|
+
// Should not contain IP addresses
|
|
86
|
+
expect(message).not.toMatch(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
|
|
87
|
+
// Should not contain stack traces
|
|
88
|
+
expect(message).not.toMatch(/at\s+\w+/);
|
|
89
|
+
// Should not contain file paths
|
|
90
|
+
expect(message).not.toMatch(/\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('DEFAULT_REALM_PERMISSIONS', () => {
|
|
96
|
+
it('should default to read-only access', () => {
|
|
97
|
+
expect(DEFAULT_REALM_PERMISSIONS.canRead).toBe(true);
|
|
98
|
+
expect(DEFAULT_REALM_PERMISSIONS.canCreate).toBe(false);
|
|
99
|
+
expect(DEFAULT_REALM_PERMISSIONS.canUpdate).toBe(false);
|
|
100
|
+
expect(DEFAULT_REALM_PERMISSIONS.canDelete).toBe(false);
|
|
101
|
+
expect(DEFAULT_REALM_PERMISSIONS.canManageRoles).toBe(false);
|
|
102
|
+
expect(DEFAULT_REALM_PERMISSIONS.canResetPassword).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should have all permission fields', () => {
|
|
106
|
+
const permissionKeys = Object.keys(DEFAULT_REALM_PERMISSIONS);
|
|
107
|
+
expect(permissionKeys).toContain('canRead');
|
|
108
|
+
expect(permissionKeys).toContain('canCreate');
|
|
109
|
+
expect(permissionKeys).toContain('canUpdate');
|
|
110
|
+
expect(permissionKeys).toContain('canDelete');
|
|
111
|
+
expect(permissionKeys).toContain('canManageRoles');
|
|
112
|
+
expect(permissionKeys).toContain('canResetPassword');
|
|
113
|
+
expect(permissionKeys).toHaveLength(6);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('STRAPI_SUPER_ADMIN_ROLE', () => {
|
|
118
|
+
it('should be the correct Strapi role code', () => {
|
|
119
|
+
expect(STRAPI_SUPER_ADMIN_ROLE).toBe('strapi-super-admin');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('TOKEN_EXPIRY_BUFFER_MS', () => {
|
|
124
|
+
it('should be 60 seconds in milliseconds', () => {
|
|
125
|
+
expect(TOKEN_EXPIRY_BUFFER_MS).toBe(60000);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('EMAIL_ACTION_LIFESPAN_SECONDS', () => {
|
|
130
|
+
it('should be 12 hours in seconds', () => {
|
|
131
|
+
expect(EMAIL_ACTION_LIFESPAN_SECONDS).toBe(43200);
|
|
132
|
+
expect(EMAIL_ACTION_LIFESPAN_SECONDS).toBe(12 * 60 * 60);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('DEFAULT_REALM_COLOR', () => {
|
|
137
|
+
it('should be a valid hex color', () => {
|
|
138
|
+
expect(DEFAULT_REALM_COLOR).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('PAGINATION', () => {
|
|
143
|
+
it('should have sensible default values', () => {
|
|
144
|
+
expect(PAGINATION.PAGE_SIZE).toBe(25);
|
|
145
|
+
expect(PAGINATION.AUDIT_LOG_LIMIT).toBe(50);
|
|
146
|
+
expect(PAGINATION.EXPORT_BATCH_SIZE).toBe(100);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should have positive integer values', () => {
|
|
150
|
+
Object.values(PAGINATION).forEach((value) => {
|
|
151
|
+
expect(Number.isInteger(value)).toBe(true);
|
|
152
|
+
expect(value).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('HTTP_STATUS', () => {
|
|
158
|
+
it('should have standard HTTP status codes', () => {
|
|
159
|
+
expect(HTTP_STATUS.OK).toBe(200);
|
|
160
|
+
expect(HTTP_STATUS.CREATED).toBe(201);
|
|
161
|
+
expect(HTTP_STATUS.NO_CONTENT).toBe(204);
|
|
162
|
+
expect(HTTP_STATUS.BAD_REQUEST).toBe(400);
|
|
163
|
+
expect(HTTP_STATUS.UNAUTHORIZED).toBe(401);
|
|
164
|
+
expect(HTTP_STATUS.FORBIDDEN).toBe(403);
|
|
165
|
+
expect(HTTP_STATUS.NOT_FOUND).toBe(404);
|
|
166
|
+
expect(HTTP_STATUS.CONFLICT).toBe(409);
|
|
167
|
+
expect(HTTP_STATUS.INTERNAL_ERROR).toBe(500);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('HTTP_HEADERS', () => {
|
|
172
|
+
it('should have correct content type values', () => {
|
|
173
|
+
expect(HTTP_HEADERS.CONTENT_TYPE_JSON).toBe('application/json');
|
|
174
|
+
expect(HTTP_HEADERS.CONTENT_TYPE_FORM).toBe('application/x-www-form-urlencoded');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('KEYCLOAK_EMAIL_ACTIONS', () => {
|
|
179
|
+
it('should have Keycloak required action types', () => {
|
|
180
|
+
expect(KEYCLOAK_EMAIL_ACTIONS.VERIFY_EMAIL).toBe('VERIFY_EMAIL');
|
|
181
|
+
expect(KEYCLOAK_EMAIL_ACTIONS.UPDATE_PASSWORD).toBe('UPDATE_PASSWORD');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('REALM_NAME_PATTERN', () => {
|
|
186
|
+
it('should match valid realm names', () => {
|
|
187
|
+
expect(REALM_NAME_PATTERN.test('my-realm')).toBe(true);
|
|
188
|
+
expect(REALM_NAME_PATTERN.test('realm123')).toBe(true);
|
|
189
|
+
expect(REALM_NAME_PATTERN.test('production-users-v2')).toBe(true);
|
|
190
|
+
expect(REALM_NAME_PATTERN.test('a')).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should reject invalid realm names', () => {
|
|
194
|
+
expect(REALM_NAME_PATTERN.test('My-Realm')).toBe(false); // uppercase
|
|
195
|
+
expect(REALM_NAME_PATTERN.test('my_realm')).toBe(false); // underscore
|
|
196
|
+
expect(REALM_NAME_PATTERN.test('my realm')).toBe(false); // space
|
|
197
|
+
expect(REALM_NAME_PATTERN.test('my.realm')).toBe(false); // dot
|
|
198
|
+
expect(REALM_NAME_PATTERN.test('')).toBe(false); // empty
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('UNKNOWN_USER_EMAIL', () => {
|
|
203
|
+
it('should be a placeholder value', () => {
|
|
204
|
+
expect(UNKNOWN_USER_EMAIL).toBe('unknown');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock Strapi instance for unit testing services.
|
|
3
|
+
* Provides configurable mocks for Strapi's document API, plugin system, and logging.
|
|
4
|
+
* @module __tests__/mocks/strapi
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a mock Strapi instance with configurable behavior.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} [overrides={}] - Override default mock implementations
|
|
11
|
+
* @returns {Object} Mock Strapi instance
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const strapi = createMockStrapi({
|
|
15
|
+
* documentResults: { findMany: [{ id: 1, name: 'test' }] }
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
export const createMockStrapi = (overrides = {}) => {
|
|
19
|
+
const documentMocks = {
|
|
20
|
+
findMany: jest.fn().mockResolvedValue(overrides.documentResults?.findMany ?? []),
|
|
21
|
+
findOne: jest.fn().mockResolvedValue(overrides.documentResults?.findOne ?? null),
|
|
22
|
+
create: jest.fn().mockResolvedValue(overrides.documentResults?.create ?? { documentId: 'new-doc-id' }),
|
|
23
|
+
update: jest.fn().mockResolvedValue(overrides.documentResults?.update ?? { documentId: 'updated-doc-id' }),
|
|
24
|
+
delete: jest.fn().mockResolvedValue(overrides.documentResults?.delete ?? { documentId: 'deleted-doc-id' }),
|
|
25
|
+
count: jest.fn().mockResolvedValue(overrides.documentResults?.count ?? 0),
|
|
26
|
+
...overrides.documentMocks,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const dbQueryMocks = {
|
|
30
|
+
findOne: jest.fn().mockResolvedValue(overrides.dbQueryResults?.findOne ?? null),
|
|
31
|
+
findMany: jest.fn().mockResolvedValue(overrides.dbQueryResults?.findMany ?? []),
|
|
32
|
+
...overrides.dbQueryMocks,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const pluginServiceMocks = overrides.pluginServices ?? {};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
documents: jest.fn().mockReturnValue(documentMocks),
|
|
39
|
+
db: {
|
|
40
|
+
query: jest.fn().mockReturnValue(dbQueryMocks),
|
|
41
|
+
},
|
|
42
|
+
plugin: jest.fn().mockReturnValue({
|
|
43
|
+
service: jest.fn((serviceName) => pluginServiceMocks[serviceName] ?? {}),
|
|
44
|
+
}),
|
|
45
|
+
log: {
|
|
46
|
+
error: jest.fn(),
|
|
47
|
+
warn: jest.fn(),
|
|
48
|
+
info: jest.fn(),
|
|
49
|
+
debug: jest.fn(),
|
|
50
|
+
},
|
|
51
|
+
...overrides.strapi,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a mock Strapi user for testing.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} [overrides={}] - Override default user properties
|
|
59
|
+
* @returns {Object} Mock Strapi user
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const superAdmin = createMockStrapiUser({ isSuperAdmin: true });
|
|
63
|
+
* const regularUser = createMockStrapiUser({ id: 2, email: 'user@test.com' });
|
|
64
|
+
*/
|
|
65
|
+
export const createMockStrapiUser = (overrides = {}) => {
|
|
66
|
+
const { isSuperAdmin = false, ...rest } = overrides;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: 1,
|
|
70
|
+
email: 'admin@test.com',
|
|
71
|
+
roles: isSuperAdmin
|
|
72
|
+
? [{ code: 'strapi-super-admin' }]
|
|
73
|
+
: [{ code: 'strapi-editor' }],
|
|
74
|
+
...rest,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a mock realm configuration for testing.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} [overrides={}] - Override default realm properties
|
|
82
|
+
* @returns {Object} Mock realm configuration
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* const realm = createMockRealmConfig({ name: 'production' });
|
|
86
|
+
*/
|
|
87
|
+
export const createMockRealmConfig = (overrides = {}) => ({
|
|
88
|
+
documentId: 'realm-doc-123',
|
|
89
|
+
name: 'test-realm',
|
|
90
|
+
displayName: 'Test Realm',
|
|
91
|
+
serverUrl: 'https://keycloak.example.com',
|
|
92
|
+
realmName: 'test',
|
|
93
|
+
clientId: 'strapi-admin',
|
|
94
|
+
clientSecret: 'secret-value',
|
|
95
|
+
enabled: true,
|
|
96
|
+
color: '#4945ff',
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
updatedAt: new Date().toISOString(),
|
|
99
|
+
...overrides,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a mock Keycloak user for testing.
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} [overrides={}] - Override default user properties
|
|
106
|
+
* @returns {Object} Mock Keycloak user
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const kcUser = createMockKeycloakUser({ username: 'john.doe' });
|
|
110
|
+
*/
|
|
111
|
+
export const createMockKeycloakUser = (overrides = {}) => ({
|
|
112
|
+
id: 'kc-user-123',
|
|
113
|
+
username: 'testuser',
|
|
114
|
+
email: 'testuser@example.com',
|
|
115
|
+
firstName: 'Test',
|
|
116
|
+
lastName: 'User',
|
|
117
|
+
enabled: true,
|
|
118
|
+
emailVerified: true,
|
|
119
|
+
createdTimestamp: Date.now(),
|
|
120
|
+
...overrides,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a mock Keycloak role for testing.
|
|
125
|
+
*
|
|
126
|
+
* @param {Object} [overrides={}] - Override default role properties
|
|
127
|
+
* @returns {Object} Mock Keycloak role
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const role = createMockKeycloakRole({ name: 'admin' });
|
|
131
|
+
*/
|
|
132
|
+
export const createMockKeycloakRole = (overrides = {}) => ({
|
|
133
|
+
id: 'role-123',
|
|
134
|
+
name: 'user',
|
|
135
|
+
description: 'Default user role',
|
|
136
|
+
composite: false,
|
|
137
|
+
clientRole: false,
|
|
138
|
+
containerId: 'test-realm',
|
|
139
|
+
...overrides,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates a mock realm admin assignment for testing.
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} [overrides={}] - Override default assignment properties
|
|
146
|
+
* @returns {Object} Mock realm admin assignment
|
|
147
|
+
*/
|
|
148
|
+
export const createMockRealmAdmin = (overrides = {}) => ({
|
|
149
|
+
documentId: 'admin-assignment-123',
|
|
150
|
+
strapiUserId: 1,
|
|
151
|
+
strapiUserEmail: 'admin@test.com',
|
|
152
|
+
realmConfig: createMockRealmConfig(),
|
|
153
|
+
permissions: {
|
|
154
|
+
canRead: true,
|
|
155
|
+
canCreate: false,
|
|
156
|
+
canUpdate: false,
|
|
157
|
+
canDelete: false,
|
|
158
|
+
canManageRoles: false,
|
|
159
|
+
canResetPassword: false,
|
|
160
|
+
},
|
|
161
|
+
...overrides,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Creates a mock audit log entry for testing.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} [overrides={}] - Override default entry properties
|
|
168
|
+
* @returns {Object} Mock audit log entry
|
|
169
|
+
*/
|
|
170
|
+
export const createMockAuditLogEntry = (overrides = {}) => ({
|
|
171
|
+
documentId: 'audit-123',
|
|
172
|
+
realmName: 'test-realm',
|
|
173
|
+
realmDisplayName: 'Test Realm',
|
|
174
|
+
action: 'CREATE_USER',
|
|
175
|
+
keycloakUserId: 'kc-user-123',
|
|
176
|
+
keycloakUsername: 'testuser',
|
|
177
|
+
details: null,
|
|
178
|
+
performedById: 1,
|
|
179
|
+
performedByEmail: 'admin@test.com',
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
...overrides,
|
|
182
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for the audit log service.
|
|
3
|
+
* @module __tests__/services/audit-log
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import auditLogService from '../../server/src/services/audit-log.js';
|
|
7
|
+
import {
|
|
8
|
+
createMockStrapi,
|
|
9
|
+
createMockStrapiUser,
|
|
10
|
+
createMockAuditLogEntry,
|
|
11
|
+
} from '../mocks/strapi.mjs';
|
|
12
|
+
import { CONTENT_TYPES, AUDIT_ACTIONS, PAGINATION, UNKNOWN_USER_EMAIL } from '../../server/src/constants.js';
|
|
13
|
+
|
|
14
|
+
describe('Audit Log Service', () => {
|
|
15
|
+
let strapi;
|
|
16
|
+
let service;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
strapi = createMockStrapi();
|
|
20
|
+
service = auditLogService({ strapi });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('log', () => {
|
|
24
|
+
it('should create an audit log entry with all fields', async () => {
|
|
25
|
+
const user = createMockStrapiUser();
|
|
26
|
+
const createMock = jest.fn().mockResolvedValue({ documentId: 'audit-123' });
|
|
27
|
+
strapi.documents.mockReturnValue({ create: createMock });
|
|
28
|
+
|
|
29
|
+
await service.log({
|
|
30
|
+
realmName: 'production',
|
|
31
|
+
realmDisplayName: 'Production Users',
|
|
32
|
+
action: AUDIT_ACTIONS.CREATE_USER,
|
|
33
|
+
keycloakUserId: 'kc-user-123',
|
|
34
|
+
keycloakUsername: 'john.doe',
|
|
35
|
+
details: { email: 'john@example.com' },
|
|
36
|
+
user,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(createMock).toHaveBeenCalledWith({
|
|
40
|
+
data: {
|
|
41
|
+
realmName: 'production',
|
|
42
|
+
realmDisplayName: 'Production Users',
|
|
43
|
+
action: AUDIT_ACTIONS.CREATE_USER,
|
|
44
|
+
keycloakUserId: 'kc-user-123',
|
|
45
|
+
keycloakUsername: 'john.doe',
|
|
46
|
+
details: { email: 'john@example.com' },
|
|
47
|
+
performedById: user.id,
|
|
48
|
+
performedByEmail: user.email,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
expect(strapi.documents).toHaveBeenCalledWith(CONTENT_TYPES.AUDIT_LOG);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should use realmName as displayName when not provided', async () => {
|
|
55
|
+
const createMock = jest.fn().mockResolvedValue({});
|
|
56
|
+
strapi.documents.mockReturnValue({ create: createMock });
|
|
57
|
+
|
|
58
|
+
await service.log({
|
|
59
|
+
realmName: 'test-realm',
|
|
60
|
+
action: AUDIT_ACTIONS.DELETE_USER,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const calledWith = createMock.mock.calls[0][0];
|
|
64
|
+
expect(calledWith.data.realmDisplayName).toBe('test-realm');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle null optional fields', async () => {
|
|
68
|
+
const createMock = jest.fn().mockResolvedValue({});
|
|
69
|
+
strapi.documents.mockReturnValue({ create: createMock });
|
|
70
|
+
|
|
71
|
+
await service.log({
|
|
72
|
+
realmName: 'test-realm',
|
|
73
|
+
action: AUDIT_ACTIONS.BULK_IMPORT,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const calledWith = createMock.mock.calls[0][0];
|
|
77
|
+
expect(calledWith.data.keycloakUserId).toBeNull();
|
|
78
|
+
expect(calledWith.data.keycloakUsername).toBeNull();
|
|
79
|
+
expect(calledWith.data.details).toBeNull();
|
|
80
|
+
expect(calledWith.data.performedById).toBeNull();
|
|
81
|
+
expect(calledWith.data.performedByEmail).toBe(UNKNOWN_USER_EMAIL);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not throw when create fails', async () => {
|
|
85
|
+
strapi.documents.mockReturnValue({
|
|
86
|
+
create: jest.fn().mockRejectedValue(new Error('Database error')),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Should not throw
|
|
90
|
+
await expect(
|
|
91
|
+
service.log({
|
|
92
|
+
realmName: 'test',
|
|
93
|
+
action: AUDIT_ACTIONS.CREATE_USER,
|
|
94
|
+
})
|
|
95
|
+
).resolves.not.toThrow();
|
|
96
|
+
|
|
97
|
+
expect(strapi.log.error).toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return created entry on success', async () => {
|
|
101
|
+
const entry = createMockAuditLogEntry();
|
|
102
|
+
strapi.documents.mockReturnValue({
|
|
103
|
+
create: jest.fn().mockResolvedValue(entry),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await service.log({
|
|
107
|
+
realmName: 'test',
|
|
108
|
+
action: AUDIT_ACTIONS.UPDATE_USER,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual(entry);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('find', () => {
|
|
116
|
+
it('should query with default pagination', async () => {
|
|
117
|
+
const findManyMock = jest.fn().mockResolvedValue([]);
|
|
118
|
+
const countMock = jest.fn().mockResolvedValue(0);
|
|
119
|
+
strapi.documents.mockReturnValue({
|
|
120
|
+
findMany: findManyMock,
|
|
121
|
+
count: countMock,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await service.find();
|
|
125
|
+
|
|
126
|
+
expect(findManyMock).toHaveBeenCalledWith({
|
|
127
|
+
filters: {},
|
|
128
|
+
sort: { createdAt: 'desc' },
|
|
129
|
+
limit: PAGINATION.AUDIT_LOG_LIMIT,
|
|
130
|
+
offset: 0,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should apply filters when provided', async () => {
|
|
135
|
+
const findManyMock = jest.fn().mockResolvedValue([]);
|
|
136
|
+
const countMock = jest.fn().mockResolvedValue(0);
|
|
137
|
+
strapi.documents.mockReturnValue({
|
|
138
|
+
findMany: findManyMock,
|
|
139
|
+
count: countMock,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await service.find({
|
|
143
|
+
realmName: 'production',
|
|
144
|
+
action: AUDIT_ACTIONS.RESET_PASSWORD,
|
|
145
|
+
keycloakUserId: 'user-123',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(findManyMock).toHaveBeenCalledWith(
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
filters: {
|
|
151
|
+
realmName: 'production',
|
|
152
|
+
action: AUDIT_ACTIONS.RESET_PASSWORD,
|
|
153
|
+
keycloakUserId: 'user-123',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should use custom pagination parameters', async () => {
|
|
160
|
+
const findManyMock = jest.fn().mockResolvedValue([]);
|
|
161
|
+
const countMock = jest.fn().mockResolvedValue(100);
|
|
162
|
+
strapi.documents.mockReturnValue({
|
|
163
|
+
findMany: findManyMock,
|
|
164
|
+
count: countMock,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await service.find({ limit: 10, offset: 20 });
|
|
168
|
+
|
|
169
|
+
expect(findManyMock).toHaveBeenCalledWith(
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
limit: 10,
|
|
172
|
+
offset: 20,
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return entries and total count', async () => {
|
|
178
|
+
const entries = [
|
|
179
|
+
createMockAuditLogEntry({ action: AUDIT_ACTIONS.CREATE_USER }),
|
|
180
|
+
createMockAuditLogEntry({ action: AUDIT_ACTIONS.UPDATE_USER }),
|
|
181
|
+
];
|
|
182
|
+
strapi.documents.mockReturnValue({
|
|
183
|
+
findMany: jest.fn().mockResolvedValue(entries),
|
|
184
|
+
count: jest.fn().mockResolvedValue(2),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await service.find();
|
|
188
|
+
|
|
189
|
+
expect(result.entries).toHaveLength(2);
|
|
190
|
+
expect(result.total).toBe(2);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should execute queries in parallel', async () => {
|
|
194
|
+
let findManyResolved = false;
|
|
195
|
+
let countResolved = false;
|
|
196
|
+
|
|
197
|
+
strapi.documents.mockReturnValue({
|
|
198
|
+
findMany: jest.fn().mockImplementation(() => {
|
|
199
|
+
findManyResolved = true;
|
|
200
|
+
return Promise.resolve([]);
|
|
201
|
+
}),
|
|
202
|
+
count: jest.fn().mockImplementation(() => {
|
|
203
|
+
// Check that findMany hasn't completed yet (parallel execution)
|
|
204
|
+
expect(findManyResolved).toBe(true); // Both start at same time
|
|
205
|
+
countResolved = true;
|
|
206
|
+
return Promise.resolve(0);
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await service.find();
|
|
211
|
+
|
|
212
|
+
expect(findManyResolved).toBe(true);
|
|
213
|
+
expect(countResolved).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('findByRealm', () => {
|
|
218
|
+
it('should call find with realm filter', async () => {
|
|
219
|
+
const entries = [createMockAuditLogEntry()];
|
|
220
|
+
strapi.documents.mockReturnValue({
|
|
221
|
+
findMany: jest.fn().mockResolvedValue(entries),
|
|
222
|
+
count: jest.fn().mockResolvedValue(1),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await service.findByRealm('production', { limit: 10 });
|
|
226
|
+
|
|
227
|
+
expect(result.entries).toHaveLength(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should use default pagination', async () => {
|
|
231
|
+
const findManyMock = jest.fn().mockResolvedValue([]);
|
|
232
|
+
strapi.documents.mockReturnValue({
|
|
233
|
+
findMany: findManyMock,
|
|
234
|
+
count: jest.fn().mockResolvedValue(0),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await service.findByRealm('production');
|
|
238
|
+
|
|
239
|
+
expect(findManyMock).toHaveBeenCalledWith(
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
filters: { realmName: 'production' },
|
|
242
|
+
limit: PAGINATION.AUDIT_LOG_LIMIT,
|
|
243
|
+
offset: 0,
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('findByKeycloakUser', () => {
|
|
250
|
+
it('should call find with keycloakUserId filter', async () => {
|
|
251
|
+
const entries = [
|
|
252
|
+
createMockAuditLogEntry({ action: AUDIT_ACTIONS.CREATE_USER }),
|
|
253
|
+
createMockAuditLogEntry({ action: AUDIT_ACTIONS.RESET_PASSWORD }),
|
|
254
|
+
];
|
|
255
|
+
strapi.documents.mockReturnValue({
|
|
256
|
+
findMany: jest.fn().mockResolvedValue(entries),
|
|
257
|
+
count: jest.fn().mockResolvedValue(2),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = await service.findByKeycloakUser('user-123');
|
|
261
|
+
|
|
262
|
+
expect(result.entries).toHaveLength(2);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should use default pagination', async () => {
|
|
266
|
+
const findManyMock = jest.fn().mockResolvedValue([]);
|
|
267
|
+
strapi.documents.mockReturnValue({
|
|
268
|
+
findMany: findManyMock,
|
|
269
|
+
count: jest.fn().mockResolvedValue(0),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await service.findByKeycloakUser('user-123');
|
|
273
|
+
|
|
274
|
+
expect(findManyMock).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
filters: { keycloakUserId: 'user-123' },
|
|
277
|
+
limit: PAGINATION.AUDIT_LOG_LIMIT,
|
|
278
|
+
offset: 0,
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|