db-model-router 1.0.7 → 1.0.9
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/README.md +25 -4
- package/db-manager/.dbmanager.sqlite-shm +0 -0
- package/db-manager/.dbmanager.sqlite-wal +0 -0
- package/demo/.env.example +1 -0
- package/demo/app.js +2 -2
- package/demo/commons/db.js +0 -11
- package/demo/middleware/tenantIsolation.js +2 -0
- package/demo/package-lock.json +1224 -62
- package/demo/package.json +6 -3
- package/demo/routes/addresses/index.js +5 -1
- package/demo/routes/auth/index.js +1 -1
- package/demo/routes/carts/cart_items/index.js +5 -1
- package/demo/routes/carts/index.js +9 -1
- package/demo/routes/categories/index.js +5 -1
- package/demo/routes/coupons/index.js +5 -1
- package/demo/routes/index.js +1 -15
- package/demo/routes/orders/index.js +13 -1
- package/demo/routes/orders/order_items/index.js +5 -1
- package/demo/routes/orders/payments/index.js +5 -1
- package/demo/routes/orders/shipments/index.js +5 -1
- package/demo/routes/products/index.js +13 -1
- package/demo/routes/products/product_images/index.js +5 -1
- package/demo/routes/products/product_reviews/index.js +5 -1
- package/demo/routes/products/product_variants/index.js +5 -1
- package/demo/routes/roles/index.js +1 -1
- package/demo/routes/tenants/index.js +1 -1
- package/demo/routes/users/index.js +1 -1
- package/demo/routes/wishlists/index.js +5 -1
- package/demo/seeds/saas-seed.js +1 -1
- package/docs/dbmr-schema-spec.md +393 -0
- package/package.json +4 -2
- package/skill/SKILL.md +47 -4
- package/src/cli/commands/generate.js +45 -15
- package/src/cli/diff-engine.js +17 -5
- package/src/cli/generate-migration.js +207 -19
- package/src/cli/generate-route.js +156 -58
- package/src/cli/generate-saas-structure.js +8 -1
- package/src/cli/init/dependencies.js +5 -1
- package/src/cli/init/generators.js +4 -81
- package/src/cli/init.js +1 -2
- package/src/cli/saas/generate-saas-middleware.js +2 -0
- package/src/cli/saas/generate-saas-routes.js +3 -13
- package/src/cli/saas/generate-saas-tests.js +473 -0
- package/src/commons/route.js +6 -6
- /package/demo/migrations/{20260509170349_create_migrations_table.sql → 20260510193736_create_migrations_table.sql} +0 -0
- /package/demo/migrations/{20260509170349_create_saas_tables.sql → 20260510193737_create_saas_tables.sql} +0 -0
- /package/demo/migrations/{20260509170349_create_tables.sql → 20260510193737_create_tables.sql} +0 -0
|
@@ -172,6 +172,7 @@ function buildEnvContent(answers, mode, secrets) {
|
|
|
172
172
|
lines.push("API_BASE_PATH=/api");
|
|
173
173
|
lines.push("");
|
|
174
174
|
lines.push("# Database");
|
|
175
|
+
lines.push(`DB_TYPE=${answers.database}`);
|
|
175
176
|
|
|
176
177
|
const vars = DB_ENV_MAP[answers.database] || [];
|
|
177
178
|
for (const v of vars) {
|
|
@@ -358,83 +359,6 @@ app.use(session({
|
|
|
358
359
|
}));`;
|
|
359
360
|
}
|
|
360
361
|
|
|
361
|
-
/**
|
|
362
|
-
* Generate the app.js file content.
|
|
363
|
-
* @param {import('./types').InitAnswers} answers
|
|
364
|
-
* @returns {string}
|
|
365
|
-
*/
|
|
366
|
-
function generateAppJs(answers) {
|
|
367
|
-
const frameworkPkg =
|
|
368
|
-
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
369
|
-
|
|
370
|
-
// Imports
|
|
371
|
-
let imports = `import express from "${frameworkPkg}";
|
|
372
|
-
import { init, db } from "db-model-router";
|
|
373
|
-
import session from "express-session";`;
|
|
374
|
-
|
|
375
|
-
if (answers.session === "redis") {
|
|
376
|
-
imports += `\nimport RedisStore from "connect-redis";
|
|
377
|
-
import { Redis } from "ioredis";`;
|
|
378
|
-
}
|
|
379
|
-
if (answers.rateLimiting) {
|
|
380
|
-
imports += `\nimport rateLimit from "express-rate-limit";`;
|
|
381
|
-
}
|
|
382
|
-
if (answers.helmet) {
|
|
383
|
-
imports += `\nimport helmet from "helmet";`;
|
|
384
|
-
}
|
|
385
|
-
imports += `\nimport logger from "./middleware/logger.js";`;
|
|
386
|
-
|
|
387
|
-
// Rate limiting block
|
|
388
|
-
const rateLimitBlock = answers.rateLimiting
|
|
389
|
-
? `app.use(rateLimit({
|
|
390
|
-
windowMs: 15 * 60 * 1000,
|
|
391
|
-
max: 100,
|
|
392
|
-
standardHeaders: true,
|
|
393
|
-
legacyHeaders: false,
|
|
394
|
-
}));`
|
|
395
|
-
: "";
|
|
396
|
-
|
|
397
|
-
const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
|
|
398
|
-
|
|
399
|
-
return `${imports}
|
|
400
|
-
import "dotenv/config";
|
|
401
|
-
|
|
402
|
-
// Initialize database adapter
|
|
403
|
-
init("${answers.database}");
|
|
404
|
-
${dbConnectBlock(answers.database)}
|
|
405
|
-
|
|
406
|
-
const app = express();
|
|
407
|
-
const PORT = process.env.PORT || 3000;
|
|
408
|
-
|
|
409
|
-
// Middleware
|
|
410
|
-
app.use(express.json());
|
|
411
|
-
app.use(express.urlencoded({ extended: true }));
|
|
412
|
-
${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
|
|
413
|
-
app.use(logger);
|
|
414
|
-
|
|
415
|
-
// Routes
|
|
416
|
-
import routes from "#routes/index.js";
|
|
417
|
-
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
418
|
-
|
|
419
|
-
// Health check
|
|
420
|
-
app.get("/health", (req, res) => {
|
|
421
|
-
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// Error handler
|
|
425
|
-
app.use((err, req, res, next) => {
|
|
426
|
-
console.error(err.stack);
|
|
427
|
-
res.status(500).json({ type: "danger", message: "Internal Server Error" });
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
app.listen(PORT, () => {
|
|
431
|
-
console.log(\`Server running on port \${PORT}\`);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
export default app;
|
|
435
|
-
`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
362
|
// ---------------------------------------------------------------------------
|
|
439
363
|
// Logger middleware generator
|
|
440
364
|
// ---------------------------------------------------------------------------
|
|
@@ -1720,7 +1644,7 @@ export default db;
|
|
|
1720
1644
|
* @param {string} [outputDir] - relative output directory for source files (e.g. "backend")
|
|
1721
1645
|
* @returns {string}
|
|
1722
1646
|
*/
|
|
1723
|
-
function
|
|
1647
|
+
function generateAppJs(answers, outputDir) {
|
|
1724
1648
|
const frameworkPkg =
|
|
1725
1649
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
1726
1650
|
|
|
@@ -1735,7 +1659,7 @@ import "${commonsPrefix}/db.js";
|
|
|
1735
1659
|
import configureSession from "${commonsPrefix}/session.js";
|
|
1736
1660
|
import applySecurity from "${commonsPrefix}/security.js";
|
|
1737
1661
|
import logger from "${middlewarePrefix}/logger.js";
|
|
1738
|
-
import
|
|
1662
|
+
import routes from "${routePrefix}/index.js";
|
|
1739
1663
|
import { fileURLToPath } from 'node:url';
|
|
1740
1664
|
import path from "path";
|
|
1741
1665
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1756,7 +1680,7 @@ app.use(configureSession());
|
|
|
1756
1680
|
app.use(logger);
|
|
1757
1681
|
|
|
1758
1682
|
// Routes
|
|
1759
|
-
app.use(
|
|
1683
|
+
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
1760
1684
|
|
|
1761
1685
|
// Error handler
|
|
1762
1686
|
app.use((err, req, res, next) => {
|
|
@@ -1777,7 +1701,6 @@ module.exports = {
|
|
|
1777
1701
|
isSql,
|
|
1778
1702
|
randomPassword,
|
|
1779
1703
|
generateAppJs,
|
|
1780
|
-
generateAppJsV2,
|
|
1781
1704
|
generateEnvFile,
|
|
1782
1705
|
generateEnvExample,
|
|
1783
1706
|
generateLoggerMiddleware,
|
package/src/cli/init.js
CHANGED
|
@@ -7,7 +7,6 @@ const { execSync } = require("child_process");
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
generateAppJs,
|
|
10
|
-
generateAppJsV2,
|
|
11
10
|
generateEnvFile,
|
|
12
11
|
generateEnvExample,
|
|
13
12
|
generateLoggerMiddleware,
|
|
@@ -109,7 +108,7 @@ function generateFiles(answers, outputDir) {
|
|
|
109
108
|
|
|
110
109
|
// Root-level files (always in cwd, not in outputDir)
|
|
111
110
|
// app.js uses the v2 generator that links commons/route modules
|
|
112
|
-
if (safeWriteFile("app.js",
|
|
111
|
+
if (safeWriteFile("app.js", generateAppJs(answers, outputDir || "")))
|
|
113
112
|
files.push("app.js");
|
|
114
113
|
if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
|
|
115
114
|
files.push(".env");
|
|
@@ -49,6 +49,8 @@ function generateTenantIsolationMiddleware() {
|
|
|
49
49
|
function tenantIsolation(req, res, next) {
|
|
50
50
|
const hasGlobal = req.session.permission.some((p) => p.scope === "global");
|
|
51
51
|
if (!hasGlobal) {
|
|
52
|
+
if (!req.query) req.query = {};
|
|
53
|
+
if (!req.body) req.body = {};
|
|
52
54
|
req.query.tenant_id = req.session.user.tenant_id;
|
|
53
55
|
req.body.tenant_id = req.session.user.tenant_id;
|
|
54
56
|
}
|
|
@@ -370,19 +370,16 @@ function generateRoutesIndex(tableNames, relationships, options) {
|
|
|
370
370
|
code += `import saasPermissionsRoute from "#routes/roles/permissions/index.js";\n\n`;
|
|
371
371
|
|
|
372
372
|
// --- dbmr schema-generated route imports (folder-based, skip SaaS-owned tables) ---
|
|
373
|
+
// Child routes are mounted inside their parent's index.js, not here
|
|
373
374
|
const dbmrTables = tableNames.filter(
|
|
374
375
|
(t) => !saasRouteModules.has(t) && !nestedChildren.has(t),
|
|
375
376
|
);
|
|
376
|
-
if (dbmrTables.length > 0
|
|
377
|
+
if (dbmrTables.length > 0) {
|
|
377
378
|
code += `// Schema-generated routes\n`;
|
|
378
379
|
}
|
|
379
380
|
for (const table of dbmrTables) {
|
|
380
381
|
code += `import ${safeVarName(table)}Route from "#routes/${table}/index.js";\n`;
|
|
381
382
|
}
|
|
382
|
-
for (const rel of relationships) {
|
|
383
|
-
if (saasRouteModules.has(rel.child)) continue;
|
|
384
|
-
code += `import ${safeVarName(rel.child)}ChildRoute from "#routes/${rel.parent}/${rel.child}/index.js";\n`;
|
|
385
|
-
}
|
|
386
383
|
|
|
387
384
|
if (options.includeDocs) {
|
|
388
385
|
code += `import docsRoute from "#routes/docs.js";\n`;
|
|
@@ -403,14 +400,7 @@ function generateRoutesIndex(tableNames, relationships, options) {
|
|
|
403
400
|
code += `router.use("/docs", docsRoute);\n`;
|
|
404
401
|
}
|
|
405
402
|
|
|
406
|
-
// --- Mount dbmr
|
|
407
|
-
for (const rel of relationships) {
|
|
408
|
-
if (saasRouteModules.has(rel.child)) continue;
|
|
409
|
-
const childVar = safeVarName(rel.child);
|
|
410
|
-
code += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// --- Mount dbmr top-level routes ---
|
|
403
|
+
// --- Mount dbmr top-level routes (children are inside parent's index.js) ---
|
|
414
404
|
if (dbmrTables.length > 0) {
|
|
415
405
|
code += `\n// Schema-generated routes\n`;
|
|
416
406
|
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS test generators.
|
|
5
|
+
*
|
|
6
|
+
* Generates test files for SaaS routes: auth (login/logout), users, tenants, roles, permissions.
|
|
7
|
+
* Tests use supertest with a mock session to bypass authentication middleware.
|
|
8
|
+
* Generated code uses ES6 module syntax.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate all SaaS test files.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
15
|
+
*/
|
|
16
|
+
function generateSaasTests() {
|
|
17
|
+
return [
|
|
18
|
+
{ relPath: "test/auth.test.js", content: generateAuthTest() },
|
|
19
|
+
{ relPath: "test/users.test.js", content: generateUsersTest() },
|
|
20
|
+
{ relPath: "test/tenants.test.js", content: generateTenantsTest() },
|
|
21
|
+
{ relPath: "test/roles.test.js", content: generateRolesTest() },
|
|
22
|
+
{ relPath: "test/permissions.test.js", content: generatePermissionsTest() },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generateAuthTest() {
|
|
27
|
+
return `import assert from "assert";
|
|
28
|
+
import express from "express";
|
|
29
|
+
import request from "supertest";
|
|
30
|
+
|
|
31
|
+
// Import the auth route
|
|
32
|
+
import authRoute from "#routes/auth/index.js";
|
|
33
|
+
|
|
34
|
+
function createApp() {
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use(express.json());
|
|
37
|
+
// Mock session object on each request
|
|
38
|
+
app.use((req, res, next) => {
|
|
39
|
+
req.session = {};
|
|
40
|
+
next();
|
|
41
|
+
});
|
|
42
|
+
app.use("/auth", authRoute);
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("Auth Routes", function () {
|
|
47
|
+
let app;
|
|
48
|
+
|
|
49
|
+
before(function () {
|
|
50
|
+
app = createApp();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("POST /auth/login", function () {
|
|
54
|
+
it("should return 401 when email is missing", async function () {
|
|
55
|
+
const res = await request(app)
|
|
56
|
+
.post("/auth/login")
|
|
57
|
+
.send({ password: "test123" });
|
|
58
|
+
assert.strictEqual(res.status, 401);
|
|
59
|
+
assert.ok(res.body.message);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return 401 when password is missing", async function () {
|
|
63
|
+
const res = await request(app)
|
|
64
|
+
.post("/auth/login")
|
|
65
|
+
.send({ email: "admin@system.local" });
|
|
66
|
+
assert.strictEqual(res.status, 401);
|
|
67
|
+
assert.ok(res.body.message);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return 401 with empty body", async function () {
|
|
71
|
+
const res = await request(app)
|
|
72
|
+
.post("/auth/login")
|
|
73
|
+
.send({});
|
|
74
|
+
assert.strictEqual(res.status, 401);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return 401 for non-existent user", async function () {
|
|
78
|
+
const res = await request(app)
|
|
79
|
+
.post("/auth/login")
|
|
80
|
+
.send({ email: "nobody@example.com", password: "wrong" });
|
|
81
|
+
assert.ok([401, 500].includes(res.status));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("POST /auth/logout", function () {
|
|
86
|
+
it("should return 401 when not authenticated", async function () {
|
|
87
|
+
const res = await request(app)
|
|
88
|
+
.post("/auth/logout");
|
|
89
|
+
assert.strictEqual(res.status, 401);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function generateUsersTest() {
|
|
97
|
+
return `import assert from "assert";
|
|
98
|
+
import express from "express";
|
|
99
|
+
import request from "supertest";
|
|
100
|
+
|
|
101
|
+
import usersRoute from "#routes/users/index.js";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a test app with a pre-populated session (bypasses real auth).
|
|
105
|
+
* Injects session data via middleware before the route.
|
|
106
|
+
*/
|
|
107
|
+
function createApp(sessionData) {
|
|
108
|
+
const app = express();
|
|
109
|
+
app.use(express.json());
|
|
110
|
+
// Mock session injection (no real session store needed for testing)
|
|
111
|
+
app.use((req, res, next) => {
|
|
112
|
+
req.session = {
|
|
113
|
+
user: sessionData.user,
|
|
114
|
+
role: sessionData.role,
|
|
115
|
+
permission: sessionData.permission,
|
|
116
|
+
};
|
|
117
|
+
next();
|
|
118
|
+
});
|
|
119
|
+
app.use("/users", usersRoute);
|
|
120
|
+
return app;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mockSession = {
|
|
124
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
125
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
126
|
+
permission: [
|
|
127
|
+
{ module: "users", action: "global", scope: "global" },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
describe("Users Routes (SaaS)", function () {
|
|
132
|
+
let app;
|
|
133
|
+
|
|
134
|
+
before(function () {
|
|
135
|
+
app = createApp(mockSession);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("GET /users/", function () {
|
|
139
|
+
it("should list users", async function () {
|
|
140
|
+
const res = await request(app).get("/users/");
|
|
141
|
+
assert.ok([200, 500].includes(res.status));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("POST /users/", function () {
|
|
146
|
+
it("should create a user", async function () {
|
|
147
|
+
const res = await request(app)
|
|
148
|
+
.post("/users/")
|
|
149
|
+
.send({
|
|
150
|
+
email: "test@example.com",
|
|
151
|
+
name: "Test User",
|
|
152
|
+
password_hash: "hashed",
|
|
153
|
+
unique_attribute: "test-unique",
|
|
154
|
+
role_id: 1,
|
|
155
|
+
});
|
|
156
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("PUT /users/:id", function () {
|
|
161
|
+
it("should update a user", async function () {
|
|
162
|
+
const res = await request(app)
|
|
163
|
+
.put("/users/1")
|
|
164
|
+
.send({ name: "Updated" });
|
|
165
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("DELETE /users/:id", function () {
|
|
170
|
+
it("should delete a user", async function () {
|
|
171
|
+
const res = await request(app).delete("/users/1");
|
|
172
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("Permission enforcement", function () {
|
|
177
|
+
it("should return 403 without proper permissions", async function () {
|
|
178
|
+
const noPermApp = createApp({
|
|
179
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
180
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
181
|
+
permission: [{ module: "tenants", action: "read", scope: "tenant" }],
|
|
182
|
+
});
|
|
183
|
+
const res = await request(noPermApp).get("/users/");
|
|
184
|
+
assert.strictEqual(res.status, 403);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function generateTenantsTest() {
|
|
192
|
+
return `import assert from "assert";
|
|
193
|
+
import express from "express";
|
|
194
|
+
import request from "supertest";
|
|
195
|
+
|
|
196
|
+
import tenantsRoute from "#routes/tenants/index.js";
|
|
197
|
+
|
|
198
|
+
function createApp(sessionData) {
|
|
199
|
+
const app = express();
|
|
200
|
+
app.use(express.json());
|
|
201
|
+
app.use((req, res, next) => {
|
|
202
|
+
req.session = {
|
|
203
|
+
user: sessionData.user,
|
|
204
|
+
role: sessionData.role,
|
|
205
|
+
permission: sessionData.permission,
|
|
206
|
+
};
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
app.use("/tenants", tenantsRoute);
|
|
210
|
+
return app;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mockSession = {
|
|
214
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
215
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
216
|
+
permission: [
|
|
217
|
+
{ module: "tenants", action: "global", scope: "global" },
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
describe("Tenants Routes (SaaS)", function () {
|
|
222
|
+
let app;
|
|
223
|
+
|
|
224
|
+
before(function () {
|
|
225
|
+
app = createApp(mockSession);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("GET /tenants/", function () {
|
|
229
|
+
it("should list tenants", async function () {
|
|
230
|
+
const res = await request(app).get("/tenants/");
|
|
231
|
+
assert.ok([200, 500].includes(res.status));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("POST /tenants/", function () {
|
|
236
|
+
it("should create a tenant", async function () {
|
|
237
|
+
const res = await request(app)
|
|
238
|
+
.post("/tenants/")
|
|
239
|
+
.send({ name: "Acme Corp", slug: "acme-corp" });
|
|
240
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("PUT /tenants/:id", function () {
|
|
245
|
+
it("should update a tenant", async function () {
|
|
246
|
+
const res = await request(app)
|
|
247
|
+
.put("/tenants/1")
|
|
248
|
+
.send({ name: "Acme Updated" });
|
|
249
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("DELETE /tenants/:id", function () {
|
|
254
|
+
it("should delete a tenant", async function () {
|
|
255
|
+
const res = await request(app).delete("/tenants/1");
|
|
256
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("Permission enforcement", function () {
|
|
261
|
+
it("should return 403 without tenants permission", async function () {
|
|
262
|
+
const noPermApp = createApp({
|
|
263
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
264
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
265
|
+
permission: [{ module: "users", action: "read", scope: "tenant" }],
|
|
266
|
+
});
|
|
267
|
+
const res = await request(noPermApp).get("/tenants/");
|
|
268
|
+
assert.strictEqual(res.status, 403);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function generateRolesTest() {
|
|
276
|
+
return `import assert from "assert";
|
|
277
|
+
import express from "express";
|
|
278
|
+
import request from "supertest";
|
|
279
|
+
|
|
280
|
+
import rolesRoute from "#routes/roles/index.js";
|
|
281
|
+
|
|
282
|
+
function createApp(sessionData) {
|
|
283
|
+
const app = express();
|
|
284
|
+
app.use(express.json());
|
|
285
|
+
app.use((req, res, next) => {
|
|
286
|
+
req.session = {
|
|
287
|
+
user: sessionData.user,
|
|
288
|
+
role: sessionData.role,
|
|
289
|
+
permission: sessionData.permission,
|
|
290
|
+
};
|
|
291
|
+
next();
|
|
292
|
+
});
|
|
293
|
+
app.use("/roles", rolesRoute);
|
|
294
|
+
return app;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const globalSession = {
|
|
298
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
299
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
300
|
+
permission: [
|
|
301
|
+
{ module: "roles", action: "global", scope: "global" },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const tenantSession = {
|
|
306
|
+
user: { user_id: 2, email: "tenant@test.com", name: "Tenant Admin", tenant_id: 1 },
|
|
307
|
+
role: { role_id: 2, name: "Tenant Admin", tenant_id: 1 },
|
|
308
|
+
permission: [
|
|
309
|
+
{ module: "roles", action: "read", scope: "tenant" },
|
|
310
|
+
{ module: "roles", action: "write", scope: "tenant" },
|
|
311
|
+
{ module: "roles", action: "update", scope: "tenant" },
|
|
312
|
+
{ module: "roles", action: "delete", scope: "tenant" },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
describe("Roles Routes (SaaS)", function () {
|
|
317
|
+
describe("with global permissions", function () {
|
|
318
|
+
let app;
|
|
319
|
+
|
|
320
|
+
before(function () {
|
|
321
|
+
app = createApp(globalSession);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("GET /roles/", function () {
|
|
325
|
+
it("should list roles", async function () {
|
|
326
|
+
const res = await request(app).get("/roles/");
|
|
327
|
+
assert.ok([200, 500].includes(res.status));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("POST /roles/", function () {
|
|
332
|
+
it("should create a role", async function () {
|
|
333
|
+
const res = await request(app)
|
|
334
|
+
.post("/roles/")
|
|
335
|
+
.send({ name: "Editor", tenant_id: 1 });
|
|
336
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should allow creating role with global permissions for global user", async function () {
|
|
340
|
+
const res = await request(app)
|
|
341
|
+
.post("/roles/")
|
|
342
|
+
.send({
|
|
343
|
+
name: "Global Role",
|
|
344
|
+
tenant_id: null,
|
|
345
|
+
permissions: [{ module: "users", action: "read", scope: "global" }],
|
|
346
|
+
});
|
|
347
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
348
|
+
// Should NOT be 403 for global user
|
|
349
|
+
assert.notStrictEqual(res.status, 403);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("with tenant permissions", function () {
|
|
355
|
+
let app;
|
|
356
|
+
|
|
357
|
+
before(function () {
|
|
358
|
+
app = createApp(tenantSession);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("POST /roles/", function () {
|
|
362
|
+
it("should return 403 when creating role with global permissions", async function () {
|
|
363
|
+
const res = await request(app)
|
|
364
|
+
.post("/roles/")
|
|
365
|
+
.send({
|
|
366
|
+
name: "Escalated Role",
|
|
367
|
+
permissions: [{ module: "users", action: "read", scope: "global" }],
|
|
368
|
+
});
|
|
369
|
+
assert.strictEqual(res.status, 403);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Permission enforcement", function () {
|
|
375
|
+
it("should return 403 without roles permission", async function () {
|
|
376
|
+
const noPermApp = createApp({
|
|
377
|
+
user: { user_id: 3, email: "noperm@test.com", name: "No Perm", tenant_id: 1 },
|
|
378
|
+
role: { role_id: 3, name: "None", tenant_id: 1 },
|
|
379
|
+
permission: [{ module: "tenants", action: "read", scope: "tenant" }],
|
|
380
|
+
});
|
|
381
|
+
const res = await request(noPermApp).get("/roles/");
|
|
382
|
+
assert.strictEqual(res.status, 403);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function generatePermissionsTest() {
|
|
390
|
+
return `import assert from "assert";
|
|
391
|
+
import express from "express";
|
|
392
|
+
import request from "supertest";
|
|
393
|
+
|
|
394
|
+
import permissionsRoute from "#routes/roles/permissions/index.js";
|
|
395
|
+
|
|
396
|
+
function createApp(sessionData) {
|
|
397
|
+
const app = express();
|
|
398
|
+
app.use(express.json());
|
|
399
|
+
app.use((req, res, next) => {
|
|
400
|
+
req.session = {
|
|
401
|
+
user: sessionData.user,
|
|
402
|
+
role: sessionData.role,
|
|
403
|
+
permission: sessionData.permission,
|
|
404
|
+
};
|
|
405
|
+
next();
|
|
406
|
+
});
|
|
407
|
+
app.use("/roles/:role_id/permissions", permissionsRoute);
|
|
408
|
+
return app;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const mockSession = {
|
|
412
|
+
user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
|
|
413
|
+
role: { role_id: 1, name: "Super Admin", tenant_id: null },
|
|
414
|
+
permission: [
|
|
415
|
+
{ module: "permissions", action: "global", scope: "global" },
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
describe("Permissions Routes (SaaS)", function () {
|
|
420
|
+
let app;
|
|
421
|
+
|
|
422
|
+
before(function () {
|
|
423
|
+
app = createApp(mockSession);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("GET /roles/:role_id/permissions/", function () {
|
|
427
|
+
it("should list permissions for a role", async function () {
|
|
428
|
+
const res = await request(app).get("/roles/1/permissions/");
|
|
429
|
+
assert.ok([200, 500].includes(res.status));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("POST /roles/:role_id/permissions/", function () {
|
|
434
|
+
it("should create a permission entry", async function () {
|
|
435
|
+
const res = await request(app)
|
|
436
|
+
.post("/roles/1/permissions/")
|
|
437
|
+
.send({ permission: { module: "users", action: "read", scope: "tenant" } });
|
|
438
|
+
assert.ok([200, 201, 400, 500].includes(res.status));
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe("PUT /roles/:role_id/permissions/:permission_id", function () {
|
|
443
|
+
it("should update a permission entry", async function () {
|
|
444
|
+
const res = await request(app)
|
|
445
|
+
.put("/roles/1/permissions/1")
|
|
446
|
+
.send({ permission: { module: "users", action: "write", scope: "tenant" } });
|
|
447
|
+
assert.ok([200, 400, 404, 500].includes(res.status));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("DELETE /roles/:role_id/permissions/:permission_id", function () {
|
|
452
|
+
it("should delete a permission entry", async function () {
|
|
453
|
+
const res = await request(app).delete("/roles/1/permissions/1");
|
|
454
|
+
assert.ok([200, 204, 404, 500].includes(res.status));
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("Permission enforcement", function () {
|
|
459
|
+
it("should return 403 without permissions module access", async function () {
|
|
460
|
+
const noPermApp = createApp({
|
|
461
|
+
user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
|
|
462
|
+
role: { role_id: 2, name: "Viewer", tenant_id: 1 },
|
|
463
|
+
permission: [{ module: "users", action: "read", scope: "tenant" }],
|
|
464
|
+
});
|
|
465
|
+
const res = await request(noPermApp).get("/roles/1/permissions/");
|
|
466
|
+
assert.strictEqual(res.status, 403);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = { generateSaasTests };
|