@oj-module/cli 0.2.64 → 0.2.67
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/bin/.oj-version +1 -0
- package/bin/oj +4 -0
- package/devkit/README.md +18 -4
- package/devkit/SKILL.md +35 -3
- package/devkit/framework-dev.md +22 -11
- package/devkit/manual.md +161 -68
- package/devkit/menu-rules.md +4 -1
- package/devkit/publish-walkthrough.md +110 -37
- package/devkit/wecom-internal-app.md +7 -2
- package/devkit/wecom-multi-corp-links.md +163 -0
- package/package.json +3 -2
- package/shell-dist/assets/ahooks.2l850ymmh0.js +18 -0
- package/shell-dist/assets/antd-es-locale-zh_CN.1gjjsvxbf0.js +1 -0
- package/shell-dist/assets/antd-img-crop.196rkty480.js +99 -0
- package/shell-dist/assets/antd-locale-en_US.561tozx860.js +1 -0
- package/shell-dist/assets/antd-locale-zh_CN.1cmpmgzh70.js +1 -0
- package/shell-dist/assets/antd.uw1coq15l0.js +348 -0
- package/shell-dist/assets/clsx.jotilrt2e0.js +1 -0
- package/shell-dist/assets/contract-errors.2o17vz9u50.js +1 -0
- package/shell-dist/assets/contract.3h327o3mj0.js +57 -0
- package/shell-dist/assets/cssinjs.atgmhan7m0.js +3 -0
- package/shell-dist/assets/dayjs-plugin-advancedFormat.3vsyd2eis0.js +1 -0
- package/shell-dist/assets/dayjs-plugin-customParseFormat.13xhghv1d0.js +1 -0
- package/shell-dist/assets/dayjs-plugin-isoWeek.1fn1tw82p0.js +1 -0
- package/shell-dist/assets/dayjs-plugin-localeData.2rwbjp9nk0.js +1 -0
- package/shell-dist/assets/dayjs-plugin-quarterOfYear.2oqo1lcx80.js +1 -0
- package/shell-dist/assets/dayjs-plugin-relativeTime.2pqs3et4y0.js +1 -0
- package/shell-dist/assets/dayjs-plugin-weekOfYear.gvn9iph080.js +1 -0
- package/shell-dist/assets/dayjs-plugin-weekYear.2u78l3ex10.js +1 -0
- package/shell-dist/assets/dayjs-plugin-weekday.2d56a4tml0.js +1 -0
- package/shell-dist/assets/dayjs.pa60l6wjo0.js +1 -0
- package/shell-dist/assets/dnd-kit-core.1xyreig6r0.js +5 -0
- package/shell-dist/assets/dnd-kit-sortable.s94qciuft0.js +1 -0
- package/shell-dist/assets/dnd-kit-utilities.1umw4xphb0.js +1 -0
- package/shell-dist/assets/i18next.33zqofapj0.js +1 -0
- package/shell-dist/assets/icons-es-components-Context.365qmflwg0.js +1 -0
- package/shell-dist/assets/icons.159wjwals0.js +57 -0
- package/shell-dist/assets/{index-xZhmujjM.js → index.6alqyx6ae0.js} +21 -1
- package/shell-dist/assets/jsx-dev-runtime.3uibuc82f0.js +14 -0
- package/shell-dist/assets/jsx-runtime.h40xu4o5b0.js +14 -0
- package/shell-dist/assets/keepalive-for-react.xenu5vam70.js +1 -0
- package/shell-dist/assets/kit.37kr8ui390.js +1 -0
- package/shell-dist/assets/ky.1cgch7gu60.js +6 -0
- package/shell-dist/assets/motion-react.pjsxytvjs0.js +12 -0
- package/shell-dist/assets/motion.21tciqutg0.js +4 -0
- package/shell-dist/assets/nprogress.2nz76n9ny0.js +7 -0
- package/shell-dist/assets/ojm-dayjs-locale-zh-cn.3vr3beiyp0.js +9 -0
- package/shell-dist/assets/pinyin-pro.3v0mnxczn0.js +1 -0
- package/shell-dist/assets/pro-components.2kyot1vcl0.js +83 -0
- package/shell-dist/assets/react-countup.2x8idnr8y0.js +10 -0
- package/shell-dist/assets/react-dom-client.ick0v05ob0.js +43 -0
- package/shell-dist/assets/react-dom.2so9uxg3f0.js +22 -0
- package/shell-dist/assets/react-error-boundary.2dncdfj3b0.js +1 -0
- package/shell-dist/assets/react-i18next.auhvjhqv00.js +1 -0
- package/shell-dist/assets/react-jss.1ei6dr9e00.js +29 -0
- package/shell-dist/assets/react-query.3rln45ow00.js +1 -0
- package/shell-dist/assets/react-router-dom.3c6zld8h40.js +44 -0
- package/shell-dist/assets/react-router.2h9rebcv50.js +56 -0
- package/shell-dist/assets/react.1ew63mwts0.js +14 -0
- package/shell-dist/assets/runtime.2dgstpjsu0.js +21702 -0
- package/shell-dist/assets/simplebar-react.2g739uvai0.js +1 -0
- package/shell-dist/assets/spin-delay.yjeyj6iwp0.js +9 -0
- package/shell-dist/assets/tailwind-merge.125ehiqjw0.js +1 -0
- package/shell-dist/assets/use-sync-external-store-shim.1nz1pi9bb0.js +22 -0
- package/shell-dist/assets/zustand-middleware.1xaxjueme0.js +6 -0
- package/shell-dist/assets/zustand-react-shallow.pybayv1hx0.js +1 -0
- package/shell-dist/assets/zustand-react.9fsl9brxy0.js +1 -0
- package/shell-dist/assets/zustand-shallow.6cz0yyv9x0.js +1 -0
- package/shell-dist/assets/zustand-vanilla-shallow.3424monv50.js +1 -0
- package/shell-dist/assets/zustand-vanilla.1mo3ka3g30.js +1 -0
- package/shell-dist/assets/zustand.3gjgzlnqo0.js +1 -0
- package/shell-dist/index.html +67 -118
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/publish-keygen.test.ts +157 -0
- package/src/__tests__/templates-migration-schema-parity.test.ts +540 -0
- package/src/__tests__/templates-schema-columns.test.ts +15 -2
- package/src/__tests__/templates-tenant-scope.test.ts +6 -2
- package/src/bundle.ts +8 -3
- package/src/cli.ts +17 -5
- package/src/dev.ts +15 -10
- package/src/esm-exports.ts +152 -70
- package/src/install.ts +11 -4
- package/src/keygen.ts +54 -0
- package/src/lifecycle/__tests__/bundle.test.ts +21 -3
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +1 -1
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +2 -2
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +5 -2
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +1 -1
- package/src/lifecycle/__tests__/manifest-contract.test.ts +75 -3
- package/src/lifecycle/__tests__/smoke.test.ts +2 -2
- package/src/manifest-contract.ts +91 -8
- package/src/market/bootstrap.ts +26 -2
- package/src/market/command.ts +1 -0
- package/src/market/publish.ts +5 -1
- package/src/market/walkthrough.ts +24 -8
- package/src/shared-deps.ts +69 -19
- package/src/site-base.ts +4 -0
- package/src/site.ts +2 -0
- package/src/usage.ts +8 -4
- package/src/vendor.ts +5 -1
- package/templates/config.yaml +4 -2
- package/templates/init.include.yaml +8 -3
- package/templates/openapi.json +460 -1
- package/templates/src/app/api/_platform/_lifecycle/compensate.ts +136 -0
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +71 -22
- package/templates/src/app/api/_platform/_lifecycle/lock.ts +23 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/deploy-web.ts +28 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +46 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +100 -36
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +7 -15
- package/templates/src/app/api/_platform/_lifecycle/text-edit.ts +71 -19
- package/templates/src/app/api/_platform/_lifecycle/types.ts +26 -2
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +93 -14
- package/templates/src/app/api/_platform/_shared/service/domain/settings.ts +8 -4
- package/templates/src/app/api/_platform/manifest.yaml +1 -0
- package/templates/src/app/api/_platform/market/api.ts +235 -10
- package/templates/src/app/api/_platform/migrations/0001__baseline.mysql.sql +178 -0
- package/templates/src/app/api/_platform/migrations/0001__baseline.pg.sql +173 -0
- package/templates/src/app/api/_platform/migrations/0001__baseline.sql +188 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.mysql.sql +17 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.pg.sql +16 -0
- package/templates/src/app/api/_platform/migrations/0002__market_release.sql +33 -0
- package/templates/src/app/api/_platform/schema.yaml +18 -0
- package/templates/src/app/api/_platform/seed.mysql.sql +10 -0
- package/templates/src/app/api/_platform/seed.pg.sql +10 -0
- package/templates/src/app/api/auth/_shared/wecom/client.ts +24 -4
- package/templates/src/app/api/home/migrations/0001__create_home_tables.mysql.sql +14 -0
- package/templates/src/app/api/home/migrations/0001__create_home_tables.pg.sql +14 -0
- package/templates/src/app/api/home/migrations/0002__tenant.pg.sql +4 -0
- package/templates/src/app/api/home/seed.mysql.sql +71 -0
- package/templates/src/app/api/home/seed.pg.sql +71 -0
- package/templates/src/app/api/notification/_shared/notify.ts +6 -6
- package/templates/src/app/api/notification/migrations/0001__create_notifications.mysql.sql +11 -0
- package/templates/src/app/api/notification/migrations/0001__create_notifications.pg.sql +11 -0
- package/templates/src/app/api/notification/migrations/0002__notifications_user.mysql.sql +8 -0
- package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.mysql.sql +21 -0
- package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.pg.sql +22 -0
- package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.mysql.sql +10 -0
- package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.pg.sql +11 -0
- package/templates/src/app/api/notification/migrations/0005__tenant.pg.sql +6 -0
- package/templates/src/app/api/notification/seed.mysql.sql +8 -0
- package/templates/src/app/api/notification/seed.pg.sql +11 -0
- package/templates/src/app/api/publish/_shared/identity.ts +69 -0
- package/templates/src/app/api/publish/api.ts +349 -0
- package/templates/src/app/api/publish/manifest.yaml +8 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.mysql.sql +23 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.pg.sql +23 -0
- package/templates/src/app/api/publish/migrations/0001__publish_credential.sql +39 -0
- package/templates/src/app/api/publish/schema.yaml +33 -0
- package/templates/src/app/api/system/login-methods/api.ts +2 -1
- package/templates/src/app/web/_platform/__client/api.schemas.ts +15 -0
- package/templates/src/app/web/_platform/__client/index.ts +25 -1
- package/templates/src/app/web/entry.ts +9 -5
- package/templates/src/app/web/home/pages/components/bar-chart.tsx +5 -3
- package/templates/src/app/web/home/pages/components/echarts-setup.ts +30 -0
- package/templates/src/app/web/home/pages/components/line-chart.tsx +5 -3
- package/templates/src/app/web/home/pages/components/pie-chart.tsx +6 -3
- package/templates/src/app/web/home/pages/index.tsx +3 -2
- package/templates/src/app/web/home/routes.ts +8 -2
- package/templates/src/app/web/login/locales/en-US.json +7 -1
- package/templates/src/app/web/login/locales/zh-CN.json +7 -1
- package/templates/src/app/web/login/pages/login.tsx +79 -86
- package/templates/src/app/web/login/routes.ts +54 -1
- package/templates/src/app/web/personal-center/locales/en-US.json +1 -0
- package/templates/src/app/web/personal-center/locales/zh-CN.json +1 -0
- package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +5 -0
- package/templates/src/app/web/system/__client/api.schemas.ts +2 -1
- package/templates/src/app/web/system/locales/en-US.json +1 -8
- package/templates/src/app/web/system/locales/zh-CN.json +1 -8
- package/templates/src/app/web/system/pages/settings/index.tsx +17 -4
- package/templates/src/app/web/system/routes.ts +0 -6
- package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +33 -0
- package/templates/src/oj-market/web/__tests__/DetailDrawer.test.tsx +69 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +7 -1
- package/templates/src/oj-market/web/api/client.ts +3 -0
- package/templates/src/oj-market/web/api/defaultClient.ts +5 -0
- package/templates/src/oj-market/web/components/CardItem.tsx +44 -1
- package/templates/src/oj-market/web/components/DetailDrawer.tsx +17 -1
- package/templates/src/oj-market/web/entry.ts +31 -2
- package/templates/src/oj-market/web/locales/en-US.json +5 -1
- package/templates/src/oj-market/web/locales/zh-CN.json +5 -1
- package/templates/src/oj-market/web/publish/__client/api.schemas.ts +61 -0
- package/templates/src/oj-market/web/publish/__client/index.ts +164 -0
- package/templates/src/oj-market/web/publish/locales/en-US.json +80 -0
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +80 -0
- package/templates/src/oj-market/web/publish/pages/history.tsx +112 -0
- package/templates/src/oj-market/web/publish/pages/index.tsx +196 -0
- package/templates/src/oj-market/web/publish/pages/model.ts +52 -0
- package/templates/src/oj-market/web/publish/pages/token-modal.tsx +82 -0
- package/templates/src/oj-market/web/publish/routes.ts +41 -0
- package/templates/src/oj-market/web/types.ts +30 -0
- package/templates/tests/lifecycle/compensate.spec.ts +207 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +38 -14
- package/templates/tests/lifecycle/installer.spec.ts +28 -9
- package/templates/tests/lifecycle/market-api.spec.ts +164 -12
- package/templates/tests/lifecycle/preflight.spec.ts +68 -0
- package/templates/tests/lifecycle/role-title-translate.spec.ts +2 -32
- package/templates/tests/lifecycle/text-edit.spec.ts +45 -27
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -0
- package/templates/tests/publish-credential.spec.ts +354 -0
- package/templates/tests/publish-identity.spec.ts +44 -0
- package/templates/tests/wecom-client-timeout.spec.ts +33 -3
- package/templates/tests/wecom-landing.spec.ts +63 -1
- package/templates/tsconfig.paths.json +1 -0
- package/templates/web.config.ts +6 -4
- package/vendor/host-versions.json +3 -3
- package/shell-dist/assets/ahooks.js +0 -6909
- package/shell-dist/assets/antd-es-locale-zh_CN.js +0 -257
- package/shell-dist/assets/antd-img-crop.js +0 -1495
- package/shell-dist/assets/antd-locale-en_US.js +0 -374
- package/shell-dist/assets/antd-locale-zh_CN.js +0 -379
- package/shell-dist/assets/antd.js +0 -106874
- package/shell-dist/assets/clsx.js +0 -33
- package/shell-dist/assets/contract-errors.js +0 -30
- package/shell-dist/assets/contract.js +0 -18900
- package/shell-dist/assets/cssinjs.js +0 -2595
- package/shell-dist/assets/dayjs-plugin-advancedFormat.js +0 -98
- package/shell-dist/assets/dayjs-plugin-customParseFormat.js +0 -173
- package/shell-dist/assets/dayjs-plugin-isoWeek.js +0 -77
- package/shell-dist/assets/dayjs-plugin-localeData.js +0 -126
- package/shell-dist/assets/dayjs-plugin-quarterOfYear.js +0 -77
- package/shell-dist/assets/dayjs-plugin-relativeTime.js +0 -92
- package/shell-dist/assets/dayjs-plugin-weekOfYear.js +0 -73
- package/shell-dist/assets/dayjs-plugin-weekYear.js +0 -63
- package/shell-dist/assets/dayjs-plugin-weekday.js +0 -63
- package/shell-dist/assets/dayjs.js +0 -326
- package/shell-dist/assets/dnd-kit-core.js +0 -3601
- package/shell-dist/assets/dnd-kit-sortable.js +0 -717
- package/shell-dist/assets/dnd-kit-utilities.js +0 -340
- package/shell-dist/assets/i18next.js +0 -2321
- package/shell-dist/assets/icons-es-components-Context.js +0 -20
- package/shell-dist/assets/icons.js +0 -34961
- package/shell-dist/assets/jsx-dev-runtime.js +0 -75
- package/shell-dist/assets/jsx-runtime.js +0 -97
- package/shell-dist/assets/keepalive-for-react.js +0 -283
- package/shell-dist/assets/kit.js +0 -151
- package/shell-dist/assets/ky.js +0 -1427
- package/shell-dist/assets/motion-react.js +0 -13589
- package/shell-dist/assets/motion.js +0 -9959
- package/shell-dist/assets/nprogress.js +0 -314
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-BarsOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CalendarOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretDownFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretDownOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretUpOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CheckCircleFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CheckOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-ClockCircleOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CloseCircleFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CloseOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-CopyOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-DeleteOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-DoubleLeftOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-DoubleRightOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-DownOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-DownloadOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-EditOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-EllipsisOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-EnterOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-ExclamationCircleFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-EyeInvisibleOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-EyeOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-FileOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-FileTextOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-FilterFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-FolderOpenOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-FolderOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-HolderOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-InfoCircleFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-LeftOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-LoadingOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-MinusOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-MinusSquareOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-PaperClipOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-PictureOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-PlusOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-PlusSquareOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-QuestionCircleOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-ReloadOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-RightOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-RotateLeftOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-RotateRightOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-SearchOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-StarFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-SwapOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-SwapRightOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-UpOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-VerticalAlignTopOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-WarningFilled.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-ZoomInOutlined.js +0 -7
- package/shell-dist/assets/ojm--ant-design-icons-es-icons-ZoomOutOutlined.js +0 -7
- package/shell-dist/assets/ojm-dayjs-locale-zh-cn.js +0 -81
- package/shell-dist/assets/pinyin-pro.js +0 -25175
- package/shell-dist/assets/pro-components.js +0 -44627
- package/shell-dist/assets/react-countup.js +0 -525
- package/shell-dist/assets/react-dom-client.js +0 -12134
- package/shell-dist/assets/react-dom.js +0 -266
- package/shell-dist/assets/react-error-boundary.js +0 -141
- package/shell-dist/assets/react-i18next.js +0 -1773
- package/shell-dist/assets/react-jss.js +0 -4089
- package/shell-dist/assets/react-query.js +0 -3072
- package/shell-dist/assets/react-router-dom.js +0 -9927
- package/shell-dist/assets/react-router.js +0 -14584
- package/shell-dist/assets/react.js +0 -590
- package/shell-dist/assets/runtime.js +0 -28311
- package/shell-dist/assets/simplebar-react.js +0 -1119
- package/shell-dist/assets/spin-delay.js +0 -111
- package/shell-dist/assets/tailwind-merge.js +0 -3364
- package/shell-dist/assets/use-sync-external-store-shim.js +0 -130
- package/shell-dist/assets/zustand-middleware.js +0 -513
- package/shell-dist/assets/zustand-react-shallow.js +0 -27
- package/shell-dist/assets/zustand-react.js +0 -39
- package/shell-dist/assets/zustand-shallow.js +0 -22
- package/shell-dist/assets/zustand-vanilla-shallow.js +0 -71
- package/shell-dist/assets/zustand-vanilla.js +0 -40
- package/shell-dist/assets/zustand.js +0 -32
- package/templates/seed.mysql.sql +0 -6
- package/templates/seed.pg.sql +0 -6
- package/templates/src/app/api/_platform/migrations/0001__create_users.mysql.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0001__create_users.pg.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0001__create_users.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.mysql.sql +0 -5
- package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.sql +0 -5
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.mysql.sql +0 -70
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.pg.sql +0 -67
- package/templates/src/app/api/_platform/migrations/0004__sys_rbac.sql +0 -67
- package/templates/src/app/api/_platform/migrations/0005__users_must_change.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0005__users_must_change.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0007__users_email.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.mysql.sql +0 -10
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.pg.sql +0 -10
- package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.sql +0 -11
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.mysql.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.pg.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0009__sys_throttle.sql +0 -13
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.mysql.sql +0 -17
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.pg.sql +0 -16
- package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.sql +0 -18
- package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.mysql.sql +0 -2
- package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0012__market_history.mysql.sql +0 -13
- package/templates/src/app/api/_platform/migrations/0012__market_history.pg.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0012__market_history.sql +0 -14
- package/templates/src/app/api/_platform/migrations/0013__users_profile.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.mysql.sql +0 -9
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.pg.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.sql +0 -8
- package/templates/src/app/api/_platform/migrations/0015__tenant.mysql.sql +0 -12
- package/templates/src/app/api/_platform/migrations/0015__tenant.pg.sql +0 -11
- package/templates/src/app/api/_platform/migrations/0015__tenant.sql +0 -44
- package/templates/src/app/api/_platform/migrations/0016__org.mysql.sql +0 -41
- package/templates/src/app/api/_platform/migrations/0016__org.pg.sql +0 -40
- package/templates/src/app/api/_platform/migrations/0016__org.sql +0 -62
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +0 -7
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +0 -4
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +0 -8
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +0 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +0 -8
- package/templates/src/app/web/system/pages/login-methods/index.tsx +0 -30
- /package/shell-dist/assets/{nprogress-css.js → nprogress-css.1cq2pzkre0.js} +0 -0
- /package/shell-dist/assets/{ojm-antd-es-button.js → ojm-antd-es-button.2uriphio50.js} +0 -0
- /package/shell-dist/assets/{ojm-antd-es-modal.js → ojm-antd-es-modal.bf3tgdh8v0.js} +0 -0
- /package/shell-dist/assets/{ojm-antd-es-slider.js → ojm-antd-es-slider.xyvptuhid0.js} +0 -0
- /package/shell-dist/assets/{ojm-antd-es-upload.js → ojm-antd-es-upload.13gx45e0g0.js} +0 -0
|
@@ -305,3 +305,71 @@ describe("preflight · C2 夹具(双端唯一真源)", () => {
|
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
+
|
|
309
|
+
describe("p0 §3.9 宿主能力门禁(写锁前失败,零痕迹)", () => {
|
|
310
|
+
it("老 oj(capabilities 不含所需)+ 新模板声明 requiresCapabilities → 写锁前 failed(host_capability),retryable=false", async () => {
|
|
311
|
+
const m = structuredClone(VALID);
|
|
312
|
+
m.requiresCapabilities = ["reload.modules_in_key", "anon.per_generation"];
|
|
313
|
+
const r = await preflight(ctx({ manifest: m, capabilities: [] }));
|
|
314
|
+
expect(r.ok).toBe(false);
|
|
315
|
+
expect(r.code).toBe("host_capability");
|
|
316
|
+
expect(r.retryable).toBe(false);
|
|
317
|
+
expect(r.message).toContain("reload.modules_in_key");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("宿主 capability 未上报(undefined)+ 模板声明 → fail-closed 同拒", async () => {
|
|
321
|
+
const m = structuredClone(VALID);
|
|
322
|
+
m.requiresCapabilities = ["reload.api"];
|
|
323
|
+
const r = await preflight(ctx({ manifest: m })); // 无 capabilities
|
|
324
|
+
expect(r.ok).toBe(false);
|
|
325
|
+
expect(r.code).toBe("host_capability");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("宿主具备所需能力 → 放行", async () => {
|
|
329
|
+
const m = structuredClone(VALID);
|
|
330
|
+
m.requiresCapabilities = ["reload.api", "reload.modules_in_key"];
|
|
331
|
+
const r = await preflight(ctx({ manifest: m, capabilities: ["reload.api", "reload.modules_in_key", "anon.per_generation"] }));
|
|
332
|
+
expect(r.ok).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("既有模板不声明 requiresCapabilities → 门禁恒放行(不依赖上游 US-1 即先落地)", async () => {
|
|
336
|
+
const r = await preflight(ctx({ capabilities: [] }));
|
|
337
|
+
expect(r.ok).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("p0 §3.2 质辩1 / E7·E8 匿名保留段与残留检测(写锁前拒装)", () => {
|
|
342
|
+
it("e7 方案声明保留段匿名条目 → 拒装(C2-26)", async () => {
|
|
343
|
+
const m = structuredClone(VALID);
|
|
344
|
+
m.api.anonymous = ["_platform/health"];
|
|
345
|
+
const r = await preflight(ctx({ manifest: m }));
|
|
346
|
+
expect(r.ok).toBe(false);
|
|
347
|
+
expect(r.message).toContain("C2-26");
|
|
348
|
+
expect(r.message).toContain("_platform");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("e8 config 存在未被本 manifest 声明的同 prefix 匿名残留 → 拒装(anonymous_residual)", async () => {
|
|
352
|
+
const m = structuredClone(VALID);
|
|
353
|
+
m.api.anonymous = ["demo-solution/ping"];
|
|
354
|
+
const r = await preflight(ctx({ manifest: m, existingAnonymousPaths: ["/demo-solution/legacy"] }));
|
|
355
|
+
expect(r.ok).toBe(false);
|
|
356
|
+
expect(r.code).toBe("anonymous_residual");
|
|
357
|
+
expect(r.message).toContain("/demo-solution/legacy");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("e8 config 残留条目恰为本 manifest 已声明 → 不误伤放行", async () => {
|
|
361
|
+
const m = structuredClone(VALID);
|
|
362
|
+
m.api.anonymous = ["demo-solution/ping"];
|
|
363
|
+
const r = await preflight(ctx({ manifest: m, existingAnonymousPaths: ["/demo-solution/ping"] }));
|
|
364
|
+
expect(r.ok).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("e8 config 残留指向保留段 → 不误伤放行(保留段属宿主内建,方案无法注入,非方案残留)", async () => {
|
|
368
|
+
const m = structuredClone(VALID);
|
|
369
|
+
m.api.anonymous = ["demo-solution/ping"];
|
|
370
|
+
// /_platform/health 是宿主内建匿名端点(如 /auth/login),方案 C2-26 已挡住声明保留段匿名,
|
|
371
|
+
// 故 config 里的保留段条目绝不会是方案残留——纳入比对只会误伤宿主内建,须放行。
|
|
372
|
+
const r = await preflight(ctx({ manifest: m, existingAnonymousPaths: ["/_platform/health", "/auth/login"] }));
|
|
373
|
+
expect(r.ok).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
// BDD:solution 角色 title 落库时同步翻译(R1–R6,计划 docs/plans/20261103-0948-role-title-translate)。
|
|
2
2
|
// title = 解析文本(站点默认语言)、title_key = 原文(UI 实时翻译锚);解析失败回退裸 key,
|
|
3
|
-
// 绝不失败安装(R1);升级元数据覆盖 = 重解析(R3
|
|
4
|
-
// builtin 不动(R2/AC4);sys_permission 不变(R5);name/code/绑定不变(R6)。
|
|
3
|
+
// 绝不失败安装(R1);升级元数据覆盖 = 重解析(R3);title_key 列由 0001 基线建表即带(R2/AC4);sys_permission 不变(R5);name/code/绑定不变(R6)。
|
|
5
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写(register.spec 同约定) */
|
|
6
|
-
import { createRequire } from "node:module";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
9
6
|
import { registerSolution, resolveDeclTitles, siteLanguage } from "../../src/app/api/_platform/_lifecycle/steps/register";
|
|
10
7
|
import { installDb } from "../fixture";
|
|
11
8
|
|
|
12
|
-
const req = createRequire(import.meta.url);
|
|
13
|
-
const { DatabaseSync } = req("node:sqlite");
|
|
14
|
-
const { readFileSync } = req("node:fs");
|
|
15
|
-
const { dirname, join } = req("node:path");
|
|
16
|
-
|
|
17
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
9
|
const enc = new TextEncoder();
|
|
19
10
|
|
|
20
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -142,30 +133,9 @@ describe("R1–R3 落库(registerSolution 消费解析后的 decl)", () => {
|
|
|
142
133
|
});
|
|
143
134
|
});
|
|
144
135
|
|
|
145
|
-
describe("R2
|
|
136
|
+
describe("R2 sys_role.title_key 列(基线建表即带)", () => {
|
|
146
137
|
it("AC4 夹具真实迁移后 sys_role 带 title_key 列(列进库 ↔ schema.yaml 一致守卫的库侧半边)", () => {
|
|
147
138
|
const cols = (dbo.prepare("PRAGMA table_info(sys_role)").all() as Array<{ name: string }>).map(c => c.name);
|
|
148
139
|
expect(cols).toContain("title_key");
|
|
149
140
|
});
|
|
150
|
-
|
|
151
|
-
it("AC4 存量 solution 角色按 key 形状回填,builtin 角色不动、title 原文不改", () => {
|
|
152
|
-
const raw = new DatabaseSync(":memory:");
|
|
153
|
-
raw.exec(`
|
|
154
|
-
CREATE TABLE sys_role (
|
|
155
|
-
name TEXT PRIMARY KEY, module TEXT NOT NULL, title TEXT, description TEXT,
|
|
156
|
-
builtin INTEGER NOT NULL DEFAULT 0, deprecated INTEGER NOT NULL DEFAULT 0
|
|
157
|
-
);
|
|
158
|
-
INSERT INTO sys_role (name, module, title, builtin) VALUES
|
|
159
|
-
('pm.sales', 'pm', 'pm:role.sales', 0),
|
|
160
|
-
('wiki.admin', 'wiki', 'wiki:role.admin', 0),
|
|
161
|
-
('admin', 'framework', '系统管理员', 1);
|
|
162
|
-
`);
|
|
163
|
-
raw.exec(readFileSync(join(HERE, "../../src/app/api/_platform/migrations/0018__sys_role_title_key.sql"), "utf8"));
|
|
164
|
-
const rows = raw.prepare("SELECT name, title, title_key FROM sys_role ORDER BY name").all() as Array<{ name: string, title: string, title_key: string | null }>;
|
|
165
|
-
expect(rows).toEqual([
|
|
166
|
-
{ name: "admin", title: "系统管理员", title_key: null },
|
|
167
|
-
{ name: "pm.sales", title: "pm:role.sales", title_key: "pm:role.sales" },
|
|
168
|
-
{ name: "wiki.admin", title: "wiki:role.admin", title_key: "wiki:role.admin" },
|
|
169
|
-
]);
|
|
170
|
-
});
|
|
171
141
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as fsp from "node:fs/promises";
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
7
|
|
|
8
|
-
import { appendAnonymousPaths,
|
|
8
|
+
import { appendAnonymousPaths, ensureReloadApi, hasAnchor, insertAnchor, removeAnonymousPaths } from "../../src/app/api/_platform/_lifecycle/text-edit";
|
|
9
9
|
|
|
10
10
|
describe("insertAnchor(R18)", () => {
|
|
11
11
|
it("带注释的 config.yaml 插入后注释/缩进/空行/键序逐字保留", () => {
|
|
@@ -105,32 +105,6 @@ describe("ensureReloadApi(P1-10,D3 一次性注入集收口:S1-1 定稿无
|
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
describe("bumpInstallSeq(B-11 reload 触发面:vars 安装序号自增,PRD E5 实测 vars 改动可热生效)", () => {
|
|
109
|
-
it("0 → 1 自增,其余行(注释/缩进/键序)逐字保留", () => {
|
|
110
|
-
const content = "mounts: []\nvars:\n ojm_install_seq: 0\n# 尾注释保留";
|
|
111
|
-
const r = bumpInstallSeq(content);
|
|
112
|
-
expect(r.seq).toBe(1);
|
|
113
|
-
expect(r.content).toBe("mounts: []\nvars:\n ojm_install_seq: 1\n# 尾注释保留");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("多次调用累加(N → N+1),行内尾注释原样保留", () => {
|
|
117
|
-
const content = "vars:\n ojm_install_seq: 41 # 安装序号";
|
|
118
|
-
const r1 = bumpInstallSeq(content);
|
|
119
|
-
expect(r1.seq).toBe(42);
|
|
120
|
-
expect(r1.content).toBe("vars:\n ojm_install_seq: 42 # 安装序号");
|
|
121
|
-
const r2 = bumpInstallSeq(r1.content);
|
|
122
|
-
expect(r2.seq).toBe(43);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("键缺失 → 可读报错(用户删行 = 显式退出热更触发,安装器不猜位置)", () => {
|
|
126
|
-
expect(() => bumpInstallSeq("vars:\n other: 1")).toThrowError(/ojm_install_seq/);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("值非数字(行存在但形状不符)→ 视同缺失,可读报错不猜", () => {
|
|
130
|
-
expect(() => bumpInstallSeq("vars:\n ojm_install_seq: abc")).toThrowError(/ojm_install_seq/);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
108
|
describe("模板 config.yaml 随发锚点(P1-16 接线②:ensureReloadApi 键缺失插入路径的闭环依据)", () => {
|
|
135
109
|
it("模板 config.yaml 带整行 # ojm:reload 锚点;键在 → ensureReloadApi no-op", async () => {
|
|
136
110
|
const cfg = await fsp.readFile(new URL("../../config.yaml", import.meta.url), "utf8");
|
|
@@ -175,3 +149,47 @@ describe("p2-15③ C2-26 anonymous 运行时接线(appendAnonymousPaths:conf
|
|
|
175
149
|
expect(() => appendAnonymousPaths("server:\n reload_api: true\n", ["x/y"])).toThrow(/anonymous_paths/);
|
|
176
150
|
});
|
|
177
151
|
});
|
|
152
|
+
|
|
153
|
+
describe("removeAnonymousPaths(P0 §3.5 补偿清单:appendAnonymousPaths 逆操作)", () => {
|
|
154
|
+
const BASE = "auth:\n anonymous_paths:\n - /auth/login\n # 行内注释保留\n - /system/login-methods\n";
|
|
155
|
+
|
|
156
|
+
it("追加后移除 = 还原到追加前形态(注释/缩进/既有项逐字保留)", () => {
|
|
157
|
+
const added = appendAnonymousPaths(BASE, ["demo-solution/ping", "demo-solution/health"]);
|
|
158
|
+
const r = removeAnonymousPaths(added.content, ["demo-solution/ping", "demo-solution/health"]);
|
|
159
|
+
expect(r.removed).toEqual(["/demo-solution/ping", "/demo-solution/health"]);
|
|
160
|
+
expect(r.content).toBe(BASE);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("只移除指定项,其余匿名条目不动", () => {
|
|
164
|
+
const added = appendAnonymousPaths(BASE, ["demo-solution/ping", "demo-solution/health"]);
|
|
165
|
+
const r = removeAnonymousPaths(added.content, ["demo-solution/ping"]);
|
|
166
|
+
expect(r.removed).toEqual(["/demo-solution/ping"]);
|
|
167
|
+
expect(r.content).toContain("- /demo-solution/health");
|
|
168
|
+
expect(r.content).toContain("- /auth/login");
|
|
169
|
+
expect(r.content).toContain("- /system/login-methods");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("幂等:未含的路径不在列表内 → removed=[] 且内容不变", () => {
|
|
173
|
+
const added = appendAnonymousPaths(BASE, ["demo-solution/ping"]);
|
|
174
|
+
const r = removeAnonymousPaths(added.content, ["other/not-present"]);
|
|
175
|
+
expect(r.removed).toEqual([]);
|
|
176
|
+
expect(r.content).toBe(added.content);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("连续两次移除同一批路径,第二次为 no-op(防重复副作用)", () => {
|
|
180
|
+
const added = appendAnonymousPaths(BASE, ["demo-solution/ping"]);
|
|
181
|
+
const once = removeAnonymousPaths(added.content, ["demo-solution/ping"]);
|
|
182
|
+
const twice = removeAnonymousPaths(once.content, ["demo-solution/ping"]);
|
|
183
|
+
expect(twice.removed).toEqual([]);
|
|
184
|
+
expect(twice.content).toBe(once.content);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("键缺失 = 没东西可删,no-op(不报错)", () => {
|
|
188
|
+
const r = removeAnonymousPaths("server:\n reload_api: true\n", ["x/y"]);
|
|
189
|
+
expect(r.removed).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("空路径列表 = no-op", () => {
|
|
193
|
+
expect(removeAnonymousPaths(BASE, []).removed).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -196,6 +196,42 @@ describe("POST uninstall 卸载编排(superadmin,R9)", () => {
|
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
describe("Phase 4:卸载 reload 失败回滚(P0 §3.4 第5步,依赖 US-1 manifests 摘要驱动的装配面,本仓先落编排)", () => {
|
|
200
|
+
it("卸载摘锁后 reload 失败 → 锁写回(模块仍挂载) + ok:false + 版本目录不删(fail-closed 一致回滚)", async () => {
|
|
201
|
+
await seedInstalled();
|
|
202
|
+
const api = createMarketApi(makeEnv({
|
|
203
|
+
reload: {
|
|
204
|
+
reloadConfig: async () => {
|
|
205
|
+
throw new Error("reload boom");
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
g.http = { user: { id: 1 }, body: { name: "demo-solution" } };
|
|
210
|
+
await api.uninstall.post();
|
|
211
|
+
// 锁写回:模块仍挂载(other 保留 + demo-solution 恢复)
|
|
212
|
+
expect(await readLock(join(sharedRoot, "manifests.yaml"))).toEqual({ "demo-solution": "0.1.0", "other": "1.0.0" });
|
|
213
|
+
// 版本目录未删(先失可达性再失文件;reload 失败即停,不删文件)
|
|
214
|
+
await expect(fsp.readdir(join(sharedRoot, "demo-solution-0.1.0"))).resolves.toBeDefined();
|
|
215
|
+
// sidecar 未删(元数据一致回滚,非半卸)
|
|
216
|
+
const state = await createStateStore(statePath);
|
|
217
|
+
expect(state.read().solutions["demo-solution"]).toBeDefined();
|
|
218
|
+
expect(last?.status).toBe(200);
|
|
219
|
+
expect(last?.data).toMatchObject({ ok: false });
|
|
220
|
+
expect(String((last?.data as { message?: string } | undefined)?.message)).toContain("reload 失败");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("卸载 reload 成功 → 版本目录正常移除、锁摘除(既有行为不破坏)", async () => {
|
|
224
|
+
await seedInstalled();
|
|
225
|
+
const api = createMarketApi(makeEnv({ reload: { reloadConfig: async () => ({ generation: 1, unchanged: false, applied: ["-demo-solution@0.1.0"], restart_pending: [] }) } }));
|
|
226
|
+
g.http = { user: { id: 1 }, body: { name: "demo-solution" } };
|
|
227
|
+
await api.uninstall.post();
|
|
228
|
+
expect(last?.status).toBe(200);
|
|
229
|
+
expect(last?.data).toMatchObject({ ok: true });
|
|
230
|
+
expect(await readLock(join(sharedRoot, "manifests.yaml"))).toEqual({ other: "1.0.0" });
|
|
231
|
+
await expect(fsp.readdir(join(sharedRoot, "demo-solution-0.1.0"))).rejects.toBeTruthy();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
199
235
|
describe("P3-3 卸载级联单一事务(fail-closed:中段失败整段回滚、锁不摘、无半残)", () => {
|
|
200
236
|
it("权限级联中段失败 → 事务回滚(route/角色/权限零残)、锁/sidecar/modules/版本目录原样保留", async () => {
|
|
201
237
|
await seedInstalled();
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// BDD:发布凭证端点族(申请 / 列表 / 撤销 / 兑换)。
|
|
2
|
+
//
|
|
3
|
+
// 这一层要钉的是**跨请求才看得见的语义**,纯函数测不到:
|
|
4
|
+
// · 一次性:兑换成功后同票再兑必拒(并发双花只有一个赢家)
|
|
5
|
+
// · 时效:过期即拒,且拒绝发生在「已经拿到身份」之前
|
|
6
|
+
// · 隔离:普通用户看不到别人的凭证,超管看得到;归属租户来自 users 行而非请求头
|
|
7
|
+
// · 不留痕:明文只在申请响应里出现一次,库里只有 sha256
|
|
8
|
+
/* eslint-disable test/prefer-lowercase-title -- 用例标题带端点路径,保留原样更易检索 */
|
|
9
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
10
|
+
import { invalidateUser } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
11
|
+
import { CREDENTIAL_TTL_SECS } from "../src/app/api/publish/_shared/identity";
|
|
12
|
+
import { createPublishApi } from "../src/app/api/publish/api";
|
|
13
|
+
import { installDb } from "./fixture";
|
|
14
|
+
|
|
15
|
+
const g = globalThis as any;
|
|
16
|
+
let dbo: ReturnType<typeof installDb>;
|
|
17
|
+
|
|
18
|
+
interface Captured { status: number | null, body: any }
|
|
19
|
+
let out: Captured;
|
|
20
|
+
|
|
21
|
+
/** 固定令牌:必须是 64 位 hex,才过得了兑换端点的形状闸门 */
|
|
22
|
+
const TOKEN_1 = "a".repeat(64);
|
|
23
|
+
const TOKEN_2 = "b".repeat(64);
|
|
24
|
+
|
|
25
|
+
let clock: number;
|
|
26
|
+
|
|
27
|
+
/** 假摘要:短、确定性、且**不含原文**(否则「明文不落库」的断言会被夹具自己污染) */
|
|
28
|
+
function digest(input: string): string {
|
|
29
|
+
let acc = 7;
|
|
30
|
+
for (const ch of input)
|
|
31
|
+
acc = (acc * 33 + ch.charCodeAt(0)) >>> 0;
|
|
32
|
+
return `h${acc.toString(16)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function installJson(): void {
|
|
36
|
+
out = { status: null, body: null };
|
|
37
|
+
g.json = {
|
|
38
|
+
ok: (data?: unknown) => {
|
|
39
|
+
out.status = 200;
|
|
40
|
+
out.body = data;
|
|
41
|
+
},
|
|
42
|
+
fail: (code: number, msg: string, extra?: unknown) => {
|
|
43
|
+
out.status = code;
|
|
44
|
+
out.body = { code, msg, extra };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function api(tokens: string[] = [TOKEN_1, TOKEN_2]) {
|
|
50
|
+
const queue = [...tokens];
|
|
51
|
+
return createPublishApi({
|
|
52
|
+
now: () => clock,
|
|
53
|
+
randomHex: () => queue.shift() ?? "c".repeat(64),
|
|
54
|
+
sha256Hex: digest,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mkUser(username: string, opts: { tenant?: string, displayName?: string, superadmin?: boolean } = {}): number {
|
|
59
|
+
const tenant = opts.tenant ?? "default";
|
|
60
|
+
dbo.prepare("INSERT INTO users (username, password_hash, display_name, tenant_id) VALUES (?, 'x', ?, ?)")
|
|
61
|
+
.run(username, opts.displayName ?? null, tenant);
|
|
62
|
+
const uid = (dbo.prepare("SELECT id FROM users WHERE username = ? AND tenant_id = ?").get(username, tenant) as { id: number }).id;
|
|
63
|
+
if (opts.superadmin)
|
|
64
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name, tenant_id) VALUES (?, 'superadmin', ?)").run(uid, tenant);
|
|
65
|
+
return uid;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 以某个登录用户身份发请求 */
|
|
69
|
+
function as(uid: number, extra: Record<string, unknown> = {}): void {
|
|
70
|
+
g.http = { user: { id: uid }, tenantId: "default", headers: { "x-real-ip": "10.0.0.1" }, ...extra };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rowOf(token: string): Record<string, unknown> | undefined {
|
|
74
|
+
return dbo.prepare("SELECT * FROM publish_credential WHERE token_hash = ?").get(digest(token)) as Record<string, unknown> | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeAll(() => {
|
|
78
|
+
dbo = installDb();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
g.http = {};
|
|
83
|
+
clock = 1_700_000_000;
|
|
84
|
+
dbo.exec("DELETE FROM publish_credential");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("POST /api/publish/credential(申请)", () => {
|
|
88
|
+
it("given 登录用户填合法产品名 When 申请 Then 返回 64 位明文、身份已定,库里只有 hash", async () => {
|
|
89
|
+
installJson();
|
|
90
|
+
clock = 1_700_000_000;
|
|
91
|
+
const uid = mkUser("p_apply", { displayName: "张三" });
|
|
92
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
93
|
+
|
|
94
|
+
await api().credential.post();
|
|
95
|
+
|
|
96
|
+
expect(out.status).toBe(200);
|
|
97
|
+
expect(out.body.token).toBe(TOKEN_1);
|
|
98
|
+
expect(out.body.key_id).toBe("demo-solution.p-apply");
|
|
99
|
+
expect(out.body.expires_at).toBe(clock + CREDENTIAL_TTL_SECS);
|
|
100
|
+
expect(out.body.token_hint).toBe(TOKEN_1.slice(0, 8));
|
|
101
|
+
|
|
102
|
+
const row = rowOf(TOKEN_1)!;
|
|
103
|
+
expect(row.token_hash).toBe(digest(TOKEN_1));
|
|
104
|
+
expect(row.display_name).toBe("张三");
|
|
105
|
+
expect(row.owner_tenant).toBe("default");
|
|
106
|
+
// 明文绝不落库
|
|
107
|
+
expect(JSON.stringify(row)).not.toContain(TOKEN_1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("given 产品名是保留名 When 申请 Then 400 且不产生记录", async () => {
|
|
111
|
+
installJson();
|
|
112
|
+
const uid = mkUser("p_bad");
|
|
113
|
+
as(uid, { body: { product: "market" } });
|
|
114
|
+
|
|
115
|
+
await api().credential.post();
|
|
116
|
+
|
|
117
|
+
expect(out.status).toBe(400);
|
|
118
|
+
expect(out.body.msg).toContain("保留名");
|
|
119
|
+
expect(dbo.prepare("SELECT count(*) AS c FROM publish_credential").get()).toEqual({ c: 0 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("given 同一产品名已被他人占用 When 另一用户申请 Then 409(同一人重复申请放行)", async () => {
|
|
123
|
+
installJson();
|
|
124
|
+
clock = 1_700_000_000;
|
|
125
|
+
|
|
126
|
+
// 同名账号在不同租户:唯一键是 (tenant_id, username),故可共存
|
|
127
|
+
const a = mkUser("p_dup", { tenant: "default" });
|
|
128
|
+
const b = mkUser("p_dup", { tenant: "t2" });
|
|
129
|
+
|
|
130
|
+
as(a, { body: { product: "demo-solution" } });
|
|
131
|
+
await api([TOKEN_1]).credential.post();
|
|
132
|
+
expect(out.status).toBe(200);
|
|
133
|
+
|
|
134
|
+
as(b, { tenantId: "t2", body: { product: "demo-solution" } });
|
|
135
|
+
await api([TOKEN_2]).credential.post();
|
|
136
|
+
expect(out.status).toBe(409);
|
|
137
|
+
expect(out.body.msg).toContain("已被其他用户占用");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("POST /api/publish/credential/exchange(兑换)", () => {
|
|
142
|
+
it("given 有效凭证 When 兑换 Then 返回申请时定下的 key_id 且该票作废", async () => {
|
|
143
|
+
installJson();
|
|
144
|
+
clock = 1_700_000_000;
|
|
145
|
+
const uid = mkUser("p_ex1");
|
|
146
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
147
|
+
const p = api([TOKEN_1]);
|
|
148
|
+
await p.credential.post();
|
|
149
|
+
const issued = out.body;
|
|
150
|
+
|
|
151
|
+
// 兑换是匿名的:不带 user
|
|
152
|
+
g.http = { headers: { "x-real-ip": "10.0.0.9" }, body: { token: issued.token } };
|
|
153
|
+
await p.credential.exchange.post();
|
|
154
|
+
|
|
155
|
+
expect(out.status).toBe(200);
|
|
156
|
+
expect(out.body).toMatchObject({ key_id: issued.key_id, product: "demo-solution", username: "p_ex1" });
|
|
157
|
+
|
|
158
|
+
const row = rowOf(TOKEN_1)!;
|
|
159
|
+
expect(Number(row.used)).toBe(1);
|
|
160
|
+
expect(Number(row.used_at)).toBe(clock);
|
|
161
|
+
|
|
162
|
+
// 再兑:同票必拒
|
|
163
|
+
g.http = { headers: { "x-real-ip": "10.0.0.9" }, body: { token: issued.token } };
|
|
164
|
+
await p.credential.exchange.post();
|
|
165
|
+
expect(out.status).toBe(400);
|
|
166
|
+
expect(out.body.msg).toContain("发布管理");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("given 凭证已过期 When 兑换 Then 400(过期判定在签发身份之前)", async () => {
|
|
170
|
+
installJson();
|
|
171
|
+
clock = 1_700_000_000;
|
|
172
|
+
const uid = mkUser("p_ex2");
|
|
173
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
174
|
+
const p = api([TOKEN_1]);
|
|
175
|
+
await p.credential.post();
|
|
176
|
+
|
|
177
|
+
clock += CREDENTIAL_TTL_SECS + 1;
|
|
178
|
+
g.http = { headers: { "x-real-ip": "10.0.0.9" }, body: { token: TOKEN_1 } };
|
|
179
|
+
await p.credential.exchange.post();
|
|
180
|
+
|
|
181
|
+
expect(out.status).toBe(400);
|
|
182
|
+
expect(Number(rowOf(TOKEN_1)!.used)).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("given 凭证已撤销 When 兑换 Then 400", async () => {
|
|
186
|
+
installJson();
|
|
187
|
+
clock = 1_700_000_000;
|
|
188
|
+
const uid = mkUser("p_ex3");
|
|
189
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
190
|
+
const p = api([TOKEN_1]);
|
|
191
|
+
await p.credential.post();
|
|
192
|
+
const id = Number(rowOf(TOKEN_1)!.id);
|
|
193
|
+
|
|
194
|
+
as(uid, { body: { id } });
|
|
195
|
+
await p.credential.revoke.post();
|
|
196
|
+
expect(out.status).toBe(200);
|
|
197
|
+
|
|
198
|
+
g.http = { headers: { "x-real-ip": "10.0.0.9" }, body: { token: TOKEN_1 } };
|
|
199
|
+
await p.credential.exchange.post();
|
|
200
|
+
expect(out.status).toBe(400);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("given 同一张票连续兑换 3 次 When 依次到达 Then 只有第一次成功,used_at 停在首次时间", async () => {
|
|
204
|
+
installJson();
|
|
205
|
+
clock = 1_700_000_000;
|
|
206
|
+
const uid = mkUser("p_ex4");
|
|
207
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
208
|
+
const p = api([TOKEN_1]);
|
|
209
|
+
await p.credential.post();
|
|
210
|
+
|
|
211
|
+
// 原子性来自那一条 `update ... where used = 0`:并发双花只可能有一个 changes=1。
|
|
212
|
+
// Node 侧 sqlite 是同步的,真并发无法复现,故钉「重复到达只有一次生效」这一可观测面。
|
|
213
|
+
const firstAt = clock;
|
|
214
|
+
const statuses: number[] = [];
|
|
215
|
+
for (let i = 0; i < 3; i++) {
|
|
216
|
+
g.http = { headers: { "x-real-ip": `10.0.0.${i + 1}` }, body: { token: TOKEN_1 } };
|
|
217
|
+
await p.credential.exchange.post();
|
|
218
|
+
statuses.push(out.status!);
|
|
219
|
+
clock += 5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(statuses).toEqual([200, 400, 400]);
|
|
223
|
+
expect(Number(rowOf(TOKEN_1)!.used_at)).toBe(firstAt);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("given 令牌形状不对 When 兑换 Then 400 且不触库(不打日志也不落审计)", async () => {
|
|
227
|
+
installJson();
|
|
228
|
+
g.http = { headers: { "x-real-ip": "10.0.0.6" }, body: { token: "not-a-token" } };
|
|
229
|
+
await api().credential.exchange.post();
|
|
230
|
+
expect(out.status).toBe(400);
|
|
231
|
+
expect(out.body.msg).toContain("重新申请");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("GET /api/publish/credential(列表)", () => {
|
|
236
|
+
it("given 普通用户 When 查列表 Then 只看到自己的且响应不含 token/hash", async () => {
|
|
237
|
+
installJson();
|
|
238
|
+
clock = 1_700_000_000;
|
|
239
|
+
const a = mkUser("p_list_a");
|
|
240
|
+
const b = mkUser("p_list_b");
|
|
241
|
+
|
|
242
|
+
as(a, { body: { product: "demo-solution" } });
|
|
243
|
+
await api([TOKEN_1]).credential.post();
|
|
244
|
+
as(b, { body: { product: "other-solution" } });
|
|
245
|
+
await api([TOKEN_2]).credential.post();
|
|
246
|
+
|
|
247
|
+
as(a);
|
|
248
|
+
await api().credential.get();
|
|
249
|
+
|
|
250
|
+
expect(out.status).toBe(200);
|
|
251
|
+
expect(out.body.total).toBe(1);
|
|
252
|
+
expect(out.body.list[0]).toMatchObject({ product: "demo-solution", key_id: "demo-solution.p-list-a", status: "active" });
|
|
253
|
+
// 明文与哈希都不出现在列表里
|
|
254
|
+
const serialized = JSON.stringify(out.body);
|
|
255
|
+
expect(serialized).not.toContain(TOKEN_1);
|
|
256
|
+
expect(serialized).not.toContain("token_hash");
|
|
257
|
+
expect(out.body.list[0].owner_tenant).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("given 超管 When 查列表 Then 能看到全部并带归属租户", async () => {
|
|
261
|
+
installJson();
|
|
262
|
+
clock = 1_700_000_000;
|
|
263
|
+
const a = mkUser("p_list_c");
|
|
264
|
+
const boss = mkUser("p_boss", { superadmin: true });
|
|
265
|
+
|
|
266
|
+
as(a, { body: { product: "demo-solution" } });
|
|
267
|
+
await api([TOKEN_1]).credential.post();
|
|
268
|
+
|
|
269
|
+
as(boss);
|
|
270
|
+
await api().credential.get();
|
|
271
|
+
|
|
272
|
+
expect(out.status).toBe(200);
|
|
273
|
+
expect(out.body.total).toBe(1);
|
|
274
|
+
expect(out.body.list[0].owner_tenant).toBe("default");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("given 状态演进 When 兑换/撤销后查列表 Then 状态字段随之变化", async () => {
|
|
278
|
+
installJson();
|
|
279
|
+
clock = 1_700_000_000;
|
|
280
|
+
const uid = mkUser("p_list_d");
|
|
281
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
282
|
+
const p = api([TOKEN_1]);
|
|
283
|
+
await p.credential.post();
|
|
284
|
+
|
|
285
|
+
g.http = { headers: { "x-real-ip": "10.0.0.5" }, body: { token: TOKEN_1 } };
|
|
286
|
+
await p.credential.exchange.post();
|
|
287
|
+
|
|
288
|
+
as(uid);
|
|
289
|
+
await p.credential.get();
|
|
290
|
+
expect(out.body.list[0].status).toBe("used");
|
|
291
|
+
expect(out.body.list[0].used_at).toBe(clock);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("POST /api/publish/credential/revoke(撤销)", () => {
|
|
296
|
+
it("given 别人的凭证 When 撤销 Then 404(与不存在同一句话,不泄露存在性)", async () => {
|
|
297
|
+
installJson();
|
|
298
|
+
clock = 1_700_000_000;
|
|
299
|
+
const owner = mkUser("p_rev_a");
|
|
300
|
+
const stranger = mkUser("p_rev_b");
|
|
301
|
+
|
|
302
|
+
as(owner, { body: { product: "demo-solution" } });
|
|
303
|
+
await api([TOKEN_1]).credential.post();
|
|
304
|
+
const id = Number(rowOf(TOKEN_1)!.id);
|
|
305
|
+
|
|
306
|
+
as(stranger, { body: { id } });
|
|
307
|
+
await api().credential.revoke.post();
|
|
308
|
+
|
|
309
|
+
expect(out.status).toBe(404);
|
|
310
|
+
expect(Number(rowOf(TOKEN_1)!.revoked)).toBe(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("given 已兑换的凭证 When 撤销 Then 400 并说明发布身份不能撤回", async () => {
|
|
314
|
+
installJson();
|
|
315
|
+
clock = 1_700_000_000;
|
|
316
|
+
const uid = mkUser("p_rev_c");
|
|
317
|
+
as(uid, { body: { product: "demo-solution" } });
|
|
318
|
+
const p = api([TOKEN_1]);
|
|
319
|
+
await p.credential.post();
|
|
320
|
+
const id = Number(rowOf(TOKEN_1)!.id);
|
|
321
|
+
|
|
322
|
+
g.http = { headers: { "x-real-ip": "10.0.0.4" }, body: { token: TOKEN_1 } };
|
|
323
|
+
await p.credential.exchange.post();
|
|
324
|
+
|
|
325
|
+
as(uid, { body: { id } });
|
|
326
|
+
await p.credential.revoke.post();
|
|
327
|
+
expect(out.status).toBe(400);
|
|
328
|
+
expect(out.body.msg).toContain("发布身份");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("门禁", () => {
|
|
333
|
+
it("given 未登录 When 访问列表/申请 Then 403", async () => {
|
|
334
|
+
installJson();
|
|
335
|
+
as(0);
|
|
336
|
+
g.http = {};
|
|
337
|
+
await api().credential.get();
|
|
338
|
+
expect(out.status).toBe(403);
|
|
339
|
+
|
|
340
|
+
installJson();
|
|
341
|
+
g.http = { body: { product: "demo-solution" } };
|
|
342
|
+
await api().credential.post();
|
|
343
|
+
expect(out.status).toBe(403);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("given 伪造租户头 When 访问列表 Then 403(sub 在当前租户查无此人)", async () => {
|
|
347
|
+
installJson();
|
|
348
|
+
const uid = mkUser("p_forge");
|
|
349
|
+
invalidateUser(uid);
|
|
350
|
+
g.http = { user: { id: uid }, tenantId: "other-tenant" };
|
|
351
|
+
await api().credential.get();
|
|
352
|
+
expect(out.status).toBe(403);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// BDD:发布身份推导规则(publish/_shared/identity.ts)。
|
|
2
|
+
// 这条规则同时决定 ~/.ojm/keys/<id>/ 的目录名、.sig 的 key_id 与市场卡片上的「发布者」,
|
|
3
|
+
// 一旦漂移就是「签出来的身份查不到凭证」这类跨端问题,故用固定用例表钉死。
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { assertKeyId, assertProduct, deriveKeyId, encodePerson, KEY_ID_RE } from "../src/app/api/publish/_shared/identity";
|
|
6
|
+
|
|
7
|
+
describe("发布身份推导", () => {
|
|
8
|
+
it("given 用户名 wecom:everpan 且产品 demo-solution When 推导 Then key_id 用连字符替换冒号", () => {
|
|
9
|
+
expect(deriveKeyId({ product: "demo-solution", username: "wecom:everpan", uid: 7 }))
|
|
10
|
+
.toBe("demo-solution.wecom-everpan");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("given 用户名含大写/下划线/连续符号 When 推导 Then 全小写、非白名单字符替换为 - 并折叠", () => {
|
|
14
|
+
expect(encodePerson("WeCom:Ever_Pan", 7)).toBe("wecom-ever-pan");
|
|
15
|
+
expect(encodePerson("--a__b--", 7)).toBe("a-b");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("given 用户名归一化后为空(纯中文)When 推导 Then 退化为 u<uid> 且仍是合法 key_id", () => {
|
|
19
|
+
const keyId = deriveKeyId({ product: "demo-solution", username: "张三", uid: 42 });
|
|
20
|
+
expect(keyId).toBe("demo-solution.u42");
|
|
21
|
+
expect(KEY_ID_RE.test(keyId)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("given 产品名为保留名 When 校验 Then 拒绝并提示换成自己的解决方案名", () => {
|
|
25
|
+
for (const reserved of ["oj", "market", "system", "registry"])
|
|
26
|
+
expect(() => assertProduct(reserved)).toThrowError(/保留名/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("given 产品名形状非法 When 校验 Then 拒绝并给出可用示例", () => {
|
|
30
|
+
for (const bad of ["", " ", "Demo", "1demo", "demo_solution", "demo solution", "a".repeat(65)])
|
|
31
|
+
expect(() => assertProduct(bad)).toThrowError(/400|产品名/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("given 合法产品名带首尾空格 When 校验 Then 去空格后通过", () => {
|
|
35
|
+
expect(assertProduct(" demo-solution ")).toBe("demo-solution");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("given 推导结果 When 自检 Then 满足 C2-37 的 key_id 形状(≥2 段、小写、仅 [a-z0-9-])", () => {
|
|
39
|
+
const keyId = assertKeyId(deriveKeyId({ product: "demo-solution", username: "wecom:everpan", uid: 7 }));
|
|
40
|
+
expect(keyId.split(".")).toHaveLength(2);
|
|
41
|
+
expect(KEY_ID_RE.test(keyId)).toBe(true);
|
|
42
|
+
expect(() => assertKeyId("nodot")).toThrowError(/不符合 key_id 规则/);
|
|
43
|
+
});
|
|
44
|
+
});
|