@terreno/api 0.16.1 → 0.18.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/dist/actions.test.js +144 -8
- package/dist/envConfigurationPlugin.test.js +49 -0
- package/package.json +1 -1
- package/src/actions.test.ts +150 -3
- package/src/envConfigurationPlugin.test.ts +37 -0
package/dist/actions.test.js
CHANGED
|
@@ -70,7 +70,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
70
70
|
var bun_test_1 = require("bun:test");
|
|
71
71
|
var mongoose_1 = require("mongoose");
|
|
72
72
|
var supertest_1 = __importDefault(require("supertest"));
|
|
73
|
-
var zod_1 = require("zod");
|
|
74
73
|
var actions_1 = require("./actions");
|
|
75
74
|
var api_1 = require("./api");
|
|
76
75
|
var auth_1 = require("./auth");
|
|
@@ -78,6 +77,7 @@ var errors_1 = require("./errors");
|
|
|
78
77
|
var permissions_1 = require("./permissions");
|
|
79
78
|
var plugins_1 = require("./plugins");
|
|
80
79
|
var tests_1 = require("./tests");
|
|
80
|
+
var zodOpenApi_1 = require("./zodOpenApi");
|
|
81
81
|
var stuffSchema = new mongoose_1.Schema({
|
|
82
82
|
name: { type: String },
|
|
83
83
|
ownerId: { type: String },
|
|
@@ -561,7 +561,7 @@ var allPermissions = {
|
|
|
561
561
|
mountFoodRouter({
|
|
562
562
|
collectionActions: {
|
|
563
563
|
notify: {
|
|
564
|
-
body:
|
|
564
|
+
body: zodOpenApi_1.z.object({ email: zodOpenApi_1.z.string().email() }),
|
|
565
565
|
handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
566
566
|
var body = _b.body;
|
|
567
567
|
return __generator(this, function (_c) {
|
|
@@ -591,7 +591,7 @@ var allPermissions = {
|
|
|
591
591
|
mountFoodRouter({
|
|
592
592
|
collectionActions: {
|
|
593
593
|
notify: {
|
|
594
|
-
body:
|
|
594
|
+
body: zodOpenApi_1.z.object({ email: zodOpenApi_1.z.string().email() }),
|
|
595
595
|
handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
596
596
|
return [2 /*return*/, ({ sent: true })];
|
|
597
597
|
}); }); },
|
|
@@ -628,7 +628,7 @@ var allPermissions = {
|
|
|
628
628
|
}); },
|
|
629
629
|
method: "GET",
|
|
630
630
|
permissions: [permissions_1.Permissions.IsAny],
|
|
631
|
-
query:
|
|
631
|
+
query: zodOpenApi_1.z.object({ count: zodOpenApi_1.z.coerce.number() }),
|
|
632
632
|
},
|
|
633
633
|
},
|
|
634
634
|
permissions: allPermissions,
|
|
@@ -642,6 +642,33 @@ var allPermissions = {
|
|
|
642
642
|
}
|
|
643
643
|
});
|
|
644
644
|
}); });
|
|
645
|
+
(0, bun_test_1.it)("returns 400 with meta.fields for invalid query", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
646
|
+
var res;
|
|
647
|
+
return __generator(this, function (_a) {
|
|
648
|
+
switch (_a.label) {
|
|
649
|
+
case 0:
|
|
650
|
+
mountFoodRouter({
|
|
651
|
+
collectionActions: {
|
|
652
|
+
lookup: {
|
|
653
|
+
handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
654
|
+
return [2 /*return*/, ({ ok: true })];
|
|
655
|
+
}); }); },
|
|
656
|
+
method: "GET",
|
|
657
|
+
permissions: [permissions_1.Permissions.IsAny],
|
|
658
|
+
query: zodOpenApi_1.z.object({ email: zodOpenApi_1.z.string().email() }),
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
permissions: allPermissions,
|
|
662
|
+
});
|
|
663
|
+
return [4 /*yield*/, server.get("/food/lookup?email=bad").expect(400)];
|
|
664
|
+
case 1:
|
|
665
|
+
res = _a.sent();
|
|
666
|
+
(0, bun_test_1.expect)(res.body.title).toBe("Validation failed");
|
|
667
|
+
(0, bun_test_1.expect)(res.body.meta.fields.email).toBeDefined();
|
|
668
|
+
return [2 /*return*/];
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}); });
|
|
645
672
|
(0, bun_test_1.it)("coerces body values via zod in ctx", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
646
673
|
var seenCount, res;
|
|
647
674
|
return __generator(this, function (_a) {
|
|
@@ -650,7 +677,7 @@ var allPermissions = {
|
|
|
650
677
|
mountFoodRouter({
|
|
651
678
|
collectionActions: {
|
|
652
679
|
tally: {
|
|
653
|
-
body:
|
|
680
|
+
body: zodOpenApi_1.z.object({ count: zodOpenApi_1.z.coerce.number() }),
|
|
654
681
|
handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
655
682
|
var parsed;
|
|
656
683
|
var body = _b.body;
|
|
@@ -684,7 +711,7 @@ var allPermissions = {
|
|
|
684
711
|
mountFoodRouter({
|
|
685
712
|
collectionActions: {
|
|
686
713
|
strictish: {
|
|
687
|
-
body:
|
|
714
|
+
body: zodOpenApi_1.z.object({ known: zodOpenApi_1.z.string() }),
|
|
688
715
|
handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
689
716
|
var body = _b.body;
|
|
690
717
|
return __generator(this, function (_c) {
|
|
@@ -910,7 +937,7 @@ var allPermissions = {
|
|
|
910
937
|
(0, bun_test_1.describe)("defineInstanceAction type ergonomics", function () {
|
|
911
938
|
(0, bun_test_1.it)("preserves handler types at compile time", function () {
|
|
912
939
|
var action = (0, actions_1.defineInstanceAction)({
|
|
913
|
-
body:
|
|
940
|
+
body: zodOpenApi_1.z.object({ notifyUsers: zodOpenApi_1.z.boolean() }),
|
|
914
941
|
handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
915
942
|
var _doc, _notify;
|
|
916
943
|
var _c, _d;
|
|
@@ -928,7 +955,7 @@ var allPermissions = {
|
|
|
928
955
|
});
|
|
929
956
|
(0, bun_test_1.it)("defineCollectionAction preserves body types", function () {
|
|
930
957
|
var action = (0, actions_1.defineCollectionAction)({
|
|
931
|
-
body:
|
|
958
|
+
body: zodOpenApi_1.z.object({ ids: zodOpenApi_1.z.array(zodOpenApi_1.z.string()) }),
|
|
932
959
|
handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
933
960
|
var _ids;
|
|
934
961
|
var body = _b.body;
|
|
@@ -943,4 +970,113 @@ var allPermissions = {
|
|
|
943
970
|
(0, bun_test_1.expect)(action.method).toBe("POST");
|
|
944
971
|
});
|
|
945
972
|
});
|
|
973
|
+
(0, bun_test_1.describe)("createActionOpenApiMiddleware", function () {
|
|
974
|
+
var createMockOpenApi = function () {
|
|
975
|
+
var captured;
|
|
976
|
+
var pathFn = function (op) {
|
|
977
|
+
captured = op;
|
|
978
|
+
return function (_req, _res, next) {
|
|
979
|
+
return next();
|
|
980
|
+
};
|
|
981
|
+
};
|
|
982
|
+
return {
|
|
983
|
+
get captured() {
|
|
984
|
+
return captured;
|
|
985
|
+
},
|
|
986
|
+
middleware: { path: pathFn },
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
(0, bun_test_1.it)("returns no-op middleware when openApi is not configured", function () {
|
|
990
|
+
var handler = (0, actions_1.createActionOpenApiMiddleware)({
|
|
991
|
+
action: { method: "GET", permissions: [] },
|
|
992
|
+
actionName: "test",
|
|
993
|
+
model: tests_1.FoodModel,
|
|
994
|
+
options: {},
|
|
995
|
+
scope: "collection",
|
|
996
|
+
});
|
|
997
|
+
(0, bun_test_1.expect)(typeof handler).toBe("function");
|
|
998
|
+
});
|
|
999
|
+
(0, bun_test_1.it)("generates query parameters from action.query schema", function () {
|
|
1000
|
+
var _a;
|
|
1001
|
+
var mock = createMockOpenApi();
|
|
1002
|
+
(0, actions_1.createActionOpenApiMiddleware)({
|
|
1003
|
+
action: {
|
|
1004
|
+
method: "GET",
|
|
1005
|
+
permissions: [],
|
|
1006
|
+
query: zodOpenApi_1.z.object({ page: zodOpenApi_1.z.coerce.number(), search: zodOpenApi_1.z.string() }),
|
|
1007
|
+
},
|
|
1008
|
+
actionName: "search",
|
|
1009
|
+
model: tests_1.FoodModel,
|
|
1010
|
+
options: { openApi: mock.middleware },
|
|
1011
|
+
scope: "collection",
|
|
1012
|
+
});
|
|
1013
|
+
var params = (_a = mock.captured) === null || _a === void 0 ? void 0 : _a.parameters;
|
|
1014
|
+
(0, bun_test_1.expect)(params.length).toBe(2);
|
|
1015
|
+
(0, bun_test_1.expect)(params.some(function (p) { return p.name === "search"; })).toBe(true);
|
|
1016
|
+
(0, bun_test_1.expect)(params.some(function (p) { return p.name === "page"; })).toBe(true);
|
|
1017
|
+
});
|
|
1018
|
+
(0, bun_test_1.it)("includes id path parameter for instance-scoped actions", function () {
|
|
1019
|
+
var _a;
|
|
1020
|
+
var mock = createMockOpenApi();
|
|
1021
|
+
(0, actions_1.createActionOpenApiMiddleware)({
|
|
1022
|
+
action: { method: "GET", permissions: [] },
|
|
1023
|
+
actionName: "peek",
|
|
1024
|
+
model: tests_1.FoodModel,
|
|
1025
|
+
options: { openApi: mock.middleware },
|
|
1026
|
+
scope: "instance",
|
|
1027
|
+
});
|
|
1028
|
+
var params = (_a = mock.captured) === null || _a === void 0 ? void 0 : _a.parameters;
|
|
1029
|
+
(0, bun_test_1.expect)(params.some(function (p) { return p.name === "id" && p.in === "path"; })).toBe(true);
|
|
1030
|
+
});
|
|
1031
|
+
(0, bun_test_1.it)("uses fallback data schema when response is not provided", function () {
|
|
1032
|
+
var _a;
|
|
1033
|
+
var mock = createMockOpenApi();
|
|
1034
|
+
(0, actions_1.createActionOpenApiMiddleware)({
|
|
1035
|
+
action: { method: "POST", permissions: [] },
|
|
1036
|
+
actionName: "noop",
|
|
1037
|
+
model: tests_1.FoodModel,
|
|
1038
|
+
options: { openApi: mock.middleware },
|
|
1039
|
+
scope: "collection",
|
|
1040
|
+
});
|
|
1041
|
+
var responses = (_a = mock.captured) === null || _a === void 0 ? void 0 : _a.responses;
|
|
1042
|
+
var ok = responses["200"];
|
|
1043
|
+
var schema = ok.content["application/json"].schema;
|
|
1044
|
+
(0, bun_test_1.expect)(schema.properties.data).toEqual({ type: "object" });
|
|
1045
|
+
});
|
|
1046
|
+
(0, bun_test_1.it)("uses zod response schema when provided", function () {
|
|
1047
|
+
var _a;
|
|
1048
|
+
var mock = createMockOpenApi();
|
|
1049
|
+
(0, actions_1.createActionOpenApiMiddleware)({
|
|
1050
|
+
action: {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
permissions: [],
|
|
1053
|
+
response: zodOpenApi_1.z.object({ count: zodOpenApi_1.z.number() }),
|
|
1054
|
+
},
|
|
1055
|
+
actionName: "tally",
|
|
1056
|
+
model: tests_1.FoodModel,
|
|
1057
|
+
options: { openApi: mock.middleware },
|
|
1058
|
+
scope: "collection",
|
|
1059
|
+
});
|
|
1060
|
+
var responses = (_a = mock.captured) === null || _a === void 0 ? void 0 : _a.responses;
|
|
1061
|
+
var ok = responses["200"];
|
|
1062
|
+
var schema = ok.content["application/json"].schema;
|
|
1063
|
+
(0, bun_test_1.expect)(schema.properties.data.properties).toBeDefined();
|
|
1064
|
+
});
|
|
1065
|
+
(0, bun_test_1.it)("includes requestBody when action.body is defined", function () {
|
|
1066
|
+
var _a;
|
|
1067
|
+
var mock = createMockOpenApi();
|
|
1068
|
+
(0, actions_1.createActionOpenApiMiddleware)({
|
|
1069
|
+
action: {
|
|
1070
|
+
body: zodOpenApi_1.z.object({ name: zodOpenApi_1.z.string() }),
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
permissions: [],
|
|
1073
|
+
},
|
|
1074
|
+
actionName: "create",
|
|
1075
|
+
model: tests_1.FoodModel,
|
|
1076
|
+
options: { openApi: mock.middleware },
|
|
1077
|
+
scope: "collection",
|
|
1078
|
+
});
|
|
1079
|
+
(0, bun_test_1.expect)((_a = mock.captured) === null || _a === void 0 ? void 0 : _a.requestBody).toBeDefined();
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
946
1082
|
});
|
|
@@ -319,4 +319,53 @@ var setupLoader = function () {
|
|
|
319
319
|
}
|
|
320
320
|
});
|
|
321
321
|
}); });
|
|
322
|
+
(0, bun_test_1.it)("mapToObject handles a plain Record (non-Map) env field", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
323
|
+
var col;
|
|
324
|
+
var _a;
|
|
325
|
+
return __generator(this, function (_b) {
|
|
326
|
+
switch (_b.label) {
|
|
327
|
+
case 0:
|
|
328
|
+
col = (_a = mongoose_1.default.connection.db) === null || _a === void 0 ? void 0 : _a.collection("testenvconfigs");
|
|
329
|
+
return [4 /*yield*/, (col === null || col === void 0 ? void 0 : col.insertOne({ env: { TERRENO_PLUGIN_KEY: "plainObj" } }))];
|
|
330
|
+
case 1:
|
|
331
|
+
_b.sent();
|
|
332
|
+
// Trigger refresh via findOneAndUpdate hook
|
|
333
|
+
return [4 /*yield*/, TestEnvConfig.findOneAndUpdate({}, { $set: { __v: 1 } })];
|
|
334
|
+
case 2:
|
|
335
|
+
// Trigger refresh via findOneAndUpdate hook
|
|
336
|
+
_b.sent();
|
|
337
|
+
(0, bun_test_1.expect)(config_1.Config.get("TERRENO_PLUGIN_KEY")).toBe("plainObj");
|
|
338
|
+
return [2 /*return*/];
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}); });
|
|
342
|
+
(0, bun_test_1.it)("refreshFromDoc logs a warning and does not throw when the model query fails", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
343
|
+
var doc, originalFind;
|
|
344
|
+
return __generator(this, function (_a) {
|
|
345
|
+
switch (_a.label) {
|
|
346
|
+
case 0:
|
|
347
|
+
doc = new TestEnvConfig();
|
|
348
|
+
doc.env.set("TERRENO_PLUGIN_KEY", "initial");
|
|
349
|
+
return [4 /*yield*/, doc.save()];
|
|
350
|
+
case 1:
|
|
351
|
+
_a.sent();
|
|
352
|
+
// Pre-set cache to verify it is NOT overwritten when the hook errors
|
|
353
|
+
config_1.Config.setCachedEnv({ TERRENO_PLUGIN_KEY: "cached" });
|
|
354
|
+
originalFind = TestEnvConfig.find;
|
|
355
|
+
TestEnvConfig.find = function () {
|
|
356
|
+
throw new Error("Simulated DB error");
|
|
357
|
+
};
|
|
358
|
+
// Trigger the post-findOneAndUpdate hook → refreshFromDoc → findOneOrNoneFor → throws
|
|
359
|
+
return [4 /*yield*/, TestEnvConfig.findOneAndUpdate({ _id: doc._id }, { $set: { __v: 2 } })];
|
|
360
|
+
case 2:
|
|
361
|
+
// Trigger the post-findOneAndUpdate hook → refreshFromDoc → findOneOrNoneFor → throws
|
|
362
|
+
_a.sent();
|
|
363
|
+
// Restore immediately
|
|
364
|
+
TestEnvConfig.find = originalFind;
|
|
365
|
+
// The catch block should have swallowed the error; cache keeps old value
|
|
366
|
+
(0, bun_test_1.expect)(config_1.Config.get("TERRENO_PLUGIN_KEY")).toBe("cached");
|
|
367
|
+
return [2 /*return*/];
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}); });
|
|
322
371
|
});
|
package/package.json
CHANGED
package/src/actions.test.ts
CHANGED
|
@@ -4,14 +4,19 @@ import type express from "express";
|
|
|
4
4
|
import {type Model, model, Schema} from "mongoose";
|
|
5
5
|
import supertest from "supertest";
|
|
6
6
|
import type TestAgent from "supertest/lib/agent";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
ACTION_NAME_PATTERN,
|
|
9
|
+
createActionOpenApiMiddleware,
|
|
10
|
+
defineCollectionAction,
|
|
11
|
+
defineInstanceAction,
|
|
12
|
+
} from "./actions";
|
|
13
|
+
import {modelRouter, type OpenApiMiddleware} from "./api";
|
|
10
14
|
import {addAuthRoutes, setupAuth} from "./auth";
|
|
11
15
|
import {apiUnauthorizedMiddleware} from "./errors";
|
|
12
16
|
import {Permissions} from "./permissions";
|
|
13
17
|
import {type IsDeleted, isDeletedPlugin} from "./plugins";
|
|
14
18
|
import {authAsUser, type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
|
|
19
|
+
import {z} from "./zodOpenApi";
|
|
15
20
|
|
|
16
21
|
interface Stuff extends IsDeleted {
|
|
17
22
|
_id: string;
|
|
@@ -430,6 +435,23 @@ describe("modelRouter actions", () => {
|
|
|
430
435
|
expect(originalQ).toBe("5");
|
|
431
436
|
});
|
|
432
437
|
|
|
438
|
+
it("returns 400 with meta.fields for invalid query", async () => {
|
|
439
|
+
mountFoodRouter({
|
|
440
|
+
collectionActions: {
|
|
441
|
+
lookup: {
|
|
442
|
+
handler: async () => ({ok: true}),
|
|
443
|
+
method: "GET",
|
|
444
|
+
permissions: [Permissions.IsAny],
|
|
445
|
+
query: z.object({email: z.string().email()}),
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
permissions: allPermissions,
|
|
449
|
+
});
|
|
450
|
+
const res = await server.get("/food/lookup?email=bad").expect(400);
|
|
451
|
+
expect(res.body.title).toBe("Validation failed");
|
|
452
|
+
expect(res.body.meta.fields.email).toBeDefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
433
455
|
it("coerces body values via zod in ctx", async () => {
|
|
434
456
|
let seenCount: number | undefined;
|
|
435
457
|
mountFoodRouter({
|
|
@@ -633,4 +655,129 @@ describe("modelRouter actions", () => {
|
|
|
633
655
|
expect(action.method).toBe("POST");
|
|
634
656
|
});
|
|
635
657
|
});
|
|
658
|
+
|
|
659
|
+
describe("createActionOpenApiMiddleware", () => {
|
|
660
|
+
const createMockOpenApi = (): {
|
|
661
|
+
captured: Record<string, unknown> | undefined;
|
|
662
|
+
middleware: OpenApiMiddleware;
|
|
663
|
+
} => {
|
|
664
|
+
let captured: Record<string, unknown> | undefined;
|
|
665
|
+
const pathFn = (op: Record<string, unknown>): express.RequestHandler => {
|
|
666
|
+
captured = op;
|
|
667
|
+
return (_req: express.Request, _res: express.Response, next: express.NextFunction) =>
|
|
668
|
+
next();
|
|
669
|
+
};
|
|
670
|
+
return {
|
|
671
|
+
get captured() {
|
|
672
|
+
return captured;
|
|
673
|
+
},
|
|
674
|
+
middleware: {path: pathFn} as unknown as OpenApiMiddleware,
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
it("returns no-op middleware when openApi is not configured", () => {
|
|
679
|
+
const handler = createActionOpenApiMiddleware({
|
|
680
|
+
action: {method: "GET", permissions: []},
|
|
681
|
+
actionName: "test",
|
|
682
|
+
model: FoodModel as Model<Food>,
|
|
683
|
+
options: {},
|
|
684
|
+
scope: "collection",
|
|
685
|
+
});
|
|
686
|
+
expect(typeof handler).toBe("function");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("generates query parameters from action.query schema", () => {
|
|
690
|
+
const mock = createMockOpenApi();
|
|
691
|
+
createActionOpenApiMiddleware({
|
|
692
|
+
action: {
|
|
693
|
+
method: "GET",
|
|
694
|
+
permissions: [],
|
|
695
|
+
query: z.object({page: z.coerce.number(), search: z.string()}),
|
|
696
|
+
},
|
|
697
|
+
actionName: "search",
|
|
698
|
+
model: FoodModel as Model<Food>,
|
|
699
|
+
options: {openApi: mock.middleware},
|
|
700
|
+
scope: "collection",
|
|
701
|
+
});
|
|
702
|
+
const params = mock.captured?.parameters as Record<string, unknown>[];
|
|
703
|
+
expect(params.length).toBe(2);
|
|
704
|
+
expect(params.some((p) => p.name === "search")).toBe(true);
|
|
705
|
+
expect(params.some((p) => p.name === "page")).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("includes id path parameter for instance-scoped actions", () => {
|
|
709
|
+
const mock = createMockOpenApi();
|
|
710
|
+
createActionOpenApiMiddleware({
|
|
711
|
+
action: {method: "GET", permissions: []},
|
|
712
|
+
actionName: "peek",
|
|
713
|
+
model: FoodModel as Model<Food>,
|
|
714
|
+
options: {openApi: mock.middleware},
|
|
715
|
+
scope: "instance",
|
|
716
|
+
});
|
|
717
|
+
const params = mock.captured?.parameters as Record<string, unknown>[];
|
|
718
|
+
expect(params.some((p) => p.name === "id" && p.in === "path")).toBe(true);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("uses fallback data schema when response is not provided", () => {
|
|
722
|
+
const mock = createMockOpenApi();
|
|
723
|
+
createActionOpenApiMiddleware({
|
|
724
|
+
action: {method: "POST", permissions: []},
|
|
725
|
+
actionName: "noop",
|
|
726
|
+
model: FoodModel as Model<Food>,
|
|
727
|
+
options: {openApi: mock.middleware},
|
|
728
|
+
scope: "collection",
|
|
729
|
+
});
|
|
730
|
+
const responses = mock.captured?.responses as Record<string, Record<string, unknown>>;
|
|
731
|
+
const ok = responses["200"] as Record<
|
|
732
|
+
string,
|
|
733
|
+
Record<string, Record<string, Record<string, unknown>>>
|
|
734
|
+
>;
|
|
735
|
+
const schema = ok.content["application/json"].schema as Record<
|
|
736
|
+
string,
|
|
737
|
+
Record<string, unknown>
|
|
738
|
+
>;
|
|
739
|
+
expect(schema.properties.data).toEqual({type: "object"});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("uses zod response schema when provided", () => {
|
|
743
|
+
const mock = createMockOpenApi();
|
|
744
|
+
createActionOpenApiMiddleware({
|
|
745
|
+
action: {
|
|
746
|
+
method: "POST",
|
|
747
|
+
permissions: [],
|
|
748
|
+
response: z.object({count: z.number()}),
|
|
749
|
+
},
|
|
750
|
+
actionName: "tally",
|
|
751
|
+
model: FoodModel as Model<Food>,
|
|
752
|
+
options: {openApi: mock.middleware},
|
|
753
|
+
scope: "collection",
|
|
754
|
+
});
|
|
755
|
+
const responses = mock.captured?.responses as Record<string, Record<string, unknown>>;
|
|
756
|
+
const ok = responses["200"] as Record<
|
|
757
|
+
string,
|
|
758
|
+
Record<string, Record<string, Record<string, unknown>>>
|
|
759
|
+
>;
|
|
760
|
+
const schema = ok.content["application/json"].schema as Record<
|
|
761
|
+
string,
|
|
762
|
+
Record<string, unknown>
|
|
763
|
+
>;
|
|
764
|
+
expect((schema.properties.data as Record<string, unknown>).properties).toBeDefined();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it("includes requestBody when action.body is defined", () => {
|
|
768
|
+
const mock = createMockOpenApi();
|
|
769
|
+
createActionOpenApiMiddleware({
|
|
770
|
+
action: {
|
|
771
|
+
body: z.object({name: z.string()}),
|
|
772
|
+
method: "POST",
|
|
773
|
+
permissions: [],
|
|
774
|
+
},
|
|
775
|
+
actionName: "create",
|
|
776
|
+
model: FoodModel as Model<Food>,
|
|
777
|
+
options: {openApi: mock.middleware},
|
|
778
|
+
scope: "collection",
|
|
779
|
+
});
|
|
780
|
+
expect(mock.captured?.requestBody).toBeDefined();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
636
783
|
});
|
|
@@ -140,4 +140,41 @@ describe("envConfigurationPlugin", () => {
|
|
|
140
140
|
// mapToObject(undefined) returns {}, so Config falls back to the registered default
|
|
141
141
|
expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("fallback");
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it("mapToObject handles a plain Record (non-Map) env field", async () => {
|
|
145
|
+
// Insert a document with env as a plain object via raw DB operation
|
|
146
|
+
const col = mongoose.connection.db?.collection("testenvconfigs");
|
|
147
|
+
await col?.insertOne({env: {TERRENO_PLUGIN_KEY: "plainObj"}});
|
|
148
|
+
|
|
149
|
+
// Trigger refresh via findOneAndUpdate hook
|
|
150
|
+
await TestEnvConfig.findOneAndUpdate({}, {$set: {__v: 1}});
|
|
151
|
+
|
|
152
|
+
expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("plainObj");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("refreshFromDoc logs a warning and does not throw when the model query fails", async () => {
|
|
156
|
+
// Seed a document so findOneAndUpdate has a target
|
|
157
|
+
const doc = new TestEnvConfig();
|
|
158
|
+
doc.env.set("TERRENO_PLUGIN_KEY", "initial");
|
|
159
|
+
await doc.save();
|
|
160
|
+
|
|
161
|
+
// Pre-set cache to verify it is NOT overwritten when the hook errors
|
|
162
|
+
Config.setCachedEnv({TERRENO_PLUGIN_KEY: "cached"});
|
|
163
|
+
|
|
164
|
+
// Override Model.find to simulate a DB error inside refreshFromDoc
|
|
165
|
+
// (findOneOrNoneFor falls back to model.find when the findOneOrNone static is absent)
|
|
166
|
+
const originalFind = TestEnvConfig.find;
|
|
167
|
+
(TestEnvConfig as any).find = () => {
|
|
168
|
+
throw new Error("Simulated DB error");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Trigger the post-findOneAndUpdate hook → refreshFromDoc → findOneOrNoneFor → throws
|
|
172
|
+
await TestEnvConfig.findOneAndUpdate({_id: doc._id}, {$set: {__v: 2}});
|
|
173
|
+
|
|
174
|
+
// Restore immediately
|
|
175
|
+
(TestEnvConfig as any).find = originalFind;
|
|
176
|
+
|
|
177
|
+
// The catch block should have swallowed the error; cache keeps old value
|
|
178
|
+
expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("cached");
|
|
179
|
+
});
|
|
143
180
|
});
|