@modular-rest/server 1.19.0 → 1.20.1

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.
Files changed (35) hide show
  1. package/.nvmrc +1 -1
  2. package/dist/application.js +5 -4
  3. package/dist/class/combinator.js +7 -3
  4. package/dist/class/directory.d.ts +2 -4
  5. package/dist/class/directory.js +42 -64
  6. package/dist/helper/data_insertion.js +93 -26
  7. package/dist/services/data_provider/model_registry.d.ts +5 -0
  8. package/dist/services/data_provider/model_registry.js +25 -0
  9. package/dist/services/data_provider/service.js +8 -0
  10. package/dist/services/file/service.d.ts +47 -78
  11. package/dist/services/file/service.js +124 -155
  12. package/dist/services/functions/service.js +4 -4
  13. package/dist/services/jwt/router.js +2 -1
  14. package/dist/services/user_manager/router.js +1 -1
  15. package/dist/services/user_manager/service.js +48 -17
  16. package/jest.config.ts +18 -0
  17. package/package.json +11 -2
  18. package/src/application.ts +5 -4
  19. package/src/class/combinator.ts +10 -3
  20. package/src/class/directory.ts +40 -58
  21. package/src/helper/data_insertion.ts +101 -27
  22. package/src/services/data_provider/model_registry.ts +28 -0
  23. package/src/services/data_provider/service.ts +6 -0
  24. package/src/services/file/service.ts +146 -178
  25. package/src/services/functions/service.ts +4 -4
  26. package/src/services/jwt/router.ts +2 -1
  27. package/src/services/user_manager/router.ts +1 -1
  28. package/src/services/user_manager/service.ts +49 -20
  29. package/tests/helpers/test-app.ts +182 -0
  30. package/tests/router/data-provider.router.int.test.ts +192 -0
  31. package/tests/router/file.router.int.test.ts +104 -0
  32. package/tests/router/functions.router.int.test.ts +91 -0
  33. package/tests/router/jwt.router.int.test.ts +69 -0
  34. package/tests/router/user-manager.router.int.test.ts +85 -0
  35. package/tests/setup/jest.setup.ts +5 -0
