@terreno/api 0.20.2 → 0.22.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/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/bunfig.toml +1 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +418 -43
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/models/consentForm.js +2 -1
- package/dist/models/consentResponse.js +2 -1
- package/dist/models/versionConfig.js +2 -1
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.d.ts +18 -0
- package/dist/openApiBuilder.js +21 -0
- package/dist/openApiBuilder.test.js +34 -10
- package/dist/permissions.test.js +10 -43
- package/dist/populate.test.js +10 -42
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/syncConsents.test.js +2 -2
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +66 -262
- package/dist/tests/createTestData.d.ts +9 -0
- package/dist/tests/createTestData.js +272 -0
- package/dist/tests/models.d.ts +71 -0
- package/dist/tests/models.js +134 -0
- package/dist/tests/mongoTestSetup.d.ts +7 -0
- package/dist/tests/mongoTestSetup.js +150 -0
- package/dist/tests/testEnv.d.ts +0 -0
- package/dist/tests/testEnv.js +6 -0
- package/dist/tests/testHelper.d.ts +22 -0
- package/dist/tests/testHelper.js +115 -0
- package/dist/tests/types.d.ts +29 -0
- package/dist/tests/types.js +2 -0
- package/dist/tests.d.ts +10 -78
- package/dist/tests.js +24 -241
- package/dist/transformers.test.js +14 -50
- package/package.json +18 -4
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +287 -39
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/models/consentForm.ts +3 -4
- package/src/models/consentResponse.ts +6 -4
- package/src/models/versionConfig.ts +3 -4
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +27 -10
- package/src/openApiBuilder.ts +24 -0
- package/src/permissions.test.ts +8 -23
- package/src/populate.test.ts +7 -22
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/syncConsents.test.ts +1 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +22 -236
- package/src/tests/createTestData.ts +176 -0
- package/src/tests/models.ts +164 -0
- package/src/tests/mongoTestSetup.ts +69 -0
- package/src/tests/testEnv.ts +4 -0
- package/src/tests/testHelper.ts +57 -0
- package/src/tests/types.ts +35 -0
- package/src/tests.ts +40 -231
- package/src/transformers.test.ts +11 -30
- package/tsconfig.typedoc.json +4 -0
- package/dist/tests/index.d.ts +0 -1
- package/dist/tests/index.js +0 -17
- package/src/tests/index.ts +0 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import mongoose, {type Model, model, Schema} from "mongoose";
|
|
2
|
+
import passportLocalMongoose from "passport-local-mongoose";
|
|
3
|
+
|
|
4
|
+
import {createdUpdatedPlugin, DateOnly, isDisabledPlugin} from "../plugins";
|
|
5
|
+
|
|
6
|
+
export interface User {
|
|
7
|
+
admin: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
username: string;
|
|
10
|
+
email: string;
|
|
11
|
+
age?: number;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SuperUser extends User {
|
|
16
|
+
superTitle: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StaffUser extends User {
|
|
20
|
+
department: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FoodCategory {
|
|
24
|
+
_id?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
show: boolean;
|
|
27
|
+
created: Date;
|
|
28
|
+
updated: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Food {
|
|
32
|
+
_id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
calories: number;
|
|
35
|
+
created: Date;
|
|
36
|
+
ownerId: mongoose.Types.ObjectId | User;
|
|
37
|
+
hidden?: boolean;
|
|
38
|
+
source: {
|
|
39
|
+
name: string;
|
|
40
|
+
href?: string;
|
|
41
|
+
dateAdded?: string;
|
|
42
|
+
};
|
|
43
|
+
tags: string[];
|
|
44
|
+
eatenBy: [Schema.Types.ObjectId | User];
|
|
45
|
+
lastEatenWith: {[name: string]: Date};
|
|
46
|
+
categories: FoodCategory[];
|
|
47
|
+
expiration: string;
|
|
48
|
+
likesIds: {userId: string; likes: boolean}[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RequiredField {
|
|
52
|
+
name: string;
|
|
53
|
+
about?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const userSchema = new Schema<User>({
|
|
57
|
+
admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
|
|
58
|
+
age: {description: "The user's age", type: Number},
|
|
59
|
+
name: {description: "The user's display name", type: String},
|
|
60
|
+
username: {description: "The user's username", type: String},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
userSchema.plugin(
|
|
64
|
+
passportLocalMongoose as unknown as (schema: Schema, options?: Record<string, unknown>) => void,
|
|
65
|
+
{
|
|
66
|
+
attemptsField: "attempts",
|
|
67
|
+
interval: process.env.NODE_ENV === "test" ? 1 : 100,
|
|
68
|
+
limitAttempts: true,
|
|
69
|
+
maxAttempts: 3,
|
|
70
|
+
maxInterval: process.env.NODE_ENV === "test" ? 1 : 300000,
|
|
71
|
+
usernameCaseInsensitive: true,
|
|
72
|
+
usernameField: "email",
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
userSchema.plugin(createdUpdatedPlugin);
|
|
76
|
+
userSchema.plugin(isDisabledPlugin);
|
|
77
|
+
userSchema.methods.postCreate = async function (body: {age?: number}) {
|
|
78
|
+
this.age = body.age;
|
|
79
|
+
return this.save();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const UserModel = model<User>("User", userSchema);
|
|
83
|
+
|
|
84
|
+
const superUserSchema = new Schema<SuperUser>({
|
|
85
|
+
superTitle: {description: "The super user's title", required: true, type: String},
|
|
86
|
+
});
|
|
87
|
+
export const SuperUserModel = UserModel.discriminator("SuperUser", superUserSchema);
|
|
88
|
+
|
|
89
|
+
const staffUserSchema = new Schema<StaffUser>({
|
|
90
|
+
department: {
|
|
91
|
+
description: "The department the staff member belongs to",
|
|
92
|
+
required: true,
|
|
93
|
+
type: String,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
export const StaffUserModel = UserModel.discriminator("Staff", staffUserSchema);
|
|
97
|
+
|
|
98
|
+
const foodCategorySchema = new Schema<FoodCategory>(
|
|
99
|
+
{
|
|
100
|
+
name: {description: "The name of the food category", type: String},
|
|
101
|
+
show: {description: "Whether this category is visible", type: Boolean},
|
|
102
|
+
},
|
|
103
|
+
{timestamps: {createdAt: "created", updatedAt: "updated"}}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
interface Likes {
|
|
107
|
+
likes: boolean;
|
|
108
|
+
userId: mongoose.Types.ObjectId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const likesSchema = new Schema<Likes>({
|
|
112
|
+
likes: {description: "Whether the user liked the item", type: Boolean},
|
|
113
|
+
userId: {description: "The user who liked the item", ref: "User", type: "ObjectId"},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const foodSchema = new Schema<Food>(
|
|
117
|
+
{
|
|
118
|
+
calories: {description: "Number of calories in the food", type: Number},
|
|
119
|
+
categories: {description: "Categories this food belongs to", type: [foodCategorySchema]},
|
|
120
|
+
created: {description: "When this food was created", type: Date},
|
|
121
|
+
eatenBy: [
|
|
122
|
+
{
|
|
123
|
+
description: "Users who have eaten this food",
|
|
124
|
+
ref: "User",
|
|
125
|
+
required: true,
|
|
126
|
+
type: Schema.Types.ObjectId,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
// biome-ignore lint/suspicious/noExplicitAny: DateOnly is a custom SchemaType not recognized by Mongoose's built-in type definitions
|
|
130
|
+
expiration: {description: "Expiration date of the food", type: DateOnly as any},
|
|
131
|
+
hidden: {
|
|
132
|
+
default: false,
|
|
133
|
+
description: "Whether this food is hidden from listings",
|
|
134
|
+
type: Boolean,
|
|
135
|
+
},
|
|
136
|
+
lastEatenWith: {
|
|
137
|
+
description: "Map of user names to dates they last ate this food with",
|
|
138
|
+
of: Date,
|
|
139
|
+
type: Map,
|
|
140
|
+
},
|
|
141
|
+
likesIds: {description: "User likes for this food", required: true, type: [likesSchema]},
|
|
142
|
+
name: {description: "The name of the food", type: String},
|
|
143
|
+
ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
|
|
144
|
+
source: {
|
|
145
|
+
dateAdded: {description: "When the source was added", type: String},
|
|
146
|
+
href: {description: "URL of the source", type: String},
|
|
147
|
+
name: {description: "Name of the source", type: String},
|
|
148
|
+
},
|
|
149
|
+
tags: {description: "Tags associated with this food", type: [String]},
|
|
150
|
+
},
|
|
151
|
+
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
foodSchema.virtual("description").get(function (this: Food) {
|
|
155
|
+
return `${this.name} has ${this.calories} calories`;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const FoodModel: Model<Food> = model<Food>("Food", foodSchema);
|
|
159
|
+
|
|
160
|
+
const requiredSchema = new Schema<RequiredField>({
|
|
161
|
+
about: {description: "Information about the item", type: String},
|
|
162
|
+
name: {description: "The name of the item", required: true, type: String},
|
|
163
|
+
});
|
|
164
|
+
export const RequiredModel = model<RequiredField>("Required", requiredSchema);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {createMongoTestCache} from "@terreno/test";
|
|
5
|
+
|
|
6
|
+
import {createTestData, loadTestDataFromDocuments, toCachedTestData} from "./createTestData";
|
|
7
|
+
import type {CachedTestData, TestData} from "./types";
|
|
8
|
+
|
|
9
|
+
const moduleDir = __dirname;
|
|
10
|
+
const defaultCacheDir =
|
|
11
|
+
process.env.TERRENO_TEST_CACHE_DIR || path.join("/tmp", "terreno-api-test-cache");
|
|
12
|
+
|
|
13
|
+
const apiTestCache = createMongoTestCache({
|
|
14
|
+
baseDatabaseName: "terrenoTest_base",
|
|
15
|
+
cacheDir: defaultCacheDir,
|
|
16
|
+
createTestData: async (): Promise<CachedTestData> => {
|
|
17
|
+
const testData = await createTestData();
|
|
18
|
+
return toCachedTestData(testData);
|
|
19
|
+
},
|
|
20
|
+
sourceDirs: [path.resolve(moduleDir, "..")],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const {cacheFilesExist, cleanCache, loadTestDataFromCache, setupTestCache} = apiTestCache;
|
|
24
|
+
|
|
25
|
+
let cachedTestData: TestData | undefined;
|
|
26
|
+
|
|
27
|
+
/** Loads fixture documents after the collection cache has been restored. */
|
|
28
|
+
export const loadTestData = async (): Promise<TestData> => {
|
|
29
|
+
if (cachedTestData) {
|
|
30
|
+
return cachedTestData;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cachedFilePath = path.join(defaultCacheDir, "cached-data.json");
|
|
34
|
+
const cached = JSON.parse(fs.readFileSync(cachedFilePath, "utf-8")) as CachedTestData;
|
|
35
|
+
cachedTestData = await loadTestDataFromDocuments(cached);
|
|
36
|
+
return cachedTestData;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const resetCachedTestData = (): void => {
|
|
40
|
+
cachedTestData = undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (process.argv[1]?.includes("mongoTestSetup")) {
|
|
44
|
+
const command = process.argv[2];
|
|
45
|
+
const force = process.argv.includes("--force");
|
|
46
|
+
|
|
47
|
+
void (async (): Promise<void> => {
|
|
48
|
+
if (command === "setup") {
|
|
49
|
+
await setupTestCache({force});
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === "clean") {
|
|
54
|
+
cleanCache();
|
|
55
|
+
resetCachedTestData();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (command === "status") {
|
|
60
|
+
console.info(`[mongoTestSetup] cache exists: ${cacheFilesExist()}`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(
|
|
65
|
+
`[mongoTestSetup] Unknown command: ${command ?? "(none)"}. Usage: setup | clean | status`
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {ensureTestMongooseConnected} from "@terreno/test";
|
|
2
|
+
|
|
3
|
+
import {logger} from "../logger";
|
|
4
|
+
import {clearTestCollections, createTestData, createTestUsers} from "./createTestData";
|
|
5
|
+
import {FoodModel, UserModel} from "./models";
|
|
6
|
+
import {loadTestData} from "./mongoTestSetup";
|
|
7
|
+
import type {TestData} from "./types";
|
|
8
|
+
|
|
9
|
+
const defaultTestMongoUri = "mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000";
|
|
10
|
+
|
|
11
|
+
export const applyTestAuthEnv = (): void => {
|
|
12
|
+
process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
|
|
13
|
+
process.env.TOKEN_SECRET = "secret";
|
|
14
|
+
process.env.TOKEN_EXPIRES_IN = "30m";
|
|
15
|
+
process.env.TOKEN_ISSUER = "example.com";
|
|
16
|
+
process.env.SESSION_SECRET = "session";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ensureConnected = async (): Promise<void> => {
|
|
20
|
+
await ensureTestMongooseConnected({
|
|
21
|
+
defaultUri: defaultTestMongoUri,
|
|
22
|
+
onConnectError: logger.catch,
|
|
23
|
+
});
|
|
24
|
+
applyTestAuthEnv();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Seeds only the standard users (legacy helper). */
|
|
28
|
+
export const setupDb = async () => {
|
|
29
|
+
await ensureConnected();
|
|
30
|
+
|
|
31
|
+
await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]).catch(logger.catch);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const users = await createTestUsers();
|
|
35
|
+
return [users.admin, users.notAdmin, users.adminOther] as const;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error("Error setting up DB", error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Seeds users, foods, and required docs — the recommended API test baseline. */
|
|
43
|
+
export const setupTestData = async (): Promise<TestData> => {
|
|
44
|
+
await ensureConnected();
|
|
45
|
+
|
|
46
|
+
if (process.env.TERRENO_TEST_USE_FIXTURE_CACHE === "true") {
|
|
47
|
+
return loadTestData();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return createTestData();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Clears all API test collections without re-seeding. */
|
|
54
|
+
export const resetTestCollections = clearTestCollections;
|
|
55
|
+
|
|
56
|
+
export {createTestData} from "./createTestData";
|
|
57
|
+
export {loadTestData} from "./mongoTestSetup";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {HydratedDocument} from "mongoose";
|
|
2
|
+
|
|
3
|
+
import type {Food, RequiredField, User} from "./models";
|
|
4
|
+
|
|
5
|
+
export interface TestUsers {
|
|
6
|
+
admin: HydratedDocument<User>;
|
|
7
|
+
adminOther: HydratedDocument<User>;
|
|
8
|
+
notAdmin: HydratedDocument<User>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TestFoods {
|
|
12
|
+
apple: HydratedDocument<Food>;
|
|
13
|
+
carrots: HydratedDocument<Food>;
|
|
14
|
+
pizza: HydratedDocument<Food>;
|
|
15
|
+
spinach: HydratedDocument<Food>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TestRequired {
|
|
19
|
+
sample: HydratedDocument<RequiredField>;
|
|
20
|
+
withAbout: HydratedDocument<RequiredField>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Canonical API integration-test fixture graph. */
|
|
24
|
+
export interface TestData {
|
|
25
|
+
foods: TestFoods;
|
|
26
|
+
required: TestRequired;
|
|
27
|
+
users: TestUsers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** JSON-serializable snapshot stored alongside collection cache files. */
|
|
31
|
+
export interface CachedTestData {
|
|
32
|
+
foods: Record<keyof TestFoods, string>;
|
|
33
|
+
required: Record<keyof TestRequired, string>;
|
|
34
|
+
users: Record<keyof TestUsers, string>;
|
|
35
|
+
}
|
package/src/tests.ts
CHANGED
|
@@ -1,200 +1,48 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
authAsUser as authAsUserWithCredentials,
|
|
3
|
+
getBaseServer as createBaseTestServer,
|
|
4
|
+
} from "@terreno/test";
|
|
5
|
+
import type express from "express";
|
|
6
|
+
import type {Express} from "express";
|
|
6
7
|
import type TestAgent from "supertest/lib/agent";
|
|
7
8
|
|
|
8
|
-
import {logger} from "./logger";
|
|
9
9
|
import {patchAppUse} from "./openApiCompat";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
calories: number;
|
|
41
|
-
created: Date;
|
|
42
|
-
ownerId: mongoose.Types.ObjectId | User;
|
|
43
|
-
hidden?: boolean;
|
|
44
|
-
source: {
|
|
45
|
-
name: string;
|
|
46
|
-
href?: string;
|
|
47
|
-
dateAdded?: string;
|
|
48
|
-
};
|
|
49
|
-
tags: string[];
|
|
50
|
-
eatenBy: [Schema.Types.ObjectId | User];
|
|
51
|
-
// We want to test that map type works.
|
|
52
|
-
lastEatenWith: {[name: string]: Date};
|
|
53
|
-
categories: FoodCategory[];
|
|
54
|
-
expiration: string;
|
|
55
|
-
likesIds: {userId: string; likes: boolean}[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const userSchema = new Schema<User>({
|
|
59
|
-
admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
|
|
60
|
-
age: {description: "The user's age", type: Number},
|
|
61
|
-
name: {description: "The user's display name", type: String},
|
|
62
|
-
username: {description: "The user's username", type: String},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
userSchema.plugin(
|
|
66
|
-
passportLocalMongoose as unknown as (schema: Schema, options?: Record<string, unknown>) => void,
|
|
67
|
-
{
|
|
68
|
-
attemptsField: "attempts",
|
|
69
|
-
interval: process.env.NODE_ENV === "test" ? 1 : 100,
|
|
70
|
-
limitAttempts: true,
|
|
71
|
-
maxAttempts: 3,
|
|
72
|
-
maxInterval: process.env.NODE_ENV === "test" ? 1 : 300000,
|
|
73
|
-
usernameCaseInsensitive: true,
|
|
74
|
-
usernameField: "email",
|
|
75
|
-
}
|
|
76
|
-
);
|
|
77
|
-
// userSchema.plugin(tokenPlugin);
|
|
78
|
-
userSchema.plugin(createdUpdatedPlugin);
|
|
79
|
-
userSchema.plugin(isDisabledPlugin);
|
|
80
|
-
userSchema.methods.postCreate = async function (body: {age?: number}) {
|
|
81
|
-
this.age = body.age;
|
|
82
|
-
return this.save();
|
|
10
|
+
import {createTestData} from "./tests/createTestData";
|
|
11
|
+
import {
|
|
12
|
+
type Food,
|
|
13
|
+
type FoodCategory,
|
|
14
|
+
FoodModel,
|
|
15
|
+
type RequiredField,
|
|
16
|
+
RequiredModel,
|
|
17
|
+
type StaffUser,
|
|
18
|
+
StaffUserModel,
|
|
19
|
+
type SuperUser,
|
|
20
|
+
SuperUserModel,
|
|
21
|
+
type User,
|
|
22
|
+
UserModel,
|
|
23
|
+
} from "./tests/models";
|
|
24
|
+
import {loadTestDataFromCache, setupTestCache} from "./tests/mongoTestSetup";
|
|
25
|
+
import {setupDb, setupTestData} from "./tests/testHelper";
|
|
26
|
+
import type {TestData} from "./tests/types";
|
|
27
|
+
|
|
28
|
+
export type {Food, FoodCategory, RequiredField, StaffUser, SuperUser, TestData, User};
|
|
29
|
+
export {
|
|
30
|
+
createTestData,
|
|
31
|
+
FoodModel,
|
|
32
|
+
loadTestDataFromCache,
|
|
33
|
+
RequiredModel,
|
|
34
|
+
StaffUserModel,
|
|
35
|
+
SuperUserModel,
|
|
36
|
+
setupDb,
|
|
37
|
+
setupTestCache,
|
|
38
|
+
setupTestData,
|
|
39
|
+
UserModel,
|
|
83
40
|
};
|
|
84
41
|
|
|
85
|
-
export const UserModel = model<User>("User", userSchema);
|
|
86
|
-
|
|
87
|
-
const superUserSchema = new Schema<SuperUser>({
|
|
88
|
-
superTitle: {description: "The super user's title", required: true, type: String},
|
|
89
|
-
});
|
|
90
|
-
export const SuperUserModel = UserModel.discriminator("SuperUser", superUserSchema);
|
|
91
|
-
|
|
92
|
-
const staffUserSchema = new Schema<StaffUser>({
|
|
93
|
-
department: {
|
|
94
|
-
description: "The department the staff member belongs to",
|
|
95
|
-
required: true,
|
|
96
|
-
type: String,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
export const StaffUserModel = UserModel.discriminator("Staff", staffUserSchema);
|
|
100
|
-
|
|
101
|
-
const foodCategorySchema = new Schema<FoodCategory>(
|
|
102
|
-
{
|
|
103
|
-
name: {description: "The name of the food category", type: String},
|
|
104
|
-
show: {description: "Whether this category is visible", type: Boolean},
|
|
105
|
-
},
|
|
106
|
-
{timestamps: {createdAt: "created", updatedAt: "updated"}}
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
interface Likes {
|
|
110
|
-
likes: boolean;
|
|
111
|
-
userId: mongoose.Types.ObjectId;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const likesSchema = new Schema<Likes>({
|
|
115
|
-
likes: {description: "Whether the user liked the item", type: Boolean},
|
|
116
|
-
userId: {description: "The user who liked the item", ref: "User", type: "ObjectId"},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const foodSchema = new Schema<Food>(
|
|
120
|
-
{
|
|
121
|
-
calories: {description: "Number of calories in the food", type: Number},
|
|
122
|
-
categories: {description: "Categories this food belongs to", type: [foodCategorySchema]},
|
|
123
|
-
created: {description: "When this food was created", type: Date},
|
|
124
|
-
eatenBy: [
|
|
125
|
-
{
|
|
126
|
-
description: "Users who have eaten this food",
|
|
127
|
-
ref: "User",
|
|
128
|
-
required: true,
|
|
129
|
-
type: Schema.Types.ObjectId,
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
// biome-ignore lint/suspicious/noExplicitAny: DateOnly is a custom SchemaType not recognized by Mongoose's built-in type definitions
|
|
133
|
-
expiration: {description: "Expiration date of the food", type: DateOnly as any},
|
|
134
|
-
hidden: {
|
|
135
|
-
default: false,
|
|
136
|
-
description: "Whether this food is hidden from listings",
|
|
137
|
-
type: Boolean,
|
|
138
|
-
},
|
|
139
|
-
lastEatenWith: {
|
|
140
|
-
description: "Map of user names to dates they last ate this food with",
|
|
141
|
-
of: Date,
|
|
142
|
-
type: Map,
|
|
143
|
-
},
|
|
144
|
-
likesIds: {description: "User likes for this food", required: true, type: [likesSchema]},
|
|
145
|
-
name: {description: "The name of the food", type: String},
|
|
146
|
-
ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
|
|
147
|
-
source: {
|
|
148
|
-
dateAdded: {description: "When the source was added", type: String},
|
|
149
|
-
href: {description: "URL of the source", type: String},
|
|
150
|
-
name: {description: "Name of the source", type: String},
|
|
151
|
-
},
|
|
152
|
-
tags: {description: "Tags associated with this food", type: [String]},
|
|
153
|
-
},
|
|
154
|
-
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
foodSchema.virtual("description").get(function (this: Food) {
|
|
158
|
-
return `${this.name} has ${this.calories} calories`;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
export const FoodModel: Model<Food> = model<Food>("Food", foodSchema);
|
|
162
|
-
|
|
163
|
-
interface RequiredField {
|
|
164
|
-
name: string;
|
|
165
|
-
about?: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const requiredSchema = new Schema<RequiredField>({
|
|
169
|
-
about: {description: "Information about the item", type: String},
|
|
170
|
-
name: {description: "The name of the item", required: true, type: String},
|
|
171
|
-
});
|
|
172
|
-
export const RequiredModel = model<RequiredField>("Required", requiredSchema);
|
|
173
|
-
|
|
174
42
|
export const getBaseServer = (): Express => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Express 5 defaults to 'simple' query parser (Node querystring) which doesn't
|
|
179
|
-
// support nested bracket notation like name[$regex]=Green. Use qs to match
|
|
180
|
-
// what setupServer() configures.
|
|
181
|
-
app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: 200}));
|
|
182
|
-
|
|
183
|
-
// Record mount paths on layers for Express 5 → OpenAPI compat
|
|
184
|
-
patchAppUse(app);
|
|
185
|
-
|
|
186
|
-
app.use((req, res, next) => {
|
|
187
|
-
res.header("Access-Control-Allow-Origin", "*");
|
|
188
|
-
res.header("Access-Control-Allow-Headers", "*");
|
|
189
|
-
// intercepts OPTIONS method
|
|
190
|
-
if (req.method === "OPTIONS") {
|
|
191
|
-
res.send(200);
|
|
192
|
-
} else {
|
|
193
|
-
next();
|
|
194
|
-
}
|
|
43
|
+
return createBaseTestServer({
|
|
44
|
+
patchOpenApiCompat: patchAppUse,
|
|
195
45
|
});
|
|
196
|
-
app.use(express.json());
|
|
197
|
-
return app;
|
|
198
46
|
};
|
|
199
47
|
|
|
200
48
|
export const authAsUser = async (
|
|
@@ -203,46 +51,7 @@ export const authAsUser = async (
|
|
|
203
51
|
): Promise<TestAgent> => {
|
|
204
52
|
const email = type === "admin" ? "admin@example.com" : "notAdmin@example.com";
|
|
205
53
|
const password = type === "admin" ? "securePassword" : "password";
|
|
206
|
-
|
|
207
|
-
const agent = supertest.agent(app);
|
|
208
|
-
const res = await agent.post("/auth/login").send({email, password}).expect(200);
|
|
209
|
-
await agent.set("authorization", `Bearer ${res.body.data.token}`);
|
|
210
|
-
return agent;
|
|
54
|
+
return authAsUserWithCredentials(app, {email, password});
|
|
211
55
|
};
|
|
212
56
|
|
|
213
|
-
export
|
|
214
|
-
await mongoose
|
|
215
|
-
.connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
|
|
216
|
-
.catch(logger.catch);
|
|
217
|
-
|
|
218
|
-
process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
|
|
219
|
-
process.env.TOKEN_SECRET = "secret";
|
|
220
|
-
process.env.TOKEN_EXPIRES_IN = "30m";
|
|
221
|
-
process.env.TOKEN_ISSUER = "example.com";
|
|
222
|
-
process.env.SESSION_SECRET = "session";
|
|
223
|
-
|
|
224
|
-
// Broken out of the try/catch below so you can test the catch logger by shutting down mongo.
|
|
225
|
-
await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]).catch(logger.catch);
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const [notAdmin, admin, adminOther] = await Promise.all([
|
|
229
|
-
UserModel.create({email: "notAdmin@example.com", name: "Not Admin"}),
|
|
230
|
-
UserModel.create({admin: true, email: "admin@example.com", name: "Admin"}),
|
|
231
|
-
UserModel.create({admin: true, email: "admin+other@example.com", name: "Admin Other"}),
|
|
232
|
-
]);
|
|
233
|
-
await (notAdmin as unknown as PassportLocalMongooseDocument).setPassword("password");
|
|
234
|
-
await notAdmin.save();
|
|
235
|
-
|
|
236
|
-
await (admin as unknown as PassportLocalMongooseDocument).setPassword("securePassword");
|
|
237
|
-
await admin.save();
|
|
238
|
-
|
|
239
|
-
await (adminOther as unknown as PassportLocalMongooseDocument).setPassword("otherPassword");
|
|
240
|
-
|
|
241
|
-
await adminOther.save();
|
|
242
|
-
|
|
243
|
-
return [admin, notAdmin, adminOther];
|
|
244
|
-
} catch (error) {
|
|
245
|
-
logger.error("Error setting up DB", error);
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
};
|
|
57
|
+
export {loadTestData} from "./tests/mongoTestSetup";
|