musora-content-services 2.155.7 → 2.155.9

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 (40) hide show
  1. package/.agent/decisions/2026-04-21-tma-239-mcs-sanity-add-a-v2-query-para.md +26 -0
  2. package/.github/workflows/automated-testing.yml +4 -1
  3. package/CHANGELOG.md +10 -0
  4. package/codecov.yml +7 -1
  5. package/package.json +1 -1
  6. package/src/services/offline/activities.ts +8 -4
  7. package/src/services/offline/practices.ts +4 -3
  8. package/src/services/sanity.js +4 -4
  9. package/test/unit/{HttpClient.test.js → HttpClient.test.ts} +7 -13
  10. package/test/unit/awards/{award-alacarte-observer.test.js → award-alacarte-observer.test.ts} +13 -11
  11. package/test/unit/awards/{award-certificate-display.test.js → award-certificate-display.test.ts} +2 -1
  12. package/test/unit/awards/{award-collection-edge-cases.test.js → award-collection-edge-cases.test.ts} +4 -3
  13. package/test/unit/awards/{award-collection-filtering.test.js → award-collection-filtering.test.ts} +4 -3
  14. package/test/unit/awards/{award-completion-flow.test.js → award-completion-flow.test.ts} +4 -3
  15. package/test/unit/awards/{award-exclusion-handling.test.js → award-exclusion-handling.test.ts} +4 -3
  16. package/test/unit/awards/{award-multi-lesson.test.js → award-multi-lesson.test.ts} +4 -3
  17. package/test/unit/awards/{award-observer-integration.test.js → award-observer-integration.test.ts} +5 -4
  18. package/test/unit/awards/{award-query-messages.test.js → award-query-messages.test.ts} +2 -1
  19. package/test/unit/awards/{award-user-collection.test.js → award-user-collection.test.ts} +2 -1
  20. package/test/unit/awards/{duplicate-prevention.test.js → duplicate-prevention.test.ts} +5 -4
  21. package/test/unit/awards/helpers/{completion-mock.js → completion-mock.ts} +9 -7
  22. package/test/unit/awards/helpers/index.ts +3 -0
  23. package/test/unit/awards/helpers/{mock-setup.js → mock-setup.ts} +12 -2
  24. package/test/unit/awards/helpers/{progress-emitter.js → progress-emitter.ts} +10 -4
  25. package/test/unit/{contentLikes.test.js → contentLikes.test.ts} +1 -1
  26. package/test/unit/{contentProgress.test.js → contentProgress.test.ts} +8 -2
  27. package/test/unit/{dataContext.test.js → dataContext.test.ts} +1 -1
  28. package/test/unit/{dateUtils.test.js → dateUtils.test.ts} +2 -2
  29. package/test/unit/{imageSRCBuilder.test.js → imageSRCBuilder.test.ts} +1 -1
  30. package/test/unit/{imageSRCVerify.test.js → imageSRCVerify.test.ts} +1 -6
  31. package/test/unit/{notifications.test.js → notifications.test.ts} +1 -4
  32. package/test/unit/{progressRows.test.js → progressRows.test.ts} +19 -13
  33. package/test/unit/{sanityQueryService.test.js → sanityQueryService.test.ts} +7 -8
  34. package/test/unit/{streakMessage.test.js → streakMessage.test.ts} +4 -7
  35. package/test/unit/{userActivity.test.js → userActivity.test.ts} +5 -2
  36. package/test/unit/awards/helpers/index.js +0 -3
  37. /package/test/unit/awards/{award-auto-refresh.test.js → award-auto-refresh.test.ts} +0 -0
  38. /package/test/unit/awards/{award-calculations.test.js → award-calculations.test.ts} +0 -0
  39. /package/test/unit/awards/{message-generator.test.js → message-generator.test.ts} +0 -0
  40. /package/test/unit/lib/{lastUpdated.test.js → lastUpdated.test.ts} +0 -0
@@ -0,0 +1,26 @@
1
+ ---
2
+ date: 2026-04-21
3
+ pr: railroadmedia/musora-content-services#933
4
+ branch: TMA-239-mcs-sanity-add-a-v2-query-parameter-to-all-sanity-
5
+ status: open
6
+ tags: [[jira]], [[bug-fix]], [[sanity]], [[cache]]
7
+ components: [[fetchSanity]]
8
+ ---
9
+
10
+ # Add v=2 query parameter to all Sanity requests to force cache refresh
11
+
12
+ ## Context
13
+ All Sanity API requests from musora-content-services were being served with cached responses. Adding a `v=2` query parameter forces a cache refresh, ensuring clients receive up-to-date content.
14
+
15
+ ## Decision
16
+ Appended `&v=2` to the `baseUrl` string inside `fetchSanity()` in `src/services/sanity.js`. This is the single location where all Sanity request URLs are constructed, so one targeted change covers every request made through the library.
17
+
18
+ ## Alternatives Considered
19
+ - Adding the parameter per call-site: There are dozens of call sites and they all funnel through `fetchSanity`, so modifying the base URL there is cleaner and less error-prone.
20
+ - Making it configurable via `sanityConfig`: The ticket does not ask for configurability — it asks for the parameter to be added to all requests unconditionally.
21
+
22
+ ## Process Notes
23
+ The `fetchSanity` function at line 1477 of `src/services/sanity.js` is the sole entry point for all Sanity API calls. It builds `baseUrl` on line 1488 and then appends `&query=...` for GET requests or uses it as the POST URL. Appending `&v=2` to `baseUrl` ensures the parameter is present in both cases.
24
+
25
+ ## Consequences
26
+ All future Sanity requests from this library will include `v=2`, bypassing any CDN or server-side cache and ensuring fresh content is returned.
@@ -19,6 +19,9 @@ jobs:
19
19
  - name: Run unit tests
