@aiquants/daily-report 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"
4
4
  import { createEpochStore, SqlResultCache } from "./cache"
5
5
  import { defineDailyReportSchema } from "./schema"
6
6
  import { createDailyReportService, type DailyReportDb, type DailyReportHubRow, formatDateValue } from "./service"
7
- import { buildVisibleSourceTypeCondition, normalizeVisibleSourceTypes } from "./visibility"
7
+ import { buildVisibleSourceTypeCondition, DailyReportViewerScope } from "./visibility"
8
8
 
9
9
  /**
10
10
  * Security regression tests for audit-actor masking in mapHubRecord.
@@ -147,6 +147,9 @@ const baseHub = (over: Partial<DailyReportHubRow>): DailyReportHubRow => ({
147
147
  ...over,
148
148
  })
149
149
 
150
+ /** テストが「絞り込みなし」を意図していることを 1 箇所で表明する束縛。 */
151
+ const UNRESTRICTED_SCOPE = DailyReportViewerScope.unrestricted("unit test: no viewer restriction under test")
152
+
150
153
  const makeService = (rows: unknown[], withAttachmentCodec = false, options: { selectQueue?: unknown[][] } = {}) => {
151
154
  const epochs = createEpochStore(undefined)
152
155
  const cache = new SqlResultCache({ defaultTtlMs: 60_000 }, epochs)
@@ -181,7 +184,7 @@ const makeService = (rows: unknown[], withAttachmentCodec = false, options: { se
181
184
  describe("mapHubRecord audit-actor masking (security)", () => {
182
185
  it("数値の updatedBy / createdBy (= 生内部 ID) は encodeUserId で難読化される", async () => {
183
186
  const svc = makeService([{ hub: baseHub({ createdBy: "8", updatedBy: "8" }), internal: null, creatorName: null }])
184
- const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
187
+ const [r] = await svc.getDailyReportsByBusinessDate(UNRESTRICTED_SCOPE, "2099-01-05", { forceRefresh: true })
185
188
  // 生 ID "8" がそのまま出ないこと (漏洩防止の核心)
186
189
  expect(r.updatedBy).toBe("ENC8")
187
190
  expect(r.updatedBy).not.toBe("8")
@@ -193,7 +196,7 @@ describe("mapHubRecord audit-actor masking (security)", () => {
193
196
 
194
197
  it("表示名 (creatorName) がある場合はマスクせず表示名を優先する", async () => {
195
198
  const svc = makeService([{ hub: baseHub({ createdBy: "8", updatedBy: "8" }), internal: null, creatorName: "山田太郎" }])
196
- const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
199
+ const [r] = await svc.getDailyReportsByBusinessDate(UNRESTRICTED_SCOPE, "2099-01-05", { forceRefresh: true })
197
200
  expect(r.author).toBe("山田太郎")
198
201
  expect(r.employeeName).toBe("山田太郎")
199
202
  // updatedBy は表示名解決を持たないため常にマスク
@@ -202,14 +205,14 @@ describe("mapHubRecord audit-actor masking (security)", () => {
202
205
 
203
206
  it("非数値の監査値 (レガシー氏名/システム文字列) はそのまま通す", async () => {
204
207
  const svc = makeService([{ hub: baseHub({ createdBy: "system-import", updatedBy: "batch" }), internal: null, creatorName: null }])
205
- const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
208
+ const [r] = await svc.getDailyReportsByBusinessDate(UNRESTRICTED_SCOPE, "2099-01-05", { forceRefresh: true })
206
209
  expect(r.author).toBe("system-import")
207
210
  expect(r.updatedBy).toBe("batch")
208
211
  })
209
212
 
210
213
  it("空 / null の監査値は null になる (生値の漏れなし)", async () => {
211
214
  const svc = makeService([{ hub: baseHub({ createdBy: "", updatedBy: "" }), internal: null, creatorName: null }])
212
- const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
215
+ const [r] = await svc.getDailyReportsByBusinessDate(UNRESTRICTED_SCOPE, "2099-01-05", { forceRefresh: true })
213
216
  expect(r.updatedBy).toBeNull()
214
217
  expect(r.author).toBe("") // author は非 null 契約のため空文字
215
218
  })
@@ -289,14 +292,14 @@ describe("添付ファイルの認可付き取得", () => {
289
292
  epochs,
290
293
  draftLabelName: "draft",
291
294
  })
292
- expect(await svc.getAttachmentForUser(1, 8, null)).toBeNull()
295
+ expect(await svc.getAttachmentForUser(UNRESTRICTED_SCOPE, 1, 8)).toBeNull()
293
296
  })
294
297
 
295
298
  it("認可述語が閲覧者の所有と下書き非該当を OR で結び、論理削除も除外する", async () => {
296
299
  // これがこの機能の唯一の認可境界。述語そのものを SQL 文字列として固定する
297
300
  // (モックは行を絞れないので、生成される WHERE 句を直接表明するしかない)
298
301
  const svc = makeService([{ fileName: "a.png", filePath: "gcs://b/a.png", fileType: "image/png", fileSize: 1 }])
299
- await svc.getAttachmentForUser(11, 8, null)
302
+ await svc.getAttachmentForUser(UNRESTRICTED_SCOPE, 11, 8)
300
303
 
301
304
  // 最後のクエリが添付の認可判定 (直前は下書きラベルの解決)
302
305
  const recorded = svc.__queries.at(-1)
@@ -313,7 +316,7 @@ describe("添付ファイルの認可付き取得", () => {
313
316
 
314
317
  it("認可判定の束縛値が入れ替わっていないこと (添付 ID と閲覧者 ID)", async () => {
315
318
  const svc = makeService([])
316
- await svc.getAttachmentForUser(777, 42, null)
319
+ await svc.getAttachmentForUser(UNRESTRICTED_SCOPE, 777, 42)
317
320
  expect(svc.__queries.at(-1)?.whereParams).toEqual([777, 42])
318
321
  })
319
322
 
@@ -323,15 +326,15 @@ describe("添付ファイルの認可付き取得", () => {
323
326
  const draftPredicate = (sql: string): boolean => /\[user_id\] = @par\d+\) or \(\(\[draft_label\]\.\[hub_id\] is null\)/.test(sql)
324
327
 
325
328
  const byDate = makeService([])
326
- await byDate.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }, null)
329
+ await byDate.getDailyReportsByBusinessDateByExternalId(UNRESTRICTED_SCOPE, "2099-01-05", "ext", { forceRefresh: true })
327
330
  expect(byDate.__queries.some((q) => q.whereSql !== null && draftPredicate(q.whereSql))).toBe(true)
328
331
 
329
332
  const byId = makeService([])
330
- await byId.getDailyReportDetailById(1, 8, { forceRefresh: true }, null)
333
+ await byId.getDailyReportDetailById(UNRESTRICTED_SCOPE, 1, 8, { forceRefresh: true })
331
334
  expect(byId.__queries.some((q) => q.whereSql !== null && draftPredicate(q.whereSql))).toBe(true)
332
335
 
333
336
  const byAttachment = makeService([])
334
- await byAttachment.getAttachmentForUser(1, 8, null)
337
+ await byAttachment.getAttachmentForUser(UNRESTRICTED_SCOPE, 1, 8)
335
338
  expect(byAttachment.__queries.some((q) => q.whereSql !== null && draftPredicate(q.whereSql))).toBe(true)
336
339
  })
337
340
 
@@ -340,7 +343,7 @@ describe("添付ファイルの認可付き取得", () => {
340
343
  // 意図的な緩和であり、返すのは ID・営業日・ソース種別だけで本文も添付も含まない。
341
344
  // 「3 経路と同一」ではないことを明示的に固定し、無自覚な差分と区別する
342
345
  const byIds = makeService([])
343
- await byIds.getDailyReportIdsByExternalId("ext", { forceRefresh: true }, null)
346
+ await byIds.getDailyReportIdsByExternalId(UNRESTRICTED_SCOPE, "ext", { forceRefresh: true })
344
347
  const idsWhere = byIds.__queries.map((q) => q.whereSql ?? "").find((sql) => sql.includes("[draft_label].[hub_id] is null"))
345
348
  expect(idsWhere).toBeDefined()
346
349
  expect(idsWhere).toContain("[user_id] is null")
@@ -350,7 +353,7 @@ describe("添付ファイルの認可付き取得", () => {
350
353
  // `draftLabelNames` には取込元ごとの下書き名を複数注入できる。1 個の ID でしか結合しないと
351
354
  // 2 つ目の下書きラベルが付いた日報が誰にでも可視になる
352
355
  const svc = makeService([{ id: 11 }, { id: 22 }])
353
- await svc.getAttachmentForUser(1, 8, null)
356
+ await svc.getAttachmentForUser(UNRESTRICTED_SCOPE, 1, 8)
354
357
  // 結合条件は where ではなく leftJoin の ON 側に載る
355
358
  const joinParams = svc.__queries.flatMap((q) => q.joinParams)
356
359
  expect(joinParams).toContain(11)
@@ -404,19 +407,19 @@ describe("コメント操作の整合性チェック", () => {
404
407
  it("addComment は存在しない日報に対して Not Found を投げる (孤児コメントを作らない)", async () => {
405
408
  // hub 取得が空 = 存在しない日報。以前は hub 未取得でもフォールスルーして INSERT していた
406
409
  const svc = makeService([])
407
- await expect(svc.addComment(8, 999, "本文", "2099-01-05", "tmp", null)).rejects.toThrow("Not Found")
410
+ await expect(svc.addComment(UNRESTRICTED_SCOPE, 8, 999, "本文", "2099-01-05", "tmp")).rejects.toThrow("Not Found")
408
411
  })
409
412
 
410
413
  it("addComment は論理削除済みの日報を拒否する", async () => {
411
414
  const svc = makeService([{ sourceType: "Internal", deletedAt: "2099-01-06 00:00:00", userId: 8 }])
412
- await expect(svc.addComment(8, 1, "本文", "2099-01-05", "tmp", null)).rejects.toThrow("Not Found")
415
+ await expect(svc.addComment(UNRESTRICTED_SCOPE, 8, 1, "本文", "2099-01-05", "tmp")).rejects.toThrow("Not Found")
413
416
  })
414
417
 
415
418
  it("deleteComment は reportHubId がコメントの実所属と一致しないと Not Found を投げる", async () => {
416
419
  // コメントは hubId=100 に属するのに、呼び出しは reportHubId=999 を渡している。
417
420
  // 不一致のまま進むと無関係な日報のキャッシュ無効化と SSE 配信を起こすため拒否する
418
421
  const svc = makeService([{ id: 5, hubId: 100, userId: 8, body: "x", createdAt: "2099-01-05 00:00:00", createdBy: "8", updatedAt: "2099-01-05 00:00:00", updatedBy: "8" }])
419
- await expect(svc.deleteComment(8, 999, 5, "2099-01-05", "tmp", null)).rejects.toThrow("Not Found")
422
+ await expect(svc.deleteComment(UNRESTRICTED_SCOPE, 8, 999, 5, "2099-01-05", "tmp")).rejects.toThrow("Not Found")
420
423
  })
421
424
 
422
425
  it("addComment: 公開済み日報のコメントは全員配信 (recipientRawUserId 無し)", async () => {
@@ -428,7 +431,7 @@ describe("コメント操作の整合性チェック", () => {
428
431
  [{ displayName: "山田" }], // コメント投稿者名
429
432
  ],
430
433
  })
431
- await svc.addComment(8, 1, "本文", "2099-01-05", "tmp", null)
434
+ await svc.addComment(UNRESTRICTED_SCOPE, 8, 1, "本文", "2099-01-05", "tmp")
432
435
  const sse = (svc as unknown as { __sse: Record<string, unknown>[] }).__sse
433
436
  const add = sse.find((m) => m.type === "comment-add")
434
437
  expect(add).toBeDefined()
@@ -444,7 +447,7 @@ describe("コメント操作の整合性チェック", () => {
444
447
  [{ displayName: "山田" }], // 投稿者名
445
448
  ],
446
449
  })
447
- await svc.addComment(8, 1, "秘密の下書きコメント", "2099-01-05", "tmp", null)
450
+ await svc.addComment(UNRESTRICTED_SCOPE, 8, 1, "秘密の下書きコメント", "2099-01-05", "tmp")
448
451
  const sse = (svc as unknown as { __sse: Record<string, unknown>[] }).__sse
449
452
  const add = sse.find((m) => m.type === "comment-add")
450
453
  expect(add?.recipientRawUserId).toBe(8)
@@ -555,9 +558,9 @@ describe("formatDateValue (Intl-free rewrite equivalence)", () => {
555
558
 
556
559
  describe("区分 (sourceType) 可視性フィルタ", () => {
557
560
  /** テスト用の可視集合 (NI と kintone のみ許可)。 */
558
- const restricted = normalizeVisibleSourceTypes(["NI", "kintone"])
561
+ const restricted = DailyReportViewerScope.restrictTo(["NI", "kintone"])
559
562
  /** 全拒否の可視集合 (権限なし・リゾルバ例外時と同じ状態)。 */
560
- const denyAll = normalizeVisibleSourceTypes([])
563
+ const denyAll = DailyReportViewerScope.restrictTo([])
561
564
 
562
565
  /** 区分述語を含む最後のクエリを取り出す。 */
563
566
  const lastQueryWithSourceType = (svc: { __queries: { whereSql: string | null; whereParams: unknown[] }[] }) => svc.__queries.filter((q) => (q.whereSql ?? "").includes("[source_type]")).at(-1)
@@ -565,7 +568,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
565
568
  describe("読み取り経路への述語注入", () => {
566
569
  it("ID 一覧 (全量) の WHERE に畳み込み済みトークンの IN 述語が載る", async () => {
567
570
  const svc = makeService([])
568
- await svc.getDailyReportIdsByExternalId("ext", { forceRefresh: true }, restricted)
571
+ await svc.getDailyReportIdsByExternalId(restricted, "ext", { forceRefresh: true })
569
572
 
570
573
  const recorded = lastQueryWithSourceType(svc)
571
574
  // レンダリングは大文字 LOWER( なので大小文字を厳密に一致させる
@@ -578,7 +581,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
578
581
 
579
582
  it("ID 一覧 (先頭ページ) にも同一の述語が載る", async () => {
580
583
  const svc = makeService([])
581
- await svc.getDailyReportIdsFirstPageByExternalId("ext", 200, restricted)
584
+ await svc.getDailyReportIdsFirstPageByExternalId(restricted, "ext", 200)
582
585
 
583
586
  const recorded = lastQueryWithSourceType(svc)
584
587
  expect(recorded?.whereSql).toContain("LOWER(")
@@ -587,7 +590,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
587
590
 
588
591
  it("営業日別一覧の WHERE に述語が載る", async () => {
589
592
  const svc = makeService([])
590
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }, restricted)
593
+ await svc.getDailyReportsByBusinessDateByExternalId(restricted, "2099-01-05", "ext", { forceRefresh: true })
591
594
 
592
595
  const recorded = lastQueryWithSourceType(svc)
593
596
  expect(recorded?.whereSql).toContain("LOWER(")
@@ -596,16 +599,42 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
596
599
 
597
600
  it("詳細取得の WHERE に述語が載る", async () => {
598
601
  const svc = makeService([])
599
- await svc.getDailyReportDetailById(1, 8, { forceRefresh: true }, restricted)
602
+ await svc.getDailyReportDetailById(restricted, 1, 8, { forceRefresh: true })
600
603
 
601
604
  const recorded = lastQueryWithSourceType(svc)
602
605
  expect(recorded?.whereSql).toContain("LOWER(")
603
606
  expect(recorded?.whereParams).toEqual(expect.arrayContaining(["kintone", "ni"]))
604
607
  })
605
608
 
609
+ it("管理用の営業日別一覧も区分でゲートされる (docstring の警告だけに頼らない)", async () => {
610
+ // v0.14.0 でゲート対象へ昇格した経路。ここが無いと、述語・短絡・ダイジェストを
611
+ // すべて剥がしても既存テスト (無制限スコープのみ) は緑のまま通ってしまう
612
+ const svc = makeService([])
613
+ await svc.getDailyReportsByBusinessDate(restricted, "2099-01-05", { forceRefresh: true })
614
+
615
+ const recorded = lastQueryWithSourceType(svc)
616
+ expect(recorded?.whereSql).toContain("LOWER(")
617
+ expect(recorded?.whereParams).toEqual(expect.arrayContaining(["kintone", "ni"]))
618
+ })
619
+
620
+ it("管理用の営業日別一覧も全拒否では DB を引かない", async () => {
621
+ const svc = makeService([])
622
+ expect(await svc.getDailyReportsByBusinessDate(denyAll, "2099-01-05", { forceRefresh: true })).toEqual([])
623
+ expect(svc.__queries).toHaveLength(0)
624
+ })
625
+
626
+ it("管理用の営業日別一覧も可視集合ごとにキャッシュが隔離される", async () => {
627
+ const svc = makeService([])
628
+ await svc.getDailyReportsByBusinessDate(DailyReportViewerScope.restrictTo(["ni"]), "2099-01-05", {})
629
+ const afterFirst = svc.__queries.length
630
+ await svc.getDailyReportsByBusinessDate(DailyReportViewerScope.restrictTo(["kintone"]), "2099-01-05", {})
631
+
632
+ expect(svc.__queries.length).toBeGreaterThan(afterFirst)
633
+ })
634
+
606
635
  it("添付の認可述語にも載る (一覧だけ絞って添付から抜ける穴を塞ぐ)", async () => {
607
636
  const svc = makeService([{ fileName: "a.png", filePath: "gcs://b/a.png", fileType: null, fileSize: 1 }])
608
- await svc.getAttachmentForUser(11, 8, restricted)
637
+ await svc.getAttachmentForUser(restricted, 11, 8)
609
638
 
610
639
  const recorded = lastQueryWithSourceType(svc)
611
640
  expect(recorded?.whereSql).toContain("LOWER(")
@@ -614,7 +643,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
614
643
 
615
644
  it("外部 ID 経由の詳細取得は可視集合をそのまま透過する (ここで null へ潰さない)", async () => {
616
645
  const svc = makeService([])
617
- await svc.getDailyReportDetailByIdByExternalId(1, "ext", { forceRefresh: true }, restricted)
646
+ await svc.getDailyReportDetailByIdByExternalId(restricted, 1, "ext", { forceRefresh: true })
618
647
 
619
648
  const recorded = lastQueryWithSourceType(svc)
620
649
  expect(recorded?.whereParams).toEqual(expect.arrayContaining(["kintone", "ni"]))
@@ -622,9 +651,9 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
622
651
 
623
652
  it("無制限 (null) では述語が一切現れない (現行互換の回帰ガード)", async () => {
624
653
  const svc = makeService([])
625
- await svc.getDailyReportIdsByExternalId("ext", { forceRefresh: true }, null)
626
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }, null)
627
- await svc.getDailyReportDetailById(1, 8, { forceRefresh: true }, null)
654
+ await svc.getDailyReportIdsByExternalId(UNRESTRICTED_SCOPE, "ext", { forceRefresh: true })
655
+ await svc.getDailyReportsByBusinessDateByExternalId(UNRESTRICTED_SCOPE, "2099-01-05", "ext", { forceRefresh: true })
656
+ await svc.getDailyReportDetailById(UNRESTRICTED_SCOPE, 1, 8, { forceRefresh: true })
628
657
 
629
658
  expect(svc.__queries.every((q) => !(q.whereSql ?? "").includes("[source_type]"))).toBe(true)
630
659
  })
@@ -634,11 +663,11 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
634
663
  it("読み取り 5 経路すべてが問い合わせ無しで空を返す", async () => {
635
664
  const svc = makeService([{ fileName: "a.png", filePath: "gcs://b/a.png", fileType: null, fileSize: 1 }], true)
636
665
 
637
- expect(await svc.getDailyReportIdsByExternalId("ext", { forceRefresh: true }, denyAll)).toEqual([])
638
- expect(await svc.getDailyReportIdsFirstPageByExternalId("ext", 200, denyAll)).toEqual([])
639
- expect(await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }, denyAll)).toEqual([])
640
- expect(await svc.getDailyReportDetailById(1, 8, { forceRefresh: true }, denyAll)).toBeNull()
641
- expect(await svc.getAttachmentForUser(11, 8, denyAll)).toBeNull()
666
+ expect(await svc.getDailyReportIdsByExternalId(denyAll, "ext", { forceRefresh: true })).toEqual([])
667
+ expect(await svc.getDailyReportIdsFirstPageByExternalId(denyAll, "ext", 200)).toEqual([])
668
+ expect(await svc.getDailyReportsByBusinessDateByExternalId(denyAll, "2099-01-05", "ext", { forceRefresh: true })).toEqual([])
669
+ expect(await svc.getDailyReportDetailById(denyAll, 1, 8, { forceRefresh: true })).toBeNull()
670
+ expect(await svc.getAttachmentForUser(denyAll, 11, 8)).toBeNull()
642
671
 
643
672
  // 短絡しているので DB は一度も引かれない
644
673
  expect(svc.__queries).toHaveLength(0)
@@ -647,7 +676,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
647
676
  it("条件生成器自体も fail-close する (呼び出し側の短絡が漏れても認可は反転しない)", () => {
648
677
  // 空集合を「制限なし」と解釈すると添付が全区分ダウンロード可能になる。
649
678
  // 短絡を迂回しても 1 = 0 が残ることを、生成 SQL で直接固定する
650
- const condition = buildVisibleSourceTypeCondition(tables.hub.sourceType, denyAll)
679
+ const condition = buildVisibleSourceTypeCondition(tables.hub.sourceType, denyAll.visible)
651
680
  expect(condition).toBeDefined()
652
681
  expect(dialect.sqlToQuery(condition as SQL).sql).toContain("1 = 0")
653
682
  })
@@ -656,32 +685,32 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
656
685
  describe("キャッシュの可視集合ダイジェスト隔離", () => {
657
686
  it("正規化後に等価な可視集合はキャッシュを再利用する (性能無退行)", async () => {
658
687
  const svc = makeService([])
659
- await svc.getDailyReportIdsByExternalId("ext", {}, normalizeVisibleSourceTypes(["NI"]))
688
+ await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo(["NI"]), "ext", {})
660
689
  const afterFirst = svc.__queries.length
661
- await svc.getDailyReportIdsByExternalId("ext", {}, normalizeVisibleSourceTypes([" ni "]))
690
+ await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo([" ni "]), "ext", {})
662
691
 
663
692
  expect(svc.__queries.length).toBe(afterFirst)
664
693
  })
665
694
 
666
695
  it("異なる可視集合は別バケットになる (別権限へ他人の結果を配らない)", async () => {
667
696
  const svc = makeService([])
668
- await svc.getDailyReportIdsByExternalId("ext", {}, normalizeVisibleSourceTypes(["ni"]))
697
+ await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo(["ni"]), "ext", {})
669
698
  const afterFirst = svc.__queries.length
670
- await svc.getDailyReportIdsByExternalId("ext", {}, normalizeVisibleSourceTypes(["kintone"]))
699
+ await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo(["kintone"]), "ext", {})
671
700
 
672
701
  expect(svc.__queries.length).toBeGreaterThan(afterFirst)
673
702
  })
674
703
 
675
704
  it("営業日別と詳細でも可視集合ごとに隔離される", async () => {
676
705
  const svc = makeService([])
677
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", {}, normalizeVisibleSourceTypes(["ni"]))
706
+ await svc.getDailyReportsByBusinessDateByExternalId(DailyReportViewerScope.restrictTo(["ni"]), "2099-01-05", "ext", {})
678
707
  const afterDateFirst = svc.__queries.length
679
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", {}, normalizeVisibleSourceTypes(["kintone"]))
708
+ await svc.getDailyReportsByBusinessDateByExternalId(DailyReportViewerScope.restrictTo(["kintone"]), "2099-01-05", "ext", {})
680
709
  expect(svc.__queries.length).toBeGreaterThan(afterDateFirst)
681
710
 
682
- await svc.getDailyReportDetailById(1, 8, {}, normalizeVisibleSourceTypes(["ni"]))
711
+ await svc.getDailyReportDetailById(DailyReportViewerScope.restrictTo(["ni"]), 1, 8, {})
683
712
  const afterDetailFirst = svc.__queries.length
684
- await svc.getDailyReportDetailById(1, 8, {}, normalizeVisibleSourceTypes(["kintone"]))
713
+ await svc.getDailyReportDetailById(DailyReportViewerScope.restrictTo(["kintone"]), 1, 8, {})
685
714
  expect(svc.__queries.length).toBeGreaterThan(afterDetailFirst)
686
715
  })
687
716
  })
@@ -711,13 +740,13 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
711
740
  isRead: false,
712
741
  },
713
742
  ])
714
- await svc.getDailyReportDetailById(1, 8, {}, restricted)
743
+ await svc.getDailyReportDetailById(restricted, 1, 8, {})
715
744
 
716
- await svc.setStarStatus(8, 1, "2099-01-05", true, "tmp", null)
745
+ await svc.setStarStatus(UNRESTRICTED_SCOPE, 8, 1, "2099-01-05", true, "tmp")
717
746
  // ❗ 基準はミューテーション「後」に取る。前に取ると setStarStatus 自身が発行する
718
747
  // 2 本の select だけで閾値を満たしてしまい、無効化が壊れていても通ってしまう
719
748
  const afterMutation = svc.__queries.length
720
- await svc.getDailyReportDetailById(1, 8, {}, restricted)
749
+ await svc.getDailyReportDetailById(restricted, 1, 8, {})
721
750
 
722
751
  // 接頭辞無効化が効いていなければキャッシュヒットで再クエリが 1 本も出ない
723
752
  expect(svc.__queries.length).toBeGreaterThan(afterMutation)
@@ -747,12 +776,12 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
747
776
  isRead: false,
748
777
  },
749
778
  ])
750
- await svc.getDailyReportDetailById(1, 8, {}, restricted)
779
+ await svc.getDailyReportDetailById(restricted, 1, 8, {})
751
780
 
752
- await svc.setReadStatus(8, 1, "2099-01-05", true, "tmp", null)
781
+ await svc.setReadStatus(UNRESTRICTED_SCOPE, 8, 1, "2099-01-05", true, "tmp")
753
782
  // 基準はミューテーション後 (理由は setStarStatus のテストと同じ)
754
783
  const afterMutation = svc.__queries.length
755
- await svc.getDailyReportDetailById(1, 8, {}, restricted)
784
+ await svc.getDailyReportDetailById(restricted, 1, 8, {})
756
785
 
757
786
  expect(svc.__queries.length).toBeGreaterThan(afterMutation)
758
787
  })
@@ -761,11 +790,11 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
761
790
  // detail 側だけ接頭辞化しても date 側が完全一致のままなら、営業日別一覧が
762
791
  // 古いスター状態を配り続ける。両者を別々の表明で固定する
763
792
  const svc = makeService([{ hub: baseHub({ id: 1 }), internal: null, isStarred: false, isRead: false, creatorName: "山田" }])
764
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", {}, restricted)
793
+ await svc.getDailyReportsByBusinessDateByExternalId(restricted, "2099-01-05", "ext", {})
765
794
 
766
- await svc.setStarStatus(8, 1, "2099-01-05", true, "tmp", null)
795
+ await svc.setStarStatus(UNRESTRICTED_SCOPE, 8, 1, "2099-01-05", true, "tmp")
767
796
  const afterMutation = svc.__queries.length
768
- await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", {}, restricted)
797
+ await svc.getDailyReportsByBusinessDateByExternalId(restricted, "2099-01-05", "ext", {})
769
798
 
770
799
  expect(svc.__queries.length).toBeGreaterThan(afterMutation)
771
800
  })
@@ -777,11 +806,11 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
777
806
  // 行を返さないモックでも create は完走する (詳細が null なら SSE を出さないだけ)。
778
807
  // 検証対象は「無効化が走ったか」なので、これで十分かつ流れの変化に強い
779
808
  const svc = makeService([])
780
- await svc.getDailyReportIdsByExternalId("ext", {}, restricted)
809
+ await svc.getDailyReportIdsByExternalId(restricted, "ext", {})
781
810
 
782
811
  await svc.createDailyReport(8, "2099-01-05", "tmp")
783
812
  const afterCreate = svc.__queries.length
784
- await svc.getDailyReportIdsByExternalId("ext", {}, restricted)
813
+ await svc.getDailyReportIdsByExternalId(restricted, "ext", {})
785
814
 
786
815
  // 無効化が効いていなければキャッシュヒットで再クエリは 0 本になる
787
816
  expect(svc.__queries.length).toBeGreaterThan(afterCreate)
@@ -791,27 +820,27 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
791
820
  describe("ミューテーションの区分ゲート", () => {
792
821
  it("addComment は不可視区分の日報を Not Found で拒否する", async () => {
793
822
  const svc = makeService([{ sourceType: "Internal", deletedAt: null, userId: 8 }])
794
- await expect(svc.addComment(8, 1, "本文", "2099-01-05", "tmp", restricted)).rejects.toThrow("Not Found")
823
+ await expect(svc.addComment(restricted, 8, 1, "本文", "2099-01-05", "tmp")).rejects.toThrow("Not Found")
795
824
  })
796
825
 
797
826
  it("setStarStatus は不可視区分を Not Found で拒否する", async () => {
798
827
  const svc = makeService([{ sourceType: "Internal", deletedAt: null }])
799
- await expect(svc.setStarStatus(8, 1, "2099-01-05", true, "tmp", restricted)).rejects.toThrow("Not Found")
828
+ await expect(svc.setStarStatus(restricted, 8, 1, "2099-01-05", true, "tmp")).rejects.toThrow("Not Found")
800
829
  })
801
830
 
802
831
  it("setReadStatus は不可視区分を Not Found で拒否する", async () => {
803
832
  const svc = makeService([{ sourceType: "Internal", deletedAt: null }])
804
- await expect(svc.setReadStatus(8, 1, "2099-01-05", true, "tmp", restricted)).rejects.toThrow("Not Found")
833
+ await expect(svc.setReadStatus(restricted, 8, 1, "2099-01-05", true, "tmp")).rejects.toThrow("Not Found")
805
834
  })
806
835
 
807
836
  it("setStarStatus は存在しない hubId を Not Found で拒否する (存在オラクルの是正)", async () => {
808
837
  const svc = makeService([])
809
- await expect(svc.setStarStatus(8, 999, "2099-01-05", true, "tmp", null)).rejects.toThrow("Not Found")
838
+ await expect(svc.setStarStatus(UNRESTRICTED_SCOPE, 8, 999, "2099-01-05", true, "tmp")).rejects.toThrow("Not Found")
810
839
  })
811
840
 
812
841
  it("setReadStatus は論理削除済みの日報を Not Found で拒否する", async () => {
813
842
  const svc = makeService([{ sourceType: "Internal", deletedAt: "2099-01-06 00:00:00" }])
814
- await expect(svc.setReadStatus(8, 1, "2099-01-05", true, "tmp", null)).rejects.toThrow("Not Found")
843
+ await expect(svc.setReadStatus(UNRESTRICTED_SCOPE, 8, 1, "2099-01-05", true, "tmp")).rejects.toThrow("Not Found")
815
844
  })
816
845
 
817
846
  it("deleteComment は所有者チェックより先に区分ゲートを適用する (403/404 の存在オラクル回避)", async () => {
@@ -820,7 +849,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
820
849
  const svc = makeService([], false, {
821
850
  selectQueue: [[{ id: 5, hubId: 1, userId: 99, body: "x", createdAt: "2099-01-05 00:00:00", createdBy: "99", updatedAt: "2099-01-05 00:00:00", updatedBy: "99" }], [{ userId: 99, sourceType: "Internal" }]],
822
851
  })
823
- await expect(svc.deleteComment(8, 1, 5, "2099-01-05", "tmp", restricted)).rejects.toThrow("Not Found")
852
+ await expect(svc.deleteComment(restricted, 8, 1, 5, "2099-01-05", "tmp")).rejects.toThrow("Not Found")
824
853
  })
825
854
  })
826
855
 
@@ -829,7 +858,7 @@ describe("区分 (sourceType) 可視性フィルタ", () => {
829
858
  const svc = makeService([], false, {
830
859
  selectQueue: [[{ sourceType: "Internal", deletedAt: null, userId: 8 }], [{ id: 10 }], [], [{ displayName: "山田" }]],
831
860
  })
832
- await svc.addComment(8, 1, "本文", "2099-01-05", "tmp", null)
861
+ await svc.addComment(UNRESTRICTED_SCOPE, 8, 1, "本文", "2099-01-05", "tmp")
833
862
 
834
863
  const add = svc.__sse.find((m) => m.type === "comment-add")
835
864
  expect(add?.sourceType).toBe("Internal")