@studious-lms/server 1.26.0 → 1.28.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.
Files changed (222) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +31 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/instrument.js +9 -4
  8. package/dist/instrument.js.map +1 -1
  9. package/dist/lib/fileUpload.d.ts +15 -0
  10. package/dist/lib/fileUpload.d.ts.map +1 -1
  11. package/dist/lib/fileUpload.js +43 -2
  12. package/dist/lib/fileUpload.js.map +1 -1
  13. package/dist/models/cloudStorage.d.ts +11 -0
  14. package/dist/models/cloudStorage.d.ts.map +1 -1
  15. package/dist/models/cloudStorage.js +18 -2
  16. package/dist/models/cloudStorage.js.map +1 -1
  17. package/dist/models/conversation.d.ts +1 -0
  18. package/dist/models/conversation.d.ts.map +1 -1
  19. package/dist/models/labChat.d.ts +40 -0
  20. package/dist/models/labChat.d.ts.map +1 -1
  21. package/dist/models/labChat.js +32 -2
  22. package/dist/models/labChat.js.map +1 -1
  23. package/dist/models/message.d.ts +3 -0
  24. package/dist/models/message.d.ts.map +1 -1
  25. package/dist/pipelines/aiLabChat.d.ts +2 -0
  26. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  27. package/dist/pipelines/aiLabChat.js +60 -16
  28. package/dist/pipelines/aiLabChat.js.map +1 -1
  29. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  30. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  31. package/dist/pipelines/aiLabChatContract.js +30 -2
  32. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  33. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  34. package/dist/pipelines/aiNewtonChat.js +30 -5
  35. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  36. package/dist/pipelines/generateVisualization.d.ts +33 -0
  37. package/dist/pipelines/generateVisualization.d.ts.map +1 -0
  38. package/dist/pipelines/generateVisualization.js +79 -0
  39. package/dist/pipelines/generateVisualization.js.map +1 -0
  40. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  41. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  42. package/dist/pipelines/labChatPrompt.js +13 -14
  43. package/dist/pipelines/labChatPrompt.js.map +1 -1
  44. package/dist/routers/_app.d.ts +1118 -70
  45. package/dist/routers/_app.d.ts.map +1 -1
  46. package/dist/routers/_app.js +4 -2
  47. package/dist/routers/_app.js.map +1 -1
  48. package/dist/routers/admin.d.ts +297 -1
  49. package/dist/routers/admin.d.ts.map +1 -1
  50. package/dist/routers/admin.js +43 -2
  51. package/dist/routers/admin.js.map +1 -1
  52. package/dist/routers/class.d.ts.map +1 -1
  53. package/dist/routers/class.js +16 -3
  54. package/dist/routers/class.js.map +1 -1
  55. package/dist/routers/conversation.d.ts +1 -0
  56. package/dist/routers/conversation.d.ts.map +1 -1
  57. package/dist/routers/labChat.d.ts +10 -0
  58. package/dist/routers/labChat.d.ts.map +1 -1
  59. package/dist/routers/labChat.js +6 -3
  60. package/dist/routers/labChat.js.map +1 -1
  61. package/dist/routers/message.d.ts +37 -4
  62. package/dist/routers/message.d.ts.map +1 -1
  63. package/dist/routers/message.js +24 -7
  64. package/dist/routers/message.js.map +1 -1
  65. package/dist/routers/module.d.ts +1 -1
  66. package/dist/routers/parent.d.ts +18 -0
  67. package/dist/routers/parent.d.ts.map +1 -1
  68. package/dist/routers/parent.js +10 -3
  69. package/dist/routers/parent.js.map +1 -1
  70. package/dist/routers/studentProgress.d.ts +44 -0
  71. package/dist/routers/studentProgress.d.ts.map +1 -1
  72. package/dist/routers/studentProgress.js +12 -3
  73. package/dist/routers/studentProgress.js.map +1 -1
  74. package/dist/routers/visualization.d.ts +124 -0
  75. package/dist/routers/visualization.d.ts.map +1 -0
  76. package/dist/routers/visualization.js +41 -0
  77. package/dist/routers/visualization.js.map +1 -0
  78. package/dist/routes/aiStream.js +32 -2
  79. package/dist/routes/aiStream.js.map +1 -1
  80. package/dist/seedDatabase.d.ts.map +1 -1
  81. package/dist/seedDatabase.js +107 -3
  82. package/dist/seedDatabase.js.map +1 -1
  83. package/dist/services/adminClass.d.ts +153 -0
  84. package/dist/services/adminClass.d.ts.map +1 -0
  85. package/dist/services/adminClass.js +196 -0
  86. package/dist/services/adminClass.js.map +1 -0
  87. package/dist/services/adminInstitution.d.ts +2 -0
  88. package/dist/services/adminInstitution.d.ts.map +1 -1
  89. package/dist/services/adminInstitution.js +11 -4
  90. package/dist/services/adminInstitution.js.map +1 -1
  91. package/dist/services/adminMetrics.d.ts +8 -0
  92. package/dist/services/adminMetrics.d.ts.map +1 -1
  93. package/dist/services/adminMetrics.js +30 -3
  94. package/dist/services/adminMetrics.js.map +1 -1
  95. package/dist/services/adminSpam.d.ts +76 -0
  96. package/dist/services/adminSpam.d.ts.map +1 -0
  97. package/dist/services/adminSpam.js +163 -0
  98. package/dist/services/adminSpam.js.map +1 -0
  99. package/dist/services/adminSql.d.ts +36 -0
  100. package/dist/services/adminSql.d.ts.map +1 -0
  101. package/dist/services/adminSql.js +172 -0
  102. package/dist/services/adminSql.js.map +1 -0
  103. package/dist/services/adminTelemetry.d.ts +56 -0
  104. package/dist/services/adminTelemetry.d.ts.map +1 -0
  105. package/dist/services/adminTelemetry.js +147 -0
  106. package/dist/services/adminTelemetry.js.map +1 -0
  107. package/dist/services/adminUser.d.ts +45 -1
  108. package/dist/services/adminUser.d.ts.map +1 -1
  109. package/dist/services/adminUser.js +53 -13
  110. package/dist/services/adminUser.js.map +1 -1
  111. package/dist/services/aiChatStream.d.ts +15 -0
  112. package/dist/services/aiChatStream.d.ts.map +1 -1
  113. package/dist/services/aiChatStream.js +144 -74
  114. package/dist/services/aiChatStream.js.map +1 -1
  115. package/dist/services/aiGeneration.d.ts +4 -0
  116. package/dist/services/aiGeneration.d.ts.map +1 -0
  117. package/dist/services/aiGeneration.js +88 -0
  118. package/dist/services/aiGeneration.js.map +1 -0
  119. package/dist/services/aiProposalTools.d.ts +4 -1
  120. package/dist/services/aiProposalTools.d.ts.map +1 -1
  121. package/dist/services/aiProposalTools.js +128 -3
  122. package/dist/services/aiProposalTools.js.map +1 -1
  123. package/dist/services/aiProposals.d.ts +70 -0
  124. package/dist/services/aiProposals.d.ts.map +1 -0
  125. package/dist/services/aiProposals.js +363 -0
  126. package/dist/services/aiProposals.js.map +1 -0
  127. package/dist/services/auth.d.ts.map +1 -1
  128. package/dist/services/auth.js +10 -4
  129. package/dist/services/auth.js.map +1 -1
  130. package/dist/services/conversation.d.ts +1 -0
  131. package/dist/services/conversation.d.ts.map +1 -1
  132. package/dist/services/fees.d.ts.map +1 -1
  133. package/dist/services/fees.js +7 -3
  134. package/dist/services/fees.js.map +1 -1
  135. package/dist/services/labChat.d.ts +10 -0
  136. package/dist/services/labChat.d.ts.map +1 -1
  137. package/dist/services/labChat.js +52 -3
  138. package/dist/services/labChat.js.map +1 -1
  139. package/dist/services/message.d.ts +24 -8
  140. package/dist/services/message.d.ts.map +1 -1
  141. package/dist/services/message.js +170 -117
  142. package/dist/services/message.js.map +1 -1
  143. package/dist/services/module.d.ts.map +1 -1
  144. package/dist/services/module.js +23 -4
  145. package/dist/services/module.js.map +1 -1
  146. package/dist/services/parentPortal.d.ts +14 -0
  147. package/dist/services/parentPortal.d.ts.map +1 -1
  148. package/dist/services/parentPortal.js +72 -2
  149. package/dist/services/parentPortal.js.map +1 -1
  150. package/dist/services/studentProgress.d.ts +50 -0
  151. package/dist/services/studentProgress.d.ts.map +1 -1
  152. package/dist/services/studentProgress.js +107 -2
  153. package/dist/services/studentProgress.js.map +1 -1
  154. package/dist/services/visualization.d.ts +72 -0
  155. package/dist/services/visualization.d.ts.map +1 -0
  156. package/dist/services/visualization.js +122 -0
  157. package/dist/services/visualization.js.map +1 -0
  158. package/dist/utils/inference.d.ts +10 -0
  159. package/dist/utils/inference.d.ts.map +1 -1
  160. package/dist/utils/inference.js +77 -2
  161. package/dist/utils/inference.js.map +1 -1
  162. package/dist/utils/sentryTrpc.d.ts +3 -0
  163. package/dist/utils/sentryTrpc.d.ts.map +1 -0
  164. package/dist/utils/sentryTrpc.js +26 -0
  165. package/dist/utils/sentryTrpc.js.map +1 -0
  166. package/dist/workers/processors/createRunJobWorker.js +17 -16
  167. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  168. package/package.json +1 -1
  169. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  170. package/prisma/migrations/20261025120000_add_visualizations/migration.sql +27 -0
  171. package/prisma/schema.prisma +32 -0
  172. package/src/index.ts +30 -4
  173. package/src/instrument.ts +8 -2
  174. package/src/lib/fileUpload.ts +49 -0
  175. package/src/models/cloudStorage.ts +19 -0
  176. package/src/models/labChat.ts +34 -0
  177. package/src/pipelines/aiLabChat.ts +79 -18
  178. package/src/pipelines/aiLabChatContract.ts +35 -0
  179. package/src/pipelines/aiNewtonChat.ts +34 -2
  180. package/src/pipelines/generateVisualization.ts +107 -0
  181. package/src/pipelines/labChatPrompt.ts +11 -12
  182. package/src/routers/_app.ts +2 -0
  183. package/src/routers/admin.ts +72 -0
  184. package/src/routers/class.ts +14 -1
  185. package/src/routers/labChat.ts +5 -0
  186. package/src/routers/message.ts +34 -5
  187. package/src/routers/parent.ts +11 -0
  188. package/src/routers/studentProgress.ts +15 -0
  189. package/src/routers/visualization.ts +60 -0
  190. package/src/routes/aiStream.ts +32 -0
  191. package/src/seedDatabase.ts +107 -2
  192. package/src/services/adminClass.ts +222 -0
  193. package/src/services/adminInstitution.ts +9 -2
  194. package/src/services/adminMetrics.ts +35 -0
  195. package/src/services/adminSpam.ts +224 -0
  196. package/src/services/adminSql.ts +214 -0
  197. package/src/services/adminTelemetry.ts +194 -0
  198. package/src/services/adminUser.ts +66 -11
  199. package/src/services/aiChatStream.ts +83 -7
  200. package/src/services/aiGeneration.ts +92 -0
  201. package/src/services/aiProposalTools.ts +166 -1
  202. package/src/services/aiProposals.ts +428 -0
  203. package/src/services/auth.ts +8 -2
  204. package/src/services/fees.ts +5 -1
  205. package/src/services/labChat.ts +65 -0
  206. package/src/services/message.ts +205 -142
  207. package/src/services/module.ts +29 -2
  208. package/src/services/parentPortal.ts +85 -0
  209. package/src/services/studentProgress.ts +135 -0
  210. package/src/services/visualization.ts +147 -0
  211. package/src/utils/inference.ts +85 -0
  212. package/src/utils/sentryTrpc.ts +25 -0
  213. package/src/workers/processors/createRunJobWorker.ts +17 -17
  214. package/tests/unit/lib/fileUploadSession.test.ts +67 -0
  215. package/tests/unit/services/adminClass.test.ts +102 -0
  216. package/tests/unit/services/adminSpam.test.ts +108 -0
  217. package/tests/unit/services/adminSql.test.ts +56 -0
  218. package/tests/unit/services/adminTelemetry.test.ts +47 -0
  219. package/tests/unit/services/adminUser.test.ts +77 -0
  220. package/tests/unit/services/visualization.test.ts +130 -0
  221. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
  222. package/tests/unit/utils/sentryTrpc.test.ts +15 -0