20
20
  run: npm test -- --coverage
21
21
  - name: Upload coverage to Codecov
22
- uses: codecov/codecov-action@v4
22
+ uses: codecov/codecov-action@v5
23
23
  with:
24
24
  token: ${{ secrets.CODECOV_TOKEN }}
25
+ slug: railroadmedia/musora-content-services
26
+ flags: unit
27
+ fail_ci_if_error: true
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.155.9](https://github.com/railroadmedia/musora-content-services/compare/v2.155.8...v2.155.9) (2026-04-28)
6
+
7
+ ### [2.155.8](https://github.com/railroadmedia/musora-content-services/compare/v2.155.7...v2.155.8) (2026-04-28)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * revert activity timestamp changes ([#942](https://github.com/railroadmedia/musora-content-services/issues/942)) ([bd9f97d](https://github.com/railroadmedia/musora-content-services/commit/bd9f97d6547409b7f2856bf716c90d6723306c36))
13
+ * **TMA-239:** MCS - Sanity - Add a v=2 query parameter to all sanity requests to force a cache refresh ([#933](https://github.com/railroadmedia/musora-content-services/issues/933)) ([eac5a2c](https://github.com/railroadmedia/musora-content-services/commit/eac5a2c6848aee80c462ba0ccbeba52a92652fa9))
14
+
5
15
  ### [2.155.7](https://github.com/railroadmedia/musora-content-services/compare/v2.155.6...v2.155.7) (2026-04-23)
6
16
 
7
17
 
package/codecov.yml CHANGED
@@ -1,5 +1,11 @@
1
- # codecov.yml
2
1
  comment:
3
2
  layout: "reach,diff,flags,tree"
4
3
  behavior: default
5
4
  require_changes: false
5
+
6
+ coverage:
7
+ status:
8
+ patch:
9
+ default:
10
+ target: 70%
11
+ threshold: 0%
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.155.7",
3
+ "version": "2.155.9",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -14,13 +14,16 @@ interface Activity {
14
14
 
15
15
  /**
16
16
  * @param offlineTimestamp - Minimum `updated_at` epoch ms to include
17
+ * @param page
18
+ * @param limit
19
+ * @param tabName
17
20
  * @param options.page - Page number (default 1)
18
21
  * @param options.limit - Results per page (default 5)
19
22
  * @param options.tabName - Restrict to `'lessons'`, `'songs'`, or both when `null`
20
- * @returns `{ currentPage, totalPages, data }` where `data` is a page of `Activity` records
23
+ * @returns {Promise<{currentPage: number, totalPages: number, data: Activity[]}>}
21
24
  */
22
25
  export async function getRecentActivityOffline(
23
- _offlineTimestamp: number,
26
+ offlineTimestamp: number,
24
27
  {
25
28
  page = 1,
26
29
  limit = 5,
@@ -34,7 +37,7 @@ export async function getRecentActivityOffline(
34
37
  // because setting up watermelon user activities table is extremely complicated.
35
38
  // Note: this implementation does not persist "activities" beyond when the corresponding record is deleted. That's ok right now.
36
39
 
37
- const clauses = getClauses(tabName)
40
+ const clauses = getClauses(offlineTimestamp, tabName)
38
41
 
39
42
  const query = await db.contentProgress.queryAll(...clauses)
40
43
  const progress = query.data
@@ -54,8 +57,9 @@ export async function getRecentActivityOffline(
54
57
  }
55
58
  }
56
59
 
57
- function getClauses(tabName: string|null) {
60
+ function getClauses(offlineTimestamp: number, tabName: string|null) {
58
61
  let clauses: Q.Clause[] = [
62
+ Q.where('updated_at', Q.gte(offlineTimestamp)),
59
63
  Q.sortBy('created_at', 'desc'),
60
64
  ]
61
65
 
@@ -6,12 +6,13 @@ import { calculateLongestStreaks } from '../userActivity.js'
6
6
 
7
7
  /**
8
8
  * @param offlineTimestamp - Minimum `updated_at` epoch ms to include
9
+ * @param day
9
10
  * @param options.day - Date in YYYY-MM-DD format, defaults to today
10
- * @returns `{ data: { practices, practiceDuration } }` where `practiceDuration` is total seconds
11
+ * @returns {Promise<{data: {practices: object[], practiceDuration: number}}>}
11
12
  */
12
13
  export async function getPracticeSessionsOffline(
13
- offlineTimestamp: number,
14
- { day = dayjs().format('YYYY-MM-DD') }: { day?: string } = {}
14
+ offlineTimestamp: number, {
15
+ day = dayjs().format('YYYY-MM-DD') }: { day?: string } = {}
15
16
  ) {
16
17
 
17
18
  const query = await db.practices.queryAll(
@@ -1296,8 +1296,8 @@ export async function fetchTopLevelParentId(railcontentId) {
1296
1296
  * Ignores learning-path-v2 parents.
1297
1297
  * ex: if railcontentId is of type 'skill-pack-lesson', return the corresponding 'skill-pack' railcontent_id
1298
1298
  *
1299
- * @param {int[]} railcontentId
1300
- * @returns {Promise<Record<[railcontentId: int],[topLevelParentId: int]>|null>}
1299
+ * @param {int[]} railcontentIds
1300
+ * @returns {Promise<Object.<number, object>|null>}
1301
1301
  */
1302
1302
  async function fetchTopLevelParentIds(railcontentIds) {
1303
1303
  const idsString = railcontentIds.join(',')
@@ -1460,7 +1460,7 @@ async function fetchALaCarteHierarchyData(railcontentId) {
1460
1460
  /**
1461
1461
  * returns a map of railcontentId to hierarchy data.
1462
1462
  * @param {int[]} railcontentIds
1463
- * @returns {Promise<Record<[topLevelParentId: int],object>|null>}
1463
+ * @returns {Promise<Object.<number, object>|null>}
1464
1464
  */
1465
1465
  async function fetchALaCarteHierarchyDataForIds(railcontentIds) {
1466
1466
  const topLevelIds = await fetchTopLevelParentIds(railcontentIds)
@@ -1567,7 +1567,7 @@ export async function fetchSanity(
1567
1567
  }
1568
1568
  const perspective = globalConfig.sanityConfig.perspective ?? 'published'
1569
1569
  const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api'
1570
- const baseUrl = `https://sanity.musora.com/${globalConfig.sanityConfig.projectId}/${api}/v${globalConfig.sanityConfig.version}/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
1570
+ const baseUrl = `https://sanity.musora.com/${globalConfig.sanityConfig.projectId}/${api}/v${globalConfig.sanityConfig.version}/${globalConfig.sanityConfig.dataset}?perspective=${perspective}&v=2`
1571
1571
 
1572
1572
  try {
1573
1573
  const encodedQuery = encodeURIComponent(query)
@@ -1,11 +1,9 @@
1
- import { HttpClient } from '../../src/infrastructure/http/HttpClient.ts'
2
- import { HeaderProvider } from '../../src/infrastructure/http/interfaces/HeaderProvider.ts'
3
- import { RequestExecutor } from '../../src/infrastructure/http/interfaces/RequestExecutor.ts'
1
+ import { HeaderProvider, HttpClient, RequestExecutor } from '../../src/infrastructure/http'
4
2
 
5
3
  describe('HttpClient', () => {
6
- let httpClient
7
- let mockHeaderProvider
8
- let mockRequestExecutor
4
+ let httpClient: HttpClient
5
+ let mockHeaderProvider: jest.Mocked<HeaderProvider>
6
+ let mockRequestExecutor: jest.Mocked<RequestExecutor>
9
7
  const baseUrl = 'https://api.example.com'
10
8
  const token = 'test-token'
11
9
  const headers = { 'Content-Type': 'application/json', Accept: 'application/json' }
@@ -190,23 +188,21 @@ describe('HttpClient', () => {
190
188
  })
191
189
 
192
190
  test('should not add Data-Version header when not provided', async () => {
193
- // Log what's happening for debugging
194
- console.log('Starting test: should not add Data-Version header when not provided')
195
191
 
196
192
  const url = '/test'
197
193
 
198
194
  // Make sure we're using exact null here, not undefined
199
- const dataVersion = null
195
+ const dataVersion: string | null = null
200
196
 
201
197
  // Create separate mocks for this test to avoid state bleeding
202
- const testHeaderProvider = {
198
+ const testHeaderProvider: jest.Mocked<HeaderProvider> = {
203
199
  getHeaders: jest.fn().mockReturnValue({
204
200
  'Content-Type': 'application/json',
205
201
  Accept: 'application/json',
206
202
  }),
207
203
  }
208
204
 
209
- const testRequestExecutor = {
205
+ const testRequestExecutor: jest.Mocked<RequestExecutor> = {
210
206
  execute: jest.fn().mockResolvedValue(responseData),
211
207
  }
212
208
 
@@ -215,8 +211,6 @@ describe('HttpClient', () => {
215
211
 
216
212
  await testClient.get(url, { dataVersion })
217
213
 
218
- console.log('Headers used in request:', testRequestExecutor.execute.mock.calls[0][1].headers)
219
-
220
214
  // Direct check on the mock to validate Data-Version absence
221
215
  const actualHeaders = testRequestExecutor.execute.mock.calls[0][1].headers
222
216
  const hasDataVersion = Object.prototype.hasOwnProperty.call(actualHeaders, 'Data-Version')
@@ -1,9 +1,9 @@
1
1
  import { contentProgressObserver } from '../../../src/services/awards/internal/content-progress-observer.js'
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
4
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
5
- import { mockCompletionStates, mockAllCompleted } from './helpers/completion-mock.js'
6
- import { COLLECTION_TYPE, emitAlaCarteProgress, emitProgress, waitForDebounce } from './helpers/progress-emitter.js'
4
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
5
+ import { mockCompletionStates, mockAllCompleted } from './helpers/completion-mock'
6
+ import { COLLECTION_TYPE, emitAlaCarteProgress, emitProgress, waitForDebounce } from './helpers/progress-emitter'
7
7
 
8
8
  jest.mock('../../../src/services/sanity.js', () => ({
9
9
  ...jest.requireActual('../../../src/services/sanity'),
@@ -32,9 +32,11 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
32
32
  })
33
33
 
34
34
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
35
- import db from '../../../src/services/sync/repository-proxy.ts'
35
+ import db from '../../../src/services/sync/repository-proxy'
36
36
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
37
37
 
38
+ const mockDb = db as jest.MockedObjectDeep<typeof db>
39
+
38
40
  describe('Award Observer - A La Carte Progress (null collection)', () => {
39
41
  let listeners
40
42
 
@@ -43,7 +45,7 @@ describe('Award Observer - A La Carte Progress (null collection)', () => {
43
45
  awardEvents.removeAllListeners()
44
46
 
45
47
  sanityClient.fetch = jest.fn().mockResolvedValue(mockAwardDefinitions)
46
- setupDefaultMocks(db, fetchSanity)
48
+ setupDefaultMocks(mockDb, fetchSanity)
47
49
 
48
50
  await awardDefinitions.refresh()
49
51
 
@@ -71,7 +73,7 @@ describe('Award Observer - A La Carte Progress (null collection)', () => {
71
73
  })
72
74
 
73
75
  test('emits awardProgress for partial completion with null collection', async () => {
74
- mockCompletionStates(db, [417045])
76
+ mockCompletionStates(mockDb, [417045])
75
77
 
76
78
  emitAlaCarteProgress(417045)
77
79
  await waitForDebounce()
@@ -114,18 +116,18 @@ describe('Award Observer - A La Carte Progress (null collection)', () => {
114
116
  test('finds all awards containing the child content id', async () => {
115
117
  const sharedChildId = 418003
116
118
 
117
- mockAllCompleted(db)
119
+ mockAllCompleted(mockDb)
118
120
 
119
121
  emitAlaCarteProgress(sharedChildId)
120
122
  await waitForDebounce()
121
123
 
122
- expect(db.userAwardProgress.recordAwardProgress).toHaveBeenCalled()
124
+ expect(mockDb.userAwardProgress.recordAwardProgress).toHaveBeenCalled()
123
125
  })
124
126
  })
125
127
 
126
128
  describe('A la carte debouncing', () => {
127
129
  beforeEach(() => {
128
- mockCompletionStates(db, [])
130
+ mockCompletionStates(mockDb, [])
129
131
  })
130
132
 
131
133
  test('debounces multiple rapid a la carte updates', async () => {
@@ -135,7 +137,7 @@ describe('Award Observer - A La Carte Progress (null collection)', () => {
135
137
 
136
138
  await waitForDebounce()
137
139
 
138
- expect(db.userAwardProgress.recordAwardProgress).toHaveBeenCalledTimes(1)
140
+ expect(mockDb.userAwardProgress.recordAwardProgress).toHaveBeenCalledTimes(1)
139
141
  })
140
142
  })
141
143
 
@@ -151,7 +153,7 @@ describe('Award Observer - A La Carte Progress (null collection)', () => {
151
153
 
152
154
  describe('A la carte already completed award', () => {
153
155
  beforeEach(() => {
154
- db.userAwardProgress.hasCompletedAward.mockResolvedValue(true)
156
+ mockDb.userAwardProgress.hasCompletedAward.mockResolvedValue(true)
155
157
  })
156
158
 
157
159
  test('does not re-grant already completed award', async () => {
@@ -25,7 +25,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
25
25
 
26
26
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
27
27
  import { getUserData } from '../../../src/services/user/management.js'
28
- import db from '../../../src/services/sync/repository-proxy.ts'
28
+ import db_ from '../../../src/services/sync/repository-proxy'
29
+ const db = db_ as any
29
30
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
30
31
 
31
32
  describe('Award Certificate Display - E2E Scenarios', () => {
@@ -1,8 +1,8 @@
1
1
  import { contentProgressObserver } from '../../../src/services/awards/internal/content-progress-observer.js'
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { mockAwardDefinitions } from '../../mockData/award-definitions.js'
4
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
5
- import { COLLECTION_TYPE, emitProgress, waitForDebounce } from './helpers/progress-emitter.js'
4
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
5
+ import { COLLECTION_TYPE, emitProgress, waitForDebounce } from './helpers/progress-emitter'
6
6
 
7
7
  jest.mock('../../../src/services/sanity.js', () => ({
8
8
  ...jest.requireActual('../../../src/services/sanity'),
@@ -31,7 +31,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
31
31
  })
32
32
 
33
33
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
34
- import db from '../../../src/services/sync/repository-proxy.ts'
34
+ import db_ from '../../../src/services/sync/repository-proxy'
35
+ const db = db_ as any
35
36
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
36
37
 
37
38
  describe('Award Collection Filtering - Edge Cases', () => {
@@ -1,8 +1,8 @@
1
1
  import { contentProgressObserver } from '../../../src/services/awards/internal/content-progress-observer.js'
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { mockAwardDefinitions } from '../../mockData/award-definitions.js'
4
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
5
- import { COLLECTION_TYPE, emitProgress, waitForDebounce } from './helpers/progress-emitter.js'
4
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
5
+ import { COLLECTION_TYPE, emitProgress, waitForDebounce } from './helpers/progress-emitter'
6
6
 
7
7
  jest.mock('../../../src/services/sanity.js', () => ({
8
8
  ...jest.requireActual('../../../src/services/sanity'),
@@ -31,7 +31,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
31
31
  })
32
32
 
33
33
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
34
- import db from '../../../src/services/sync/repository-proxy.ts'
34
+ import db_ from '../../../src/services/sync/repository-proxy'
35
+ const db = db_ as any
35
36
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
36
37
 
37
38
  describe('Award Collection Filtering', () => {
@@ -1,8 +1,8 @@
1
1
  import { awardManager } from '../../../src/services/awards/internal/award-manager.js'
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
4
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
5
- import { mockCompletionStates, mockAllCompleted } from './helpers/completion-mock.js'
4
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
5
+ import { mockCompletionStates, mockAllCompleted } from './helpers/completion-mock'
6
6
 
7
7
  jest.mock('../../../src/services/sanity.js', () => ({
8
8
  ...jest.requireActual('../../../src/services/sanity'),
@@ -31,7 +31,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
31
31
  })
32
32
 
33
33
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
34
- import db from '../../../src/services/sync/repository-proxy.ts'
34
+ import db_ from '../../../src/services/sync/repository-proxy'
35
+ const db = db_ as any
35
36
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
36
37
 
37
38
  describe('Award Completion Flow - E2E Scenarios', () => {
@@ -3,8 +3,8 @@ import { awardEvents } from '../../../src/services/awards/internal/award-events.
3
3
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
4
4
  import { globalConfig } from '../../../src/services/config.js'
5
5
  import { LocalStorageMock } from '../../localStorageMock.js'
6
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
7
- import { mockCompletionStates, mockAllCompleted, mockNoneCompleted } from './helpers/completion-mock.js'
6
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
7
+ import { mockCompletionStates, mockAllCompleted, mockNoneCompleted } from './helpers/completion-mock'
8
8
 
9
9
  jest.mock('../../../src/services/sanity.js', () => ({
10
10
  ...jest.requireActual('../../../src/services/sanity'),
@@ -39,7 +39,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
39
39
  })
40
40
 
41
41
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
42
- import db from '../../../src/services/sync/repository-proxy.ts'
42
+ import db_ from '../../../src/services/sync/repository-proxy'
43
+ const db = db_ as any
43
44
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
44
45
 
45
46
  describe('Award Content Exclusion Handling - E2E Scenarios', () => {
@@ -1,8 +1,8 @@
1
1
  import { awardManager } from '../../../src/services/awards/internal/award-manager.js'
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
4
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
5
- import { mockCompletionStates, mockAllCompleted, mockNoneCompleted } from './helpers/completion-mock.js'
4
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
5
+ import { mockCompletionStates, mockAllCompleted, mockNoneCompleted } from './helpers/completion-mock'
6
6
 
7
7
  jest.mock('../../../src/services/sanity.js', () => ({
8
8
  ...jest.requireActual('../../../src/services/sanity'),
@@ -31,7 +31,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
31
31
  })
32
32
 
33
33
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
34
- import db from '../../../src/services/sync/repository-proxy.ts'
34
+ import db_ from '../../../src/services/sync/repository-proxy'
35
+ const db = db_ as any
35
36
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
36
37
 
37
38
  describe('Award Progress Calculation', () => {
@@ -2,9 +2,9 @@ import { contentProgressObserver } from '../../../src/services/awards/internal/c
2
2
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
3
3
  import { emitProgressSaved } from '../../../src/services/progress-events.js'
4
4
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
5
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
6
- import { mockCompletionStates } from './helpers/completion-mock.js'
7
- import { COLLECTION_TYPE, waitForDebounce } from './helpers/progress-emitter.js'
5
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
6
+ import { mockCompletionStates } from './helpers/completion-mock'
7
+ import { COLLECTION_TYPE, waitForDebounce } from './helpers/progress-emitter'
8
8
 
9
9
  jest.mock('../../../src/services/sanity.js', () => ({
10
10
  ...jest.requireActual('../../../src/services/sanity'),
@@ -33,7 +33,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
33
33
  })
34
34
 
35
35
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
36
- import db from '../../../src/services/sync/repository-proxy.ts'
36
+ import db_ from '../../../src/services/sync/repository-proxy'
37
+ const db = db_ as any
37
38
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
38
39
 
39
40
  describe('Award Observer Integration - E2E Scenarios', () => {
@@ -21,7 +21,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
21
21
  })
22
22
 
23
23
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
24
- import db from '../../../src/services/sync/repository-proxy.ts'
24
+ import db_ from '../../../src/services/sync/repository-proxy'
25
+ const db = db_ as any
25
26
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
26
27
  import { getCompletedAwards, getContentAwards, getInProgressAwards } from '../../../src/services/awards/award-query.js'
27
28
 
@@ -20,7 +20,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
20
20
  })
21
21
 
22
22
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
23
- import db from '../../../src/services/sync/repository-proxy.ts'
23
+ import db_ from '../../../src/services/sync/repository-proxy'
24
+ const db = db_ as any
24
25
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
25
26
  import { getCompletedAwards, getInProgressAwards, getAwardStatistics } from '../../../src/services/awards/award-query.js'
26
27
 
@@ -1,7 +1,7 @@
1
1
  import { mockAwardDefinitions, getAwardByContentId } from '../../mockData/award-definitions.js'
2
- import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index.js'
3
- import { COLLECTION_TYPE, emitLearningPathProgress, emitAlaCarteProgress, waitForDebounce } from './helpers/progress-emitter.js'
4
- import { mockCollectionAwareCompletion } from './helpers/completion-mock.js'
2
+ import { setupDefaultMocks, setupAwardEventListeners } from './helpers/index'
3
+ import { COLLECTION_TYPE, emitLearningPathProgress, emitAlaCarteProgress, waitForDebounce } from './helpers/progress-emitter'
4
+ import { mockCollectionAwareCompletion } from './helpers/completion-mock'
5
5
 
6
6
  jest.mock('../../../src/services/sanity.js', () => ({
7
7
  ...jest.requireActual('../../../src/services/sanity'),
@@ -30,7 +30,8 @@ jest.mock('../../../src/services/sync/repository-proxy.ts', () => {
30
30
  })
31
31
 
32
32
  import sanityClient, { fetchSanity } from '../../../src/services/sanity.js'
33
- import db from '../../../src/services/sync/repository-proxy.ts'
33
+ import db_ from '../../../src/services/sync/repository-proxy'
34
+ const db = db_ as any
34
35
  import { awardDefinitions } from '../../../src/services/awards/internal/award-definitions.js'
35
36
  import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
36
37
  import { contentProgressObserver } from '../../../src/services/awards/internal/content-progress-observer.js'
@@ -1,5 +1,7 @@
1
- export const mockCompletionStates = (db, completedIds = [], collection = null) => {
2
- db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds, requestedCollection) => {
1
+ type Collection = { type: string; id: number } | null
2
+
3
+ export const mockCompletionStates = (db: any, completedIds: number[] = [], collection: Collection = null) => {
4
+ db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds: number[], requestedCollection: Collection) => {
3
5
  const collectionMatches = !collection || (
4
6
  requestedCollection?.type === collection.type &&
5
7
  requestedCollection?.id === collection.id
@@ -17,8 +19,8 @@ export const mockCompletionStates = (db, completedIds = [], collection = null) =
17
19
  })
18
20
  }
19
21
 
20
- export const mockAllCompleted = (db) => {
21
- db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds) => {
22
+ export const mockAllCompleted = (db: any) => {
23
+ db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds: number[]) => {
22
24
  const completedRecords = contentIds.map(id => ({
23
25
  content_id: id,
24
26
  state: 'completed',
@@ -29,12 +31,12 @@ export const mockAllCompleted = (db) => {
29
31
  })
30
32
  }
31
33
 
32
- export const mockNoneCompleted = (db) => {
34
+ export const mockNoneCompleted = (db: any) => {
33
35
  db.contentProgress.getSomeProgressByContentIds.mockResolvedValue({ data: [] })
34
36
  }
35
37
 
36
- export const mockCollectionAwareCompletion = (db, completionMap) => {
37
- db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds, collection) => {
38
+ export const mockCollectionAwareCompletion = (db: any, completionMap: Record<string, boolean>) => {
39
+ db.contentProgress.getSomeProgressByContentIds.mockImplementation((contentIds: number[], collection: Collection) => {
38
40
  const completedRecords = contentIds
39
41
  .filter(id => {
40
42
  const key = collection
@@ -0,0 +1,3 @@
1
+ export * from './mock-setup'
2
+ export * from './progress-emitter'
3
+ export * from './completion-mock'
@@ -1,5 +1,7 @@
1
1
  import { mockAwardDefinitions } from '../../../mockData/award-definitions.js'
2
2
 
3
+ export type RepositoryProxyMock = ReturnType<typeof createRepositoryProxyMock>
4
+
3
5
  export const createRepositoryProxyMock = () => ({
4
6
  contentProgress: {
5
7
  getOneProgressByContentId: jest.fn(),
@@ -19,7 +21,15 @@ export const createRepositoryProxyMock = () => ({
19
21
  }
20
22
  })
21
23
 
22
- export const setupDefaultMocks = (db, fetchSanity, options = {}) => {
24
+ export const setupDefaultMocks = (
25
+ db: any,
26
+ fetchSanity: jest.Mock,
27
+ options: {
28
+ definitions?: typeof mockAwardDefinitions,
29
+ practiceMinutes?: number,
30
+ hasCompleted?: boolean
31
+ } = {}
32
+ ) => {
23
33
  const {
24
34
  definitions = mockAwardDefinitions,
25
35
  practiceMinutes = 200,
@@ -56,7 +66,7 @@ export const setupDefaultMocks = (db, fetchSanity, options = {}) => {
56
66
  })
57
67
  }
58
68
 
59
- export const setupAwardEventListeners = (awardEvents) => {
69
+ export const setupAwardEventListeners = (awardEvents: any) => {
60
70
  const listeners = {
61
71
  progress: jest.fn(),
62
72
  granted: jest.fn()
@@ -1,5 +1,5 @@
1
1
  import { emitProgressSaved } from '../../../../src/services/progress-events.js'
2
- import { COLLECTION_TYPE } from '../../../../src/services/sync/models/ContentProgress.ts'
2
+ import { COLLECTION_TYPE } from '../../../../src/services/sync/models/ContentProgress'
3
3
 
4
4
  export { COLLECTION_TYPE }
5
5
 
@@ -9,6 +9,12 @@ export const emitProgress = ({
9
9
  collectionId = null,
10
10
  progressPercent = 100,
11
11
  userId = 123
12
+ }: {
13
+ contentId: number
14
+ collectionType?: string | null
15
+ collectionId?: number | null
16
+ progressPercent?: number
17
+ userId?: number
12
18
  }) => {
13
19
  emitProgressSaved({
14
20
  userId,
@@ -23,7 +29,7 @@ export const emitProgress = ({
23
29
  })
24
30
  }
25
31
 
26
- export const emitLearningPathProgress = (contentId, learningPathId, progressPercent = 100) => {
32
+ export const emitLearningPathProgress = (contentId: number, learningPathId: number, progressPercent = 100) => {
27
33
  emitProgress({
28
34
  contentId,
29
35
  collectionType: COLLECTION_TYPE.LEARNING_PATH,
@@ -32,8 +38,8 @@ export const emitLearningPathProgress = (contentId, learningPathId, progressPerc
32
38
  })
33
39
  }
34
40
 
35
- export const emitAlaCarteProgress = (contentId, progressPercent = 100) => {
41
+ export const emitAlaCarteProgress = (contentId: number, progressPercent = 100) => {
36
42
  emitProgress({ contentId, progressPercent })
37
43
  }
38
44
 
39
- export const waitForDebounce = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms))
45
+ export const waitForDebounce = (ms = 100): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
@@ -5,7 +5,7 @@ import {
5
5
  } from '../../src/services/contentLikes.js'
6
6
  import { initializeTestService } from '../initializeTests.js'
7
7
 
8
- let mockLikedIds = new Set()
8
+ let mockLikedIds: Set<number> = new Set()
9
9
 
10
10
  jest.mock('../../src/services/sync/repository-proxy.ts', () => {
11
11
  const mockFns = {
@@ -1,7 +1,13 @@
1
1
  import { initializeTestService } from '../initializeTests.js'
2
- import { getAllStarted, getAllStartedOrCompleted, getProgressState } from '../../src/index.js'
2
+ import { getAllStarted, getAllStartedOrCompleted, getProgressState } from '../../src/services/contentProgress.js'
3
3
 
4
- let mockProgressRecords = []
4
+ let mockProgressRecords: {
5
+ content_id: number;
6
+ state: string;
7
+ progress_percent: number;
8
+ updated_at: number;
9
+ last_interacted_a_la_carte?: number
10
+ }[] = []
5
11
 
6
12
  jest.mock('../../src/services/sync/repository-proxy', () => {
7
13
  const mockFns = {
@@ -2,7 +2,7 @@ import { initializeTestService } from '../initializeTests.js'
2
2
  import { DataContext, verifyLocalDataContext } from '../../src/services/dataContext.js'
3
3
 
4
4
  describe('dataContext', function () {
5
- let mock = null
5
+ let mock: jest.SpyInstance
6
6
  let testVersion = 1
7
7
  const dataVersionKey = 1
8
8
  const dataContext = new DataContext(dataVersionKey, null)
@@ -8,7 +8,7 @@ import {
8
8
  getToday,
9
9
  } from '../../src/services/dateUtils.js'
10
10
 
11
- const mockTimezone = (tz) => {
11
+ const mockTimezone = (tz: string): void => {
12
12
  const RealDateTimeFormat = Intl.DateTimeFormat
13
13
  jest.spyOn(Intl, 'DateTimeFormat').mockImplementation((...args) => {
14
14
  const instance = new RealDateTimeFormat(...args)
@@ -16,7 +16,7 @@ const mockTimezone = (tz) => {
16
16
  format: instance.format.bind(instance),
17
17
  formatToParts: instance.formatToParts.bind(instance),
18
18
  resolvedOptions: () => ({ ...instance.resolvedOptions(), timeZone: tz }),
19
- }
19
+ } as unknown as Intl.DateTimeFormat
20
20
  })
21
21
  }
22
22
 
@@ -1,4 +1,4 @@
1
- const { buildImageSRC, applySanityTransformations } = require('../../src/services/imageSRCBuilder.js')
1
+ import { buildImageSRC, applySanityTransformations } from '../../src/services/imageSRCBuilder.js'
2
2
 
3
3
  describe('imageSRCBuilder', function () {
4
4
  beforeEach(() => {})
@@ -1,4 +1,3 @@
1
- // Mock console.warn to avoid cluttering test output and to verify warnings
2
1
  import { extractSanityUrl, isBucketUrl, verifyImageSRC } from '../../src/services/imageSRCVerify.js'
3
2
 
4
3
  const originalConsoleWarn = console.warn
@@ -6,20 +5,16 @@ const originalConsoleError = console.error
6
5
  const originalNodeEnv = process.env.NODE_ENV
7
6
 
8
7
  describe('Image URL Verification', () => {
9
- let consoleWarnMock
8
+ let consoleWarnMock: any
10
9
 
11
10
  beforeEach(() => {
12
- // Mock console.warn and console.error
13
11
  consoleWarnMock = jest.fn()
14
12
  console.warn = consoleWarnMock
15
13
  console.error = jest.fn()
16
-
17
- // Set NODE_ENV to development for all tests
18
14
  process.env.NODE_ENV = 'development'
19
15
  })
20
16
 
21
17
  afterEach(() => {
22
- // Restore the original console methods and NODE_ENV
23
18
  console.warn = originalConsoleWarn
24
19
  console.error = originalConsoleError
25
20
  process.env.NODE_ENV = originalNodeEnv
@@ -22,16 +22,13 @@ jest.mock('../../src/services/eventsAPI.js', () => ({
22
22
  }
23
23
  }))
24
24
 
25
- const { GET, PUT, DELETE } = require('../../src/infrastructure/http/HttpClient.ts')
25
+ import { GET, PUT, DELETE } from '../../src/infrastructure/http/HttpClient'
26
26
 
27
27
  const baseUrl = `/api/notifications`
28
28
 
29
29
  describe('UserNotifications module', function () {
30
30
  beforeEach(() => {
31
31
  initializeTestService()
32
- GET.mockReset()
33
- PUT.mockReset()
34
- DELETE.mockReset()
35
32
  })
36
33
 
37
34
  describe('fetchNotifications', () => {
@@ -81,22 +81,28 @@ describe('getProgressRows', () => {
81
81
  },
82
82
  ];
83
83
 
84
- fetchUserPlaylists.mockResolvedValue({ data: mockPlaylists });
85
- fetchByRailContentIds.mockResolvedValue(mockPlaylistContents);
86
- getAllStartedOrCompleted.mockResolvedValue([201]);
87
- getProgressStateByIds.mockResolvedValue({ '201': 'in_progress' });
84
+ fetchUserPlaylists.mockResolvedValue({ data: mockPlaylists })
85
+ fetchByRailContentIds.mockResolvedValue(mockPlaylistContents)
86
+ getAllStartedOrCompleted.mockResolvedValue([201])
87
+ getProgressStateByIds.mockResolvedValue({ '201': 'in_progress' })
88
88
 
89
- const result = await getProgressRows({ brand: 'brand1', limit: 8 });
89
+ const result = await getProgressRows({ brand: 'brand1', limit: 8 })
90
90
 
91
- expect(result).toHaveProperty('type', 'progress_rows');
91
+ expect(result).toHaveProperty('type', 'progress_rows')
92
92
  expect(result).toHaveProperty('data');
93
- expect(Array.isArray(result.data)).toBe(true);
94
- expect(result.data.length).toBe(2);
95
- expect(result.data[0]).toHaveProperty('id', 'progressType', 'header', 'body', 'cta');
96
- expect(result.data[0].progressType).toBe('playlist');
97
- expect(result.data[0].body).toHaveProperty('first_items_thumbnail_url', 'title', 'subtitle');
98
- expect(result.data[0].cta).toHaveProperty('text');
99
- expect(result.data[0].cta).toHaveProperty('action');
93
+ expect(Array.isArray(result.data)).toBe(true)
94
+ expect(result.data.length).toBe(2)
95
+ expect(result.data[0]).toHaveProperty('id')
96
+ expect(result.data[0]).toHaveProperty('progressType')
97
+ expect(result.data[0]).toHaveProperty('header')
98
+ expect(result.data[0]).toHaveProperty('body')
99
+ expect(result.data[0]).toHaveProperty('cta')
100
+ expect(result.data[0].progressType).toBe('playlist')
101
+ expect(result.data[0].body).toHaveProperty('first_items_thumbnail_url')
102
+ expect(result.data[0].body).toHaveProperty('title')
103
+ expect(result.data[0].body).toHaveProperty('subtitle')
104
+ expect(result.data[0].cta).toHaveProperty('text')
105
+ expect(result.data[0].cta).toHaveProperty('action')
100
106
 
101
107
  });
102
108
 
@@ -1,11 +1,7 @@
1
1
  import { initializeTestService } from '../initializeTests.js'
2
- import { log } from '../log.js'
3
- import {
4
- getSortOrder
5
- } from '../../src/index.js'
2
+ import { getSortOrder } from '../../src/services/sanity.js'
6
3
  import { processMetadata } from '../../src/contentMetaData.js'
7
-
8
- const { FilterBuilder } = require('../../src/filterBuilder.js')
4
+ import { FilterBuilder } from '../../src/filterBuilder.js'
9
5
 
10
6
  jest.mock('../../src/services/permissions/index.ts', () => ({
11
7
  ...jest.requireActual('../../src/services/permissions/index.ts'),
@@ -137,7 +133,11 @@ describe('Filter Builder', function() {
137
133
  expect(clauses[3].operator).toBe('>=')
138
134
  })
139
135
 
140
- function spliceFilterForAnds(filter) {
136
+ function spliceFilterForAnds(filter: string): {
137
+ phrase: string;
138
+ field: string;
139
+ operator: string;
140
+ condition: string }[] {
141
141
  // this will not correctly split complex filters with && and || conditions.
142
142
  let phrases = filter.split(' && ')
143
143
  let clauses = []
@@ -152,7 +152,6 @@ describe('Filter Builder', function() {
152
152
  })
153
153
  return clauses
154
154
  }
155
-
156
155
  })
157
156
 
158
157
  describe('Sanity Queries', function() {
@@ -1,11 +1,9 @@
1
1
  import { initializeTestService } from '../initializeTests.js'
2
2
  import {getUserWeeklyStats, userActivityContext} from '../../src/services/userActivity.js'
3
- import { streakCalculator } from '../../src/services/user/streakCalculator.ts'
3
+ import { streakCalculator } from '../../src/services/user/streakCalculator'
4
4
  import {log} from '../log.js'
5
5
 
6
- import path from 'path';
7
-
8
- let mockPracticeData = []
6
+ let mockPracticeData: { date: string; duration_seconds: number }[] = []
9
7
 
10
8
  jest.mock('../../src/services/sync/repository-proxy.ts', () => {
11
9
  const mockFns = {
@@ -200,15 +198,14 @@ describe('Streak Messages', function () {
200
198
  })
201
199
  })
202
200
 
203
- function incrementFakeDate(nDays = 1){
201
+ function incrementFakeDate(nDays = 1) {
204
202
  let today = new Date();
205
203
  today.setFullYear(today.getFullYear(), today.getMonth(), (today.getDate() + nDays));
206
204
  jest.useFakeTimers();
207
205
  jest.setSystemTime(today);
208
206
  }
209
207
 
210
- function sliceExampleData(startDate, nDays, includeToday, activeDays)
211
- {
208
+ function sliceExampleData(startDate, nDays, includeToday, activeDays) {
212
209
  if (nDays === 0 && !includeToday) {
213
210
  return []
214
211
  }
@@ -1,7 +1,10 @@
1
1
  import { initializeTestService } from '../initializeTests.js'
2
2
  import { getUserMonthlyStats, getUserWeeklyStats, recordUserPractice } from '../../src/services/userActivity.js'
3
3
 
4
- let mockPracticesData = []
4
+ let mockPracticesData: {
5
+ date: string;
6
+ duration_seconds: number
7
+ }[] = []
5
8
 
6
9
  jest.mock('../../src/services/sync/repository-proxy.ts', () => {
7
10
  const mockFns = {
@@ -33,7 +36,7 @@ jest.mock('../../src/services/railcontent.js', () => ({
33
36
  fetchUserPermissionsData: jest.fn(() => ({ permissions: [78, 91, 92], isAdmin: false }))
34
37
  }))
35
38
 
36
- const repositoryProxy = require('../../src/services/sync/repository-proxy.ts')
39
+ const repositoryProxy: any = require('../../src/services/sync/repository-proxy.ts')
37
40
 
38
41
  describe('User Activity API Tests', function () {
39
42
  beforeEach(() => {
@@ -1,3 +0,0 @@
1
- export * from './mock-setup.js'
2
- export * from './progress-emitter.js'
3
- export * from './completion-mock.js'