canopycms 0.0.0 → 0.0.2
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/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
package/src/api/groups.test.ts
DELETED
|
@@ -1,1013 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import type { ApiContext, ApiRequest } from './types'
|
|
3
|
-
import type { InternalGroup } from '../authorization'
|
|
4
|
-
import type { CanopyGroupId, CanopyUserId } from '../types'
|
|
5
|
-
import { RESERVED_GROUPS } from '../authorization'
|
|
6
|
-
import { createMockApiContext, createMockBranchContext, createMockGitManager } from '../test-utils'
|
|
7
|
-
|
|
8
|
-
// Mock authorization module (specifically the groups loader functions)
|
|
9
|
-
vi.mock('../authorization', async (importOriginal) => {
|
|
10
|
-
const { vi } = await import('vitest')
|
|
11
|
-
const original = await importOriginal<typeof import('../authorization')>()
|
|
12
|
-
return {
|
|
13
|
-
...original,
|
|
14
|
-
loadInternalGroups: vi.fn(),
|
|
15
|
-
loadGroupsFile: vi.fn(),
|
|
16
|
-
saveInternalGroups: vi.fn(),
|
|
17
|
-
}
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
GROUP_ROUTES,
|
|
22
|
-
validateAdminGroupUpdate,
|
|
23
|
-
validateReservedGroups,
|
|
24
|
-
type UpdateInternalGroupsBody,
|
|
25
|
-
type SearchExternalGroupsParams,
|
|
26
|
-
} from './groups'
|
|
27
|
-
import * as authorization from '../authorization'
|
|
28
|
-
|
|
29
|
-
// Alias for convenience (tests reference groupsLoader)
|
|
30
|
-
const groupsLoader = {
|
|
31
|
-
loadInternalGroups: authorization.loadInternalGroups,
|
|
32
|
-
loadGroupsFile: authorization.loadGroupsFile,
|
|
33
|
-
saveInternalGroups: authorization.saveInternalGroups,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Extract handlers for testing
|
|
37
|
-
const getInternalGroups = GROUP_ROUTES.getInternal.handler
|
|
38
|
-
const updateInternalGroups = GROUP_ROUTES.updateInternal.handler
|
|
39
|
-
const searchExternalGroups = GROUP_ROUTES.searchExternal.handler
|
|
40
|
-
|
|
41
|
-
describe('groups API', () => {
|
|
42
|
-
let mockContext: ApiContext
|
|
43
|
-
let mockGit: ReturnType<typeof createMockGitManager>
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
mockGit = createMockGitManager()
|
|
47
|
-
|
|
48
|
-
mockContext = createMockApiContext({
|
|
49
|
-
services: {
|
|
50
|
-
config: {
|
|
51
|
-
defaultBaseBranch: 'main',
|
|
52
|
-
mode: 'dev',
|
|
53
|
-
gitBotAuthorName: 'Canopy Bot',
|
|
54
|
-
gitBotAuthorEmail: 'bot@example.com',
|
|
55
|
-
sourceRoot: '/test/workspace',
|
|
56
|
-
} as any,
|
|
57
|
-
createGitManagerFor: vi.fn(() => mockGit) as any,
|
|
58
|
-
},
|
|
59
|
-
branchContext: createMockBranchContext({
|
|
60
|
-
branchName: 'main',
|
|
61
|
-
createdBy: 'admin-1' as CanopyUserId,
|
|
62
|
-
baseRoot: '/test',
|
|
63
|
-
branchRoot: '/test/main',
|
|
64
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
65
|
-
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
66
|
-
}),
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('getInternalGroups', () => {
|
|
71
|
-
it('should return 403 for non-admin users', async () => {
|
|
72
|
-
const req: ApiRequest<undefined> = {
|
|
73
|
-
user: {
|
|
74
|
-
type: 'authenticated',
|
|
75
|
-
userId: 'user-1' as CanopyUserId,
|
|
76
|
-
groups: [],
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const result = await getInternalGroups(mockContext, req)
|
|
81
|
-
|
|
82
|
-
expect(result).toEqual({
|
|
83
|
-
ok: false,
|
|
84
|
-
status: 403,
|
|
85
|
-
error: 'Admin access required',
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should return empty array when groups file does not exist', async () => {
|
|
90
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
91
|
-
|
|
92
|
-
const req: ApiRequest<undefined> = {
|
|
93
|
-
user: {
|
|
94
|
-
type: 'authenticated',
|
|
95
|
-
userId: 'admin-1' as CanopyUserId,
|
|
96
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const result = await getInternalGroups(mockContext, req)
|
|
101
|
-
|
|
102
|
-
expect(result.ok).toBe(true)
|
|
103
|
-
expect(result.status).toBe(200)
|
|
104
|
-
if (result.ok) {
|
|
105
|
-
expect(result.data?.groups).toEqual([])
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('updateInternalGroups', () => {
|
|
111
|
-
it('should return 403 for non-admin users', async () => {
|
|
112
|
-
const req: ApiRequest = {
|
|
113
|
-
user: {
|
|
114
|
-
type: 'authenticated',
|
|
115
|
-
userId: 'user-1' as CanopyUserId,
|
|
116
|
-
groups: [],
|
|
117
|
-
},
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const result = await updateInternalGroups(mockContext, req, {
|
|
121
|
-
groups: [],
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
expect(result).toEqual({
|
|
125
|
-
ok: false,
|
|
126
|
-
status: 403,
|
|
127
|
-
error: 'Admin access required',
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('should return 400 if groups not provided', async () => {
|
|
132
|
-
const req: ApiRequest = {
|
|
133
|
-
user: {
|
|
134
|
-
type: 'authenticated',
|
|
135
|
-
userId: 'admin-1' as CanopyUserId,
|
|
136
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const result = await updateInternalGroups(mockContext, req, {} as UpdateInternalGroupsBody)
|
|
141
|
-
|
|
142
|
-
expect(result).toEqual({
|
|
143
|
-
ok: false,
|
|
144
|
-
status: 400,
|
|
145
|
-
error: 'groups array required',
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('should save groups and commit changes for admin', async () => {
|
|
150
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
151
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
152
|
-
// Add bootstrap admin so validation passes
|
|
153
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
154
|
-
|
|
155
|
-
const groups: InternalGroup[] = [
|
|
156
|
-
{
|
|
157
|
-
id: '' as CanopyGroupId, // Empty ID for new group
|
|
158
|
-
name: 'Content Editors',
|
|
159
|
-
description: 'Team members who can edit content',
|
|
160
|
-
members: ['user-1' as CanopyUserId, 'user-2' as CanopyUserId],
|
|
161
|
-
},
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
const req: ApiRequest = {
|
|
165
|
-
user: {
|
|
166
|
-
type: 'authenticated',
|
|
167
|
-
userId: 'admin-1' as CanopyUserId,
|
|
168
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
173
|
-
|
|
174
|
-
expect(result.ok).toBe(true)
|
|
175
|
-
expect(result.status).toBe(200)
|
|
176
|
-
|
|
177
|
-
// Verify groups were saved with generated IDs
|
|
178
|
-
// In dev mode, uses workspace root instead of branch root
|
|
179
|
-
expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
|
|
180
|
-
'/test/workspace',
|
|
181
|
-
expect.arrayContaining([
|
|
182
|
-
expect.objectContaining({
|
|
183
|
-
name: 'Content Editors',
|
|
184
|
-
description: 'Team members who can edit content',
|
|
185
|
-
members: ['user-1', 'user-2'],
|
|
186
|
-
id: expect.any(String), // Should have generated ID
|
|
187
|
-
}),
|
|
188
|
-
]),
|
|
189
|
-
'admin-1',
|
|
190
|
-
'dev',
|
|
191
|
-
1, // contentVersion starts at 1 when file doesn't exist
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
// Verify the generated ID is not empty
|
|
195
|
-
const savedGroups = vi.mocked(groupsLoader.saveInternalGroups).mock.calls[0][1]
|
|
196
|
-
expect(savedGroups[0].id).not.toBe('')
|
|
197
|
-
expect(savedGroups[0].id.length).toBeGreaterThan(0)
|
|
198
|
-
|
|
199
|
-
// In dev mode (default), no git operations are performed
|
|
200
|
-
expect(mockContext.services.commitFiles).not.toHaveBeenCalled()
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe('searchExternalGroups', () => {
|
|
205
|
-
it('should return 403 for non-admin users', async () => {
|
|
206
|
-
const req: ApiRequest<undefined> = {
|
|
207
|
-
user: {
|
|
208
|
-
type: 'authenticated',
|
|
209
|
-
userId: 'user-1' as CanopyUserId,
|
|
210
|
-
groups: [],
|
|
211
|
-
},
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const params: SearchExternalGroupsParams = { query: 'test' }
|
|
215
|
-
|
|
216
|
-
const result = await searchExternalGroups(mockContext, req, params)
|
|
217
|
-
|
|
218
|
-
expect(result).toEqual({
|
|
219
|
-
ok: false,
|
|
220
|
-
status: 403,
|
|
221
|
-
error: 'Admin access required',
|
|
222
|
-
})
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('should return 501 if auth plugin not configured', async () => {
|
|
226
|
-
mockContext.services.config.authPlugin = undefined
|
|
227
|
-
|
|
228
|
-
const req: ApiRequest<undefined> = {
|
|
229
|
-
user: {
|
|
230
|
-
type: 'authenticated',
|
|
231
|
-
userId: 'admin-1' as CanopyUserId,
|
|
232
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
233
|
-
},
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const params: SearchExternalGroupsParams = { query: 'test' }
|
|
237
|
-
|
|
238
|
-
const result = await searchExternalGroups(mockContext, req, params)
|
|
239
|
-
|
|
240
|
-
expect(result).toEqual({
|
|
241
|
-
ok: false,
|
|
242
|
-
status: 501,
|
|
243
|
-
error: 'External group search not configured',
|
|
244
|
-
})
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('should return 501 if searchExternalGroups method not available', async () => {
|
|
248
|
-
mockContext.services.config.authPlugin = {
|
|
249
|
-
searchUsers: vi.fn(),
|
|
250
|
-
// searchExternalGroups not provided
|
|
251
|
-
} as any
|
|
252
|
-
|
|
253
|
-
const req: ApiRequest<undefined> = {
|
|
254
|
-
user: {
|
|
255
|
-
type: 'authenticated',
|
|
256
|
-
userId: 'admin-1' as CanopyUserId,
|
|
257
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
258
|
-
},
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const params: SearchExternalGroupsParams = { query: 'test' }
|
|
262
|
-
|
|
263
|
-
const result = await searchExternalGroups(mockContext, req, params)
|
|
264
|
-
|
|
265
|
-
expect(result).toEqual({
|
|
266
|
-
ok: false,
|
|
267
|
-
status: 501,
|
|
268
|
-
error: 'External group search not configured',
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('should return search results from auth plugin', async () => {
|
|
273
|
-
const mockGroups = [
|
|
274
|
-
{ id: 'org_123' as CanopyGroupId, name: 'Acme Corporation' },
|
|
275
|
-
{ id: 'org_456' as CanopyGroupId, name: 'Partner Organization' },
|
|
276
|
-
]
|
|
277
|
-
|
|
278
|
-
mockContext.authPlugin = {
|
|
279
|
-
searchExternalGroups: vi.fn(async () => mockGroups),
|
|
280
|
-
} as any
|
|
281
|
-
|
|
282
|
-
const req: ApiRequest<undefined> = {
|
|
283
|
-
user: {
|
|
284
|
-
type: 'authenticated',
|
|
285
|
-
userId: 'admin-1' as CanopyUserId,
|
|
286
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const params: SearchExternalGroupsParams = { query: 'test' }
|
|
291
|
-
|
|
292
|
-
const result = await searchExternalGroups(mockContext, req, params)
|
|
293
|
-
|
|
294
|
-
expect(result.ok).toBe(true)
|
|
295
|
-
expect(result.status).toBe(200)
|
|
296
|
-
if (result.ok) {
|
|
297
|
-
expect(result.data?.groups).toEqual(mockGroups)
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it('should return 500 on auth plugin error', async () => {
|
|
302
|
-
mockContext.authPlugin = {
|
|
303
|
-
searchExternalGroups: vi.fn(async () => {
|
|
304
|
-
throw new Error('Search failed')
|
|
305
|
-
}),
|
|
306
|
-
} as any
|
|
307
|
-
|
|
308
|
-
const req: ApiRequest<undefined> = {
|
|
309
|
-
user: {
|
|
310
|
-
type: 'authenticated',
|
|
311
|
-
userId: 'admin-1' as CanopyUserId,
|
|
312
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
313
|
-
},
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const params: SearchExternalGroupsParams = { query: 'test' }
|
|
317
|
-
|
|
318
|
-
const result = await searchExternalGroups(mockContext, req, params)
|
|
319
|
-
|
|
320
|
-
expect(result).toEqual({
|
|
321
|
-
ok: false,
|
|
322
|
-
status: 500,
|
|
323
|
-
error: 'Search failed',
|
|
324
|
-
})
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe('validateAdminGroupUpdate', () => {
|
|
329
|
-
it('should return valid when Admins group has members', () => {
|
|
330
|
-
const groups: InternalGroup[] = [
|
|
331
|
-
{
|
|
332
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
333
|
-
name: 'Admins',
|
|
334
|
-
members: ['admin-1' as CanopyUserId],
|
|
335
|
-
},
|
|
336
|
-
]
|
|
337
|
-
const result = validateAdminGroupUpdate(groups, new Set())
|
|
338
|
-
expect(result.valid).toBe(true)
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
it('should return valid when bootstrap admins exist even if Admins group is empty', () => {
|
|
342
|
-
const groups: InternalGroup[] = [
|
|
343
|
-
{
|
|
344
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
345
|
-
name: 'Admins',
|
|
346
|
-
members: [],
|
|
347
|
-
},
|
|
348
|
-
]
|
|
349
|
-
const result = validateAdminGroupUpdate(groups, new Set(['bootstrap-admin']))
|
|
350
|
-
expect(result.valid).toBe(true)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('should return valid when bootstrap admins exist and Admins group is missing', () => {
|
|
354
|
-
const groups: InternalGroup[] = []
|
|
355
|
-
const result = validateAdminGroupUpdate(groups, new Set(['bootstrap-admin']))
|
|
356
|
-
expect(result.valid).toBe(true)
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('should return invalid when no admins exist', () => {
|
|
360
|
-
const groups: InternalGroup[] = [
|
|
361
|
-
{
|
|
362
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
363
|
-
name: 'Admins',
|
|
364
|
-
members: [],
|
|
365
|
-
},
|
|
366
|
-
]
|
|
367
|
-
const result = validateAdminGroupUpdate(groups, new Set())
|
|
368
|
-
expect(result.valid).toBe(false)
|
|
369
|
-
expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
it('should return invalid when Admins group is missing and no bootstrap admins', () => {
|
|
373
|
-
const groups: InternalGroup[] = []
|
|
374
|
-
const result = validateAdminGroupUpdate(groups, new Set())
|
|
375
|
-
expect(result.valid).toBe(false)
|
|
376
|
-
expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('should not double count when bootstrap admin is also in Admins group', () => {
|
|
380
|
-
const groups: InternalGroup[] = [
|
|
381
|
-
{
|
|
382
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
383
|
-
name: 'Admins',
|
|
384
|
-
members: ['admin-1' as CanopyUserId],
|
|
385
|
-
},
|
|
386
|
-
]
|
|
387
|
-
// Same user is bootstrap admin
|
|
388
|
-
const result = validateAdminGroupUpdate(groups, new Set(['admin-1']))
|
|
389
|
-
expect(result.valid).toBe(true)
|
|
390
|
-
// Still valid but only counts as 1 admin, not 2
|
|
391
|
-
})
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
describe('validateReservedGroups', () => {
|
|
395
|
-
it('should return valid for non-reserved groups', () => {
|
|
396
|
-
const groups: InternalGroup[] = [
|
|
397
|
-
{
|
|
398
|
-
id: 'editors' as CanopyGroupId,
|
|
399
|
-
name: 'Content Editors',
|
|
400
|
-
members: [],
|
|
401
|
-
},
|
|
402
|
-
]
|
|
403
|
-
const result = validateReservedGroups(groups)
|
|
404
|
-
expect(result.valid).toBe(true)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it('should return valid when reserved group name matches ID', () => {
|
|
408
|
-
const groups: InternalGroup[] = [
|
|
409
|
-
{
|
|
410
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
411
|
-
name: 'Admins',
|
|
412
|
-
members: [],
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
id: RESERVED_GROUPS.REVIEWERS as CanopyGroupId,
|
|
416
|
-
name: 'Reviewers',
|
|
417
|
-
members: [],
|
|
418
|
-
},
|
|
419
|
-
]
|
|
420
|
-
const result = validateReservedGroups(groups)
|
|
421
|
-
expect(result.valid).toBe(true)
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('should return invalid when Admins group is renamed', () => {
|
|
425
|
-
const groups: InternalGroup[] = [
|
|
426
|
-
{
|
|
427
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
428
|
-
name: 'Administrators',
|
|
429
|
-
members: [],
|
|
430
|
-
},
|
|
431
|
-
]
|
|
432
|
-
const result = validateReservedGroups(groups)
|
|
433
|
-
expect(result.valid).toBe(false)
|
|
434
|
-
expect(result.error).toBe("Reserved group 'Admins' cannot be renamed")
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('should return invalid when Reviewers group is renamed', () => {
|
|
438
|
-
const groups: InternalGroup[] = [
|
|
439
|
-
{
|
|
440
|
-
id: RESERVED_GROUPS.REVIEWERS as CanopyGroupId,
|
|
441
|
-
name: 'Content Reviewers',
|
|
442
|
-
members: [],
|
|
443
|
-
},
|
|
444
|
-
]
|
|
445
|
-
const result = validateReservedGroups(groups)
|
|
446
|
-
expect(result.valid).toBe(false)
|
|
447
|
-
expect(result.error).toBe("Reserved group 'Reviewers' cannot be renamed")
|
|
448
|
-
})
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
describe('updateInternalGroups safety validations', () => {
|
|
452
|
-
it('should reject update that removes last admin', async () => {
|
|
453
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
454
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
455
|
-
|
|
456
|
-
const groups: InternalGroup[] = [
|
|
457
|
-
{
|
|
458
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
459
|
-
name: 'Admins',
|
|
460
|
-
members: [],
|
|
461
|
-
},
|
|
462
|
-
]
|
|
463
|
-
|
|
464
|
-
const req: ApiRequest = {
|
|
465
|
-
user: {
|
|
466
|
-
type: 'authenticated',
|
|
467
|
-
userId: 'admin-1' as CanopyUserId,
|
|
468
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
469
|
-
},
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
473
|
-
|
|
474
|
-
expect(result.ok).toBe(false)
|
|
475
|
-
expect(result.status).toBe(400)
|
|
476
|
-
expect(result.error).toBe('Cannot remove last admin - at least one admin is required')
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('should reject update that renames reserved group', async () => {
|
|
480
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
481
|
-
// Mock existing Admins group so it's recognized as existing
|
|
482
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
|
|
483
|
-
{
|
|
484
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
485
|
-
name: RESERVED_GROUPS.ADMINS,
|
|
486
|
-
members: ['admin-1' as CanopyUserId],
|
|
487
|
-
},
|
|
488
|
-
])
|
|
489
|
-
|
|
490
|
-
const groups: InternalGroup[] = [
|
|
491
|
-
{
|
|
492
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
493
|
-
name: 'Super Admins',
|
|
494
|
-
members: ['admin-1' as CanopyUserId],
|
|
495
|
-
},
|
|
496
|
-
]
|
|
497
|
-
|
|
498
|
-
const req: ApiRequest = {
|
|
499
|
-
user: {
|
|
500
|
-
type: 'authenticated',
|
|
501
|
-
userId: 'admin-1' as CanopyUserId,
|
|
502
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
503
|
-
},
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
507
|
-
|
|
508
|
-
expect(result.ok).toBe(false)
|
|
509
|
-
expect(result.status).toBe(400)
|
|
510
|
-
expect(result.error).toBe("Reserved group 'Admins' cannot be renamed")
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
it('should allow update when bootstrap admin exists even with empty Admins group', async () => {
|
|
514
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
515
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
516
|
-
|
|
517
|
-
// Add bootstrap admin
|
|
518
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
519
|
-
|
|
520
|
-
const groups: InternalGroup[] = [
|
|
521
|
-
{
|
|
522
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
523
|
-
name: 'Admins',
|
|
524
|
-
members: [],
|
|
525
|
-
},
|
|
526
|
-
]
|
|
527
|
-
|
|
528
|
-
const req: ApiRequest = {
|
|
529
|
-
user: {
|
|
530
|
-
type: 'authenticated',
|
|
531
|
-
userId: 'admin-1' as CanopyUserId,
|
|
532
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
533
|
-
},
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
537
|
-
|
|
538
|
-
expect(result.ok).toBe(true)
|
|
539
|
-
expect(result.status).toBe(200)
|
|
540
|
-
})
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
describe('collision detection', () => {
|
|
544
|
-
it('should reject duplicate group names', async () => {
|
|
545
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
546
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
547
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
548
|
-
|
|
549
|
-
const groups: InternalGroup[] = [
|
|
550
|
-
{
|
|
551
|
-
id: '' as CanopyGroupId,
|
|
552
|
-
name: 'Editors',
|
|
553
|
-
members: [],
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
id: '' as CanopyGroupId,
|
|
557
|
-
name: 'Editors', // Duplicate name
|
|
558
|
-
members: [],
|
|
559
|
-
},
|
|
560
|
-
]
|
|
561
|
-
|
|
562
|
-
const req: ApiRequest = {
|
|
563
|
-
user: {
|
|
564
|
-
type: 'authenticated',
|
|
565
|
-
userId: 'admin-1' as CanopyUserId,
|
|
566
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
567
|
-
},
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
571
|
-
|
|
572
|
-
expect(result.ok).toBe(false)
|
|
573
|
-
expect(result.status).toBe(400)
|
|
574
|
-
expect(result.error).toBe('Duplicate group name detected: Editors')
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
it('should reject duplicate group names case-insensitively', async () => {
|
|
578
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
579
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
580
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
581
|
-
|
|
582
|
-
const groups: InternalGroup[] = [
|
|
583
|
-
{
|
|
584
|
-
id: '' as CanopyGroupId,
|
|
585
|
-
name: 'Editors',
|
|
586
|
-
members: [],
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
id: '' as CanopyGroupId,
|
|
590
|
-
name: 'editors', // Same name, different case
|
|
591
|
-
members: [],
|
|
592
|
-
},
|
|
593
|
-
]
|
|
594
|
-
|
|
595
|
-
const req: ApiRequest = {
|
|
596
|
-
user: {
|
|
597
|
-
type: 'authenticated',
|
|
598
|
-
userId: 'admin-1' as CanopyUserId,
|
|
599
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
600
|
-
},
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
604
|
-
|
|
605
|
-
expect(result.ok).toBe(false)
|
|
606
|
-
expect(result.status).toBe(400)
|
|
607
|
-
expect(result.error).toBe('Duplicate group name detected: editors')
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
it('should reject groups with manually specified duplicate IDs', async () => {
|
|
611
|
-
const existingGroups: InternalGroup[] = [
|
|
612
|
-
{
|
|
613
|
-
id: 'existing-1' as CanopyGroupId,
|
|
614
|
-
name: 'Existing Group',
|
|
615
|
-
members: [],
|
|
616
|
-
},
|
|
617
|
-
]
|
|
618
|
-
|
|
619
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
620
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
|
|
621
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
622
|
-
|
|
623
|
-
const groups: InternalGroup[] = [
|
|
624
|
-
{
|
|
625
|
-
id: 'existing-1' as CanopyGroupId,
|
|
626
|
-
name: 'Existing Group',
|
|
627
|
-
members: [],
|
|
628
|
-
},
|
|
629
|
-
{
|
|
630
|
-
id: 'existing-1' as CanopyGroupId, // Duplicate ID
|
|
631
|
-
name: 'Another Group',
|
|
632
|
-
members: [],
|
|
633
|
-
},
|
|
634
|
-
]
|
|
635
|
-
|
|
636
|
-
const req: ApiRequest = {
|
|
637
|
-
user: {
|
|
638
|
-
type: 'authenticated',
|
|
639
|
-
userId: 'admin-1' as CanopyUserId,
|
|
640
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
641
|
-
},
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
645
|
-
|
|
646
|
-
expect(result.ok).toBe(false)
|
|
647
|
-
expect(result.status).toBe(400)
|
|
648
|
-
expect(result.error).toBe('Duplicate group ID detected: existing-1')
|
|
649
|
-
})
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
describe('autogenerated group IDs', () => {
|
|
653
|
-
it('should generate unique IDs for new groups', async () => {
|
|
654
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
655
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
656
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
657
|
-
|
|
658
|
-
const groups: InternalGroup[] = [
|
|
659
|
-
{
|
|
660
|
-
id: '' as CanopyGroupId,
|
|
661
|
-
name: 'Group 1',
|
|
662
|
-
members: [],
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
id: '' as CanopyGroupId,
|
|
666
|
-
name: 'Group 2',
|
|
667
|
-
members: [],
|
|
668
|
-
},
|
|
669
|
-
]
|
|
670
|
-
|
|
671
|
-
const req: ApiRequest = {
|
|
672
|
-
user: {
|
|
673
|
-
type: 'authenticated',
|
|
674
|
-
userId: 'admin-1' as CanopyUserId,
|
|
675
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
676
|
-
},
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
680
|
-
|
|
681
|
-
expect(result.ok).toBe(true)
|
|
682
|
-
|
|
683
|
-
// Check that saveInternalGroups was called
|
|
684
|
-
expect(groupsLoader.saveInternalGroups).toHaveBeenCalled()
|
|
685
|
-
const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
|
|
686
|
-
const savedGroups = calls[calls.length - 1][1] // Get last call
|
|
687
|
-
expect(savedGroups).toBeDefined()
|
|
688
|
-
expect(savedGroups.length).toBe(2)
|
|
689
|
-
expect(savedGroups[0].id).not.toBe('')
|
|
690
|
-
expect(savedGroups[1].id).not.toBe('')
|
|
691
|
-
expect(savedGroups[0].id).not.toBe(savedGroups[1].id) // Different IDs
|
|
692
|
-
})
|
|
693
|
-
|
|
694
|
-
it('should preserve IDs for existing groups', async () => {
|
|
695
|
-
const existingGroups: InternalGroup[] = [
|
|
696
|
-
{
|
|
697
|
-
id: 'existing-group-id' as CanopyGroupId,
|
|
698
|
-
name: 'Existing Group',
|
|
699
|
-
members: [],
|
|
700
|
-
},
|
|
701
|
-
]
|
|
702
|
-
|
|
703
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
704
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
|
|
705
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
706
|
-
|
|
707
|
-
const groups: InternalGroup[] = [
|
|
708
|
-
{
|
|
709
|
-
id: 'existing-group-id' as CanopyGroupId,
|
|
710
|
-
name: 'Existing Group (updated)',
|
|
711
|
-
members: ['user-1' as CanopyUserId],
|
|
712
|
-
},
|
|
713
|
-
]
|
|
714
|
-
|
|
715
|
-
const req: ApiRequest = {
|
|
716
|
-
user: {
|
|
717
|
-
type: 'authenticated',
|
|
718
|
-
userId: 'admin-1' as CanopyUserId,
|
|
719
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
720
|
-
},
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
724
|
-
|
|
725
|
-
expect(result.ok).toBe(true)
|
|
726
|
-
|
|
727
|
-
const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
|
|
728
|
-
const savedGroups = calls[calls.length - 1][1]
|
|
729
|
-
expect(savedGroups.length).toBe(1)
|
|
730
|
-
expect(savedGroups[0].id).toBe('existing-group-id') // ID preserved
|
|
731
|
-
expect(savedGroups[0].name).toBe('Existing Group (updated)') // But name updated
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
it('should use group name as ID for reserved groups', async () => {
|
|
735
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
736
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
737
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
738
|
-
|
|
739
|
-
const groups: InternalGroup[] = [
|
|
740
|
-
{
|
|
741
|
-
id: '' as CanopyGroupId,
|
|
742
|
-
name: 'Admins',
|
|
743
|
-
members: ['admin-1' as CanopyUserId],
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
id: '' as CanopyGroupId,
|
|
747
|
-
name: 'Reviewers',
|
|
748
|
-
members: [],
|
|
749
|
-
},
|
|
750
|
-
]
|
|
751
|
-
|
|
752
|
-
const req: ApiRequest = {
|
|
753
|
-
user: {
|
|
754
|
-
type: 'authenticated',
|
|
755
|
-
userId: 'admin-1' as CanopyUserId,
|
|
756
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
757
|
-
},
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
761
|
-
|
|
762
|
-
expect(result.ok).toBe(true)
|
|
763
|
-
|
|
764
|
-
const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
|
|
765
|
-
const savedGroups = calls[calls.length - 1][1]
|
|
766
|
-
expect(savedGroups.length).toBe(2)
|
|
767
|
-
expect(savedGroups[0].id).toBe('Admins') // Reserved group uses name as ID
|
|
768
|
-
expect(savedGroups[1].id).toBe('Reviewers') // Reserved group uses name as ID
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
it('should mix existing groups with new groups correctly', async () => {
|
|
772
|
-
const existingGroups: InternalGroup[] = [
|
|
773
|
-
{
|
|
774
|
-
id: 'existing-1' as CanopyGroupId,
|
|
775
|
-
name: 'Existing Group',
|
|
776
|
-
members: [],
|
|
777
|
-
},
|
|
778
|
-
]
|
|
779
|
-
|
|
780
|
-
vi.mocked(groupsLoader.saveInternalGroups).mockResolvedValue()
|
|
781
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue(existingGroups)
|
|
782
|
-
;(mockContext.services as any).bootstrapAdminIds = new Set(['bootstrap-admin'])
|
|
783
|
-
|
|
784
|
-
const groups: InternalGroup[] = [
|
|
785
|
-
{
|
|
786
|
-
id: 'existing-1' as CanopyGroupId,
|
|
787
|
-
name: 'Existing Group',
|
|
788
|
-
members: [],
|
|
789
|
-
},
|
|
790
|
-
{
|
|
791
|
-
id: '' as CanopyGroupId,
|
|
792
|
-
name: 'New Group',
|
|
793
|
-
members: [],
|
|
794
|
-
},
|
|
795
|
-
]
|
|
796
|
-
|
|
797
|
-
const req: ApiRequest = {
|
|
798
|
-
user: {
|
|
799
|
-
type: 'authenticated',
|
|
800
|
-
userId: 'admin-1' as CanopyUserId,
|
|
801
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
802
|
-
},
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const result = await updateInternalGroups(mockContext, req, { groups })
|
|
806
|
-
|
|
807
|
-
expect(result.ok).toBe(true)
|
|
808
|
-
|
|
809
|
-
const calls = vi.mocked(groupsLoader.saveInternalGroups).mock.calls
|
|
810
|
-
const savedGroups = calls[calls.length - 1][1]
|
|
811
|
-
expect(savedGroups.length).toBe(2)
|
|
812
|
-
expect(savedGroups[0].id).toBe('existing-1') // Existing ID preserved
|
|
813
|
-
expect(savedGroups[1].id).not.toBe('') // New group got generated ID
|
|
814
|
-
expect(savedGroups[1].id).not.toBe('existing-1') // Different from existing
|
|
815
|
-
})
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
describe('optimistic locking with contentVersion', () => {
|
|
819
|
-
it('should return 409 when expectedContentVersion does not match current version', async () => {
|
|
820
|
-
// Mock loadGroupsFile to return a file with contentVersion 5
|
|
821
|
-
vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
|
|
822
|
-
version: 1,
|
|
823
|
-
contentVersion: 5,
|
|
824
|
-
updatedAt: '2024-01-01T00:00:00Z',
|
|
825
|
-
updatedBy: 'other-admin' as CanopyUserId,
|
|
826
|
-
groups: [
|
|
827
|
-
{
|
|
828
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
829
|
-
name: 'Admins',
|
|
830
|
-
members: ['admin-1' as CanopyUserId],
|
|
831
|
-
},
|
|
832
|
-
],
|
|
833
|
-
})
|
|
834
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
|
|
835
|
-
{
|
|
836
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
837
|
-
name: 'Admins',
|
|
838
|
-
members: ['admin-1' as CanopyUserId],
|
|
839
|
-
},
|
|
840
|
-
])
|
|
841
|
-
|
|
842
|
-
const req: ApiRequest = {
|
|
843
|
-
user: {
|
|
844
|
-
type: 'authenticated',
|
|
845
|
-
userId: 'admin-1' as CanopyUserId,
|
|
846
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
847
|
-
},
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const body = {
|
|
851
|
-
groups: [
|
|
852
|
-
{
|
|
853
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
854
|
-
name: 'Admins',
|
|
855
|
-
members: ['admin-1' as CanopyUserId],
|
|
856
|
-
},
|
|
857
|
-
],
|
|
858
|
-
expectedContentVersion: 3, // Client thinks version is 3, but it's actually 5
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
const result = await updateInternalGroups(mockContext, req, body)
|
|
862
|
-
|
|
863
|
-
expect(result.ok).toBe(false)
|
|
864
|
-
expect(result.status).toBe(409)
|
|
865
|
-
expect(result.error).toBe(
|
|
866
|
-
'Groups were modified by another user. Please reload and try again.',
|
|
867
|
-
)
|
|
868
|
-
})
|
|
869
|
-
|
|
870
|
-
it('should succeed when expectedContentVersion matches current version', async () => {
|
|
871
|
-
// Mock loadGroupsFile to return a file with contentVersion 5
|
|
872
|
-
vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
|
|
873
|
-
version: 1,
|
|
874
|
-
contentVersion: 5,
|
|
875
|
-
updatedAt: '2024-01-01T00:00:00Z',
|
|
876
|
-
updatedBy: 'admin-1' as CanopyUserId,
|
|
877
|
-
groups: [
|
|
878
|
-
{
|
|
879
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
880
|
-
name: 'Admins',
|
|
881
|
-
members: ['admin-1' as CanopyUserId],
|
|
882
|
-
},
|
|
883
|
-
],
|
|
884
|
-
})
|
|
885
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
|
|
886
|
-
{
|
|
887
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
888
|
-
name: 'Admins',
|
|
889
|
-
members: ['admin-1' as CanopyUserId],
|
|
890
|
-
},
|
|
891
|
-
])
|
|
892
|
-
|
|
893
|
-
const req: ApiRequest = {
|
|
894
|
-
user: {
|
|
895
|
-
type: 'authenticated',
|
|
896
|
-
userId: 'admin-1' as CanopyUserId,
|
|
897
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
898
|
-
},
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const body = {
|
|
902
|
-
groups: [
|
|
903
|
-
{
|
|
904
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
905
|
-
name: 'Admins',
|
|
906
|
-
members: ['admin-1' as CanopyUserId],
|
|
907
|
-
},
|
|
908
|
-
],
|
|
909
|
-
expectedContentVersion: 5, // Matches current version
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const result = await updateInternalGroups(mockContext, req, body)
|
|
913
|
-
|
|
914
|
-
expect(result.ok).toBe(true)
|
|
915
|
-
expect(result.status).toBe(200)
|
|
916
|
-
|
|
917
|
-
// Verify saveInternalGroups was called with incremented version
|
|
918
|
-
expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
|
|
919
|
-
expect.any(String), // branchRoot varies by test context
|
|
920
|
-
[{ id: RESERVED_GROUPS.ADMINS, name: 'Admins', members: ['admin-1'] }],
|
|
921
|
-
'admin-1',
|
|
922
|
-
'dev',
|
|
923
|
-
6, // Should be 5 + 1
|
|
924
|
-
)
|
|
925
|
-
})
|
|
926
|
-
|
|
927
|
-
it('should allow update when expectedContentVersion is not provided (backward compatible)', async () => {
|
|
928
|
-
// Mock loadGroupsFile to return a file with contentVersion 5
|
|
929
|
-
vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue({
|
|
930
|
-
version: 1,
|
|
931
|
-
contentVersion: 5,
|
|
932
|
-
updatedAt: '2024-01-01T00:00:00Z',
|
|
933
|
-
updatedBy: 'admin-1' as CanopyUserId,
|
|
934
|
-
groups: [
|
|
935
|
-
{
|
|
936
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
937
|
-
name: 'Admins',
|
|
938
|
-
members: ['admin-1' as CanopyUserId],
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
})
|
|
942
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([
|
|
943
|
-
{
|
|
944
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
945
|
-
name: 'Admins',
|
|
946
|
-
members: ['admin-1' as CanopyUserId],
|
|
947
|
-
},
|
|
948
|
-
])
|
|
949
|
-
|
|
950
|
-
const req: ApiRequest = {
|
|
951
|
-
user: {
|
|
952
|
-
type: 'authenticated',
|
|
953
|
-
userId: 'admin-1' as CanopyUserId,
|
|
954
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
955
|
-
},
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const body = {
|
|
959
|
-
groups: [
|
|
960
|
-
{
|
|
961
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
962
|
-
name: 'Admins',
|
|
963
|
-
members: ['admin-1' as CanopyUserId],
|
|
964
|
-
},
|
|
965
|
-
],
|
|
966
|
-
// No expectedContentVersion provided
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const result = await updateInternalGroups(mockContext, req, body)
|
|
970
|
-
|
|
971
|
-
expect(result.ok).toBe(true)
|
|
972
|
-
expect(result.status).toBe(200)
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
it('should start at version 1 for new files without contentVersion', async () => {
|
|
976
|
-
// Mock loadGroupsFile to return null (file doesn't exist)
|
|
977
|
-
vi.mocked(groupsLoader.loadGroupsFile).mockResolvedValue(null)
|
|
978
|
-
vi.mocked(groupsLoader.loadInternalGroups).mockResolvedValue([])
|
|
979
|
-
|
|
980
|
-
const req: ApiRequest = {
|
|
981
|
-
user: {
|
|
982
|
-
type: 'authenticated',
|
|
983
|
-
userId: 'admin-1' as CanopyUserId,
|
|
984
|
-
groups: [RESERVED_GROUPS.ADMINS],
|
|
985
|
-
},
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const body = {
|
|
989
|
-
groups: [
|
|
990
|
-
{
|
|
991
|
-
id: RESERVED_GROUPS.ADMINS as CanopyGroupId,
|
|
992
|
-
name: 'Admins',
|
|
993
|
-
members: ['admin-1' as CanopyUserId],
|
|
994
|
-
},
|
|
995
|
-
],
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const result = await updateInternalGroups(mockContext, req, body)
|
|
999
|
-
|
|
1000
|
-
expect(result.ok).toBe(true)
|
|
1001
|
-
expect(result.status).toBe(200)
|
|
1002
|
-
|
|
1003
|
-
// Verify saveInternalGroups was called with version 1 (0 + 1)
|
|
1004
|
-
expect(groupsLoader.saveInternalGroups).toHaveBeenCalledWith(
|
|
1005
|
-
expect.any(String), // branchRoot varies by test context
|
|
1006
|
-
[{ id: RESERVED_GROUPS.ADMINS, name: 'Admins', members: ['admin-1'] }],
|
|
1007
|
-
'admin-1',
|
|
1008
|
-
'dev',
|
|
1009
|
-
1,
|
|
1010
|
-
)
|
|
1011
|
-
})
|
|
1012
|
-
})
|
|
1013
|
-
})
|