@@ -0,0 +1,47 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ const prismaMocks = vi.hoisted(() => ({
4
+ $queryRaw: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../../../src/lib/prisma.js', () => ({
8
+ prisma: prismaMocks,
9
+ }));
10
+
11
+ import { getActivitySeriesForAdmin } from '../../../src/services/adminTelemetry.js';
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ describe('getActivitySeriesForAdmin', () => {
18
+ test('zero-fills every day in the window and keeps them ordered', async () => {
19
+ prismaMocks.$queryRaw.mockResolvedValue([]);
20
+
21
+ const { points } = await getActivitySeriesForAdmin({ days: 5 });
22
+
23
+ expect(points).toHaveLength(5);
24
+ expect(points.map((p) => p.date)).toEqual([...points.map((p) => p.date)].sort());
25
+ expect(points.every((p) => p.signups === 0 && p.logins === 0)).toBe(true);
26
+ });
27
+
28
+ test('maps counted rows onto their day', async () => {
29
+ const today = new Date();
30
+ today.setUTCHours(0, 0, 0, 0);
31
+ const key = today.toISOString().slice(0, 10);
32
+
33
+ // Same shape for every series query; only signups are asserted.
34
+ prismaMocks.$queryRaw.mockResolvedValue([{ day: today, total: 3n }]);
35
+
36
+ const { points } = await getActivitySeriesForAdmin({ days: 3 });
37
+ const todayPoint = points.find((p) => p.date === key);
38
+
39
+ expect(todayPoint).toMatchObject({ signups: 3, submissions: 3 });
40
+ });
41
+
42
+ test('clamps the window to at most 90 days', async () => {
43
+ prismaMocks.$queryRaw.mockResolvedValue([]);
44
+ const { points } = await getActivitySeriesForAdmin({ days: 500 });
45
+ expect(points).toHaveLength(90);
46
+ });
47
+ });
@@ -0,0 +1,77 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { TRPCError } from '@trpc/server';
3
+
4
+ const prismaMocks = vi.hoisted(() => ({
5
+ user: { findUnique: vi.fn() },
6
+ class: { findMany: vi.fn() },
7
+ }));
8
+
9
+ vi.mock('../../../src/lib/prisma.js', () => ({
10
+ prisma: prismaMocks,
11
+ }));
12
+
13
+ import { getUserForAdmin } from '../../../src/services/adminUser.js';
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ prismaMocks.class.findMany.mockResolvedValue([]);
18
+ });
19
+
20
+ describe('getUserForAdmin', () => {
21
+ test('does not select session ids (they are bearer tokens) and only returns AUTH sessions', async () => {
22
+ prismaMocks.user.findUnique.mockResolvedValue({
23
+ id: 'user-1',
24
+ username: 'ada',
25
+ email: 'ada@example.com',
26
+ verified: true,
27
+ termsAcceptedAt: null,
28
+ termsVersion: null,
29
+ profile: null,
30
+ school: null,
31
+ schoolMemberships: [],
32
+ sessions: [
33
+ {
34
+ type: 'AUTH',
35
+ createdAt: new Date('2026-08-01T00:00:00Z'),
36
+ expiresAt: new Date('2026-09-01T00:00:00Z'),
37
+ },
38
+ ],
39
+ _count: {
40
+ teacherIn: 0,
41
+ studentIn: 0,
42
+ sessions: 1,
43
+ submissions: 0,
44
+ files: 0,
45
+ },
46
+ });
47
+
48
+ const user = await getUserForAdmin('user-1');
49
+
50
+ expect(prismaMocks.user.findUnique).toHaveBeenCalledWith(
51
+ expect.objectContaining({
52
+ where: { id: 'user-1' },
53
+ select: expect.objectContaining({
54
+ sessions: {
55
+ where: { type: 'AUTH' },
56
+ orderBy: { createdAt: 'desc' },
57
+ take: 10,
58
+ select: { type: true, createdAt: true, expiresAt: true },
59
+ },
60
+ _count: {
61
+ select: expect.objectContaining({
62
+ sessions: { where: { type: 'AUTH' } },
63
+ }),
64
+ },
65
+ }),
66
+ }),
67
+ );
68
+ expect(user.sessions[0]).not.toHaveProperty('id');
69
+ });
70
+
71
+ test('throws NOT_FOUND when the user is missing', async () => {
72
+ prismaMocks.user.findUnique.mockResolvedValue(null);
73
+ await expect(getUserForAdmin('missing')).rejects.toMatchObject({
74
+ code: 'NOT_FOUND',
75
+ } satisfies Partial<TRPCError>);
76
+ });
77
+ });
@@ -0,0 +1,130 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { TRPCError } from "@trpc/server";
3
+
4
+ const { prismaMocks, inferenceMock, teacherMock } = vi.hoisted(() => ({
5
+ prismaMocks: {
6
+ class: { findUnique: vi.fn() },
7
+ visualization: {
8
+ findFirst: vi.fn(),
9
+ findMany: vi.fn(),
10
+ create: vi.fn(),
11
+ update: vi.fn(),
12
+ delete: vi.fn(),
13
+ },
14
+ courseModuleItem: { deleteMany: vi.fn() },
15
+ $transaction: vi.fn(),
16
+ },
17
+ inferenceMock: vi.fn(),
18
+ teacherMock: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("../../../src/lib/prisma.js", () => ({ prisma: prismaMocks }));
22
+ vi.mock("../../../src/utils/inference.js", () => ({ inference: inferenceMock }));
23
+ vi.mock("../../../src/models/class.js", () => ({ isTeacherInClass: teacherMock }));
24
+
25
+ import {
26
+ createVisualization,
27
+ deleteVisualization,
28
+ getVisualization,
29
+ listVisualizations,
30
+ reviseVisualizationRecord,
31
+ } from "../../../src/services/visualization.js";
32
+
33
+ const HTML = "<!DOCTYPE html><html><body><canvas></canvas></body></html>";
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ prismaMocks.class.findUnique.mockResolvedValue({
38
+ name: "Physics 10",
39
+ subject: "Physics",
40
+ schoolId: "s1",
41
+ });
42
+ });
43
+
44
+ describe("listVisualizations / getVisualization", () => {
45
+ test("students only see published visualisations", async () => {
46
+ teacherMock.mockResolvedValue(null);
47
+ prismaMocks.visualization.findMany.mockResolvedValue([]);
48
+ await listVisualizations("u1", "c1");
49
+ expect(prismaMocks.visualization.findMany.mock.calls[0][0].where).toEqual({
50
+ classId: "c1",
51
+ published: true,
52
+ });
53
+ });
54
+
55
+ test("teachers see drafts too", async () => {
56
+ teacherMock.mockResolvedValue({ id: "c1" });
57
+ prismaMocks.visualization.findMany.mockResolvedValue([]);
58
+ await listVisualizations("t1", "c1");
59
+ expect(prismaMocks.visualization.findMany.mock.calls[0][0].where).toEqual({
60
+ classId: "c1",
61
+ });
62
+ });
63
+
64
+ test("students get NOT_FOUND for drafts", async () => {
65
+ teacherMock.mockResolvedValue(null);
66
+ prismaMocks.visualization.findFirst.mockResolvedValue(null);
67
+ await expect(getVisualization("u1", "c1", "v1")).rejects.toBeInstanceOf(TRPCError);
68
+ expect(prismaMocks.visualization.findFirst.mock.calls[0][0].where).toEqual({
69
+ id: "v1",
70
+ classId: "c1",
71
+ published: true,
72
+ });
73
+ });
74
+ });
75
+
76
+ describe("createVisualization", () => {
77
+ test("generates via inference and stores an unpublished draft", async () => {
78
+ inferenceMock.mockResolvedValue({ title: "Projectile motion", html: HTML });
79
+ prismaMocks.visualization.create.mockImplementation(({ data }) => ({ id: "v1", ...data }));
80
+
81
+ const viz = await createVisualization("t1", { classId: "c1", prompt: "projectile sliders" });
82
+
83
+ expect(inferenceMock).toHaveBeenCalledTimes(1);
84
+ const [messages, , options] = inferenceMock.mock.calls[0];
85
+ expect(messages[1].content).toContain("projectile sliders");
86
+ expect(messages[1].content).toContain("Subject: Physics");
87
+ expect(options.attribution).toEqual({ userId: "t1", classId: "c1", schoolId: "s1" });
88
+ expect(viz).toMatchObject({ title: "Projectile motion", html: HTML, createdById: "t1" });
89
+ expect(prismaMocks.visualization.create.mock.calls[0][0].data.published).toBeUndefined();
90
+ });
91
+
92
+ test("rejects output that isn't an HTML document", async () => {
93
+ inferenceMock.mockResolvedValue({ title: "x", html: "just text" });
94
+ await expect(
95
+ createVisualization("t1", { classId: "c1", prompt: "anything" }),
96
+ ).rejects.toMatchObject({ code: "INTERNAL_SERVER_ERROR" });
97
+ expect(prismaMocks.visualization.create).not.toHaveBeenCalled();
98
+ });
99
+ });
100
+
101
+ describe("reviseVisualizationRecord", () => {
102
+ test("sends current html and saves the revision", async () => {
103
+ prismaMocks.visualization.findFirst.mockResolvedValue({
104
+ id: "v1",
105
+ title: "Waves",
106
+ html: HTML,
107
+ prompt: "waves",
108
+ });
109
+ inferenceMock.mockResolvedValue({ title: "Waves", html: HTML.replace("canvas", "svg") });
110
+ prismaMocks.visualization.update.mockResolvedValue({ id: "v1" });
111
+
112
+ await reviseVisualizationRecord("t1", { classId: "c1", id: "v1", instruction: "add damping" });
113
+
114
+ expect(inferenceMock.mock.calls[0][0][1].content).toContain(HTML);
115
+ expect(prismaMocks.visualization.update.mock.calls[0][0].data.prompt).toBe(
116
+ "waves\n\nadd damping",
117
+ );
118
+ });
119
+ });
120
+
121
+ describe("deleteVisualization", () => {
122
+ test("removes attached module items", async () => {
123
+ prismaMocks.visualization.findFirst.mockResolvedValue({ id: "v1" });
124
+ prismaMocks.$transaction.mockResolvedValue([]);
125
+ await deleteVisualization({ classId: "c1", id: "v1" });
126
+ expect(prismaMocks.courseModuleItem.deleteMany).toHaveBeenCalledWith({
127
+ where: { itemType: "VISUALIZATION", itemId: "v1" },
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TRPCError } from '@trpc/server';
3
+ import type { Request } from 'express';
4
+ import { assertAdminPanelPassword } from '../../../src/utils/adminPanelAccess.js';
5
+
6
+ function reqWithHeader(value: string | string[] | undefined): Request {
7
+ return { headers: { 'x-admin-password': value } } as unknown as Request;
8
+ }
9
+
10
+ describe('assertAdminPanelPassword', () => {
11
+ test('accepts the configured admin password header', () => {
12
+ expect(() => assertAdminPanelPassword(reqWithHeader('alan6767'))).not.toThrow();
13
+ });
14
+
15
+ test('rejects a missing or wrong header with FORBIDDEN', () => {
16
+ expect(() => assertAdminPanelPassword(reqWithHeader(undefined))).toThrow(
17
+ TRPCError,
18
+ );
19
+ try {
20
+ assertAdminPanelPassword(reqWithHeader('nope'));
21
+ } catch (error) {
22
+ expect(error).toBeInstanceOf(TRPCError);
23
+ expect((error as TRPCError).code).toBe('FORBIDDEN');
24
+ }
25
+ });
26
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isReportableTrpcError } from '../../../src/utils/sentryTrpc.js';
3
+
4
+ describe('isReportableTrpcError', () => {
5
+ it('reports server-side failures', () => {
6
+ expect(isReportableTrpcError({ code: 'INTERNAL_SERVER_ERROR' })).toBe(true);
7
+ expect(isReportableTrpcError({ code: 'TIMEOUT' })).toBe(true);
8
+ });
9
+
10
+ it('ignores expected client errors', () => {
11
+ for (const code of ['BAD_REQUEST', 'UNAUTHORIZED', 'FORBIDDEN', 'NOT_FOUND'] as const) {
12
+ expect(isReportableTrpcError({ code })).toBe(false);
13
+ }
14
+ });
15
+ });