@saluzi/saluzi-edu 0.2.12 → 0.2.13
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 +212 -140
- package/dist/rcs-web/assets/Dashboard-YYwDrKTN.js +1 -0
- package/dist/rcs-web/assets/Join-DKCP5X6X.js +1 -0
- package/dist/rcs-web/assets/Login-0VGZs85I.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/SessionDetail-DSoMXZJb.js → dist/rcs-web/assets/SessionDetail-DmK-DfBQ.js} +11 -11
- package/dist/rcs-web/assets/Setup-C0cjxXMt.js +1 -0
- package/dist/rcs-web/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
- package/dist/rcs-web/assets/TeamCreate-B8ZV6EdY.js +1 -0
- package/dist/rcs-web/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/dist/rcs-web/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
- package/dist/rcs-web/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
- package/dist/rcs-web/assets/UserSettings-DRdz9kYE.js +1 -0
- package/dist/rcs-web/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
- package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → dist/rcs-web/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
- package/dist/rcs-web/assets/channel-DaI_n8dp.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-Ch7yD8cR.js → dist/rcs-web/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → dist/rcs-web/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
- package/dist/rcs-web/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → dist/rcs-web/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
- package/dist/rcs-web/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/dist/rcs-web/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/dist/rcs-web/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/dist/rcs-web/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/dist/rcs-web/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → dist/rcs-web/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/dist/rcs-web/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/dist/rcs-web/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
- package/dist/rcs-web/assets/main-CkqtCllF.js +238 -0
- package/dist/rcs-web/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/dist/rcs-web/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
- package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/dist/rcs-web/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Be7t_3rC.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/dist/rcs-web/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
- package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/dist/rcs-web/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
- package/dist/rcs-web/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
- package/dist/rcs-web/index.html +1 -1
- package/package.json +2 -2
- package/packages/remote-control-server/src/__tests__/cookie.test.ts +0 -71
- package/packages/remote-control-server/src/__tests__/data-layer-round2.test.ts +556 -0
- package/packages/remote-control-server/src/__tests__/route-mounting.test.ts +212 -0
- package/packages/remote-control-server/src/__tests__/routes.test.ts +5 -44
- package/packages/remote-control-server/src/__tests__/security-fixes.test.ts +526 -0
- package/packages/remote-control-server/src/__tests__/security-round3.test.ts +780 -0
- package/packages/remote-control-server/src/__tests__/shares.test.ts +5 -2
- package/packages/remote-control-server/src/auth/constants.ts +0 -6
- package/packages/remote-control-server/src/auth/cookie.ts +0 -43
- package/packages/remote-control-server/src/auth/csrf.ts +58 -0
- package/packages/remote-control-server/src/auth/middleware.ts +7 -4
- package/packages/remote-control-server/src/index.ts +26 -0
- package/packages/remote-control-server/src/registerRoutes.ts +17 -1
- package/packages/remote-control-server/src/routes/web/auth-routes.ts +93 -54
- package/packages/remote-control-server/src/routes/web/auth.ts +16 -23
- package/packages/remote-control-server/src/routes/web/sessions.ts +30 -5
- package/packages/remote-control-server/src/routes/web/shares.ts +38 -1
- package/packages/remote-control-server/src/routes/web/teams.ts +22 -4
- package/packages/remote-control-server/src/services/session.ts +143 -9
- package/packages/remote-control-server/src/store.ts +34 -1
- package/packages/remote-control-server/src/types/api.ts +0 -1
- package/packages/remote-control-server/web/dist/assets/Dashboard-YYwDrKTN.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Join-DKCP5X6X.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Login-0VGZs85I.js +1 -0
- package/{dist/rcs-web/assets/SessionDetail-DSoMXZJb.js → packages/remote-control-server/web/dist/assets/SessionDetail-DmK-DfBQ.js} +11 -11
- package/packages/remote-control-server/web/dist/assets/Setup-C0cjxXMt.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/TeamCreate-B8ZV6EdY.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/UserSettings-DRdz9kYE.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{dist/rcs-web/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/channel-DaI_n8dp.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-Ch7yD8cR.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{dist/rcs-web/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{dist/rcs-web/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{dist/rcs-web/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/main-CkqtCllF.js +238 -0
- package/packages/remote-control-server/web/dist/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Be7t_3rC.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
- package/packages/remote-control-server/web/dist/index.html +1 -1
- package/dist/rcs-web/assets/Dashboard-D_Im4UId.js +0 -1
- package/dist/rcs-web/assets/Join-BTnvMsB9.js +0 -1
- package/dist/rcs-web/assets/Login-ClT7RckC.js +0 -1
- package/dist/rcs-web/assets/Setup-PaDA6Et-.js +0 -1
- package/dist/rcs-web/assets/TeamCreate-249rK3U3.js +0 -1
- package/dist/rcs-web/assets/UserSettings-X627Fdni.js +0 -1
- package/dist/rcs-web/assets/channel-BUwWbT6U.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
- package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +0 -1
- package/dist/rcs-web/assets/main-DvfxciBy.js +0 -238
- package/packages/remote-control-server/web/dist/assets/Dashboard-D_Im4UId.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Join-BTnvMsB9.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Login-ClT7RckC.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Setup-PaDA6Et-.js +0 -1
- package/packages/remote-control-server/web/dist/assets/TeamCreate-249rK3U3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/UserSettings-X627Fdni.js +0 -1
- package/packages/remote-control-server/web/dist/assets/channel-BUwWbT6U.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
- package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/main-DvfxciBy.js +0 -238
|
@@ -0,0 +1,556 @@
|
|
|
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 { Database } from 'bun:sqlite'
|
|
6
|
+
import { mockConfigModule } from './helpers/mock-config'
|
|
7
|
+
|
|
8
|
+
// Mock config before module imports
|
|
9
|
+
mock.module('../config', () => mockConfigModule())
|
|
10
|
+
|
|
11
|
+
import { migrateDatabase, initDatabase, resetDbSingleton, getDb } from '../db/sqlite'
|
|
12
|
+
import {
|
|
13
|
+
storeReset,
|
|
14
|
+
storeCreateSession,
|
|
15
|
+
storeCreateEnvironment,
|
|
16
|
+
storeUpdateSession,
|
|
17
|
+
storeDeleteSession,
|
|
18
|
+
storeUpdateEnvironment,
|
|
19
|
+
storeGetSession,
|
|
20
|
+
storeGetEnvironment,
|
|
21
|
+
storeBindSession,
|
|
22
|
+
storeIsSessionOwner,
|
|
23
|
+
} from '../store'
|
|
24
|
+
import {
|
|
25
|
+
isSessionVisibleToUser,
|
|
26
|
+
resolveOwnedWebSessionId,
|
|
27
|
+
} from '../services/session'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Database helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function freshDb(): Database {
|
|
34
|
+
try {
|
|
35
|
+
resetDbSingleton()
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
const path = resolve(
|
|
40
|
+
tmpdir(),
|
|
41
|
+
`rcs-r2-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`,
|
|
42
|
+
)
|
|
43
|
+
return initDatabase(path)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function seedUser(
|
|
47
|
+
db: Database,
|
|
48
|
+
opts: { id?: string; username?: string; role?: string },
|
|
49
|
+
): string {
|
|
50
|
+
const id = opts.id || `usr_${randomUUID().replace(/-/g, '')}`
|
|
51
|
+
const now = new Date().toISOString()
|
|
52
|
+
db.query(
|
|
53
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
54
|
+
VALUES ($id, $username, 'hash', $role, $now)`,
|
|
55
|
+
).run({
|
|
56
|
+
$id: id,
|
|
57
|
+
$username: opts.username || `user_${id.slice(0, 8)}`,
|
|
58
|
+
$role: opts.role || 'member',
|
|
59
|
+
$now: now,
|
|
60
|
+
})
|
|
61
|
+
return id
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
// Task 1: Store dual-write consistency
|
|
66
|
+
// ===========================================================================
|
|
67
|
+
|
|
68
|
+
describe('Task 1: store dual-write consistency', () => {
|
|
69
|
+
let db: Database
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
storeReset()
|
|
73
|
+
db = freshDb()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('storeUpdateSession throws on DB failure', () => {
|
|
77
|
+
test('throws when DB write fails', () => {
|
|
78
|
+
const session = storeCreateSession({})
|
|
79
|
+
// Close DB to force a write failure
|
|
80
|
+
db.close()
|
|
81
|
+
|
|
82
|
+
expect(() => {
|
|
83
|
+
storeUpdateSession(session.id, { title: 'Updated' })
|
|
84
|
+
}).toThrow()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('succeeds when DB is available', () => {
|
|
88
|
+
const session = storeCreateSession({})
|
|
89
|
+
const result = storeUpdateSession(session.id, { title: 'Updated' })
|
|
90
|
+
expect(result).toBe(true)
|
|
91
|
+
expect(storeGetSession(session.id)?.title).toBe('Updated')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('storeDeleteSession throws on DB failure', () => {
|
|
96
|
+
test('throws when DB write fails', () => {
|
|
97
|
+
const session = storeCreateSession({})
|
|
98
|
+
// Close DB to force a write failure
|
|
99
|
+
db.close()
|
|
100
|
+
|
|
101
|
+
expect(() => {
|
|
102
|
+
storeDeleteSession(session.id)
|
|
103
|
+
}).toThrow()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('succeeds when DB is available', () => {
|
|
107
|
+
const session = storeCreateSession({})
|
|
108
|
+
const result = storeDeleteSession(session.id)
|
|
109
|
+
expect(result).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('storeUpdateEnvironment writes to DB', () => {
|
|
114
|
+
test('persists status change to DB', () => {
|
|
115
|
+
const env = storeCreateEnvironment({ secret: 'test-secret' })
|
|
116
|
+
storeUpdateEnvironment(env.id, { status: 'offline' })
|
|
117
|
+
|
|
118
|
+
// Verify DB was updated
|
|
119
|
+
const row = db
|
|
120
|
+
.query('SELECT status FROM environments WHERE id = $id')
|
|
121
|
+
.get({ $id: env.id }) as { status: string } | null
|
|
122
|
+
expect(row).not.toBeNull()
|
|
123
|
+
expect(row!.status).toBe('offline')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('persists machineName to DB', () => {
|
|
127
|
+
const env = storeCreateEnvironment({ secret: 'test-secret' })
|
|
128
|
+
storeUpdateEnvironment(env.id, { machineName: 'my-machine' })
|
|
129
|
+
|
|
130
|
+
const row = db
|
|
131
|
+
.query('SELECT machine_name FROM environments WHERE id = $id')
|
|
132
|
+
.get({ $id: env.id }) as { machine_name: string } | null
|
|
133
|
+
expect(row).not.toBeNull()
|
|
134
|
+
expect(row!.machine_name).toBe('my-machine')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('persists lastPollAt to DB', () => {
|
|
138
|
+
const env = storeCreateEnvironment({ secret: 'test-secret' })
|
|
139
|
+
const newDate = new Date('2026-06-15T12:00:00Z')
|
|
140
|
+
storeUpdateEnvironment(env.id, { lastPollAt: newDate })
|
|
141
|
+
|
|
142
|
+
const row = db
|
|
143
|
+
.query('SELECT last_poll_at FROM environments WHERE id = $id')
|
|
144
|
+
.get({ $id: env.id }) as { last_poll_at: string } | null
|
|
145
|
+
expect(row).not.toBeNull()
|
|
146
|
+
expect(row!.last_poll_at).toBe(newDate.toISOString())
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('persists multiple fields at once', () => {
|
|
150
|
+
const env = storeCreateEnvironment({ secret: 'test-secret' })
|
|
151
|
+
storeUpdateEnvironment(env.id, {
|
|
152
|
+
status: 'offline',
|
|
153
|
+
machineName: 'mac1',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const row = db
|
|
157
|
+
.query(
|
|
158
|
+
'SELECT status, machine_name FROM environments WHERE id = $id',
|
|
159
|
+
)
|
|
160
|
+
.get({ $id: env.id }) as {
|
|
161
|
+
status: string
|
|
162
|
+
machine_name: string
|
|
163
|
+
} | null
|
|
164
|
+
expect(row).not.toBeNull()
|
|
165
|
+
expect(row!.status).toBe('offline')
|
|
166
|
+
expect(row!.machine_name).toBe('mac1')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('returns false for non-existent environment', () => {
|
|
170
|
+
expect(storeUpdateEnvironment('env_no', { status: 'active' })).toBe(
|
|
171
|
+
false,
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('throws on DB failure', () => {
|
|
176
|
+
const env = storeCreateEnvironment({ secret: 'test-secret' })
|
|
177
|
+
db.close()
|
|
178
|
+
|
|
179
|
+
expect(() => {
|
|
180
|
+
storeUpdateEnvironment(env.id, { status: 'offline' })
|
|
181
|
+
}).toThrow()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ===========================================================================
|
|
187
|
+
// Task 2: isSessionVisibleToUser + resolveOwnedWebSessionId with shares/teams
|
|
188
|
+
// ===========================================================================
|
|
189
|
+
|
|
190
|
+
describe('Task 2: session visibility with shares and teams', () => {
|
|
191
|
+
let db: Database
|
|
192
|
+
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
storeReset()
|
|
195
|
+
db = freshDb()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('isSessionVisibleToUser', () => {
|
|
199
|
+
test('returns owner for direct session owner', () => {
|
|
200
|
+
const userId = seedUser(db, { role: 'member' })
|
|
201
|
+
const session = storeCreateSession({})
|
|
202
|
+
storeBindSession(session.id, userId)
|
|
203
|
+
|
|
204
|
+
const result = isSessionVisibleToUser(session.id, userId, db)
|
|
205
|
+
expect(result).toBe('owner')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('returns write for user with write share', () => {
|
|
209
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
210
|
+
const shareUserId = seedUser(db, {
|
|
211
|
+
username: 'shareuser',
|
|
212
|
+
role: 'member',
|
|
213
|
+
})
|
|
214
|
+
const session = storeCreateSession({})
|
|
215
|
+
storeBindSession(session.id, ownerId)
|
|
216
|
+
|
|
217
|
+
// Create a write share
|
|
218
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
219
|
+
const now = new Date().toISOString()
|
|
220
|
+
db.query(
|
|
221
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at)
|
|
222
|
+
VALUES ($id, $sessionId, $userId, 'write', $grantedBy, $now)`,
|
|
223
|
+
).run({
|
|
224
|
+
$id: shareId,
|
|
225
|
+
$sessionId: session.id,
|
|
226
|
+
$userId: shareUserId,
|
|
227
|
+
$grantedBy: ownerId,
|
|
228
|
+
$now: now,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const result = isSessionVisibleToUser(session.id, shareUserId, db)
|
|
232
|
+
expect(result).toBe('write')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('returns read for user with read share', () => {
|
|
236
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
237
|
+
const shareUserId = seedUser(db, {
|
|
238
|
+
username: 'reader',
|
|
239
|
+
role: 'member',
|
|
240
|
+
})
|
|
241
|
+
const session = storeCreateSession({})
|
|
242
|
+
storeBindSession(session.id, ownerId)
|
|
243
|
+
|
|
244
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
245
|
+
const now = new Date().toISOString()
|
|
246
|
+
db.query(
|
|
247
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at)
|
|
248
|
+
VALUES ($id, $sessionId, $userId, 'read', $grantedBy, $now)`,
|
|
249
|
+
).run({
|
|
250
|
+
$id: shareId,
|
|
251
|
+
$sessionId: session.id,
|
|
252
|
+
$userId: shareUserId,
|
|
253
|
+
$grantedBy: ownerId,
|
|
254
|
+
$now: now,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const result = isSessionVisibleToUser(session.id, shareUserId, db)
|
|
258
|
+
expect(result).toBe('read')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('returns null for expired share', () => {
|
|
262
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
263
|
+
const shareUserId = seedUser(db, {
|
|
264
|
+
username: 'expired',
|
|
265
|
+
role: 'member',
|
|
266
|
+
})
|
|
267
|
+
const session = storeCreateSession({})
|
|
268
|
+
storeBindSession(session.id, ownerId)
|
|
269
|
+
|
|
270
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
271
|
+
const pastDate = new Date('2020-01-01T00:00:00Z').toISOString()
|
|
272
|
+
const now = new Date().toISOString()
|
|
273
|
+
db.query(
|
|
274
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, expires_at, granted_by, granted_at)
|
|
275
|
+
VALUES ($id, $sessionId, $userId, 'write', $exp, $grantedBy, $now)`,
|
|
276
|
+
).run({
|
|
277
|
+
$id: shareId,
|
|
278
|
+
$sessionId: session.id,
|
|
279
|
+
$userId: shareUserId,
|
|
280
|
+
$exp: pastDate,
|
|
281
|
+
$grantedBy: ownerId,
|
|
282
|
+
$now: now,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const result = isSessionVisibleToUser(session.id, shareUserId, db)
|
|
286
|
+
expect(result).toBeNull()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('returns owner for team owner when session owned by team', () => {
|
|
290
|
+
const userId = seedUser(db, { role: 'member' })
|
|
291
|
+
const teamId = `team_${randomUUID().replace(/-/g, '')}`
|
|
292
|
+
const now = new Date().toISOString()
|
|
293
|
+
|
|
294
|
+
// Create team and add user as owner
|
|
295
|
+
db.query(
|
|
296
|
+
`INSERT INTO teams (id, name, slug, created_by, created_at, updated_at)
|
|
297
|
+
VALUES ($id, 'Test Team', 'test-team-r2', $userId, $now, $now)`,
|
|
298
|
+
).run({ $id: teamId, $userId: userId, $now: now })
|
|
299
|
+
db.query(
|
|
300
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
301
|
+
VALUES ($teamId, $userId, 'owner', $now, $userId)`,
|
|
302
|
+
).run({ $teamId: teamId, $userId: userId, $now: now })
|
|
303
|
+
|
|
304
|
+
// Create session owned by team
|
|
305
|
+
const session = storeCreateSession({})
|
|
306
|
+
storeBindSession(session.id, teamId, 'team')
|
|
307
|
+
|
|
308
|
+
const result = isSessionVisibleToUser(session.id, userId, db)
|
|
309
|
+
expect(result).toBe('owner')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('returns read for team share', () => {
|
|
313
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
314
|
+
const userId = seedUser(db, {
|
|
315
|
+
username: 'teammember',
|
|
316
|
+
role: 'member',
|
|
317
|
+
})
|
|
318
|
+
const teamId = `team_${randomUUID().replace(/-/g, '')}`
|
|
319
|
+
const now = new Date().toISOString()
|
|
320
|
+
|
|
321
|
+
// Create team and add user as member
|
|
322
|
+
db.query(
|
|
323
|
+
`INSERT INTO teams (id, name, slug, created_by, created_at, updated_at)
|
|
324
|
+
VALUES ($id, 'Share Team', 'share-team-r2', $ownerId, $now, $now)`,
|
|
325
|
+
).run({ $id: teamId, $ownerId: ownerId, $now: now })
|
|
326
|
+
db.query(
|
|
327
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
328
|
+
VALUES ($teamId, $userId, 'member', $now, $ownerId)`,
|
|
329
|
+
).run({ $teamId: teamId, $userId: userId, $now: now, $ownerId: ownerId })
|
|
330
|
+
|
|
331
|
+
// Create session and share to team
|
|
332
|
+
const session = storeCreateSession({})
|
|
333
|
+
storeBindSession(session.id, ownerId)
|
|
334
|
+
|
|
335
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
336
|
+
db.query(
|
|
337
|
+
`INSERT INTO session_shares (id, session_id, granted_to_team, permission, granted_by, granted_at)
|
|
338
|
+
VALUES ($id, $sessionId, $teamId, 'read', $grantedBy, $now)`,
|
|
339
|
+
).run({
|
|
340
|
+
$id: shareId,
|
|
341
|
+
$sessionId: session.id,
|
|
342
|
+
$teamId: teamId,
|
|
343
|
+
$grantedBy: ownerId,
|
|
344
|
+
$now: now,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const result = isSessionVisibleToUser(session.id, userId, db)
|
|
348
|
+
expect(result).toBe('read')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('returns null for user with no access', () => {
|
|
352
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
353
|
+
const otherUserId = seedUser(db, {
|
|
354
|
+
username: 'outsider',
|
|
355
|
+
role: 'member',
|
|
356
|
+
})
|
|
357
|
+
const session = storeCreateSession({})
|
|
358
|
+
storeBindSession(session.id, ownerId)
|
|
359
|
+
|
|
360
|
+
const result = isSessionVisibleToUser(session.id, otherUserId, db)
|
|
361
|
+
expect(result).toBeNull()
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('resolveOwnedWebSessionId with shares', () => {
|
|
366
|
+
test('resolves session for user with write share', () => {
|
|
367
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
368
|
+
const shareUserId = seedUser(db, {
|
|
369
|
+
username: 'writer',
|
|
370
|
+
role: 'member',
|
|
371
|
+
})
|
|
372
|
+
const session = storeCreateSession({})
|
|
373
|
+
storeBindSession(session.id, ownerId)
|
|
374
|
+
|
|
375
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
376
|
+
const now = new Date().toISOString()
|
|
377
|
+
db.query(
|
|
378
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at)
|
|
379
|
+
VALUES ($id, $sessionId, $userId, 'write', $grantedBy, $now)`,
|
|
380
|
+
).run({
|
|
381
|
+
$id: shareId,
|
|
382
|
+
$sessionId: session.id,
|
|
383
|
+
$userId: shareUserId,
|
|
384
|
+
$grantedBy: ownerId,
|
|
385
|
+
$now: now,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Write-level check: should resolve for write share
|
|
389
|
+
const resolved = resolveOwnedWebSessionId(session.id, shareUserId, true)
|
|
390
|
+
expect(resolved).toBe(session.id)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('resolves session for user with read share (read-only)', () => {
|
|
394
|
+
const ownerId = seedUser(db, { role: 'member' })
|
|
395
|
+
const shareUserId = seedUser(db, {
|
|
396
|
+
username: 'reader2',
|
|
397
|
+
role: 'member',
|
|
398
|
+
})
|
|
399
|
+
const session = storeCreateSession({})
|
|
400
|
+
storeBindSession(session.id, ownerId)
|
|
401
|
+
|
|
402
|
+
const shareId = `share_${randomUUID().replace(/-/g, '')}`
|
|
403
|
+
const now = new Date().toISOString()
|
|
404
|
+
db.query(
|
|
405
|
+
`INSERT INTO session_shares (id, session_id, granted_to_user, permission, granted_by, granted_at)
|
|
406
|
+
VALUES ($id, $sessionId, $userId, 'read', $grantedBy, $now)`,
|
|
407
|
+
).run({
|
|
408
|
+
$id: shareId,
|
|
409
|
+
$sessionId: session.id,
|
|
410
|
+
$userId: shareUserId,
|
|
411
|
+
$grantedBy: ownerId,
|
|
412
|
+
$now: now,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Read-level check: should resolve
|
|
416
|
+
const resolved = resolveOwnedWebSessionId(session.id, shareUserId, false)
|
|
417
|
+
expect(resolved).toBe(session.id)
|
|
418
|
+
|
|
419
|
+
// Write-level check: should NOT resolve (read-only share)
|
|
420
|
+
const writeResolved = resolveOwnedWebSessionId(
|
|
421
|
+
session.id,
|
|
422
|
+
shareUserId,
|
|
423
|
+
true,
|
|
424
|
+
)
|
|
425
|
+
expect(writeResolved).toBeNull()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test('resolves session for team member when team owns session', () => {
|
|
429
|
+
const userId = seedUser(db, { role: 'member' })
|
|
430
|
+
const teamId = `team_${randomUUID().replace(/-/g, '')}`
|
|
431
|
+
const now = new Date().toISOString()
|
|
432
|
+
|
|
433
|
+
db.query(
|
|
434
|
+
`INSERT INTO teams (id, name, slug, created_by, created_at, updated_at)
|
|
435
|
+
VALUES ($id, 'Resolve Team', 'resolve-team-r2', $userId, $now, $now)`,
|
|
436
|
+
).run({ $id: teamId, $userId: userId, $now: now })
|
|
437
|
+
db.query(
|
|
438
|
+
`INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
|
|
439
|
+
VALUES ($teamId, $userId, 'owner', $now, $userId)`,
|
|
440
|
+
).run({ $teamId: teamId, $userId: userId, $now: now })
|
|
441
|
+
|
|
442
|
+
const session = storeCreateSession({})
|
|
443
|
+
storeBindSession(session.id, teamId, 'team')
|
|
444
|
+
|
|
445
|
+
const resolved = resolveOwnedWebSessionId(session.id, userId, true)
|
|
446
|
+
expect(resolved).toBe(session.id)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// Task 3: teams.ts admin created_by determinism
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
|
|
455
|
+
describe('Task 3: admin created_by for team creation', () => {
|
|
456
|
+
let db: Database
|
|
457
|
+
|
|
458
|
+
beforeEach(() => {
|
|
459
|
+
storeReset()
|
|
460
|
+
db = freshDb()
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('admin query uses ORDER BY created_at ASC for determinism', () => {
|
|
464
|
+
// Create two admins with different creation times
|
|
465
|
+
const now1 = new Date('2026-01-01T00:00:00Z').toISOString()
|
|
466
|
+
const now2 = new Date('2026-06-01T00:00:00Z').toISOString()
|
|
467
|
+
|
|
468
|
+
db.query(
|
|
469
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
470
|
+
VALUES ('usr_second_admin', 'admin2', 'hash', 'admin', $now2)`,
|
|
471
|
+
).run({ $now2: now2 })
|
|
472
|
+
db.query(
|
|
473
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
474
|
+
VALUES ('usr_first_admin', 'admin1', 'hash', 'admin', $now1)`,
|
|
475
|
+
).run({ $now1: now1 })
|
|
476
|
+
|
|
477
|
+
// The query used in teams.ts should pick the earliest admin
|
|
478
|
+
const adminUser = db
|
|
479
|
+
.query(
|
|
480
|
+
"SELECT id FROM users WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1",
|
|
481
|
+
)
|
|
482
|
+
.get() as { id: string } | null
|
|
483
|
+
|
|
484
|
+
expect(adminUser).not.toBeNull()
|
|
485
|
+
expect(adminUser!.id).toBe('usr_first_admin')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('no admin user returns null (for 400 response)', () => {
|
|
489
|
+
// No admin users in DB
|
|
490
|
+
const adminUser = db
|
|
491
|
+
.query(
|
|
492
|
+
"SELECT id FROM users WHERE role = 'admin' ORDER BY created_at ASC LIMIT 1",
|
|
493
|
+
)
|
|
494
|
+
.get() as { id: string } | null
|
|
495
|
+
|
|
496
|
+
expect(adminUser).toBeNull()
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// ===========================================================================
|
|
501
|
+
// Task 4: API key session creation should not bind __system__
|
|
502
|
+
// ===========================================================================
|
|
503
|
+
|
|
504
|
+
describe('Task 4: API key session creation skips __system__ binding', () => {
|
|
505
|
+
let db: Database
|
|
506
|
+
|
|
507
|
+
beforeEach(() => {
|
|
508
|
+
storeReset()
|
|
509
|
+
db = freshDb()
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
test('session created without binding when userId is __system__', () => {
|
|
513
|
+
const session = storeCreateSession({ source: 'web' })
|
|
514
|
+
// Simulate the fixed logic: skip bind when userId is __system__
|
|
515
|
+
const userId = '__system__'
|
|
516
|
+
if (userId !== '__system__') {
|
|
517
|
+
storeBindSession(session.id, userId)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Verify no ownership was created
|
|
521
|
+
const isOwner = storeIsSessionOwner(session.id, '__system__')
|
|
522
|
+
expect(isOwner).toBe(false)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('session is bound when X-User-Id header provides real user', () => {
|
|
526
|
+
const realUserId = seedUser(db, { role: 'member' })
|
|
527
|
+
const session = storeCreateSession({ source: 'web' })
|
|
528
|
+
|
|
529
|
+
// Simulate: API key auth + X-User-Id header
|
|
530
|
+
const userId = '__system__'
|
|
531
|
+
const headerUserId = realUserId
|
|
532
|
+
if (userId === '__system__' && headerUserId) {
|
|
533
|
+
storeBindSession(session.id, headerUserId)
|
|
534
|
+
} else if (userId !== '__system__') {
|
|
535
|
+
storeBindSession(session.id, userId)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
expect(storeIsSessionOwner(session.id, realUserId)).toBe(true)
|
|
539
|
+
expect(storeIsSessionOwner(session.id, '__system__')).toBe(false)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('session is orphan when API key without X-User-Id', () => {
|
|
543
|
+
const session = storeCreateSession({ source: 'web' })
|
|
544
|
+
|
|
545
|
+
// Simulate: API key auth without X-User-Id
|
|
546
|
+
const userId = '__system__'
|
|
547
|
+
const headerUserId: string | undefined = undefined
|
|
548
|
+
if (userId === '__system__' && headerUserId) {
|
|
549
|
+
storeBindSession(session.id, headerUserId)
|
|
550
|
+
} else if (userId !== '__system__') {
|
|
551
|
+
storeBindSession(session.id, userId)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
expect(storeIsSessionOwner(session.id, '__system__')).toBe(false)
|
|
555
|
+
})
|
|
556
|
+
})
|