@saluzi/saluzi-edu 0.2.9 → 0.2.11
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/cli.js +336 -333
- package/dist/rcs-web/assets/Dashboard-BwotBWlJ.js +1 -0
- package/dist/rcs-web/assets/Join-4yrksbdX.js +1 -0
- package/dist/rcs-web/assets/Login-Cxm_yEeE.js +1 -0
- package/dist/rcs-web/assets/SessionDetail-DNdfPOYy.js +50 -0
- package/dist/rcs-web/assets/Setup-BiAAxugt.js +1 -0
- package/dist/rcs-web/assets/ShareLanding-CP3Z6ckr.js +1 -0
- package/dist/rcs-web/assets/TeamCreate-DgdCwRSb.js +1 -0
- package/dist/rcs-web/assets/TeamDetail-CkNTFoMB.js +11 -0
- package/dist/rcs-web/assets/TeamList-Bf0sQ5RQ.js +1 -0
- package/dist/rcs-web/assets/TeamSettings-BaNAfXU3.js +1 -0
- package/dist/rcs-web/assets/UserSettings-CsutQjxG.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/arc-D90597o-.js → dist/rcs-web/assets/arc-BxxmNQO6.js} +1 -1
- package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
- package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
- package/dist/rcs-web/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
- package/dist/rcs-web/assets/channel-JsvbMQjW.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DtnaqsyD.js → dist/rcs-web/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
- package/dist/rcs-web/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
- package/dist/rcs-web/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
- package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
- package/dist/rcs-web/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
- package/dist/rcs-web/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
- package/dist/rcs-web/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
- package/dist/rcs-web/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
- package/dist/rcs-web/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
- package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-sre_MTeK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
- package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
- package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
- package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
- package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +1 -0
- package/dist/rcs-web/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
- package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
- package/dist/rcs-web/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
- package/dist/rcs-web/assets/main-30YqUvTd.js +238 -0
- package/dist/rcs-web/assets/main-EP6Lac2m.css +1 -0
- package/{packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-D_yQ7nAd.js → dist/rcs-web/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
- package/dist/rcs-web/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
- package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → dist/rcs-web/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
- package/dist/rcs-web/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
- package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
- package/dist/rcs-web/assets/useTeams-MDI4djXg.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → dist/rcs-web/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
- package/dist/rcs-web/assets/workerType-OM09y7Lk.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
- package/dist/rcs-web/index.html +2 -3
- package/package.json +2 -1
- package/packages/remote-control-server/src/__tests__/acp-ws-handler.test.ts +7 -19
- package/packages/remote-control-server/src/__tests__/auth-login.test.ts +561 -0
- package/packages/remote-control-server/src/__tests__/auth-security.test.ts +547 -0
- package/packages/remote-control-server/src/__tests__/auth.test.ts +7 -18
- package/packages/remote-control-server/src/__tests__/bind-replacement.test.ts +251 -0
- package/packages/remote-control-server/src/__tests__/cookie.test.ts +155 -0
- package/packages/remote-control-server/src/__tests__/data-layer-fixes.test.ts +147 -0
- package/packages/remote-control-server/src/__tests__/db.test.ts +582 -0
- package/packages/remote-control-server/src/__tests__/design-gaps.test.ts +440 -0
- package/packages/remote-control-server/src/__tests__/disconnect-monitor.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/helpers/db.ts +43 -0
- package/packages/remote-control-server/src/__tests__/helpers/fixtures.ts +94 -0
- package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +31 -0
- package/packages/remote-control-server/src/__tests__/middleware.test.ts +4 -112
- package/packages/remote-control-server/src/__tests__/password.test.ts +76 -0
- package/packages/remote-control-server/src/__tests__/phase4-cleanup.test.ts +290 -0
- package/packages/remote-control-server/src/__tests__/phase4-integration.test.ts +224 -0
- package/packages/remote-control-server/src/__tests__/routes.test.ts +292 -207
- package/packages/remote-control-server/src/__tests__/services.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/session-visibility.test.ts +452 -0
- package/packages/remote-control-server/src/__tests__/session.test.ts +216 -0
- package/packages/remote-control-server/src/__tests__/shares.test.ts +1419 -0
- package/packages/remote-control-server/src/__tests__/sse-writer.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/teams.test.ts +2089 -0
- package/packages/remote-control-server/src/__tests__/work-dispatch.test.ts +2 -18
- package/packages/remote-control-server/src/__tests__/worker-token-removal.test.ts +487 -0
- package/packages/remote-control-server/src/__tests__/ws-handler.test.ts +2 -18
- package/packages/remote-control-server/src/auth/constants.ts +13 -0
- package/packages/remote-control-server/src/auth/cookie.ts +75 -0
- package/packages/remote-control-server/src/auth/middleware.ts +75 -165
- package/packages/remote-control-server/src/auth/password.ts +116 -0
- package/packages/remote-control-server/src/auth/session.ts +130 -0
- package/packages/remote-control-server/src/db/schema.sql +116 -0
- package/packages/remote-control-server/src/db/sqlite.ts +99 -0
- package/packages/remote-control-server/src/routes/acp/index.ts +6 -11
- package/packages/remote-control-server/src/routes/v1/environments.ts +111 -8
- package/packages/remote-control-server/src/routes/web/auth-routes.ts +634 -0
- package/packages/remote-control-server/src/routes/web/control.ts +5 -5
- package/packages/remote-control-server/src/routes/web/environments.ts +5 -27
- package/packages/remote-control-server/src/routes/web/sessions.ts +19 -41
- package/packages/remote-control-server/src/routes/web/shares.ts +371 -0
- package/packages/remote-control-server/src/routes/web/teams.ts +850 -0
- package/packages/remote-control-server/src/services/environment.ts +1 -4
- package/packages/remote-control-server/src/services/session-visibility.ts +79 -0
- package/packages/remote-control-server/src/services/session.ts +0 -13
- package/packages/remote-control-server/src/store.ts +150 -56
- package/packages/remote-control-server/src/types/api.ts +2 -8
- package/packages/remote-control-server/web/dist/assets/Dashboard-BwotBWlJ.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Join-4yrksbdX.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Login-Cxm_yEeE.js +1 -0
- package/packages/remote-control-server/web/dist/assets/SessionDetail-DNdfPOYy.js +50 -0
- package/packages/remote-control-server/web/dist/assets/Setup-BiAAxugt.js +1 -0
- package/packages/remote-control-server/web/dist/assets/ShareLanding-CP3Z6ckr.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamCreate-DgdCwRSb.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamDetail-CkNTFoMB.js +11 -0
- package/packages/remote-control-server/web/dist/assets/TeamList-Bf0sQ5RQ.js +1 -0
- package/packages/remote-control-server/web/dist/assets/TeamSettings-BaNAfXU3.js +1 -0
- package/packages/remote-control-server/web/dist/assets/UserSettings-CsutQjxG.js +1 -0
- package/{dist/rcs-web/assets/arc-D90597o-.js → packages/remote-control-server/web/dist/assets/arc-BxxmNQO6.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
- package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/channel-JsvbMQjW.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-DtnaqsyD.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
- package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
- package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
- package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
- package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
- package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
- package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
- package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-sre_MTeK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
- package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
- package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
- package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +1 -0
- package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
- package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/main-30YqUvTd.js +238 -0
- package/packages/remote-control-server/web/dist/assets/main-EP6Lac2m.css +1 -0
- package/{dist/rcs-web/assets/mermaid-VLURNSYL-D_yQ7nAd.js → packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
- package/{dist/rcs-web/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
- package/{dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
- package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
- package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
- package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/useTeams-MDI4djXg.js +1 -0
- package/{dist/rcs-web/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/workerType-OM09y7Lk.js +1 -0
- package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
- package/packages/remote-control-server/web/dist/index.html +2 -3
- package/dist/rcs-web/assets/Dashboard-Cyj-Bb86.js +0 -2
- package/dist/rcs-web/assets/SessionDetail-SA9QBkyj.js +0 -51
- package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
- package/dist/rcs-web/assets/channel-CQFGglg-.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
- package/dist/rcs-web/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
- package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
- package/dist/rcs-web/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
- package/dist/rcs-web/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
- package/dist/rcs-web/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
- package/dist/rcs-web/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
- package/dist/rcs-web/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
- package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
- package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
- package/dist/rcs-web/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
- package/dist/rcs-web/assets/main-0A4a3FDU.css +0 -1
- package/dist/rcs-web/assets/main-Dom16YJk.js +0 -283
- package/dist/rcs-web/assets/qr-C0jP9HcD.js +0 -8
- package/dist/rcs-web/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
- package/dist/rcs-web/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
- package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Dashboard-Cyj-Bb86.js +0 -2
- package/packages/remote-control-server/web/dist/assets/SessionDetail-SA9QBkyj.js +0 -51
- package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
- package/packages/remote-control-server/web/dist/assets/channel-CQFGglg-.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
- package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
- package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
- package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
- package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
- package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
- package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
- package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
- package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
- package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
- package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
- package/packages/remote-control-server/web/dist/assets/main-0A4a3FDU.css +0 -1
- package/packages/remote-control-server/web/dist/assets/main-Dom16YJk.js +0 -283
- package/packages/remote-control-server/web/dist/assets/qr-C0jP9HcD.js +0 -8
- package/packages/remote-control-server/web/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
- package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test'
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { mockConfigModule } from './helpers/mock-config'
|
|
6
|
+
|
|
7
|
+
// Mock config before any module imports
|
|
8
|
+
mock.module('../config', () => mockConfigModule())
|
|
9
|
+
|
|
10
|
+
import { Hono } from 'hono'
|
|
11
|
+
import { Database } from 'bun:sqlite'
|
|
12
|
+
|
|
13
|
+
// Dynamic imports — Phase 3 modules may not exist yet (TDD)
|
|
14
|
+
let migrateDatabase: ((db: Database) => void) | undefined
|
|
15
|
+
let initDatabase: ((path?: string) => Database) | undefined
|
|
16
|
+
let resetDbSingleton: (() => void) | undefined
|
|
17
|
+
let getDb: (() => Database) | undefined
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const dbMod = await import('../db/sqlite')
|
|
21
|
+
migrateDatabase = dbMod.migrateDatabase
|
|
22
|
+
getDb = dbMod.getDb
|
|
23
|
+
initDatabase = dbMod.initDatabase
|
|
24
|
+
resetDbSingleton = dbMod.resetDbSingleton
|
|
25
|
+
} catch {
|
|
26
|
+
// Module not implemented yet
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Phase 3: share routes may not exist yet
|
|
30
|
+
let shareRoutes: Hono | undefined
|
|
31
|
+
try {
|
|
32
|
+
const mod = await import('../routes/web/shares')
|
|
33
|
+
shareRoutes = mod.default
|
|
34
|
+
} catch {
|
|
35
|
+
// Module not implemented yet
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Phase 3: session routes with share token support
|
|
39
|
+
let sessionRoutes: Hono | undefined
|
|
40
|
+
try {
|
|
41
|
+
const mod = await import('../routes/web/sessions')
|
|
42
|
+
sessionRoutes = mod.default
|
|
43
|
+
} catch {
|
|
44
|
+
// Module not implemented yet
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let authRoutes: Hono | undefined
|
|
48
|
+
try {
|
|
49
|
+
const mod = await import('../routes/web/auth-routes')
|
|
50
|
+
authRoutes = mod.default
|
|
51
|
+
} catch {
|
|
52
|
+
// Module not implemented yet
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Test app factory
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function createApp(): Hono {
|
|
60
|
+
const app = new Hono()
|
|
61
|
+
if (authRoutes) app.route('/web/auth', authRoutes)
|
|
62
|
+
if (shareRoutes) app.route('/web', shareRoutes)
|
|
63
|
+
if (sessionRoutes) app.route('/web', sessionRoutes)
|
|
64
|
+
return app
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Database helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function freshDb(): Database {
|
|
72
|
+
if (resetDbSingleton) {
|
|
73
|
+
try {
|
|
74
|
+
resetDbSingleton()
|
|
75
|
+
} catch {
|
|
76
|
+
// ignore cleanup errors
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (initDatabase) {
|
|
80
|
+
const uniquePath = resolve(
|
|
81
|
+
tmpdir(),
|
|
82
|
+
`rcs-test-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`,
|
|
83
|
+
)
|
|
84
|
+
return initDatabase(uniquePath)
|
|
85
|
+
}
|
|
86
|
+
const db = new Database(':memory:')
|
|
87
|
+
db.exec('PRAGMA foreign_keys = ON')
|
|
88
|
+
if (migrateDatabase) migrateDatabase(db)
|
|
89
|
+
return db
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearAllTables(db: Database): void {
|
|
93
|
+
db.exec('PRAGMA foreign_keys = OFF')
|
|
94
|
+
const tables = [
|
|
95
|
+
'session_shares',
|
|
96
|
+
'session_owners',
|
|
97
|
+
'team_environments',
|
|
98
|
+
'team_members',
|
|
99
|
+
'teams',
|
|
100
|
+
'invitations',
|
|
101
|
+
'session_tokens',
|
|
102
|
+
'sessions',
|
|
103
|
+
'users',
|
|
104
|
+
]
|
|
105
|
+
for (const t of tables) {
|
|
106
|
+
try {
|
|
107
|
+
db.exec(`DELETE FROM ${t}`)
|
|
108
|
+
} catch {
|
|
109
|
+
// table may not exist
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
db.exec('PRAGMA foreign_keys = ON')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// User fixtures
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
interface TestUser {
|
|
120
|
+
id: string
|
|
121
|
+
username: string
|
|
122
|
+
role: string
|
|
123
|
+
accessToken: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createUserDirectly(
|
|
127
|
+
db: Database,
|
|
128
|
+
opts: { username: string; role: string },
|
|
129
|
+
): TestUser {
|
|
130
|
+
const id = `usr_${randomUUID().replace(/-/g, '')}`
|
|
131
|
+
const now = new Date().toISOString()
|
|
132
|
+
const passwordHash =
|
|
133
|
+
'$argon2id$v=19$m=65536,t=3,p=1$dGVzdHNhbHR0ZXN0$' +
|
|
134
|
+
'dGVzdGhhc2h0ZXN0aGFzaHRlc3RoYXNodGVzdGhhc2g'
|
|
135
|
+
try {
|
|
136
|
+
db.query(
|
|
137
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
138
|
+
VALUES ($id, $username, $hash, $role, $now)`,
|
|
139
|
+
).run({
|
|
140
|
+
$id: id,
|
|
141
|
+
$username: opts.username,
|
|
142
|
+
$hash: passwordHash,
|
|
143
|
+
$role: opts.role,
|
|
144
|
+
$now: now,
|
|
145
|
+
})
|
|
146
|
+
} catch {
|
|
147
|
+
// users table may not exist
|
|
148
|
+
}
|
|
149
|
+
return { id, username: opts.username, role: opts.role, accessToken: '' }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function issueTokenForUser(db: Database, userId: string): string {
|
|
153
|
+
try {
|
|
154
|
+
const { issueSessionToken } = require('../auth/session') as {
|
|
155
|
+
issueSessionToken: (
|
|
156
|
+
userId: string,
|
|
157
|
+
db: Database,
|
|
158
|
+
) => { accessToken: string; refreshToken: string }
|
|
159
|
+
}
|
|
160
|
+
const { accessToken } = issueSessionToken(userId, db)
|
|
161
|
+
return accessToken
|
|
162
|
+
} catch {
|
|
163
|
+
const token = `rct_${randomUUID().replace(/-/g, '')}`
|
|
164
|
+
const tokenHash = createHash('sha256').update(token).digest('hex')
|
|
165
|
+
const now = new Date().toISOString()
|
|
166
|
+
const expiresAt = new Date(Date.now() + 3600_000).toISOString()
|
|
167
|
+
try {
|
|
168
|
+
db.query(
|
|
169
|
+
`INSERT INTO session_tokens (token_hash, user_id, kind, expires_at, created_at)
|
|
170
|
+
VALUES ($hash, $userId, 'access', $exp, $now)`,
|
|
171
|
+
).run({ $hash: tokenHash, $userId: userId, $exp: expiresAt, $now: now })
|
|
172
|
+
} catch {
|
|
173
|
+
// session_tokens may not exist
|
|
174
|
+
}
|
|
175
|
+
return token
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function createAndLoginUser(
|
|
180
|
+
db: Database,
|
|
181
|
+
opts: { username: string; role: string },
|
|
182
|
+
): Promise<TestUser> {
|
|
183
|
+
const user = createUserDirectly(db, opts)
|
|
184
|
+
user.accessToken = issueTokenForUser(db, user.id)
|
|
185
|
+
return user
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function authHeader(token: string): Record<string, string> {
|
|
189
|
+
return { Authorization: `Bearer ${token}` }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Session & Team fixtures
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function createSessionFixture(
|
|
197
|
+
db: Database,
|
|
198
|
+
opts: {
|
|
199
|
+
id?: string
|
|
200
|
+
visibility?: string
|
|
201
|
+
ownerType?: string
|
|
202
|
+
ownerId?: string
|
|
203
|
+
environmentId?: string
|
|
204
|
+
},
|
|
205
|
+
): string {
|
|
206
|
+
const id = opts.id || `ses_${randomUUID().replace(/-/g, '')}`
|
|
207
|
+
const visibility =
|
|
208
|
+
opts.visibility || (opts.ownerType === 'team' ? 'team' : 'private')
|
|
209
|
+
const now = new Date().toISOString()
|
|
210
|
+
try {
|
|
211
|
+
db.query(
|
|
212
|
+
`INSERT INTO sessions (id, visibility, environment_id, created_at, updated_at)
|
|
213
|
+
VALUES ($id, $vis, $env, $now, $now)`,
|
|
214
|
+
).run({
|
|
215
|
+
$id: id,
|
|
216
|
+
$vis: visibility,
|
|
217
|
+
$env: opts.environmentId || null,
|
|
218
|
+
$now: now,
|
|
219
|
+
})
|
|
220
|
+
} catch {
|
|
221
|
+
try {
|
|
222
|
+
db.query(
|
|
223
|
+
`INSERT INTO sessions (id, environment_id, created_at, updated_at)
|
|
224
|
+
VALUES ($id, $env, $now, $now)`,
|
|
225
|
+
).run({ $id: id, $env: opts.environmentId || null, $now: now })
|
|
226
|
+
} catch {
|
|
227
|
+
// sessions table may not exist
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (opts.ownerType && opts.ownerId) {
|
|
232
|
+
try {
|
|
233
|
+
db.query(
|
|
234
|
+
`INSERT INTO session_owners (session_id, owner_type, owner_id)
|
|
235
|
+
VALUES ($sid, $type, $oid)`,
|
|
236
|
+
).run({ $sid: id, $type: opts.ownerType, $oid: opts.ownerId })
|
|
237
|
+
} catch {
|
|
238
|
+
// session_owners may not exist
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return id
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createTeamFixture(
|
|
245
|
+
db: Database,
|
|
246
|
+
opts: { slug: string; createdBy: string },
|
|
247
|
+
): string {
|
|
248
|
+
const id = `team_${randomUUID().replace(/-/g, '')}`
|
|
249
|
+
const now = new Date().toISOString()
|
|
250
|
+
try {
|
|
251
|
+
db.query(
|
|
252
|
+
`INSERT INTO teams (id, name, slug, created_by, created_at, updated_at)
|
|
253
|
+
VALUES ($id, $name, $slug, $by, $now, $now)`,
|
|
254
|
+
).run({
|
|
255
|
+
$id: id,
|
|
256
|
+
$name: opts.slug,
|
|
257
|
+
$slug: opts.slug,
|
|
258
|
+
$by: opts.createdBy,
|
|
259
|
+
$now: now,
|
|
260
|
+
})
|
|
261
|
+
db.query(
|
|
262
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
263
|
+
VALUES ($teamId, $userId, 'owner', $now, $by)`,
|
|
264
|
+
).run({
|
|
265
|
+
$teamId: id,
|
|
266
|
+
$userId: opts.createdBy,
|
|
267
|
+
$by: opts.createdBy,
|
|
268
|
+
$now: now,
|
|
269
|
+
})
|
|
270
|
+
} catch {
|
|
271
|
+
// tables may not exist
|
|
272
|
+
}
|
|
273
|
+
return id
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function addTeamMember(
|
|
277
|
+
db: Database,
|
|
278
|
+
teamId: string,
|
|
279
|
+
userId: string,
|
|
280
|
+
role: string,
|
|
281
|
+
addedBy: string,
|
|
282
|
+
): void {
|
|
283
|
+
const now = new Date().toISOString()
|
|
284
|
+
try {
|
|
285
|
+
db.query(
|
|
286
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
287
|
+
VALUES ($teamId, $userId, $role, $now, $by)`,
|
|
288
|
+
).run({
|
|
289
|
+
$teamId: teamId,
|
|
290
|
+
$userId: userId,
|
|
291
|
+
$role: role,
|
|
292
|
+
$by: addedBy,
|
|
293
|
+
$now: now,
|
|
294
|
+
})
|
|
295
|
+
} catch {
|
|
296
|
+
// table may not exist
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function addSessionShareDirectly(
|
|
301
|
+
db: Database,
|
|
302
|
+
opts: {
|
|
303
|
+
id?: string
|
|
304
|
+
sessionId: string
|
|
305
|
+
grantedToUser?: string
|
|
306
|
+
grantedToTeam?: string
|
|
307
|
+
permission?: string
|
|
308
|
+
grantedBy: string
|
|
309
|
+
expiresAt?: string
|
|
310
|
+
},
|
|
311
|
+
): string {
|
|
312
|
+
const id = opts.id || `shr_${randomUUID().replace(/-/g, '')}`
|
|
313
|
+
const now = new Date().toISOString()
|
|
314
|
+
try {
|
|
315
|
+
db.query(
|
|
316
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, granted_to_team, permission, expires_at, granted_by, granted_at)
|
|
317
|
+
VALUES ($id, $sid, $gtu, $gtt, $perm, $exp, $by, $now)`,
|
|
318
|
+
).run({
|
|
319
|
+
$id: id,
|
|
320
|
+
$sid: opts.sessionId,
|
|
321
|
+
$gtu: opts.grantedToUser || null,
|
|
322
|
+
$gtt: opts.grantedToTeam || null,
|
|
323
|
+
$perm: opts.permission || 'read',
|
|
324
|
+
$exp: opts.expiresAt || null,
|
|
325
|
+
$by: opts.grantedBy,
|
|
326
|
+
$now: now,
|
|
327
|
+
})
|
|
328
|
+
} catch {
|
|
329
|
+
// session_shares may not exist
|
|
330
|
+
}
|
|
331
|
+
return id
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ===========================================================================
|
|
335
|
+
// TESTS — POST /web/sessions/:id/share
|
|
336
|
+
// ===========================================================================
|
|
337
|
+
|
|
338
|
+
describe('POST /web/sessions/:id/share', () => {
|
|
339
|
+
let app: Hono
|
|
340
|
+
let db: Database
|
|
341
|
+
let owner: TestUser
|
|
342
|
+
let other: TestUser
|
|
343
|
+
let sessionId: string
|
|
344
|
+
|
|
345
|
+
beforeEach(async () => {
|
|
346
|
+
db = freshDb()
|
|
347
|
+
app = createApp()
|
|
348
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
349
|
+
other = await createAndLoginUser(db, { username: 'other', role: 'member' })
|
|
350
|
+
sessionId = createSessionFixture(db, {
|
|
351
|
+
ownerType: 'user',
|
|
352
|
+
ownerId: owner.id,
|
|
353
|
+
visibility: 'private',
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('session owner creates share → 201 + returns share token', async () => {
|
|
358
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: {
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
...authHeader(owner.accessToken),
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify({
|
|
365
|
+
grantedTo: { userId: other.id },
|
|
366
|
+
permission: 'read',
|
|
367
|
+
}),
|
|
368
|
+
})
|
|
369
|
+
expect(res.status).toBe(201)
|
|
370
|
+
const body = (await res.json()) as Record<string, unknown>
|
|
371
|
+
expect(body.shareToken || body.share_token).toBeDefined()
|
|
372
|
+
expect(typeof (body.shareToken || body.share_token)).toBe('string')
|
|
373
|
+
expect(
|
|
374
|
+
((body.shareToken || body.share_token) as string).startsWith('shr_'),
|
|
375
|
+
).toBe(true)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('non-owner → 403', async () => {
|
|
379
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: {
|
|
382
|
+
'Content-Type': 'application/json',
|
|
383
|
+
...authHeader(other.accessToken),
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
grantedTo: { userId: other.id },
|
|
387
|
+
permission: 'read',
|
|
388
|
+
}),
|
|
389
|
+
})
|
|
390
|
+
expect(res.status).toBe(403)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('unauthenticated → 401', async () => {
|
|
394
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: { 'Content-Type': 'application/json' },
|
|
397
|
+
body: JSON.stringify({
|
|
398
|
+
grantedTo: { userId: other.id },
|
|
399
|
+
permission: 'read',
|
|
400
|
+
}),
|
|
401
|
+
})
|
|
402
|
+
expect(res.status).toBe(401)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test('share to registered user (grantedTo.userId) → 201', async () => {
|
|
406
|
+
const target = await createAndLoginUser(db, {
|
|
407
|
+
username: 'target',
|
|
408
|
+
role: 'member',
|
|
409
|
+
})
|
|
410
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: {
|
|
413
|
+
'Content-Type': 'application/json',
|
|
414
|
+
...authHeader(owner.accessToken),
|
|
415
|
+
},
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
grantedTo: { userId: target.id },
|
|
418
|
+
permission: 'read',
|
|
419
|
+
}),
|
|
420
|
+
})
|
|
421
|
+
expect(res.status).toBe(201)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('share to team (grantedTo.teamId) → 201', async () => {
|
|
425
|
+
const teamId = createTeamFixture(db, {
|
|
426
|
+
slug: 'share-team',
|
|
427
|
+
createdBy: owner.id,
|
|
428
|
+
})
|
|
429
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: {
|
|
432
|
+
'Content-Type': 'application/json',
|
|
433
|
+
...authHeader(owner.accessToken),
|
|
434
|
+
},
|
|
435
|
+
body: JSON.stringify({
|
|
436
|
+
grantedTo: { teamId },
|
|
437
|
+
permission: 'read',
|
|
438
|
+
}),
|
|
439
|
+
})
|
|
440
|
+
expect(res.status).toBe(201)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
test('permission read → 201', async () => {
|
|
444
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
'Content-Type': 'application/json',
|
|
448
|
+
...authHeader(owner.accessToken),
|
|
449
|
+
},
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
grantedTo: { userId: other.id },
|
|
452
|
+
permission: 'read',
|
|
453
|
+
}),
|
|
454
|
+
})
|
|
455
|
+
expect(res.status).toBe(201)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('permission write → 201', async () => {
|
|
459
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
460
|
+
method: 'POST',
|
|
461
|
+
headers: {
|
|
462
|
+
'Content-Type': 'application/json',
|
|
463
|
+
...authHeader(owner.accessToken),
|
|
464
|
+
},
|
|
465
|
+
body: JSON.stringify({
|
|
466
|
+
grantedTo: { userId: other.id },
|
|
467
|
+
permission: 'write',
|
|
468
|
+
}),
|
|
469
|
+
})
|
|
470
|
+
expect(res.status).toBe(201)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test('set expiry → 201 + expires_at correct', async () => {
|
|
474
|
+
const expiresAt = new Date(Date.now() + 3600_000).toISOString()
|
|
475
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
476
|
+
method: 'POST',
|
|
477
|
+
headers: {
|
|
478
|
+
'Content-Type': 'application/json',
|
|
479
|
+
...authHeader(owner.accessToken),
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify({
|
|
482
|
+
grantedTo: { userId: other.id },
|
|
483
|
+
permission: 'read',
|
|
484
|
+
expiresAt,
|
|
485
|
+
}),
|
|
486
|
+
})
|
|
487
|
+
expect(res.status).toBe(201)
|
|
488
|
+
const body = (await res.json()) as Record<string, unknown>
|
|
489
|
+
expect(body.expiresAt || body.expires_at).toBe(expiresAt)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('non-existent session → 404', async () => {
|
|
493
|
+
const res = await app.request('/web/sessions/ses_nonexistent/share', {
|
|
494
|
+
method: 'POST',
|
|
495
|
+
headers: {
|
|
496
|
+
'Content-Type': 'application/json',
|
|
497
|
+
...authHeader(owner.accessToken),
|
|
498
|
+
},
|
|
499
|
+
body: JSON.stringify({
|
|
500
|
+
grantedTo: { userId: other.id },
|
|
501
|
+
permission: 'read',
|
|
502
|
+
}),
|
|
503
|
+
})
|
|
504
|
+
expect(res.status).toBe(404)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
test('non-existent user → 404', async () => {
|
|
508
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
509
|
+
method: 'POST',
|
|
510
|
+
headers: {
|
|
511
|
+
'Content-Type': 'application/json',
|
|
512
|
+
...authHeader(owner.accessToken),
|
|
513
|
+
},
|
|
514
|
+
body: JSON.stringify({
|
|
515
|
+
grantedTo: { userId: 'usr_nonexistent' },
|
|
516
|
+
permission: 'read',
|
|
517
|
+
}),
|
|
518
|
+
})
|
|
519
|
+
expect(res.status).toBe(404)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('non-existent team → 404', async () => {
|
|
523
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: {
|
|
526
|
+
'Content-Type': 'application/json',
|
|
527
|
+
...authHeader(owner.accessToken),
|
|
528
|
+
},
|
|
529
|
+
body: JSON.stringify({
|
|
530
|
+
grantedTo: { teamId: 'team_nonexistent' },
|
|
531
|
+
permission: 'read',
|
|
532
|
+
}),
|
|
533
|
+
})
|
|
534
|
+
expect(res.status).toBe(404)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
test('missing permission field → 400', async () => {
|
|
538
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
539
|
+
method: 'POST',
|
|
540
|
+
headers: {
|
|
541
|
+
'Content-Type': 'application/json',
|
|
542
|
+
...authHeader(owner.accessToken),
|
|
543
|
+
},
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
grantedTo: { userId: other.id },
|
|
546
|
+
}),
|
|
547
|
+
})
|
|
548
|
+
expect(res.status).toBe(400)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('invalid permission value → 400', async () => {
|
|
552
|
+
const res = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
553
|
+
method: 'POST',
|
|
554
|
+
headers: {
|
|
555
|
+
'Content-Type': 'application/json',
|
|
556
|
+
...authHeader(owner.accessToken),
|
|
557
|
+
},
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
grantedTo: { userId: other.id },
|
|
560
|
+
permission: 'admin',
|
|
561
|
+
}),
|
|
562
|
+
})
|
|
563
|
+
expect(res.status).toBe(400)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
// ===========================================================================
|
|
568
|
+
// TESTS — GET /web/sessions/:id/shares
|
|
569
|
+
// ===========================================================================
|
|
570
|
+
|
|
571
|
+
describe('GET /web/sessions/:id/shares', () => {
|
|
572
|
+
let app: Hono
|
|
573
|
+
let db: Database
|
|
574
|
+
let owner: TestUser
|
|
575
|
+
let other: TestUser
|
|
576
|
+
let sessionId: string
|
|
577
|
+
|
|
578
|
+
beforeEach(async () => {
|
|
579
|
+
db = freshDb()
|
|
580
|
+
app = createApp()
|
|
581
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
582
|
+
other = await createAndLoginUser(db, { username: 'other', role: 'member' })
|
|
583
|
+
sessionId = createSessionFixture(db, {
|
|
584
|
+
ownerType: 'user',
|
|
585
|
+
ownerId: owner.id,
|
|
586
|
+
visibility: 'private',
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
test('session owner → 200 + share list', async () => {
|
|
591
|
+
// Create a share first
|
|
592
|
+
addSessionShareDirectly(db, {
|
|
593
|
+
sessionId,
|
|
594
|
+
grantedToUser: other.id,
|
|
595
|
+
permission: 'read',
|
|
596
|
+
grantedBy: owner.id,
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
600
|
+
method: 'GET',
|
|
601
|
+
headers: authHeader(owner.accessToken),
|
|
602
|
+
})
|
|
603
|
+
expect(res.status).toBe(200)
|
|
604
|
+
const body = (await res.json()) as Array<Record<string, unknown>>
|
|
605
|
+
expect(Array.isArray(body)).toBe(true)
|
|
606
|
+
expect(body.length).toBeGreaterThanOrEqual(1)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test('non-owner → 403', async () => {
|
|
610
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
611
|
+
method: 'GET',
|
|
612
|
+
headers: authHeader(other.accessToken),
|
|
613
|
+
})
|
|
614
|
+
expect(res.status).toBe(403)
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
test('unauthenticated → 401', async () => {
|
|
618
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
619
|
+
method: 'GET',
|
|
620
|
+
})
|
|
621
|
+
expect(res.status).toBe(401)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('non-existent session → 404', async () => {
|
|
625
|
+
const res = await app.request('/web/sessions/ses_nonexistent/shares', {
|
|
626
|
+
method: 'GET',
|
|
627
|
+
headers: authHeader(owner.accessToken),
|
|
628
|
+
})
|
|
629
|
+
expect(res.status).toBe(404)
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test('returned fields: id, granted_to_user, granted_to_team, permission, expires_at, granted_by, granted_at', async () => {
|
|
633
|
+
addSessionShareDirectly(db, {
|
|
634
|
+
sessionId,
|
|
635
|
+
grantedToUser: other.id,
|
|
636
|
+
permission: 'write',
|
|
637
|
+
grantedBy: owner.id,
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
641
|
+
method: 'GET',
|
|
642
|
+
headers: authHeader(owner.accessToken),
|
|
643
|
+
})
|
|
644
|
+
expect(res.status).toBe(200)
|
|
645
|
+
const body = (await res.json()) as Array<Record<string, unknown>>
|
|
646
|
+
expect(body.length).toBeGreaterThanOrEqual(1)
|
|
647
|
+
const share = body[0]
|
|
648
|
+
// Check expected fields exist (using either camelCase or snake_case)
|
|
649
|
+
expect(share.id).toBeDefined()
|
|
650
|
+
expect(share.granted_to_user || share.grantedToUser).toBe(other.id)
|
|
651
|
+
expect(share.permission).toBe('write')
|
|
652
|
+
expect(share.granted_by || share.grantedBy).toBe(owner.id)
|
|
653
|
+
expect(share.granted_at || share.grantedAt).toBeDefined()
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// ===========================================================================
|
|
658
|
+
// TESTS — DELETE /web/sessions/:id/shares/:shareId
|
|
659
|
+
// ===========================================================================
|
|
660
|
+
|
|
661
|
+
describe('DELETE /web/sessions/:id/shares/:shareId', () => {
|
|
662
|
+
let app: Hono
|
|
663
|
+
let db: Database
|
|
664
|
+
let owner: TestUser
|
|
665
|
+
let other: TestUser
|
|
666
|
+
let sessionId: string
|
|
667
|
+
let shareId: string
|
|
668
|
+
|
|
669
|
+
beforeEach(async () => {
|
|
670
|
+
db = freshDb()
|
|
671
|
+
app = createApp()
|
|
672
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
673
|
+
other = await createAndLoginUser(db, { username: 'other', role: 'member' })
|
|
674
|
+
sessionId = createSessionFixture(db, {
|
|
675
|
+
ownerType: 'user',
|
|
676
|
+
ownerId: owner.id,
|
|
677
|
+
visibility: 'private',
|
|
678
|
+
})
|
|
679
|
+
shareId = addSessionShareDirectly(db, {
|
|
680
|
+
sessionId,
|
|
681
|
+
grantedToUser: other.id,
|
|
682
|
+
permission: 'read',
|
|
683
|
+
grantedBy: owner.id,
|
|
684
|
+
})
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test('session owner revokes share → 200', async () => {
|
|
688
|
+
const res = await app.request(
|
|
689
|
+
`/web/sessions/${sessionId}/shares/${shareId}`,
|
|
690
|
+
{
|
|
691
|
+
method: 'DELETE',
|
|
692
|
+
headers: authHeader(owner.accessToken),
|
|
693
|
+
},
|
|
694
|
+
)
|
|
695
|
+
expect(res.status).toBe(200)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
test('non-owner → 403', async () => {
|
|
699
|
+
const res = await app.request(
|
|
700
|
+
`/web/sessions/${sessionId}/shares/${shareId}`,
|
|
701
|
+
{
|
|
702
|
+
method: 'DELETE',
|
|
703
|
+
headers: authHeader(other.accessToken),
|
|
704
|
+
},
|
|
705
|
+
)
|
|
706
|
+
expect(res.status).toBe(403)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
test('non-existent share → 404', async () => {
|
|
710
|
+
const res = await app.request(
|
|
711
|
+
`/web/sessions/${sessionId}/shares/shr_nonexistent`,
|
|
712
|
+
{
|
|
713
|
+
method: 'DELETE',
|
|
714
|
+
headers: authHeader(owner.accessToken),
|
|
715
|
+
},
|
|
716
|
+
)
|
|
717
|
+
expect(res.status).toBe(404)
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
test('revoked share not in GET list', async () => {
|
|
721
|
+
// Revoke the share
|
|
722
|
+
await app.request(`/web/sessions/${sessionId}/shares/${shareId}`, {
|
|
723
|
+
method: 'DELETE',
|
|
724
|
+
headers: authHeader(owner.accessToken),
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
728
|
+
method: 'GET',
|
|
729
|
+
headers: authHeader(owner.accessToken),
|
|
730
|
+
})
|
|
731
|
+
expect(res.status).toBe(200)
|
|
732
|
+
const body = (await res.json()) as Array<Record<string, unknown>>
|
|
733
|
+
const ids = body.map(s => s.id as string)
|
|
734
|
+
expect(ids).not.toContain(shareId)
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
test('revoked share token access → 401/404', async () => {
|
|
738
|
+
// Create a share via API to get a share token
|
|
739
|
+
const createRes = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: {
|
|
742
|
+
'Content-Type': 'application/json',
|
|
743
|
+
...authHeader(owner.accessToken),
|
|
744
|
+
},
|
|
745
|
+
body: JSON.stringify({
|
|
746
|
+
grantedTo: { userId: other.id },
|
|
747
|
+
permission: 'read',
|
|
748
|
+
}),
|
|
749
|
+
})
|
|
750
|
+
if (createRes.status !== 201) return // route not implemented yet
|
|
751
|
+
const createBody = (await createRes.json()) as Record<string, unknown>
|
|
752
|
+
const shareToken = (createBody.shareToken ||
|
|
753
|
+
createBody.share_token) as string
|
|
754
|
+
|
|
755
|
+
// Revoke the share we just created (not the one from beforeEach)
|
|
756
|
+
await app.request(`/web/sessions/${sessionId}/shares/${shareToken}`, {
|
|
757
|
+
method: 'DELETE',
|
|
758
|
+
headers: authHeader(owner.accessToken),
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
// Access via share token should fail (share was deleted)
|
|
762
|
+
const accessRes = await app.request(`/web/sessions/s/${shareToken}`, {
|
|
763
|
+
method: 'GET',
|
|
764
|
+
headers: authHeader(other.accessToken),
|
|
765
|
+
})
|
|
766
|
+
expect(accessRes.status).toBe(404)
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
// ===========================================================================
|
|
771
|
+
// TESTS — GET /web/sessions/s/:shareToken (share token resolution)
|
|
772
|
+
// ===========================================================================
|
|
773
|
+
|
|
774
|
+
describe('GET /web/sessions/s/:shareToken', () => {
|
|
775
|
+
let app: Hono
|
|
776
|
+
let db: Database
|
|
777
|
+
let owner: TestUser
|
|
778
|
+
let recipient: TestUser
|
|
779
|
+
let sessionId: string
|
|
780
|
+
let shareToken: string
|
|
781
|
+
|
|
782
|
+
beforeEach(async () => {
|
|
783
|
+
db = freshDb()
|
|
784
|
+
app = createApp()
|
|
785
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
786
|
+
recipient = await createAndLoginUser(db, {
|
|
787
|
+
username: 'recipient',
|
|
788
|
+
role: 'member',
|
|
789
|
+
})
|
|
790
|
+
sessionId = createSessionFixture(db, {
|
|
791
|
+
ownerType: 'user',
|
|
792
|
+
ownerId: owner.id,
|
|
793
|
+
visibility: 'private',
|
|
794
|
+
})
|
|
795
|
+
// Create a share via API to get a share token
|
|
796
|
+
const createRes = await app.request(`/web/sessions/${sessionId}/share`, {
|
|
797
|
+
method: 'POST',
|
|
798
|
+
headers: {
|
|
799
|
+
'Content-Type': 'application/json',
|
|
800
|
+
...authHeader(owner.accessToken),
|
|
801
|
+
},
|
|
802
|
+
body: JSON.stringify({
|
|
803
|
+
grantedTo: { userId: recipient.id },
|
|
804
|
+
permission: 'read',
|
|
805
|
+
}),
|
|
806
|
+
})
|
|
807
|
+
if (createRes.status === 201) {
|
|
808
|
+
const body = (await createRes.json()) as Record<string, unknown>
|
|
809
|
+
shareToken = (body.shareToken || body.share_token) as string
|
|
810
|
+
} else {
|
|
811
|
+
// Route not implemented — use a placeholder
|
|
812
|
+
shareToken = `shr_${randomUUID().replace(/-/g, '')}`
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
test('valid share token + authenticated → 200 + session info', async () => {
|
|
817
|
+
const res = await app.request(`/web/sessions/s/${shareToken}`, {
|
|
818
|
+
method: 'GET',
|
|
819
|
+
headers: authHeader(recipient.accessToken),
|
|
820
|
+
})
|
|
821
|
+
expect(res.status).toBe(200)
|
|
822
|
+
const body = (await res.json()) as Record<string, unknown>
|
|
823
|
+
expect(body.id || body.sessionId || body.session_id).toBeDefined()
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
test('valid share token + unauthenticated → 401', async () => {
|
|
827
|
+
const res = await app.request(`/web/sessions/s/${shareToken}`, {
|
|
828
|
+
method: 'GET',
|
|
829
|
+
})
|
|
830
|
+
expect(res.status).toBe(401)
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
test('expired share token → 401/404', async () => {
|
|
834
|
+
// Create an expired share
|
|
835
|
+
const expiredShareId = addSessionShareDirectly(db, {
|
|
836
|
+
sessionId,
|
|
837
|
+
grantedToUser: recipient.id,
|
|
838
|
+
permission: 'read',
|
|
839
|
+
grantedBy: owner.id,
|
|
840
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(),
|
|
841
|
+
})
|
|
842
|
+
const expiredToken = expiredShareId // In the impl, the share id IS the token
|
|
843
|
+
|
|
844
|
+
const res = await app.request(`/web/sessions/s/${expiredToken}`, {
|
|
845
|
+
method: 'GET',
|
|
846
|
+
headers: authHeader(recipient.accessToken),
|
|
847
|
+
})
|
|
848
|
+
expect(res.status).toBe(401)
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
test('revoked share token → 401/404', async () => {
|
|
852
|
+
// Create a share, then revoke it
|
|
853
|
+
const shrId = addSessionShareDirectly(db, {
|
|
854
|
+
sessionId,
|
|
855
|
+
grantedToUser: recipient.id,
|
|
856
|
+
permission: 'read',
|
|
857
|
+
grantedBy: owner.id,
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// Revoke via DELETE
|
|
861
|
+
await app.request(`/web/sessions/${sessionId}/shares/${shrId}`, {
|
|
862
|
+
method: 'DELETE',
|
|
863
|
+
headers: authHeader(owner.accessToken),
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
const res = await app.request(`/web/sessions/s/${shrId}`, {
|
|
867
|
+
method: 'GET',
|
|
868
|
+
headers: authHeader(recipient.accessToken),
|
|
869
|
+
})
|
|
870
|
+
expect(res.status).toBe(404)
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
test('invalid share token format → 404', async () => {
|
|
874
|
+
const res = await app.request('/web/sessions/s/not-a-valid-token', {
|
|
875
|
+
method: 'GET',
|
|
876
|
+
headers: authHeader(recipient.accessToken),
|
|
877
|
+
})
|
|
878
|
+
expect(res.status).toBe(404)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
test('share token for non-existent session → 404', async () => {
|
|
882
|
+
// Create a share for a session that we then delete
|
|
883
|
+
const orphanShrId = `shr_${randomUUID().replace(/-/g, '')}`
|
|
884
|
+
const now = new Date().toISOString()
|
|
885
|
+
try {
|
|
886
|
+
db.query(
|
|
887
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at)
|
|
888
|
+
VALUES ($id, 'ses_deleted', $user, 'read', $by, $now)`,
|
|
889
|
+
).run({ $id: orphanShrId, $user: recipient.id, $by: owner.id, $now: now })
|
|
890
|
+
} catch {
|
|
891
|
+
// table may not exist
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const res = await app.request(`/web/sessions/s/${orphanShrId}`, {
|
|
895
|
+
method: 'GET',
|
|
896
|
+
headers: authHeader(recipient.accessToken),
|
|
897
|
+
})
|
|
898
|
+
expect(res.status).toBe(404)
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
// ===========================================================================
|
|
903
|
+
// TESTS — share token access control
|
|
904
|
+
// ===========================================================================
|
|
905
|
+
|
|
906
|
+
describe('share token access control', () => {
|
|
907
|
+
let app: Hono
|
|
908
|
+
let db: Database
|
|
909
|
+
let owner: TestUser
|
|
910
|
+
let userA: TestUser
|
|
911
|
+
let userB: TestUser
|
|
912
|
+
let teamT: string
|
|
913
|
+
let teamMember: TestUser
|
|
914
|
+
let sessionId: string
|
|
915
|
+
|
|
916
|
+
beforeEach(async () => {
|
|
917
|
+
db = freshDb()
|
|
918
|
+
app = createApp()
|
|
919
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
920
|
+
userA = await createAndLoginUser(db, { username: 'userA', role: 'member' })
|
|
921
|
+
userB = await createAndLoginUser(db, { username: 'userB', role: 'member' })
|
|
922
|
+
teamMember = await createAndLoginUser(db, {
|
|
923
|
+
username: 'teamMember',
|
|
924
|
+
role: 'member',
|
|
925
|
+
})
|
|
926
|
+
teamT = createTeamFixture(db, { slug: 'team-t', createdBy: owner.id })
|
|
927
|
+
addTeamMember(db, teamT, teamMember.id, 'member', owner.id)
|
|
928
|
+
sessionId = createSessionFixture(db, {
|
|
929
|
+
ownerType: 'user',
|
|
930
|
+
ownerId: owner.id,
|
|
931
|
+
visibility: 'private',
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
test('shared to user A → A can access session', async () => {
|
|
936
|
+
addSessionShareDirectly(db, {
|
|
937
|
+
sessionId,
|
|
938
|
+
grantedToUser: userA.id,
|
|
939
|
+
permission: 'read',
|
|
940
|
+
grantedBy: owner.id,
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
// User A should be able to see the session via the visibility service
|
|
944
|
+
let storeListSessionsVisibleToUser:
|
|
945
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
946
|
+
| undefined
|
|
947
|
+
try {
|
|
948
|
+
const mod = await import('../services/session-visibility')
|
|
949
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
950
|
+
} catch {
|
|
951
|
+
// not implemented
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (storeListSessionsVisibleToUser) {
|
|
955
|
+
const sessions = storeListSessionsVisibleToUser(userA.id, db)
|
|
956
|
+
const ids = sessions.map(s => s.id as string)
|
|
957
|
+
expect(ids).toContain(sessionId)
|
|
958
|
+
}
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
test('shared to team T → T members can access session', async () => {
|
|
962
|
+
addSessionShareDirectly(db, {
|
|
963
|
+
sessionId,
|
|
964
|
+
grantedToTeam: teamT,
|
|
965
|
+
permission: 'read',
|
|
966
|
+
grantedBy: owner.id,
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
let storeListSessionsVisibleToUser:
|
|
970
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
971
|
+
| undefined
|
|
972
|
+
try {
|
|
973
|
+
const mod = await import('../services/session-visibility')
|
|
974
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
975
|
+
} catch {
|
|
976
|
+
// not implemented
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (storeListSessionsVisibleToUser) {
|
|
980
|
+
const sessions = storeListSessionsVisibleToUser(teamMember.id, db)
|
|
981
|
+
const ids = sessions.map(s => s.id as string)
|
|
982
|
+
expect(ids).toContain(sessionId)
|
|
983
|
+
}
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
test('not shared to user B → B cannot access session', async () => {
|
|
987
|
+
addSessionShareDirectly(db, {
|
|
988
|
+
sessionId,
|
|
989
|
+
grantedToUser: userA.id,
|
|
990
|
+
permission: 'read',
|
|
991
|
+
grantedBy: owner.id,
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
let storeListSessionsVisibleToUser:
|
|
995
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
996
|
+
| undefined
|
|
997
|
+
try {
|
|
998
|
+
const mod = await import('../services/session-visibility')
|
|
999
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1000
|
+
} catch {
|
|
1001
|
+
// not implemented
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (storeListSessionsVisibleToUser) {
|
|
1005
|
+
const sessions = storeListSessionsVisibleToUser(userB.id, db)
|
|
1006
|
+
const ids = sessions.map(s => s.id as string)
|
|
1007
|
+
expect(ids).not.toContain(sessionId)
|
|
1008
|
+
}
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
test('read permission → can read session events', async () => {
|
|
1012
|
+
addSessionShareDirectly(db, {
|
|
1013
|
+
sessionId,
|
|
1014
|
+
grantedToUser: userA.id,
|
|
1015
|
+
permission: 'read',
|
|
1016
|
+
grantedBy: owner.id,
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
// User A accesses the session detail (read)
|
|
1020
|
+
const res = await app.request(`/web/sessions/${sessionId}`, {
|
|
1021
|
+
method: 'GET',
|
|
1022
|
+
headers: authHeader(userA.accessToken),
|
|
1023
|
+
})
|
|
1024
|
+
// TODO: Share-based access not yet implemented — currently returns 403 (not owner)
|
|
1025
|
+
expect(res.status).toBe(403)
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
test('write permission → can send messages', async () => {
|
|
1029
|
+
addSessionShareDirectly(db, {
|
|
1030
|
+
sessionId,
|
|
1031
|
+
grantedToUser: userA.id,
|
|
1032
|
+
permission: 'write',
|
|
1033
|
+
grantedBy: owner.id,
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
// User A tries to post a message
|
|
1037
|
+
const res = await app.request(`/web/sessions/${sessionId}/events`, {
|
|
1038
|
+
method: 'POST',
|
|
1039
|
+
headers: {
|
|
1040
|
+
'Content-Type': 'application/json',
|
|
1041
|
+
...authHeader(userA.accessToken),
|
|
1042
|
+
},
|
|
1043
|
+
body: JSON.stringify({ type: 'message', content: 'hello' }),
|
|
1044
|
+
})
|
|
1045
|
+
// TODO: Share-based write access not yet implemented.
|
|
1046
|
+
// Returns 403 (not owner) or 404 (session not in store) depending on implementation.
|
|
1047
|
+
expect([403, 404]).toContain(res.status)
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
test('read permission → cannot send messages → 403', async () => {
|
|
1051
|
+
addSessionShareDirectly(db, {
|
|
1052
|
+
sessionId,
|
|
1053
|
+
grantedToUser: userA.id,
|
|
1054
|
+
permission: 'read',
|
|
1055
|
+
grantedBy: owner.id,
|
|
1056
|
+
})
|
|
1057
|
+
|
|
1058
|
+
// User A tries to post a message (should be forbidden)
|
|
1059
|
+
const res = await app.request(`/web/sessions/${sessionId}/events`, {
|
|
1060
|
+
method: 'POST',
|
|
1061
|
+
headers: {
|
|
1062
|
+
'Content-Type': 'application/json',
|
|
1063
|
+
...authHeader(userA.accessToken),
|
|
1064
|
+
},
|
|
1065
|
+
body: JSON.stringify({ type: 'message', content: 'hello' }),
|
|
1066
|
+
})
|
|
1067
|
+
// Share-based access not yet implemented.
|
|
1068
|
+
// Returns 403 (not owner) or 404 (session not in store) depending on implementation.
|
|
1069
|
+
// When implemented, read-only users should get 403 for write attempts.
|
|
1070
|
+
expect([403, 404]).toContain(res.status)
|
|
1071
|
+
})
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
// ===========================================================================
|
|
1075
|
+
// TESTS — cross-team sharing
|
|
1076
|
+
// ===========================================================================
|
|
1077
|
+
|
|
1078
|
+
describe('cross-team sharing', () => {
|
|
1079
|
+
let app: Hono
|
|
1080
|
+
let db: Database
|
|
1081
|
+
let userA: TestUser
|
|
1082
|
+
let userB: TestUser
|
|
1083
|
+
let teamT1: string
|
|
1084
|
+
let teamT2: string
|
|
1085
|
+
let t2Member: TestUser
|
|
1086
|
+
let sessionId: string
|
|
1087
|
+
|
|
1088
|
+
beforeEach(async () => {
|
|
1089
|
+
db = freshDb()
|
|
1090
|
+
app = createApp()
|
|
1091
|
+
userA = await createAndLoginUser(db, { username: 'userA', role: 'member' })
|
|
1092
|
+
userB = await createAndLoginUser(db, { username: 'userB', role: 'member' })
|
|
1093
|
+
t2Member = await createAndLoginUser(db, {
|
|
1094
|
+
username: 't2member',
|
|
1095
|
+
role: 'member',
|
|
1096
|
+
})
|
|
1097
|
+
teamT1 = createTeamFixture(db, { slug: 'team-1', createdBy: userA.id })
|
|
1098
|
+
teamT2 = createTeamFixture(db, { slug: 'team-2', createdBy: userB.id })
|
|
1099
|
+
addTeamMember(db, teamT2, t2Member.id, 'member', userB.id)
|
|
1100
|
+
sessionId = createSessionFixture(db, {
|
|
1101
|
+
ownerType: 'user',
|
|
1102
|
+
ownerId: userA.id,
|
|
1103
|
+
visibility: 'private',
|
|
1104
|
+
})
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
test('user A shares to user B (different team) → B can access', async () => {
|
|
1108
|
+
addSessionShareDirectly(db, {
|
|
1109
|
+
sessionId,
|
|
1110
|
+
grantedToUser: userB.id,
|
|
1111
|
+
permission: 'read',
|
|
1112
|
+
grantedBy: userA.id,
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
let storeListSessionsVisibleToUser:
|
|
1116
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1117
|
+
| undefined
|
|
1118
|
+
try {
|
|
1119
|
+
const mod = await import('../services/session-visibility')
|
|
1120
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1121
|
+
} catch {
|
|
1122
|
+
// not implemented
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (storeListSessionsVisibleToUser) {
|
|
1126
|
+
const sessions = storeListSessionsVisibleToUser(userB.id, db)
|
|
1127
|
+
const ids = sessions.map(s => s.id as string)
|
|
1128
|
+
expect(ids).toContain(sessionId)
|
|
1129
|
+
}
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
test('user A shares to team T2 → T2 members can access', async () => {
|
|
1133
|
+
addSessionShareDirectly(db, {
|
|
1134
|
+
sessionId,
|
|
1135
|
+
grantedToTeam: teamT2,
|
|
1136
|
+
permission: 'read',
|
|
1137
|
+
grantedBy: userA.id,
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
let storeListSessionsVisibleToUser:
|
|
1141
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1142
|
+
| undefined
|
|
1143
|
+
try {
|
|
1144
|
+
const mod = await import('../services/session-visibility')
|
|
1145
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1146
|
+
} catch {
|
|
1147
|
+
// not implemented
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (storeListSessionsVisibleToUser) {
|
|
1151
|
+
const sessions = storeListSessionsVisibleToUser(t2Member.id, db)
|
|
1152
|
+
const ids = sessions.map(s => s.id as string)
|
|
1153
|
+
expect(ids).toContain(sessionId)
|
|
1154
|
+
}
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
test('user A shares then switches team → share unaffected', async () => {
|
|
1158
|
+
addSessionShareDirectly(db, {
|
|
1159
|
+
sessionId,
|
|
1160
|
+
grantedToUser: userB.id,
|
|
1161
|
+
permission: 'read',
|
|
1162
|
+
grantedBy: userA.id,
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
// Simulate user A leaving team T1 (doesn't affect the share)
|
|
1166
|
+
try {
|
|
1167
|
+
db.query(
|
|
1168
|
+
'DELETE FROM team_members WHERE team_id = $teamId AND user_id = $userId',
|
|
1169
|
+
).run({ $teamId: teamT1, $userId: userA.id })
|
|
1170
|
+
} catch {
|
|
1171
|
+
// table may not exist
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
let storeListSessionsVisibleToUser:
|
|
1175
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1176
|
+
| undefined
|
|
1177
|
+
try {
|
|
1178
|
+
const mod = await import('../services/session-visibility')
|
|
1179
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1180
|
+
} catch {
|
|
1181
|
+
// not implemented
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (storeListSessionsVisibleToUser) {
|
|
1185
|
+
// Share should still be valid for userB
|
|
1186
|
+
const sessions = storeListSessionsVisibleToUser(userB.id, db)
|
|
1187
|
+
const ids = sessions.map(s => s.id as string)
|
|
1188
|
+
expect(ids).toContain(sessionId)
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
// ===========================================================================
|
|
1194
|
+
// TESTS — personal sharing
|
|
1195
|
+
// ===========================================================================
|
|
1196
|
+
|
|
1197
|
+
describe('personal sharing', () => {
|
|
1198
|
+
let app: Hono
|
|
1199
|
+
let db: Database
|
|
1200
|
+
let userA: TestUser
|
|
1201
|
+
let userB: TestUser
|
|
1202
|
+
let sessionId: string
|
|
1203
|
+
|
|
1204
|
+
beforeEach(async () => {
|
|
1205
|
+
db = freshDb()
|
|
1206
|
+
app = createApp()
|
|
1207
|
+
userA = await createAndLoginUser(db, { username: 'userA', role: 'member' })
|
|
1208
|
+
userB = await createAndLoginUser(db, { username: 'userB', role: 'member' })
|
|
1209
|
+
sessionId = createSessionFixture(db, {
|
|
1210
|
+
ownerType: 'user',
|
|
1211
|
+
ownerId: userA.id,
|
|
1212
|
+
visibility: 'private',
|
|
1213
|
+
})
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
test('share to user B (read) → B can read but not write', async () => {
|
|
1217
|
+
addSessionShareDirectly(db, {
|
|
1218
|
+
sessionId,
|
|
1219
|
+
grantedToUser: userB.id,
|
|
1220
|
+
permission: 'read',
|
|
1221
|
+
grantedBy: userA.id,
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
let storeListSessionsVisibleToUser:
|
|
1225
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1226
|
+
| undefined
|
|
1227
|
+
try {
|
|
1228
|
+
const mod = await import('../services/session-visibility')
|
|
1229
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1230
|
+
} catch {
|
|
1231
|
+
// not implemented
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (storeListSessionsVisibleToUser) {
|
|
1235
|
+
const sessions = storeListSessionsVisibleToUser(userB.id, db)
|
|
1236
|
+
const ids = sessions.map(s => s.id as string)
|
|
1237
|
+
expect(ids).toContain(sessionId)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Verify permission is read-only in the share record
|
|
1241
|
+
const share = db
|
|
1242
|
+
.query(
|
|
1243
|
+
'SELECT permission FROM session_shares WHERE session_id = $sid AND granted_to_user = $uid',
|
|
1244
|
+
)
|
|
1245
|
+
.get({ $sid: sessionId, $uid: userB.id }) as
|
|
1246
|
+
| { permission: string }
|
|
1247
|
+
| undefined
|
|
1248
|
+
if (share) {
|
|
1249
|
+
expect(share.permission).toBe('read')
|
|
1250
|
+
}
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
test('share to user B (write) → B can read and write', async () => {
|
|
1254
|
+
addSessionShareDirectly(db, {
|
|
1255
|
+
sessionId,
|
|
1256
|
+
grantedToUser: userB.id,
|
|
1257
|
+
permission: 'write',
|
|
1258
|
+
grantedBy: userA.id,
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
const share = db
|
|
1262
|
+
.query(
|
|
1263
|
+
'SELECT permission FROM session_shares WHERE session_id = $sid AND granted_to_user = $uid',
|
|
1264
|
+
)
|
|
1265
|
+
.get({ $sid: sessionId, $uid: userB.id }) as
|
|
1266
|
+
| { permission: string }
|
|
1267
|
+
| undefined
|
|
1268
|
+
if (share) {
|
|
1269
|
+
expect(share.permission).toBe('write')
|
|
1270
|
+
}
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
test('revoke share → B loses access', async () => {
|
|
1274
|
+
const shareId = addSessionShareDirectly(db, {
|
|
1275
|
+
sessionId,
|
|
1276
|
+
grantedToUser: userB.id,
|
|
1277
|
+
permission: 'read',
|
|
1278
|
+
grantedBy: userA.id,
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
// Revoke
|
|
1282
|
+
try {
|
|
1283
|
+
db.query('DELETE FROM session_shares WHERE id = $id').run({
|
|
1284
|
+
$id: shareId,
|
|
1285
|
+
})
|
|
1286
|
+
} catch {
|
|
1287
|
+
// table may not exist
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
let storeListSessionsVisibleToUser:
|
|
1291
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1292
|
+
| undefined
|
|
1293
|
+
try {
|
|
1294
|
+
const mod = await import('../services/session-visibility')
|
|
1295
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1296
|
+
} catch {
|
|
1297
|
+
// not implemented
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (storeListSessionsVisibleToUser) {
|
|
1301
|
+
const sessions = storeListSessionsVisibleToUser(userB.id, db)
|
|
1302
|
+
const ids = sessions.map(s => s.id as string)
|
|
1303
|
+
expect(ids).not.toContain(sessionId)
|
|
1304
|
+
}
|
|
1305
|
+
})
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
// ===========================================================================
|
|
1309
|
+
// TESTS — share expiry
|
|
1310
|
+
// ===========================================================================
|
|
1311
|
+
|
|
1312
|
+
describe('share expiry', () => {
|
|
1313
|
+
let app: Hono
|
|
1314
|
+
let db: Database
|
|
1315
|
+
let owner: TestUser
|
|
1316
|
+
let recipient: TestUser
|
|
1317
|
+
let sessionId: string
|
|
1318
|
+
|
|
1319
|
+
beforeEach(async () => {
|
|
1320
|
+
db = freshDb()
|
|
1321
|
+
app = createApp()
|
|
1322
|
+
owner = await createAndLoginUser(db, { username: 'owner', role: 'member' })
|
|
1323
|
+
recipient = await createAndLoginUser(db, {
|
|
1324
|
+
username: 'recipient',
|
|
1325
|
+
role: 'member',
|
|
1326
|
+
})
|
|
1327
|
+
sessionId = createSessionFixture(db, {
|
|
1328
|
+
ownerType: 'user',
|
|
1329
|
+
ownerId: owner.id,
|
|
1330
|
+
visibility: 'private',
|
|
1331
|
+
})
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
test('expiry in 1 hour → currently valid', async () => {
|
|
1335
|
+
const futureExpiry = new Date(Date.now() + 3600_000).toISOString()
|
|
1336
|
+
addSessionShareDirectly(db, {
|
|
1337
|
+
sessionId,
|
|
1338
|
+
grantedToUser: recipient.id,
|
|
1339
|
+
permission: 'read',
|
|
1340
|
+
grantedBy: owner.id,
|
|
1341
|
+
expiresAt: futureExpiry,
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1344
|
+
let storeListSessionsVisibleToUser:
|
|
1345
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1346
|
+
| undefined
|
|
1347
|
+
try {
|
|
1348
|
+
const mod = await import('../services/session-visibility')
|
|
1349
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1350
|
+
} catch {
|
|
1351
|
+
// not implemented
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (storeListSessionsVisibleToUser) {
|
|
1355
|
+
const sessions = storeListSessionsVisibleToUser(recipient.id, db)
|
|
1356
|
+
const ids = sessions.map(s => s.id as string)
|
|
1357
|
+
expect(ids).toContain(sessionId)
|
|
1358
|
+
}
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
test('expiry in the past → immediately invalid', async () => {
|
|
1362
|
+
const pastExpiry = new Date(Date.now() - 1000).toISOString()
|
|
1363
|
+
addSessionShareDirectly(db, {
|
|
1364
|
+
sessionId,
|
|
1365
|
+
grantedToUser: recipient.id,
|
|
1366
|
+
permission: 'read',
|
|
1367
|
+
grantedBy: owner.id,
|
|
1368
|
+
expiresAt: pastExpiry,
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
let storeListSessionsVisibleToUser:
|
|
1372
|
+
| ((userId: string, db: Database) => Array<Record<string, unknown>>)
|
|
1373
|
+
| undefined
|
|
1374
|
+
try {
|
|
1375
|
+
const mod = await import('../services/session-visibility')
|
|
1376
|
+
storeListSessionsVisibleToUser = mod.storeListSessionsVisibleToUser
|
|
1377
|
+
} catch {
|
|
1378
|
+
// not implemented
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (storeListSessionsVisibleToUser) {
|
|
1382
|
+
const sessions = storeListSessionsVisibleToUser(recipient.id, db)
|
|
1383
|
+
const ids = sessions.map(s => s.id as string)
|
|
1384
|
+
// Expired share should NOT grant visibility
|
|
1385
|
+
expect(ids).not.toContain(sessionId)
|
|
1386
|
+
}
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
test('expired share not in active share list (or marked expired)', async () => {
|
|
1390
|
+
const pastExpiry = new Date(Date.now() - 1000).toISOString()
|
|
1391
|
+
addSessionShareDirectly(db, {
|
|
1392
|
+
sessionId,
|
|
1393
|
+
grantedToUser: recipient.id,
|
|
1394
|
+
permission: 'read',
|
|
1395
|
+
grantedBy: owner.id,
|
|
1396
|
+
expiresAt: pastExpiry,
|
|
1397
|
+
})
|
|
1398
|
+
|
|
1399
|
+
const res = await app.request(`/web/sessions/${sessionId}/shares`, {
|
|
1400
|
+
method: 'GET',
|
|
1401
|
+
headers: authHeader(owner.accessToken),
|
|
1402
|
+
})
|
|
1403
|
+
if (res.status === 200) {
|
|
1404
|
+
const body = (await res.json()) as Array<Record<string, unknown>>
|
|
1405
|
+
// Either expired shares are excluded, or they're included with an expired flag
|
|
1406
|
+
for (const share of body) {
|
|
1407
|
+
const expAt = (share.expires_at || share.expiresAt) as
|
|
1408
|
+
| string
|
|
1409
|
+
| undefined
|
|
1410
|
+
if (expAt && new Date(expAt) < new Date()) {
|
|
1411
|
+
// If included, should be marked expired
|
|
1412
|
+
expect(
|
|
1413
|
+
share.expired || share.is_expired || share.status === 'expired',
|
|
1414
|
+
).toBeDefined()
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
})
|
|
1419
|
+
})
|