@rebasepro/server-core 0.4.0 → 0.6.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/README.md +62 -25
- package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
- package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
- package/dist/{server-core/src/api → api}/types.d.ts +2 -0
- package/dist/auth/admin-user-ops.d.ts +79 -0
- package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
- package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
- package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
- package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
- package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/reset-password-admin.d.ts +29 -0
- package/dist/auth/session-routes.d.ts +25 -0
- package/dist/backend-CIxN4FVm.js +15 -0
- package/dist/backend-CIxN4FVm.js.map +1 -0
- package/dist/chunk-Dze3rakg.js +42 -0
- package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
- package/dist/dist-CZKP-Xz4.js +832 -0
- package/dist/dist-CZKP-Xz4.js.map +1 -0
- package/dist/env.d.ts +102 -0
- package/dist/from-VbwD7xRf.js +3849 -0
- package/dist/from-VbwD7xRf.js.map +1 -0
- package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
- package/dist/index.es.js +60871 -50424
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +73151 -51256
- package/dist/index.umd.js.map +1 -1
- package/dist/init/docs.d.ts +4 -0
- package/dist/init/health.d.ts +2 -0
- package/dist/init/middlewares.d.ts +10 -0
- package/dist/init/shutdown.d.ts +11 -0
- package/dist/init/storage.d.ts +5 -0
- package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
- package/dist/jwt-DHcQRGC3.js +4168 -0
- package/dist/jwt-DHcQRGC3.js.map +1 -0
- package/dist/logger-BYU66ENZ.js +94 -0
- package/dist/logger-BYU66ENZ.js.map +1 -0
- package/dist/ms-BeBSuOXN.js +125 -0
- package/dist/ms-BeBSuOXN.js.map +1 -0
- package/dist/multipart-parser-CedBDOeC.js +299 -0
- package/dist/multipart-parser-CedBDOeC.js.map +1 -0
- package/dist/src-COaj0G3P.js +1182 -0
- package/dist/src-COaj0G3P.js.map +1 -0
- package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
- package/dist/utils/request-id.d.ts +4 -0
- package/package.json +29 -29
- package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
- package/src/api/errors.ts +95 -20
- package/src/api/graphql/graphql-schema-generator.ts +15 -10
- package/src/api/logs-routes.ts +5 -2
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator-count.test.ts +21 -10
- package/src/api/rest/api-generator.ts +151 -36
- package/src/api/rest/query-parser.ts +18 -12
- package/src/api/server.ts +13 -2
- package/src/api/types.ts +2 -0
- package/src/auth/adapter-middleware.ts +17 -10
- package/src/auth/admin-user-ops.ts +236 -0
- package/src/auth/api-keys/api-key-middleware.ts +15 -9
- package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
- package/src/auth/api-keys/api-key-routes.ts +3 -3
- package/src/auth/api-keys/api-key-store.ts +14 -8
- package/src/auth/api-keys/index.ts +2 -2
- package/src/auth/apple-oauth.ts +4 -3
- package/src/auth/auth-hooks.ts +50 -10
- package/src/auth/bitbucket-oauth.ts +5 -4
- package/src/auth/builtin-auth-adapter.ts +64 -54
- package/src/auth/custom-auth-adapter.ts +4 -5
- package/src/auth/discord-oauth.ts +5 -4
- package/src/auth/facebook-oauth.ts +5 -4
- package/src/auth/github-oauth.ts +6 -5
- package/src/auth/gitlab-oauth.ts +5 -4
- package/src/auth/google-oauth.ts +2 -1
- package/src/auth/index.ts +6 -2
- package/src/auth/jwt.ts +15 -4
- package/src/auth/linkedin-oauth.ts +4 -3
- package/src/auth/mfa-routes.ts +299 -0
- package/src/auth/mfa.ts +2 -1
- package/src/auth/microsoft-oauth.ts +5 -4
- package/src/auth/middleware.ts +5 -4
- package/src/auth/rate-limiter.ts +1 -1
- package/src/auth/reset-password-admin.ts +144 -0
- package/src/auth/rls-scope.ts +1 -1
- package/src/auth/routes.ts +52 -600
- package/src/auth/session-routes.ts +341 -0
- package/src/auth/slack-oauth.ts +6 -5
- package/src/auth/spotify-oauth.ts +5 -4
- package/src/auth/twitter-oauth.ts +4 -3
- package/src/collections/loader.ts +5 -4
- package/src/cron/cron-scheduler.test.ts +23 -11
- package/src/cron/cron-scheduler.ts +14 -8
- package/src/cron/cron-store.ts +1 -1
- package/src/email/smtp-email-service.ts +3 -2
- package/src/env.ts +11 -10
- package/src/index.ts +1 -0
- package/src/init/docs.ts +47 -0
- package/src/init/health.ts +37 -0
- package/src/init/middlewares.ts +61 -0
- package/src/init/shutdown.ts +56 -0
- package/src/init/storage.ts +57 -0
- package/src/init.ts +93 -295
- package/src/serve-spa.ts +12 -5
- package/src/services/driver-registry.ts +4 -3
- package/src/storage/S3StorageController.ts +1 -2
- package/src/storage/image-transform.ts +25 -10
- package/src/storage/routes.ts +30 -11
- package/src/storage/storage-registry.ts +4 -3
- package/src/storage/tus-handler.ts +12 -12
- package/src/utils/request-id.ts +40 -0
- package/src/utils/request-logger.ts +6 -0
- package/test/api-generator.test.ts +90 -2
- package/test/api-key-permission-guard.test.ts +24 -12
- package/test/auth-routes.test.ts +5 -3
- package/test/backend-hooks-data.test.ts +99 -30
- package/test/custom-auth-adapter.test.ts +16 -15
- package/test/email-templates.test.ts +10 -5
- package/test/env.test.ts +10 -10
- package/test/function-loader.test.ts +7 -4
- package/test/graphql-schema-generator.test.ts +554 -0
- package/test/jwt-security.test.ts +1 -1
- package/test/query-parser.test.ts +0 -1
- package/test/reset-password-admin.test.ts +113 -0
- package/test/singleton.test.ts +5 -5
- package/test/smtp-email-service.test.ts +21 -21
- package/test/webhook-service.test.ts +22 -11
- package/tsconfig.json +2 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/index-Cr1D21av.js +0 -49
- package/dist/index-Cr1D21av.js.map +0 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
- package/dist/server-core/src/env.d.ts +0 -137
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/src/auth/admin-routes.ts +0 -534
- package/test/admin-routes.test.ts +0 -544
- package/test/backend-hooks-admin.test.ts +0 -388
- package/test.ts +0 -6
- /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
- /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
- /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
- /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
- /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
- /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
- /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
- /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
- /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
- /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
- /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
- /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
- /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
- /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
- /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
- /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
- /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { GraphQLSchemaGenerator } from "../src/api/graphql/graphql-schema-generator";
|
|
2
|
+
import { DataDriver } from "../../types/src/controllers/data_driver";
|
|
3
|
+
import { EntityCollection } from "../../types/src/types/collections";
|
|
4
|
+
import {
|
|
5
|
+
GraphQLSchema,
|
|
6
|
+
GraphQLObjectType,
|
|
7
|
+
GraphQLNonNull,
|
|
8
|
+
GraphQLString,
|
|
9
|
+
GraphQLFloat,
|
|
10
|
+
GraphQLBoolean,
|
|
11
|
+
GraphQLList,
|
|
12
|
+
GraphQLInputObjectType,
|
|
13
|
+
graphql,
|
|
14
|
+
printSchema
|
|
15
|
+
} from "graphql";
|
|
16
|
+
|
|
17
|
+
describe("GraphQLSchemaGenerator", () => {
|
|
18
|
+
let mockDriver: jest.Mocked<DataDriver>;
|
|
19
|
+
let collections: EntityCollection[];
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockDriver = {
|
|
23
|
+
key: "postgres",
|
|
24
|
+
initialised: true,
|
|
25
|
+
fetchCollection: jest.fn(),
|
|
26
|
+
listenCollection: jest.fn(),
|
|
27
|
+
fetchEntity: jest.fn(),
|
|
28
|
+
listenEntity: jest.fn(),
|
|
29
|
+
saveEntity: jest.fn(),
|
|
30
|
+
deleteEntity: jest.fn(),
|
|
31
|
+
checkUniqueField: jest.fn(),
|
|
32
|
+
countEntities: jest.fn(),
|
|
33
|
+
withAuth: jest.fn(),
|
|
34
|
+
admin: {} as any
|
|
35
|
+
} as unknown as jest.Mocked<DataDriver>;
|
|
36
|
+
|
|
37
|
+
collections = [
|
|
38
|
+
{
|
|
39
|
+
slug: "posts",
|
|
40
|
+
name: "Posts",
|
|
41
|
+
singularName: "Post",
|
|
42
|
+
description: "Blog posts",
|
|
43
|
+
properties: {
|
|
44
|
+
title: { name: "Title",
|
|
45
|
+
type: "string",
|
|
46
|
+
validation: { required: true } },
|
|
47
|
+
body: { name: "Body",
|
|
48
|
+
type: "string" },
|
|
49
|
+
views: { name: "Views",
|
|
50
|
+
type: "number" },
|
|
51
|
+
published: { name: "Published",
|
|
52
|
+
type: "boolean" },
|
|
53
|
+
createdAt: { name: "Created At",
|
|
54
|
+
type: "date" }
|
|
55
|
+
}
|
|
56
|
+
} as unknown as EntityCollection,
|
|
57
|
+
{
|
|
58
|
+
slug: "authors",
|
|
59
|
+
name: "Authors",
|
|
60
|
+
singularName: "Author",
|
|
61
|
+
properties: {
|
|
62
|
+
name: { name: "Name",
|
|
63
|
+
type: "string",
|
|
64
|
+
validation: { required: true } },
|
|
65
|
+
bio: { name: "Bio",
|
|
66
|
+
type: "string" }
|
|
67
|
+
}
|
|
68
|
+
} as unknown as EntityCollection
|
|
69
|
+
];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ─── Schema Generation ──────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
describe("generateSchema", () => {
|
|
75
|
+
it("returns a valid GraphQLSchema", () => {
|
|
76
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
77
|
+
const schema = generator.generateSchema();
|
|
78
|
+
expect(schema).toBeInstanceOf(GraphQLSchema);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("creates Query and Mutation root types", () => {
|
|
82
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
83
|
+
const schema = generator.generateSchema();
|
|
84
|
+
expect(schema.getQueryType()).toBeDefined();
|
|
85
|
+
expect(schema.getMutationType()).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("generates a printable SDL without errors", () => {
|
|
89
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
90
|
+
const schema = generator.generateSchema();
|
|
91
|
+
const sdl = printSchema(schema);
|
|
92
|
+
expect(sdl).toContain("type Post");
|
|
93
|
+
expect(sdl).toContain("type Author");
|
|
94
|
+
expect(sdl).toContain("type Query");
|
|
95
|
+
expect(sdl).toContain("type Mutation");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── Entity Types ───────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("entity types", () => {
|
|
102
|
+
it("creates an object type for each collection", () => {
|
|
103
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
104
|
+
const schema = generator.generateSchema();
|
|
105
|
+
const typeMap = schema.getTypeMap();
|
|
106
|
+
expect(typeMap["Post"]).toBeInstanceOf(GraphQLObjectType);
|
|
107
|
+
expect(typeMap["Author"]).toBeInstanceOf(GraphQLObjectType);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("generates an id field as NonNull String", () => {
|
|
111
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
112
|
+
const schema = generator.generateSchema();
|
|
113
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
114
|
+
const fields = postType.getFields();
|
|
115
|
+
expect(fields.id).toBeDefined();
|
|
116
|
+
expect(fields.id.type).toBeInstanceOf(GraphQLNonNull);
|
|
117
|
+
expect((fields.id.type as GraphQLNonNull<any>).ofType).toBe(GraphQLString);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("maps string properties to GraphQLString", () => {
|
|
121
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
122
|
+
const schema = generator.generateSchema();
|
|
123
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
124
|
+
const bodyField = postType.getFields().body;
|
|
125
|
+
expect(bodyField.type).toBe(GraphQLString);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("maps number properties to GraphQLFloat", () => {
|
|
129
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
130
|
+
const schema = generator.generateSchema();
|
|
131
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
132
|
+
const viewsField = postType.getFields().views;
|
|
133
|
+
expect(viewsField.type).toBe(GraphQLFloat);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("maps boolean properties to GraphQLBoolean", () => {
|
|
137
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
138
|
+
const schema = generator.generateSchema();
|
|
139
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
140
|
+
const publishedField = postType.getFields().published;
|
|
141
|
+
expect(publishedField.type).toBe(GraphQLBoolean);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("maps date properties to GraphQLString", () => {
|
|
145
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
146
|
+
const schema = generator.generateSchema();
|
|
147
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
148
|
+
const createdAtField = postType.getFields().createdAt;
|
|
149
|
+
// Date is non-null because the original property definition doesn't set required,
|
|
150
|
+
// so it should be nullable GraphQLString
|
|
151
|
+
expect(createdAtField).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("wraps required properties in NonNull", () => {
|
|
155
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
156
|
+
const schema = generator.generateSchema();
|
|
157
|
+
const postType = schema.getType("Post") as GraphQLObjectType;
|
|
158
|
+
const titleField = postType.getFields().title;
|
|
159
|
+
expect(titleField.type).toBeInstanceOf(GraphQLNonNull);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("skips relation properties from entity fields", () => {
|
|
163
|
+
const withRelation: EntityCollection[] = [
|
|
164
|
+
{
|
|
165
|
+
slug: "comments",
|
|
166
|
+
name: "Comments",
|
|
167
|
+
singularName: "Comment",
|
|
168
|
+
properties: {
|
|
169
|
+
text: { name: "Text",
|
|
170
|
+
type: "string" },
|
|
171
|
+
author: { name: "Author",
|
|
172
|
+
type: "relation" }
|
|
173
|
+
}
|
|
174
|
+
} as unknown as EntityCollection
|
|
175
|
+
];
|
|
176
|
+
const generator = new GraphQLSchemaGenerator(withRelation, mockDriver);
|
|
177
|
+
const schema = generator.generateSchema();
|
|
178
|
+
const commentType = schema.getType("Comment") as GraphQLObjectType;
|
|
179
|
+
const fields = commentType.getFields();
|
|
180
|
+
expect(fields.text).toBeDefined();
|
|
181
|
+
expect(fields.author).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── Input Types ────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("input types", () => {
|
|
188
|
+
it("creates input types for each collection", () => {
|
|
189
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
190
|
+
const schema = generator.generateSchema();
|
|
191
|
+
const typeMap = schema.getTypeMap();
|
|
192
|
+
expect(typeMap["PostInput"]).toBeInstanceOf(GraphQLInputObjectType);
|
|
193
|
+
expect(typeMap["AuthorInput"]).toBeInstanceOf(GraphQLInputObjectType);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("input types skip relation properties", () => {
|
|
197
|
+
const withRelation: EntityCollection[] = [
|
|
198
|
+
{
|
|
199
|
+
slug: "comments",
|
|
200
|
+
name: "Comments",
|
|
201
|
+
singularName: "Comment",
|
|
202
|
+
properties: {
|
|
203
|
+
text: { name: "Text",
|
|
204
|
+
type: "string" },
|
|
205
|
+
post: { name: "Post",
|
|
206
|
+
type: "relation" }
|
|
207
|
+
}
|
|
208
|
+
} as unknown as EntityCollection
|
|
209
|
+
];
|
|
210
|
+
const generator = new GraphQLSchemaGenerator(withRelation, mockDriver);
|
|
211
|
+
const schema = generator.generateSchema();
|
|
212
|
+
const inputType = schema.getType("CommentInput") as GraphQLInputObjectType;
|
|
213
|
+
const fields = inputType.getFields();
|
|
214
|
+
expect(fields.text).toBeDefined();
|
|
215
|
+
expect(fields.post).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── Query Resolvers ────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe("query resolvers", () => {
|
|
222
|
+
it("creates single-entity and list queries for each collection", () => {
|
|
223
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
224
|
+
const schema = generator.generateSchema();
|
|
225
|
+
const queryType = schema.getQueryType()!;
|
|
226
|
+
const fields = queryType.getFields();
|
|
227
|
+
|
|
228
|
+
// Single entity queries (lowercase singular name)
|
|
229
|
+
expect(fields.post).toBeDefined();
|
|
230
|
+
expect(fields.author).toBeDefined();
|
|
231
|
+
|
|
232
|
+
// List queries (slug)
|
|
233
|
+
expect(fields.posts).toBeDefined();
|
|
234
|
+
expect(fields.authors).toBeDefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("single-entity query calls fetchEntity with correct args", async () => {
|
|
238
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
239
|
+
const schema = generator.generateSchema();
|
|
240
|
+
|
|
241
|
+
const mockEntity = {
|
|
242
|
+
id: "1",
|
|
243
|
+
path: "posts",
|
|
244
|
+
values: { title: "Hello World",
|
|
245
|
+
body: "Content" }
|
|
246
|
+
};
|
|
247
|
+
mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
|
|
248
|
+
|
|
249
|
+
const result = await graphql({
|
|
250
|
+
schema,
|
|
251
|
+
source: "query { post(id: \"1\") { id title body } }",
|
|
252
|
+
contextValue: { driver: mockDriver }
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(result.errors).toBeUndefined();
|
|
256
|
+
expect(mockDriver.fetchEntity).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
path: "posts",
|
|
259
|
+
entityId: "1"
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("list query calls fetchCollection with limit and offset", async () => {
|
|
265
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
266
|
+
const schema = generator.generateSchema();
|
|
267
|
+
|
|
268
|
+
mockDriver.fetchCollection.mockResolvedValue([]);
|
|
269
|
+
|
|
270
|
+
const result = await graphql({
|
|
271
|
+
schema,
|
|
272
|
+
source: "query { posts(limit: 10, offset: 5) { id title } }",
|
|
273
|
+
contextValue: { driver: mockDriver }
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(result.errors).toBeUndefined();
|
|
277
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
278
|
+
expect.objectContaining({
|
|
279
|
+
path: "posts",
|
|
280
|
+
limit: 10,
|
|
281
|
+
startAfter: "5"
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("list query parses JSON where filter", async () => {
|
|
287
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
288
|
+
const schema = generator.generateSchema();
|
|
289
|
+
|
|
290
|
+
mockDriver.fetchCollection.mockResolvedValue([]);
|
|
291
|
+
|
|
292
|
+
const result = await graphql({
|
|
293
|
+
schema,
|
|
294
|
+
source: "query { posts(where: \"{\\\"published\\\": true}\") { id } }",
|
|
295
|
+
contextValue: { driver: mockDriver }
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.errors).toBeUndefined();
|
|
299
|
+
expect(mockDriver.fetchCollection).toHaveBeenCalledWith(
|
|
300
|
+
expect.objectContaining({
|
|
301
|
+
filter: { published: true }
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("list query rejects invalid JSON where filter", async () => {
|
|
307
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
308
|
+
const schema = generator.generateSchema();
|
|
309
|
+
|
|
310
|
+
const result = await graphql({
|
|
311
|
+
schema,
|
|
312
|
+
source: "query { posts(where: \"not-json\") { id } }",
|
|
313
|
+
contextValue: { driver: mockDriver }
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result.errors).toBeDefined();
|
|
317
|
+
expect(result.errors![0].message).toContain("Invalid 'where' filter");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("list query rejects non-object JSON where filter", async () => {
|
|
321
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
322
|
+
const schema = generator.generateSchema();
|
|
323
|
+
|
|
324
|
+
const result = await graphql({
|
|
325
|
+
schema,
|
|
326
|
+
source: "query { posts(where: \"[1,2,3]\") { id } }",
|
|
327
|
+
contextValue: { driver: mockDriver }
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(result.errors).toBeDefined();
|
|
331
|
+
expect(result.errors![0].message).toContain("Filter must be a JSON object");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ─── Mutation Resolvers ─────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
describe("mutation resolvers", () => {
|
|
338
|
+
it("creates create, update, and delete mutations for each collection", () => {
|
|
339
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
340
|
+
const schema = generator.generateSchema();
|
|
341
|
+
const mutationType = schema.getMutationType()!;
|
|
342
|
+
const fields = mutationType.getFields();
|
|
343
|
+
|
|
344
|
+
expect(fields.createPost).toBeDefined();
|
|
345
|
+
expect(fields.updatePost).toBeDefined();
|
|
346
|
+
expect(fields.deletePost).toBeDefined();
|
|
347
|
+
expect(fields.createAuthor).toBeDefined();
|
|
348
|
+
expect(fields.updateAuthor).toBeDefined();
|
|
349
|
+
expect(fields.deleteAuthor).toBeDefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("create mutation calls saveEntity with status 'new'", async () => {
|
|
353
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
354
|
+
const schema = generator.generateSchema();
|
|
355
|
+
|
|
356
|
+
const mockEntity = {
|
|
357
|
+
id: "1",
|
|
358
|
+
path: "posts",
|
|
359
|
+
values: { title: "New Post" }
|
|
360
|
+
};
|
|
361
|
+
mockDriver.saveEntity.mockResolvedValue(mockEntity as any);
|
|
362
|
+
|
|
363
|
+
const result = await graphql({
|
|
364
|
+
schema,
|
|
365
|
+
source: "mutation { createPost(input: { title: \"New Post\" }) { id } }",
|
|
366
|
+
contextValue: { driver: mockDriver }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(result.errors).toBeUndefined();
|
|
370
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
371
|
+
expect.objectContaining({
|
|
372
|
+
path: "posts",
|
|
373
|
+
values: { title: "New Post" },
|
|
374
|
+
status: "new"
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("update mutation calls saveEntity with status 'existing' and entityId", async () => {
|
|
380
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
381
|
+
const schema = generator.generateSchema();
|
|
382
|
+
|
|
383
|
+
const mockEntity = {
|
|
384
|
+
id: "1",
|
|
385
|
+
path: "posts",
|
|
386
|
+
values: { title: "Updated" }
|
|
387
|
+
};
|
|
388
|
+
mockDriver.saveEntity.mockResolvedValue(mockEntity as any);
|
|
389
|
+
|
|
390
|
+
const result = await graphql({
|
|
391
|
+
schema,
|
|
392
|
+
source: "mutation { updatePost(id: \"1\", input: { title: \"Updated\" }) { id } }",
|
|
393
|
+
contextValue: { driver: mockDriver }
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(result.errors).toBeUndefined();
|
|
397
|
+
expect(mockDriver.saveEntity).toHaveBeenCalledWith(
|
|
398
|
+
expect.objectContaining({
|
|
399
|
+
path: "posts",
|
|
400
|
+
entityId: "1",
|
|
401
|
+
values: { title: "Updated" },
|
|
402
|
+
status: "existing"
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("delete mutation returns true on success", async () => {
|
|
408
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
409
|
+
const schema = generator.generateSchema();
|
|
410
|
+
|
|
411
|
+
const mockEntity = { id: "1",
|
|
412
|
+
path: "posts",
|
|
413
|
+
values: {} };
|
|
414
|
+
mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
|
|
415
|
+
mockDriver.deleteEntity.mockResolvedValue(undefined);
|
|
416
|
+
|
|
417
|
+
const result = await graphql({
|
|
418
|
+
schema,
|
|
419
|
+
source: "mutation { deletePost(id: \"1\") }",
|
|
420
|
+
contextValue: { driver: mockDriver }
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(result.errors).toBeUndefined();
|
|
424
|
+
expect(result.data?.deletePost).toBe(true);
|
|
425
|
+
expect(mockDriver.deleteEntity).toHaveBeenCalledWith(
|
|
426
|
+
expect.objectContaining({
|
|
427
|
+
entity: mockEntity
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("delete mutation returns false when entity not found", async () => {
|
|
433
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
434
|
+
const schema = generator.generateSchema();
|
|
435
|
+
|
|
436
|
+
mockDriver.fetchEntity.mockResolvedValue(undefined as any);
|
|
437
|
+
|
|
438
|
+
const result = await graphql({
|
|
439
|
+
schema,
|
|
440
|
+
source: "mutation { deletePost(id: \"nonexistent\") }",
|
|
441
|
+
contextValue: { driver: mockDriver }
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(result.errors).toBeUndefined();
|
|
445
|
+
expect(result.data?.deletePost).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("delete mutation returns false when deleteEntity throws", async () => {
|
|
449
|
+
const generator = new GraphQLSchemaGenerator(collections, mockDriver);
|
|
450
|
+
const schema = generator.generateSchema();
|
|
451
|
+
|
|
452
|
+
const mockEntity = { id: "1",
|
|
453
|
+
path: "posts",
|
|
454
|
+
values: {} };
|
|
455
|
+
mockDriver.fetchEntity.mockResolvedValue(mockEntity as any);
|
|
456
|
+
mockDriver.deleteEntity.mockRejectedValue(new Error("DB error"));
|
|
457
|
+
|
|
458
|
+
const result = await graphql({
|
|
459
|
+
schema,
|
|
460
|
+
source: "mutation { deletePost(id: \"1\") }",
|
|
461
|
+
contextValue: { driver: mockDriver }
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(result.errors).toBeUndefined();
|
|
465
|
+
expect(result.data?.deletePost).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ─── Edge Cases ─────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
describe("edge cases", () => {
|
|
472
|
+
it("handles empty collections array", () => {
|
|
473
|
+
const generator = new GraphQLSchemaGenerator([], mockDriver);
|
|
474
|
+
const schema = generator.generateSchema();
|
|
475
|
+
expect(schema).toBeInstanceOf(GraphQLSchema);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("handles collection with array property", () => {
|
|
479
|
+
const arrayCollection: EntityCollection[] = [
|
|
480
|
+
{
|
|
481
|
+
slug: "tags",
|
|
482
|
+
name: "Tags",
|
|
483
|
+
singularName: "Tag",
|
|
484
|
+
properties: {
|
|
485
|
+
labels: { name: "Labels",
|
|
486
|
+
type: "array" }
|
|
487
|
+
}
|
|
488
|
+
} as unknown as EntityCollection
|
|
489
|
+
];
|
|
490
|
+
const generator = new GraphQLSchemaGenerator(arrayCollection, mockDriver);
|
|
491
|
+
const schema = generator.generateSchema();
|
|
492
|
+
const tagType = schema.getType("Tag") as GraphQLObjectType;
|
|
493
|
+
const labelsField = tagType.getFields().labels;
|
|
494
|
+
expect(labelsField.type).toBeInstanceOf(GraphQLList);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("handles collection with vector property", () => {
|
|
498
|
+
const vectorCollection: EntityCollection[] = [
|
|
499
|
+
{
|
|
500
|
+
slug: "embeddings",
|
|
501
|
+
name: "Embeddings",
|
|
502
|
+
singularName: "Embedding",
|
|
503
|
+
properties: {
|
|
504
|
+
embedding: { name: "Embedding",
|
|
505
|
+
type: "vector" }
|
|
506
|
+
}
|
|
507
|
+
} as unknown as EntityCollection
|
|
508
|
+
];
|
|
509
|
+
const generator = new GraphQLSchemaGenerator(vectorCollection, mockDriver);
|
|
510
|
+
const schema = generator.generateSchema();
|
|
511
|
+
const embeddingType = schema.getType("Embedding") as GraphQLObjectType;
|
|
512
|
+
const embeddingField = embeddingType.getFields().embedding;
|
|
513
|
+
expect(embeddingField.type).toBeInstanceOf(GraphQLList);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("handles collection with binary property", () => {
|
|
517
|
+
const binaryCollection: EntityCollection[] = [
|
|
518
|
+
{
|
|
519
|
+
slug: "files",
|
|
520
|
+
name: "Files",
|
|
521
|
+
singularName: "File",
|
|
522
|
+
properties: {
|
|
523
|
+
data: { name: "Data",
|
|
524
|
+
type: "binary" }
|
|
525
|
+
}
|
|
526
|
+
} as unknown as EntityCollection
|
|
527
|
+
];
|
|
528
|
+
const generator = new GraphQLSchemaGenerator(binaryCollection, mockDriver);
|
|
529
|
+
const schema = generator.generateSchema();
|
|
530
|
+
const fileType = schema.getType("File") as GraphQLObjectType;
|
|
531
|
+
const dataField = fileType.getFields().data;
|
|
532
|
+
expect(dataField.type).toBe(GraphQLString);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("deduplicates types when generateSchema is called", () => {
|
|
536
|
+
const dupeCollections: EntityCollection[] = [
|
|
537
|
+
{
|
|
538
|
+
slug: "items",
|
|
539
|
+
name: "Items",
|
|
540
|
+
singularName: "Item",
|
|
541
|
+
properties: {
|
|
542
|
+
name: { name: "Name",
|
|
543
|
+
type: "string" }
|
|
544
|
+
}
|
|
545
|
+
} as unknown as EntityCollection
|
|
546
|
+
];
|
|
547
|
+
const generator = new GraphQLSchemaGenerator(dupeCollections, mockDriver);
|
|
548
|
+
// Calling generateSchema should not throw even if types are created internally multiple times
|
|
549
|
+
const schema = generator.generateSchema();
|
|
550
|
+
expect(schema.getType("Item")).toBeDefined();
|
|
551
|
+
expect(schema.getType("ItemInput")).toBeDefined();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
});
|
|
@@ -66,7 +66,7 @@ refreshExpiresIn: "30d" });
|
|
|
66
66
|
|
|
67
67
|
it("throws when secret is not configured", () => {
|
|
68
68
|
// Force empty secret
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
Object.defineProperty(require("../src/auth/jwt"), "jwtConfig", { value: { secret: "" },
|
|
71
71
|
writable: true });
|
|
72
72
|
// This won't work since jwtConfig is module-scoped, but generateAccessToken has its own check
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { createResetPasswordRoute } from "../src/auth/reset-password-admin";
|
|
3
|
+
import type { AuthRepository } from "../src/auth/interfaces";
|
|
4
|
+
import { configureJwt, generateAccessToken } from "../src/auth/jwt";
|
|
5
|
+
import { errorHandler } from "../src/api/errors";
|
|
6
|
+
|
|
7
|
+
const TEST_SECRET = "integration-test-secret-key-that-is-definitely-32-chars-long!!";
|
|
8
|
+
|
|
9
|
+
describe("createResetPasswordRoute & onAdminResetPassword", () => {
|
|
10
|
+
let mockAuthRepo: jest.Mocked<AuthRepository>;
|
|
11
|
+
let mockEmailService: { send: jest.Mock; isConfigured: jest.Mock };
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
configureJwt({ secret: TEST_SECRET,
|
|
15
|
+
accessExpiresIn: "1h" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockAuthRepo = {
|
|
20
|
+
getUserById: jest.fn(),
|
|
21
|
+
getUserRoleIds: jest.fn().mockResolvedValue(["admin"]),
|
|
22
|
+
updatePassword: jest.fn().mockResolvedValue(undefined),
|
|
23
|
+
createPasswordResetToken: jest.fn().mockResolvedValue(undefined)
|
|
24
|
+
} as unknown as jest.Mocked<AuthRepository>;
|
|
25
|
+
|
|
26
|
+
mockEmailService = {
|
|
27
|
+
send: jest.fn().mockResolvedValue(undefined),
|
|
28
|
+
isConfigured: jest.fn().mockReturnValue(true)
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function createApp(authHooks?: any) {
|
|
33
|
+
const app = new Hono();
|
|
34
|
+
app.onError(errorHandler);
|
|
35
|
+
const adminRoutes = createResetPasswordRoute({
|
|
36
|
+
authRepo: mockAuthRepo,
|
|
37
|
+
emailService: mockEmailService as any,
|
|
38
|
+
emailConfig: {
|
|
39
|
+
from: "test@example.com",
|
|
40
|
+
resetPasswordUrl: "https://reset.com"
|
|
41
|
+
},
|
|
42
|
+
authHooks
|
|
43
|
+
});
|
|
44
|
+
app.route("/api/admin", adminRoutes);
|
|
45
|
+
return app;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it("falls back to default password reset logic when no hook is provided", async () => {
|
|
49
|
+
mockAuthRepo.getUserById.mockResolvedValue({
|
|
50
|
+
id: "user-123",
|
|
51
|
+
email: "user@example.com",
|
|
52
|
+
displayName: "User One"
|
|
53
|
+
} as any);
|
|
54
|
+
|
|
55
|
+
const app = createApp();
|
|
56
|
+
const adminToken = generateAccessToken("admin-user", ["admin"]);
|
|
57
|
+
|
|
58
|
+
const res = await app.request("/api/admin/users/user-123/reset-password", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Authorization": `Bearer ${adminToken}`
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
const body = await res.json() as any;
|
|
67
|
+
expect(body.invitationSent).toBe(true);
|
|
68
|
+
expect(body.user.uid).toBe("user-123");
|
|
69
|
+
expect(mockAuthRepo.createPasswordResetToken).toHaveBeenCalled();
|
|
70
|
+
expect(mockEmailService.send).toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("calls onAdminResetPassword hook when provided", async () => {
|
|
74
|
+
mockAuthRepo.getUserById.mockResolvedValue({
|
|
75
|
+
id: "user-123",
|
|
76
|
+
email: "user@example.com",
|
|
77
|
+
displayName: "User One"
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
const onAdminResetPasswordMock = jest.fn().mockResolvedValue({
|
|
81
|
+
temporaryPassword: "hook-temp-password",
|
|
82
|
+
invitationSent: false
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const app = createApp({
|
|
86
|
+
onAdminResetPassword: onAdminResetPasswordMock
|
|
87
|
+
});
|
|
88
|
+
const adminToken = generateAccessToken("admin-user", ["admin"]);
|
|
89
|
+
|
|
90
|
+
const res = await app.request("/api/admin/users/user-123/reset-password", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Authorization": `Bearer ${adminToken}`
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
const body = await res.json() as any;
|
|
99
|
+
expect(body.invitationSent).toBe(false);
|
|
100
|
+
expect(body.temporaryPassword).toBe("hook-temp-password");
|
|
101
|
+
expect(body.user.uid).toBe("user-123");
|
|
102
|
+
|
|
103
|
+
expect(onAdminResetPasswordMock).toHaveBeenCalledWith(
|
|
104
|
+
"user-123",
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
authRepo: mockAuthRepo,
|
|
107
|
+
emailService: mockEmailService
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
expect(mockAuthRepo.createPasswordResetToken).not.toHaveBeenCalled();
|
|
111
|
+
expect(mockEmailService.send).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|