@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.
- package/.nvmrc +1 -1
- package/dist/application.js +5 -4
- package/dist/class/combinator.js +7 -3
- package/dist/class/directory.d.ts +2 -4
- package/dist/class/directory.js +42 -64
- package/dist/helper/data_insertion.js +93 -26
- package/dist/services/data_provider/model_registry.d.ts +5 -0
- package/dist/services/data_provider/model_registry.js +25 -0
- package/dist/services/data_provider/service.js +8 -0
- package/dist/services/file/service.d.ts +47 -78
- package/dist/services/file/service.js +124 -155
- package/dist/services/functions/service.js +4 -4
- package/dist/services/jwt/router.js +2 -1
- package/dist/services/user_manager/router.js +1 -1
- package/dist/services/user_manager/service.js +48 -17
- package/jest.config.ts +18 -0
- package/package.json +11 -2
- package/src/application.ts +5 -4
- package/src/class/combinator.ts +10 -3
- package/src/class/directory.ts +40 -58
- package/src/helper/data_insertion.ts +101 -27
- package/src/services/data_provider/model_registry.ts +28 -0
- package/src/services/data_provider/service.ts +6 -0
- package/src/services/file/service.ts +146 -178
- package/src/services/functions/service.ts +4 -4
- package/src/services/jwt/router.ts +2 -1
- package/src/services/user_manager/router.ts +1 -1
- package/src/services/user_manager/service.ts +49 -20
- package/tests/helpers/test-app.ts +182 -0
- package/tests/router/data-provider.router.int.test.ts +192 -0
- package/tests/router/file.router.int.test.ts +104 -0
- package/tests/router/functions.router.int.test.ts +91 -0
- package/tests/router/jwt.router.int.test.ts +69 -0
- package/tests/router/user-manager.router.int.test.ts +85 -0
- 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
|
+
});
|