@@ -0,0 +1,182 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import supertest from 'supertest';
5
+ import mongoose from 'mongoose';
6
+ import { modelRegistry } from '../../src/services/data_provider/model_registry';
7
+ import type { RestOptions } from '../../src/config';
8
+ import { createRest } from '../../src/application';
9
+
10
+ import crypto from 'crypto';
11
+
12
+ const defaultMongoUri = process.env.MONGO_URL || 'mongodb://localhost:27017';
13
+
14
+ // Generate a valid RSA keypair for the environment
15
+ const generated = crypto.generateKeyPairSync('rsa', {
16
+ modulusLength: 2048,
17
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
18
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
19
+ });
20
+
21
+ const fastKeyPair = {
22
+ public: generated.publicKey,
23
+ private: generated.privateKey,
24
+ };
25
+
26
+ export interface TestAppContext {
27
+ request: supertest.SuperTest<supertest.Test>;
28
+ uploadDir: string;
29
+ adminToken: string;
30
+ dbPrefix: string;
31
+ cleanup: () => Promise<void>;
32
+ }
33
+
34
+ export async function createIntegrationTestApp(
35
+ overrides: Partial<RestOptions> = {}
36
+ ): Promise<TestAppContext> {
37
+ const setupStart = Date.now();
38
+ console.log(`[test-app] Starting context creation...`);
39
+
40
+ // Clear model registry to ensure a fresh start with new db prefixes
41
+ await modelRegistry.clear();
42
+
43
+ const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mrest-upload-'));
44
+ const dbPrefix = `mrest_test_${Date.now()}_${Math.random().toString(16).slice(2)}_`;
45
+
46
+ const options: RestOptions = {
47
+ dontListen: true, // Use app callback for supertest, no real server needed
48
+ port: 0,
49
+ mongo: {
50
+ mongoBaseAddress: defaultMongoUri,
51
+ dbPrefix,
52
+ },
53
+ uploadDirectoryConfig: {
54
+ directory: uploadDir,
55
+ urlPath: '/assets',
56
+ },
57
+ adminUser: {
58
+ email: 'admin@email.com',
59
+ password: '@dmin',
60
+ },
61
+ keypair: fastKeyPair, // Use pre-generated keys to avoid slow generation
62
+ ...overrides,
63
+ };
64
+
65
+ // Add timeout wrapper to catch hanging operations
66
+ let timeoutId: NodeJS.Timeout;
67
+ const createRestWithTimeout = Promise.race([
68
+ createRest(options),
69
+ new Promise<never>((_, reject) => {
70
+ timeoutId = setTimeout(
71
+ () =>
72
+ reject(
73
+ new Error(
74
+ `createRest timed out after 45s - check MongoDB connection. Prefix: ${dbPrefix}`
75
+ )
76
+ ),
77
+ 45000
78
+ );
79
+ }),
80
+ ]) as Promise<{ app: any; server?: any }>;
81
+
82
+ let app: any;
83
+ let server: any;
84
+
85
+ try {
86
+ const result = await createRestWithTimeout;
87
+ if (timeoutId!) clearTimeout(timeoutId);
88
+ app = result.app;
89
+ server = result.server;
90
+ console.log(`[test-app] createRest resolved (+${Date.now() - setupStart}ms)`);
91
+
92
+ // Wait for database to be fully ready for queries
93
+ // This ensures createAdminUser queries don't hang
94
+ await new Promise(resolve => setTimeout(resolve, 500));
95
+ } catch (error) {
96
+ // Cleanup on failure
97
+ if (uploadDir) {
98
+ fs.rmSync(uploadDir, { recursive: true, force: true });
99
+ }
100
+ const errorMessage = error instanceof Error ? error.message : String(error);
101
+ throw new Error(`Failed to create test app: ${errorMessage}.`);
102
+ }
103
+
104
+ const request = supertest(app.callback()) as unknown as supertest.SuperTest<supertest.Test>;
105
+
106
+ console.log(`[test-app] Attempting admin login... (+${Date.now() - setupStart}ms)`);
107
+ const loginRes = await request.post('/user/login').send({
108
+ id: options.adminUser?.email || 'admin@email.com',
109
+ idType: 'email',
110
+ password: options.adminUser?.password || '@dmin',
111
+ });
112
+
113
+ if (loginRes.status >= 300 || loginRes.body.status !== 'success' || !loginRes.body.token) {
114
+ console.error(`[test-app] Login failed:`, loginRes.status, loginRes.body);
115
+ await cleanupConnections(dbPrefix);
116
+ if (uploadDir) fs.rmSync(uploadDir, { recursive: true, force: true });
117
+ throw new Error(`Failed to login admin: ${loginRes.status} ${JSON.stringify(loginRes.body)}`);
118
+ }
119
+
120
+ const adminToken = loginRes.body.token as string;
121
+ console.log(`[test-app] Context creation complete (+${Date.now() - setupStart}ms)`);
122
+
123
+ const cleanup = async (): Promise<void> => {
124
+ if (server) {
125
+ await new Promise<void>(resolve => server.close(() => resolve()));
126
+ }
127
+
128
+ if (uploadDir) {
129
+ fs.rmSync(uploadDir, { recursive: true, force: true });
130
+ }
131
+
132
+ await modelRegistry.clear();
133
+ await cleanupConnections(dbPrefix);
134
+ };
135
+
136
+ return {
137
+ request,
138
+ uploadDir,
139
+ adminToken,
140
+ dbPrefix,
141
+ cleanup,
142
+ };
143
+ }
144
+
145
+ async function cleanupConnections(dbPrefix: string): Promise<void> {
146
+ await Promise.all(
147
+ mongoose.connections.map(async connection => {
148
+ const dbName = (connection as any).db?.databaseName;
149
+ if (dbName && dbPrefix && dbName.startsWith(dbPrefix)) {
150
+ try {
151
+ await (connection as any).dropDatabase();
152
+ } catch (err) {
153
+ // ignore drop errors during cleanup
154
+ }
155
+ }
156
+ if (connection.readyState !== 0) {
157
+ try {
158
+ await connection.close();
159
+ } catch (err) {
160
+ // ignore close errors during cleanup
161
+ }
162
+ }
163
+ })
164
+ );
165
+
166
+ await mongoose.disconnect().catch(() => {
167
+ /* ignore */
168
+ });
169
+ }
170
+
171
+ export function ensureUploadPath(uploadDir: string, format: string, tag: string): void {
172
+ fs.mkdirSync(path.join(uploadDir, format, tag), { recursive: true });
173
+ }
174
+
175
+ export function createTempFile(extension: string, content = 'file-content'): string {
176
+ const filePath = path.join(
177
+ fs.mkdtempSync(path.join(os.tmpdir(), 'mrest-file-')),
178
+ `temp.${extension}`
179
+ );
180
+ fs.writeFileSync(filePath, content);
181
+ return filePath;
182
+ }
@@ -0,0 +1,192 @@
1
+ import { TestAppContext, createIntegrationTestApp } from '../helpers/test-app';
2
+
3
+ describe('data-provider router integration', () => {
4
+ let ctx: TestAppContext;
5
+ let createdId: string;
6
+
7
+ beforeAll(async () => {
8
+ ctx = await createIntegrationTestApp();
9
+ });
10
+
11
+ afterAll(async () => {
12
+ if (ctx) {
13
+ await ctx.cleanup();
14
+ }
15
+ });
16
+
17
+ it('rejects request without authorization', async () => {
18
+ const res = await ctx.request.post('/data-provider/find').send({
19
+ database: 'cms',
20
+ collection: 'file',
21
+ query: {},
22
+ });
23
+ expect(res.status).toBe(401);
24
+ });
25
+
26
+ it('fails when database or collection is missing', async () => {
27
+ const res = await ctx.request
28
+ .post('/data-provider/find')
29
+ .set('authorization', ctx.adminToken)
30
+ .send({
31
+ query: {},
32
+ });
33
+ expect(res.status).toBe(412);
34
+ const body = JSON.parse(res.text);
35
+ expect(body.status).toBe('error');
36
+ });
37
+
38
+ it('inserts a document using /insert-one', async () => {
39
+ const res = await ctx.request
40
+ .post('/data-provider/insert-one')
41
+ .set('authorization', ctx.adminToken)
42
+ .send({
43
+ database: 'cms',
44
+ collection: 'file',
45
+ doc: {
46
+ originalName: 'test-file.txt',
47
+ fileName: 'test-uuid.txt',
48
+ format: 'text',
49
+ tag: 'test-tag',
50
+ size: 1024,
51
+ owner: 'admin-id',
52
+ },
53
+ });
54
+
55
+ expect(res.status).toBe(200);
56
+ expect(res.body.data).toBeDefined();
57
+ expect(res.body.data.originalName).toBe('test-file.txt');
58
+ createdId = res.body.data._id;
59
+ });
60
+
61
+ it('finds the inserted document using /find-one', async () => {
62
+ const res = await ctx.request
63
+ .post('/data-provider/find-one')
64
+ .set('authorization', ctx.adminToken)
65
+ .send({
66
+ database: 'cms',
67
+ collection: 'file',
68
+ query: { _id: createdId },
69
+ });
70
+
71
+ expect(res.status).toBe(200);
72
+ expect(res.body.data).toBeDefined();
73
+ expect(res.body.data._id).toBe(createdId);
74
+ });
75
+
76
+ it('updates the document using /update-one', async () => {
77
+ const res = await ctx.request
78
+ .post('/data-provider/update-one')
79
+ .set('authorization', ctx.adminToken)
80
+ .send({
81
+ database: 'cms',
82
+ collection: 'file',
83
+ query: { _id: createdId },
84
+ update: { $set: { originalName: 'updated-name.txt' } },
85
+ });
86
+
87
+ expect(res.status).toBe(200);
88
+ // Mongoose updateOne returns { n, nModified, ok }
89
+ expect(res.body.data.ok).toBe(1);
90
+
91
+ // Verify update
92
+ const verifyRes = await ctx.request
93
+ .post('/data-provider/find-one')
94
+ .set('authorization', ctx.adminToken)
95
+ .send({
96
+ database: 'cms',
97
+ collection: 'file',
98
+ query: { _id: createdId },
99
+ });
100
+ expect(verifyRes.body.data.originalName).toBe('updated-name.txt');
101
+ });
102
+
103
+ it('counts documents using /count', async () => {
104
+ const res = await ctx.request
105
+ .post('/data-provider/count')
106
+ .set('authorization', ctx.adminToken)
107
+ .send({
108
+ database: 'cms',
109
+ collection: 'file',
110
+ query: { _id: createdId },
111
+ });
112
+
113
+ expect(res.status).toBe(200);
114
+ expect(res.body.data).toBe(1);
115
+ });
116
+
117
+ it('finds multiple documents using /find', async () => {
118
+ const res = await ctx.request
119
+ .post('/data-provider/find')
120
+ .set('authorization', ctx.adminToken)
121
+ .send({
122
+ database: 'cms',
123
+ collection: 'file',
124
+ query: { _id: createdId },
125
+ });
126
+
127
+ expect(res.status).toBe(200);
128
+ expect(Array.isArray(res.body.data)).toBe(true);
129
+ expect(res.body.data.length).toBe(1);
130
+ });
131
+
132
+ it('finds by ids using /findByIds', async () => {
133
+ const res = await ctx.request
134
+ .post('/data-provider/findByIds')
135
+ .set('authorization', ctx.adminToken)
136
+ .send({
137
+ database: 'cms',
138
+ collection: 'file',
139
+ ids: [createdId],
140
+ });
141
+
142
+ expect(res.status).toBe(200);
143
+ expect(Array.isArray(res.body.data)).toBe(true);
144
+ expect(res.body.data.length).toBe(1);
145
+ expect(res.body.data[0]._id).toBe(createdId);
146
+ });
147
+
148
+ it('aggregates documents using /aggregate', async () => {
149
+ const res = await ctx.request
150
+ .post('/data-provider/aggregate')
151
+ .set('authorization', ctx.adminToken)
152
+ .send({
153
+ database: 'cms',
154
+ collection: 'file',
155
+ accessQuery: {},
156
+ pipelines: [{ $match: { tag: 'test-tag' } }], // Use string field to avoid ID casting issues for now
157
+ });
158
+
159
+ if (res.status !== 200) {
160
+ console.error('Aggregate Error:', res.text);
161
+ }
162
+
163
+ expect(res.status).toBe(200);
164
+ expect(Array.isArray(res.body.data)).toBe(true);
165
+ expect(res.body.data.length).toBeGreaterThan(0);
166
+ });
167
+
168
+ it('removes the document using /remove-one', async () => {
169
+ const res = await ctx.request
170
+ .post('/data-provider/remove-one')
171
+ .set('authorization', ctx.adminToken)
172
+ .send({
173
+ database: 'cms',
174
+ collection: 'file',
175
+ query: { _id: createdId },
176
+ });
177
+
178
+ expect(res.status).toBe(200);
179
+ expect(res.body.data.n).toBe(1);
180
+
181
+ // Verify removal
182
+ const verifyRes = await ctx.request
183
+ .post('/data-provider/find-one')
184
+ .set('authorization', ctx.adminToken)
185
+ .send({
186
+ database: 'cms',
187
+ collection: 'file',
188
+ query: { _id: createdId },
189
+ });
190
+ expect(verifyRes.body.data).toBeNull();
191
+ });
192
+ });
@@ -0,0 +1,104 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getCollection } from '../../src/services/data_provider/service';
4
+ import { IFile } from '../../src/class/db_schemas';
5
+ import {
6
+ TestAppContext,
7
+ createIntegrationTestApp,
8
+ createTempFile,
9
+ ensureUploadPath,
10
+ } from '../helpers/test-app';
11
+
12
+ describe('file router integration', () => {
13
+ let ctx: TestAppContext;
14
+ let createdFileId: string;
15
+ let storedPath: string;
16
+
17
+ beforeAll(async () => {
18
+ ctx = await createIntegrationTestApp();
19
+ });
20
+
21
+ afterAll(async () => {
22
+ if (ctx) {
23
+ await ctx.cleanup();
24
+ }
25
+ });
26
+
27
+ it('rejects upload without authorization', async () => {
28
+ const res = await ctx.request.post('/file').field('tag', 'avatar');
29
+ expect(res.status).toBe(401);
30
+ });
31
+
32
+ it('fails upload when tag is missing', async () => {
33
+ const res = await ctx.request.post('/file').set('authorization', ctx.adminToken);
34
+ expect(res.status).toBe(412);
35
+ expect(res.body.status).toBe('error');
36
+ });
37
+
38
+ it('fails upload when file field is missing', async () => {
39
+ const res = await ctx.request
40
+ .post('/file')
41
+ .set('authorization', ctx.adminToken)
42
+ .field('tag', 'avatar');
43
+
44
+ expect(res.status).toBe(412);
45
+ expect(res.body.status).toBe('fail');
46
+ expect(res.body.message).toBe('file field required');
47
+ });
48
+
49
+ it('uploads a file and stores metadata', async () => {
50
+ const tag = 'avatar';
51
+ const filePath = createTempFile('txt', 'hello-world');
52
+
53
+ // Ensure upload path exists for format/tag (service expects path to exist)
54
+ ensureUploadPath(ctx.uploadDir, 'plain', tag);
55
+
56
+ const res = await ctx.request
57
+ .post('/file')
58
+ .set('authorization', ctx.adminToken)
59
+ .field('tag', tag)
60
+ .attach('file', filePath);
61
+
62
+ expect(res.status).toBe(200);
63
+ expect(res.body.status).toBe('success');
64
+ expect(res.body.file).toBeDefined();
65
+
66
+ createdFileId = res.body.file._id;
67
+ expect(createdFileId).toBeTruthy();
68
+
69
+ const fileModel = getCollection<IFile>('cms', 'file');
70
+ const doc = await fileModel.findById(createdFileId).lean();
71
+
72
+ expect(doc).toBeTruthy();
73
+ expect(doc?.originalName).toBe('temp.txt');
74
+ expect(doc?.tag).toBe(tag);
75
+ expect(doc?.format).toBe('plain');
76
+
77
+ storedPath = path.join(ctx.uploadDir, doc!.format, doc!.tag, doc!.fileName);
78
+ expect(fs.existsSync(storedPath)).toBe(true);
79
+
80
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
81
+ });
82
+
83
+ it('deletes a file by id', async () => {
84
+ const res = await ctx.request
85
+ .delete('/file')
86
+ .set('authorization', ctx.adminToken)
87
+ .query({ id: createdFileId });
88
+
89
+ expect(res.status).toBe(200);
90
+ expect(res.body.status).toBe('success');
91
+
92
+ const fileModel = getCollection<IFile>('cms', 'file');
93
+ const doc = await fileModel.findById(createdFileId).lean();
94
+ expect(doc).toBeNull();
95
+
96
+ expect(storedPath ? fs.existsSync(storedPath) : false).toBe(false);
97
+ });
98
+
99
+ it('returns validation error when id is missing on delete', async () => {
100
+ const res = await ctx.request.delete('/file').set('authorization', ctx.adminToken);
101
+ expect(res.status).toBe(412);
102
+ expect(res.body.status).toBe('fail');
103
+ });
104
+ });
@@ -0,0 +1,91 @@
1
+ import { TestAppContext, createIntegrationTestApp } from '../helpers/test-app';
2
+ import { defineFunction } from '../../src/services/functions/service';
3
+
4
+ describe('functions router integration', () => {
5
+ let ctx: TestAppContext;
6
+
7
+ const testFunction = defineFunction({
8
+ name: 'testAdd',
9
+ permissionTypes: ['user_access'],
10
+ callback: (args: { a: number; b: number }) => args.a + args.b,
11
+ });
12
+
13
+ beforeAll(async () => {
14
+ // Pass the test function to createRest via overrides
15
+ ctx = await createIntegrationTestApp({
16
+ functions: [testFunction],
17
+ });
18
+ });
19
+
20
+ afterAll(async () => {
21
+ if (ctx) {
22
+ await ctx.cleanup();
23
+ }
24
+ });
25
+
26
+ it('rejects execution without authorization', async () => {
27
+ const res = await ctx.request.post('/function/run').send({
28
+ name: 'testAdd',
29
+ args: { a: 1, b: 2 },
30
+ });
31
+ expect(res.status).toBe(401);
32
+ });
33
+
34
+ it('fails with missing fields', async () => {
35
+ const res = await ctx.request.post('/function/run').set('authorization', ctx.adminToken).send({
36
+ name: 'testAdd',
37
+ });
38
+ expect(res.status).toBe(412);
39
+ const body = JSON.parse(res.text);
40
+ expect(body.status).toBe('error');
41
+ });
42
+
43
+ it('runs a defined function successfully', async () => {
44
+ const res = await ctx.request
45
+ .post('/function/run')
46
+ .set('authorization', ctx.adminToken)
47
+ .send({
48
+ name: 'testAdd',
49
+ args: { a: 5, b: 10 },
50
+ });
51
+
52
+ if (res.status !== 200) {
53
+ console.error('Run Function Error:', res.body);
54
+ }
55
+
56
+ expect(res.status).toBe(200);
57
+ expect(res.body.status).toBe('success');
58
+ expect(res.body.data).toBe(15);
59
+ });
60
+
61
+ it('fails when function not found', async () => {
62
+ const res = await ctx.request.post('/function/run').set('authorization', ctx.adminToken).send({
63
+ name: 'nonExistent',
64
+ args: {},
65
+ });
66
+
67
+ expect(res.status).toBe(400);
68
+ expect(res.body.status).toBe('error');
69
+ expect(res.body.message).toContain('not found');
70
+ });
71
+
72
+ it('fails when user lacks permission', async () => {
73
+ // 1. Get anonymous token
74
+ const loginRes = await ctx.request.get('/user/loginAnonymous');
75
+ expect(loginRes.status).toBe(200);
76
+ const anonToken = loginRes.body.token;
77
+
78
+ // 2. Try to run testAdd (requires user_access) with anonymous token
79
+ const res = await ctx.request
80
+ .post('/function/run')
81
+ .set('authorization', anonToken)
82
+ .send({
83
+ name: 'testAdd',
84
+ args: { a: 1, b: 2 },
85
+ });
86
+
87
+ expect(res.status).toBe(400);
88
+ expect(res.body.status).toBe('error');
89
+ expect(res.body.message).toContain('does not have permission');
90
+ });
91
+ });
@@ -0,0 +1,69 @@
1
+ import { TestAppContext, createIntegrationTestApp } from '../helpers/test-app';
2
+ import * as JWT from '../../src/services/jwt/service';
3
+
4
+ describe('jwt router integration', () => {
5
+ let ctx: TestAppContext;
6
+
7
+ beforeAll(async () => {
8
+ ctx = await createIntegrationTestApp();
9
+ });
10
+
11
+ afterAll(async () => {
12
+ if (ctx) {
13
+ await ctx.cleanup();
14
+ }
15
+ });
16
+
17
+ it('GET /verify/ready returns success', async () => {
18
+ const res = await ctx.request.get('/verify/ready');
19
+ expect(res.status).toBe(200);
20
+ expect(res.body.status).toBe('success');
21
+ });
22
+
23
+ it('POST /verify/token fails with missing token', async () => {
24
+ const res = await ctx.request.post('/verify/token').send({});
25
+ expect(res.status).toBe(412);
26
+ expect(res.body.status).toBe('error');
27
+ });
28
+
29
+ it('POST /verify/token succeeds with valid token', async () => {
30
+ // Generate a token
31
+ const payload = { id: 'test-user', email: 'test@email.com' };
32
+ const token = await JWT.main.sign(payload);
33
+
34
+ const res = await ctx.request.post('/verify/token').send({ token });
35
+ expect(res.status).toBe(200);
36
+ expect(res.body.status).toBe('success');
37
+ expect(res.body.user.id).toBe(payload.id);
38
+ expect(res.body.user.email).toBe(payload.email);
39
+ });
40
+
41
+ it('POST /verify/token fails with invalid token', async () => {
42
+ const res = await ctx.request.post('/verify/token').send({ token: 'invalid-token' });
43
+ expect(res.status).toBe(412);
44
+ expect(res.body.status).toBe('error');
45
+ });
46
+
47
+ it('POST /verify/checkAccess fails with missing fields', async () => {
48
+ const res = await ctx.request.post('/verify/checkAccess').send({ token: 'something' });
49
+ expect(res.status).toBe(412);
50
+ expect(res.body.status).toBe('error');
51
+ });
52
+
53
+ it('POST /verify/checkAccess succeeds for admin', async () => {
54
+ // Use the admin token from context
55
+ // We need to extract the token string (it might have 'Bearer ' prefix if we set it that way,
56
+ // but in createIntegrationTestApp it is just the token)
57
+ const token = ctx.adminToken;
58
+
59
+ const res = await ctx.request.post('/verify/checkAccess').send({
60
+ token,
61
+ permissionField: 'advanced_settings', // Admin should have this
62
+ });
63
+
64
+ // If global.services is not set, this will likely return 500 or 412 depending on error handling
65
+ expect(res.status).toBe(200);
66
+ expect(res.body.status).toBe('success');
67
+ expect(res.body.access).toBe(true);
68
+ });
69
+ });