@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.
- package/README.md +3 -0
- package/dist/server.d.mts +91 -30
- package/dist/server.d.ts +91 -30
- package/dist/server.js +8 -8
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +8 -8
- package/dist/server.mjs.map +1 -1
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +7 -5
- package/src/server/handlers.ids-stream.spec.ts +4 -2
- package/src/server/handlers.ts +30 -26
- package/src/server/handlers.visibility.spec.ts +70 -0
- package/src/server/service.spec.ts +90 -61
- package/src/server/service.ts +57 -52
- package/src/server/test-helpers/handlers-config.ts +19 -19
- package/src/server/visibility.spec.ts +129 -3
- package/src/server/visibility.ts +106 -11
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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 }
|
|
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 }
|
|
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
|
|
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 }
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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 =
|
|
561
|
+
const restricted = DailyReportViewerScope.restrictTo(["NI", "kintone"])
|
|
559
562
|
/** 全拒否の可視集合 (権限なし・リゾルバ例外時と同じ状態)。 */
|
|
560
|
-
const denyAll =
|
|
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 }
|
|
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
|
|
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 }
|
|
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 }
|
|
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
|
|
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 }
|
|
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 }
|
|
626
|
-
await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }
|
|
627
|
-
await svc.getDailyReportDetailById(1, 8, { forceRefresh: true }
|
|
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 }
|
|
638
|
-
expect(await svc.getDailyReportIdsFirstPageByExternalId("ext", 200
|
|
639
|
-
expect(await svc.getDailyReportsByBusinessDateByExternalId("2099-01-05", "ext", { forceRefresh: true }
|
|
640
|
-
expect(await svc.getDailyReportDetailById(1, 8, { forceRefresh: true }
|
|
641
|
-
expect(await svc.getAttachmentForUser(11, 8
|
|
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", {}
|
|
688
|
+
await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo(["NI"]), "ext", {})
|
|
660
689
|
const afterFirst = svc.__queries.length
|
|
661
|
-
await svc.getDailyReportIdsByExternalId(
|
|
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", {}
|
|
697
|
+
await svc.getDailyReportIdsByExternalId(DailyReportViewerScope.restrictTo(["ni"]), "ext", {})
|
|
669
698
|
const afterFirst = svc.__queries.length
|
|
670
|
-
await svc.getDailyReportIdsByExternalId("ext", {}
|
|
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", {}
|
|
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", {}
|
|
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, {}
|
|
711
|
+
await svc.getDailyReportDetailById(DailyReportViewerScope.restrictTo(["ni"]), 1, 8, {})
|
|
683
712
|
const afterDetailFirst = svc.__queries.length
|
|
684
|
-
await svc.getDailyReportDetailById(1, 8, {}
|
|
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, {}
|
|
743
|
+
await svc.getDailyReportDetailById(restricted, 1, 8, {})
|
|
715
744
|
|
|
716
|
-
await svc.setStarStatus(8, 1, "2099-01-05", true, "tmp"
|
|
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, {}
|
|
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, {}
|
|
779
|
+
await svc.getDailyReportDetailById(restricted, 1, 8, {})
|
|
751
780
|
|
|
752
|
-
await svc.setReadStatus(8, 1, "2099-01-05", true, "tmp"
|
|
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, {}
|
|
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", {}
|
|
793
|
+
await svc.getDailyReportsByBusinessDateByExternalId(restricted, "2099-01-05", "ext", {})
|
|
765
794
|
|
|
766
|
-
await svc.setStarStatus(8, 1, "2099-01-05", true, "tmp"
|
|
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", {}
|
|
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", {}
|
|
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", {}
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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")
|