musora-content-services 2.153.0 → 2.154.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.
@@ -17,4 +17,8 @@ jobs:
17
17
  - name: Install dependencies
18
18
  run: npm ci
19
19
  - name: Run unit tests
20
- run: npm test
20
+ run: npm test -- --coverage
21
+ - name: Upload coverage to Codecov
22
+ uses: codecov/codecov-action@v4
23
+ with:
24
+ token: ${{ secrets.CODECOV_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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.154.0](https://github.com/railroadmedia/musora-content-services/compare/v2.153.0...v2.154.0) (2026-04-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEHLTP-23:** add multi-user account update endpoint ([#914](https://github.com/railroadmedia/musora-content-services/issues/914)) ([98f6c4b](https://github.com/railroadmedia/musora-content-services/commit/98f6c4bb830c7a8a25ba556ca07fc97023667011))
11
+ * muppet - add invite validity/error data ([#923](https://github.com/railroadmedia/musora-content-services/issues/923)) ([3938fa5](https://github.com/railroadmedia/musora-content-services/commit/3938fa56ec9295023b6ba624c5f238d1645a911b))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * add typeof before LokiJSAdapter in parameter list ([6a135b5](https://github.com/railroadmedia/musora-content-services/commit/6a135b57c516c277a50d707417544ab2f9eeb194))
17
+ * **BR-632:** dont trickle progress to children when enrolling in GC ([#916](https://github.com/railroadmedia/musora-content-services/issues/916)) ([67a96d6](https://github.com/railroadmedia/musora-content-services/commit/67a96d69bd8b61f12d0efd45475a7b0b08c58813))
18
+
5
19
  ## [2.153.0](https://github.com/railroadmedia/musora-content-services/compare/v2.152.1...v2.153.0) (2026-04-15)
6
20
 
7
21
 
package/codecov.yml ADDED
@@ -0,0 +1,5 @@
1
+ # codecov.yml
2
+ comment:
3
+ layout: "reach,diff,flags,tree"
4
+ behavior: default
5
+ require_changes: false
package/jest.config.js CHANGED
@@ -22,16 +22,38 @@ export default {
22
22
  // Indicates whether the coverage information should be collected while executing the test
23
23
  collectCoverage: true,
24
24
 
25
- // An array of glob patterns indicating a set of files for which coverage information should be collected
26
- // collectCoverageFrom: undefined,
25
+ // Exclude pure transport/adapter layers with no business logic.
26
+ // Files with business logic stay in even at low coverage — gaps should be visible.
27
+ collectCoverageFrom: [
28
+ 'src/**/*.{js,ts}',
29
+ '!src/services/sanity.js',
30
+ '!src/services/railcontent.js',
31
+ '!src/services/recommendations.js',
32
+ '!src/index.js',
33
+ '!src/index.d.ts',
34
+ '!src/services/user/account.ts',
35
+ '!src/services/user/sessions.js',
36
+ '!src/services/user/profile.js',
37
+ '!src/services/user/management.js',
38
+ '!src/services/user/interests.js',
39
+ '!src/services/user/payments.ts',
40
+ '!src/services/user/chat.js',
41
+ ],
27
42
 
28
43
  // The directory where Jest should output its coverage files
29
44
  coverageDirectory: 'coverage',
30
45
 
31
- // An array of regexp pattern strings used to skip coverage collection
32
- // coveragePathIgnorePatterns: [
33
- // "/node_modules/"
34
- // ],
46
+ // Global threshold set just below current baseline.
47
+ // Intent is to ratchet up over time as coverage improves.
48
+ // Do not lower these numbers — raise them as tests are added.
49
+ coverageThreshold: {
50
+ global: {
51
+ statements: 40,
52
+ branches: 25,
53
+ functions: 40,
54
+ lines: 40,
55
+ },
56
+ },
35
57
 
36
58
  // Indicates which provider should be used to instrument code for coverage
37
59
  // coverageProvider: "babel",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.153.0",
3
+ "version": "2.154.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -149,7 +149,8 @@ import {
149
149
  getWeekNumber,
150
150
  isNextDay,
151
151
  isSameDate,
152
- toDayjs
152
+ toDayjs,
153
+ toLocalDay
153
154
  } from './services/dateUtils.js';
154
155
 
155
156
  import {
@@ -217,9 +218,11 @@ import {
217
218
  acceptInvite,
218
219
  createAccount,
219
220
  createInvites,
221
+ fetchInvite,
220
222
  fetchUsersMultiAccountDetails,
221
223
  removeUserFromActiveMultiUserAccount,
222
- rescindInvite
224
+ rescindInvite,
225
+ updateMultiUserAccount
223
226
  } from './services/multi-user-accounts/multi-user-accounts.ts';
224
227
 
225
228
  import {
@@ -550,6 +553,7 @@ declare module 'musora-content-services' {
550
553
  fetchInstructorLessons,
551
554
  fetchInstructors,
552
555
  fetchInterests,
556
+ fetchInvite,
553
557
  fetchLastSubscriptionPlatform,
554
558
  fetchLatestThreads,
555
559
  fetchLearningPathLessons,
@@ -759,6 +763,7 @@ declare module 'musora-content-services' {
759
763
  startOnboarding,
760
764
  status,
761
765
  toDayjs,
766
+ toLocalDay,
762
767
  togglePlaylistPrivate,
763
768
  toggleSignaturePrivate,
764
769
  toggleStudentView,
@@ -779,6 +784,7 @@ declare module 'musora-content-services' {
779
784
  updateDailySession,
780
785
  updateDisplayName,
781
786
  updateForumCategory,
787
+ updateMultiUserAccount,
782
788
  updateNotificationSetting,
783
789
  updateOnboarding,
784
790
  updatePlaylist,
package/src/index.js CHANGED
@@ -153,7 +153,8 @@ import {
153
153
  getWeekNumber,
154
154
  isNextDay,
155
155
  isSameDate,
156
- toDayjs
156
+ toDayjs,
157
+ toLocalDay
157
158
  } from './services/dateUtils.js';
158
159
 
159
160
  import {
@@ -221,9 +222,11 @@ import {
221
222
  acceptInvite,
222
223
  createAccount,
223
224
  createInvites,
225
+ fetchInvite,
224
226
  fetchUsersMultiAccountDetails,
225
227
  removeUserFromActiveMultiUserAccount,
226
- rescindInvite
228
+ rescindInvite,
229
+ updateMultiUserAccount
227
230
  } from './services/multi-user-accounts/multi-user-accounts.ts';
228
231
 
229
232
  import {
@@ -549,6 +552,7 @@ export {
549
552
  fetchInstructorLessons,
550
553
  fetchInstructors,
551
554
  fetchInterests,
555
+ fetchInvite,
552
556
  fetchLastSubscriptionPlatform,
553
557
  fetchLatestThreads,
554
558
  fetchLearningPathLessons,
@@ -758,6 +762,7 @@ export {
758
762
  startOnboarding,
759
763
  status,
760
764
  toDayjs,
765
+ toLocalDay,
761
766
  togglePlaylistPrivate,
762
767
  toggleSignaturePrivate,
763
768
  toggleStudentView,
@@ -778,6 +783,7 @@ export {
778
783
  updateDailySession,
779
784
  updateDisplayName,
780
785
  updateForumCategory,
786
+ updateMultiUserAccount,
781
787
  updateNotificationSetting,
782
788
  updateOnboarding,
783
789
  updatePlaylist,
@@ -14,7 +14,7 @@ export async function enrollUserInGuidedCourse(guidedCourse, { notifications_ena
14
14
  const response = await POST(url, { notifications_enabled })
15
15
  const state = await getProgressState(guidedCourse)
16
16
  if (!state) {
17
- await contentStatusStarted(guidedCourse)
17
+ await contentStatusStarted(guidedCourse, null, { skipBubbleTrickle: true })
18
18
  }
19
19
  return response
20
20
  }
@@ -471,12 +471,14 @@ export async function contentStatusCompletedMany(contentIds, collection = null)
471
471
  )
472
472
  }
473
473
 
474
- export async function contentStatusStarted(contentId, collection = null) {
474
+ // skipBubbleTrickle is only for starting enrolled GC's as a hack to get them into the progress row.
475
+ export async function contentStatusStarted(contentId, collection = null, {skipPush = false, skipBubbleTrickle = false} = {}) {
475
476
  collection = collection ?? {id: COLLECTION_ID_SELF, type: COLLECTION_TYPE.SELF}
476
477
  return setStartedOrCompletedStatus(
477
478
  normalizeContentId(contentId),
478
479
  normalizeCollection(collection),
479
- false
480
+ false,
481
+ {skipPush, skipBubbleTrickle}
480
482
  )
481
483
  }
482
484
  export async function contentStatusReset(contentId, collection = null, {skipPush = false} = {}) {
@@ -491,9 +493,9 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
491
493
 
492
494
  // filter out contentIds that are setting progress lower than existing
493
495
  const contentIdProgress = await getProgressDataByIds([contentId], collection)
494
- const currentProgress = contentIdProgress[contentId].progress;
496
+ const currentProgress = contentIdProgress[contentId].progress
495
497
  if (progress <= currentProgress) {
496
- progress = currentProgress;
498
+ progress = currentProgress
497
499
  }
498
500
 
499
501
  const hierarchy = await getHierarchy(contentId, collection)
@@ -559,7 +561,7 @@ async function saveContentProgress(contentId, collection, progress, currentSecon
559
561
  return response
560
562
  }
561
563
 
562
- async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false} = {}) {
564
+ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {skipPush = false, skipBubbleTrickle = false} = {}) {
563
565
  const isLP = collection?.type === COLLECTION_TYPE.LEARNING_PATH
564
566
 
565
567
  const hierarchy = await getHierarchy(contentId, collection)
@@ -575,24 +577,25 @@ async function setStartedOrCompletedStatus(contentId, collection, isCompleted, {
575
577
  {skipPush: true}
576
578
  )
577
579
 
578
- let progresses = {
579
- ...trickleProgress(hierarchy, contentId, collection, progress),
580
- ...await bubbleProgress(hierarchy, contentId, collection)
581
- }
580
+ let allProgresses = {}
581
+ allProgresses[contentId] = progress
582
582
 
583
- // have to do this so we dont unnecessarily create a 0% record for each child on set to started/completed
584
- await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
583
+ if (!skipBubbleTrickle) {
584
+ let progresses = {
585
+ ...trickleProgress(hierarchy, contentId, collection, progress),
586
+ ...await bubbleProgress(hierarchy, contentId, collection)
587
+ }
588
+ Object.assign(allProgresses, progresses)
585
589
 
586
- if (isLP) {
587
- let exportProgresses = progresses
588
- exportProgresses[contentId] = progress
589
- await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
590
+ await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
590
591
  }
591
592
 
592
- if (progress === 100) await onContentCompletedLearningPathActions(contentId, collection)
593
+ if (isLP) {
594
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
595
+ }
593
596
 
594
- for (const [id, progress] of Object.entries(progresses)) {
595
- if (progress === 100) {
597
+ for (const [id, prog] of Object.entries(allProgresses)) {
598
+ if (prog === 100) {
596
599
  await onContentCompletedLearningPathActions(Number(id), collection)
597
600
  }
598
601
  }
@@ -618,6 +621,8 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
618
621
  {skipPush: true}
619
622
  )
620
623
 
624
+ let allProgresses = Object.fromEntries(contentIds.map(id => [id, progress]))
625
+
621
626
  let progresses = {}
622
627
  for (const contentId of contentIds) {
623
628
  progresses = {
@@ -626,24 +631,16 @@ async function setStartedOrCompletedStatusMany(contentIds, collection, isComplet
626
631
  ...(await bubbleProgress(hierarchy, contentId, collection)),
627
632
  }
628
633
  }
629
- // have to do this so we dont unnecessarily create a 0% record for each child on set to started/completed
634
+ Object.assign(allProgresses, progresses)
635
+
630
636
  await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
631
637
 
632
638
  if (isLP) {
633
- let exportProgresses = progresses
634
- for (const contentId of contentIds){
635
- exportProgresses[contentId] = progress
636
- }
637
- await duplicateProgressToALaCarte(exportProgresses, collection, {skipPush: true})
639
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
638
640
  }
639
641
 
640
- if (progress === 100) {
641
- for (const contentId of contentIds) {
642
- await onContentCompletedLearningPathActions(contentId, collection)
643
- }
644
- }
645
- for (const [id, progress] of Object.entries(progresses)) {
646
- if (progress === 100) {
642
+ for (const [id, prog] of Object.entries(allProgresses)) {
643
+ if (prog === 100) {
647
644
  await onContentCompletedLearningPathActions(Number(id), collection)
648
645
  }
649
646
  }
@@ -662,17 +659,20 @@ async function resetStatus(contentId, collection = null, {skipPush = false} = {}
662
659
  const hierarchy = await getHierarchy(contentId, collection)
663
660
  const metadata = hierarchy.metadata || {}
664
661
 
662
+ let allProgresses = {}
663
+ allProgresses[contentId] = progress
664
+
665
665
  let progresses = {
666
666
  ...trickleProgress(hierarchy, contentId, collection, progress),
667
667
  ...await bubbleProgress(hierarchy, contentId, collection)
668
668
  }
669
+ Object.assign(allProgresses, progresses)
669
670
 
670
- // have to use different endpoints for erase vs record
671
671
  await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, true)
672
672
 
673
+
673
674
  if (isLP) {
674
- progresses[contentId] = progress
675
- await duplicateProgressToALaCarte(progresses, collection, {skipPush: true})
675
+ await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
676
676
  }
677
677
 
678
678
  if (!skipPush) db.contentProgress.requestPushUnsynced('reset-status')
@@ -14,11 +14,16 @@ export interface User {
14
14
  }
15
15
 
16
16
  export interface InviteResponse {
17
- email: string
18
17
  id: number
19
18
  created_at: string
20
19
  expires_at: string
21
- existing_user_details: User
20
+ can_be_accepted: boolean
21
+ is_account_valid: boolean
22
+ is_invite_active: boolean
23
+ can_user_join: boolean
24
+ // These fields leak user information and are excluded entirely for the public endpoint
25
+ existing_user_details?: User
26
+ email?: string
22
27
  }
23
28
 
24
29
  export interface UsersMultiAccountResponse {
@@ -27,7 +32,7 @@ export interface UsersMultiAccountResponse {
27
32
  last_cancelled_multi_user_account: MultiUserAccountResponse
28
33
  is_active_primary: boolean
29
34
  is_active_sub: boolean
30
- active_invite: InviteResponse
35
+ active_invites: InviteResponse[]
31
36
  }
32
37
 
33
38
  export interface MultiUserAccountResponse {
@@ -35,13 +40,15 @@ export interface MultiUserAccountResponse {
35
40
  product_name: string
36
41
  is_active: boolean
37
42
  primary_user: User
38
- active_invited_emails: string[]
39
- available_seats: number
40
- available_invites: number
41
43
  total_seats: number
42
- active_subs: User[]
43
44
  end_time: string
44
45
  is_primary_account_holder: boolean
46
+ // The following fields are not included for public or subaccount users
47
+ active_invited_emails?: InviteResponse[]
48
+ available_seats?: number
49
+ available_invites?: InviteResponse[]
50
+ active_subs?: User[]
51
+ show_welcome?: boolean
45
52
  }
46
53
 
47
54
  export interface CreateAccountParams {
@@ -53,6 +60,10 @@ export interface CreateInvitesParams {
53
60
  emails: string[]
54
61
  }
55
62
 
63
+ export interface UpdateMultiUserAccountParams {
64
+ show_welcome: boolean
65
+ }
66
+
56
67
 
57
68
  /**
58
69
  * Creates a new multi-user account with optional invites and seat count.
@@ -78,6 +89,19 @@ export async function fetchUsersMultiAccountDetails(userId: number): Promise<Use
78
89
  return httpClient.get<UsersMultiAccountResponse>(`${baseUrl}/${userId}/details`)
79
90
  }
80
91
 
92
+
93
+ /**
94
+ * Fetch invite details
95
+ *
96
+ * @param {number} inviteId - The ID of the invite to check
97
+ * @returns {Promise<InviteResponse>} - A promise that resolves to the invite details.
98
+ * @throws {HttpError} - If the HTTP request fails.
99
+ */
100
+ export async function fetchInvite(inviteId: number): Promise<InviteResponse> {
101
+ const httpClient = new HttpClient(globalConfig.baseUrl)
102
+ return httpClient.get<InviteResponse>(`${baseUrl}/invites/${inviteId}`)
103
+ }
104
+
81
105
  /**
82
106
  * Creates invitations for an existing multi-user account.
83
107
  *
@@ -125,3 +149,15 @@ export async function rescindInvite(inviteId: number): Promise<void> {
125
149
  export async function removeUserFromActiveMultiUserAccount(userId: number): Promise<MultiUserAccountResponse|void> {
126
150
  return DELETE(`${globalConfig.baseUrl}${baseUrl}/${userId}/remove`, {})
127
151
  }
152
+
153
+ /**
154
+ * Updates specified fields on a multi-user account. Authorized user must be the primary account owner
155
+ *
156
+ * @param {UpdateMultiUserAccountParams} params - The parameters for updating the account.
157
+ * @returns {Promise<MultiUserAccountResponse>} - Updated MultiUserAccountResponse if account owner
158
+ * @throws {HttpError} - If the request fails.
159
+ */
160
+ export async function updateMultiUserAccount(params: UpdateMultiUserAccountParams): Promise<MultiUserAccountResponse> {
161
+ const httpClient = new HttpClient(globalConfig.baseUrl)
162
+ return httpClient.patch(`${globalConfig.baseUrl}${baseUrl}/update`, params)
163
+ }
@@ -165,7 +165,7 @@ export function simulateIndexedDBQuotaExceeded() {
165
165
  })
166
166
  }
167
167
 
168
- export function abortWritesToDatabase(adapter: LokiJSAdapter) {
168
+ export function abortWritesToDatabase(adapter: typeof LokiJSAdapter) {
169
169
  // acts as handy helper to disable loki's save methods entirely
170
170
  lokiFatalError(adapter._driver.loki)
171
171
  return Promise.resolve()
@@ -177,7 +177,7 @@ export function abortWritesToDatabase(adapter: LokiJSAdapter) {
177
177
  * Haven't encountered live issues related to this yet, but theoretically provides
178
178
  * the cleanest slate for a user to recover from schema issues?
179
179
  */
180
- export function destroyDatabase(dbName: string, adapter: LokiJSAdapter): Promise<void> {
180
+ export function destroyDatabase(dbName: string, adapter: typeof LokiJSAdapter): Promise<void> {
181
181
  return new Promise(async (resolve, reject) => {
182
182
  if (adapter._driver) {
183
183
  try {
@@ -0,0 +1,29 @@
1
+ # test/live
2
+
3
+ This directory is reserved for live tests that require real external services.
4
+
5
+ ## Why is this empty?
6
+
7
+ Tests that previously lived here were written before the WatermelonDB sync rewrite.
8
+ Progress tracking now goes through the local sync store rather than calling Railcontent
9
+ directly — they were never updated, none were passing, and were removed rather than
10
+ left as misleading dead code.
11
+
12
+ ## Where live verification actually belongs
13
+
14
+ **`mcs-cli`** is the right tool for manually verifying behaviour against real services.
15
+ Once the WatermelonDB ESM interop issues are resolved it can run commands like
16
+ `mcs progress reset`, `mcs progress get` against a local BE and staging Sanity
17
+ without needing a test framework.
18
+
19
+ **Postman collections** are better suited for verifying Railcontent API contracts —
20
+ checking that endpoint response shapes haven't changed in ways that would break MCS.
21
+
22
+ **Jest live tests** are a last resort — only for logic that genuinely cannot be
23
+ verified any other way. If added, they require:
24
+
25
+ - WatermelonDB test harness from TP-1192
26
+ - Dedicated test user account (not a real user)
27
+ - Valid `.env` credentials — see 1Password
28
+ - Manual trigger only: `npx jest --config=jest.live.config.js --no-coverage --forceExit`
29
+ - Must never run in CI
@@ -6,6 +6,10 @@ import { initializeTestService } from '../initializeTests.js';
6
6
  import mockData_progress_content from '../mockData/mockData_progress_content.json';
7
7
  import mockData_sanity_progress_content from "../mockData/mockData_sanity_progress_content.json";
8
8
 
9
+ jest.mock('../../src/services/progress-row/rows/method-card.js', () => ({
10
+ getMethodCard: jest.fn().mockResolvedValue(null),
11
+ }))
12
+
9
13
  jest.mock('../../src/services/sync/repository-proxy.ts', () => {
10
14
  const mockFns = {
11
15
  contentProgress: {
@@ -1,110 +0,0 @@
1
- import {
2
- recordWatchSession,
3
- getProgressPercentage,
4
- dataContext,
5
- getProgressState,
6
- contentStatusCompleted,
7
- contentStatusReset,
8
- assignmentStatusCompleted,
9
- } from '../../src/services/contentProgress'
10
- import { initializeTestService } from '../initializeTests'
11
-
12
- describe('contentProgressDataContextLocal', function () {
13
- beforeEach(async () => {
14
- await initializeTestService(true)
15
- }, 1000000)
16
-
17
- test('verifyProgressPercentage', async () => {
18
- let contentId = 241250
19
- await contentStatusReset(contentId)
20
-
21
- await recordWatchSession(contentId, null, 'video', 'vimeo', 100, 50, 50)
22
-
23
- let result = await getProgressPercentage(contentId)
24
- expect(result).toBe(50)
25
- dataContext.clearCache()
26
-
27
- result = await getProgressPercentage(contentId)
28
- expect(result).toBe(50)
29
- }, 100000)
30
-
31
- test('verifyState', async () => {
32
- let contentId = 241251
33
- await contentStatusReset(contentId)
34
-
35
- let result = await getProgressState(contentId)
36
- expect(result).toBe('')
37
-
38
- await recordWatchSession(contentId, null, 'video', 'vimeo', 100, 50, 50)
39
-
40
- result = await getProgressState(contentId)
41
- expect(result).toBe('started')
42
- dataContext.clearCache()
43
- await new Promise((resolve) => setTimeout(resolve, 3000)) // 3 sec
44
-
45
- result = await getProgressState(contentId)
46
- expect(result).toBe('started')
47
- }, 100000)
48
-
49
- test('verifyStateCompleted', async () => {
50
- let contentId = 241252
51
- await contentStatusReset(contentId)
52
-
53
- await contentStatusCompleted(241252)
54
- let result = await getProgressState(241252)
55
- expect(result).toBe('completed')
56
-
57
- result = await getProgressState(contentId)
58
- expect(result).toBe('completed')
59
- dataContext.clearCache()
60
-
61
- result = await getProgressState(contentId)
62
- expect(result).toBe('completed')
63
- }, 100000)
64
-
65
- test('verifyStateReset', async () => {
66
- let contentId = 241253
67
- await contentStatusReset(contentId)
68
-
69
- await contentStatusCompleted(contentId)
70
-
71
- let result = await getProgressState(contentId)
72
- expect(result).toBe('completed')
73
- await contentStatusReset(contentId)
74
-
75
- result = await getProgressState(contentId)
76
- expect(result).toBe('')
77
- dataContext.clearCache()
78
-
79
- result = await getProgressState(contentId)
80
- expect(result).toBe('')
81
- }, 100000)
82
-
83
- test('assignmentCompleteBubblingToCompletedMultiple', async () => {
84
- let contentId = 281709
85
- await contentStatusReset(contentId)
86
-
87
- let state = await getProgressState(contentId)
88
- expect(state).toBe('')
89
-
90
- let assignmentIds = [281710, 281711, 281712, 281713, 281714, 281715]
91
- for (const assignmentId of assignmentIds) {
92
- await assignmentStatusCompleted(assignmentId, contentId)
93
- state = await getProgressState(assignmentId)
94
- expect(state).toBe('completed')
95
- }
96
-
97
- state = await getProgressState(contentId) //assignment
98
- expect(state).toBe('completed')
99
-
100
- dataContext.clearCache()
101
-
102
- state = await getProgressState(contentId) //assignment
103
- expect(state).toBe('completed')
104
-
105
- for (const assignmentId of assignmentIds) {
106
- state = await getProgressState(assignmentId)
107
- expect(state).toBe('completed')
108
- }
109
- }, 100000)
110
- })
@@ -1,70 +0,0 @@
1
- import { initializeTestService } from '../initializeTests.js'
2
- import {
3
- fetchLearningPathLessons,
4
- getEnrichedLearningPath,
5
- startLearningPath,
6
- resetAllLearningPaths,
7
- getActivePath,
8
- } from '../../src/services/content-org/learning-paths.ts'
9
- import {
10
- contentStatusCompleted,
11
- contentStatusReset,
12
- getProgressDataByIds,
13
- } from '../../src/index.js'
14
- describe('learning-paths', function () {
15
- beforeEach(async () => {
16
- await initializeTestService(true)
17
- })
18
-
19
- afterEach(async () => {
20
- // Flush all pending promises
21
- await new Promise((resolve) => setImmediate(resolve))
22
- })
23
-
24
- // test('getLearningPathsV2Test', async () => {
25
- // const results = await getEnrichedLearningPath(417140)
26
- // })
27
- // test('getlearningPathLessonsTestNew', async () => {
28
- // await contentStatusCompleted(417105)
29
- // const userDate = new Date('2025-10-31')
30
- // const results = await fetchLearningPathLessons(422533, 'drumeo', userDate)
31
- // console.log(results)
32
- // })
33
- // test('getlearningPathLessonsTestNew', async () => {
34
- // await contentStatusCompleted(417105)
35
- // const userDate = new Date('2025-10-31')
36
- // const results = await fetchLearningPathLessons(422533, 'drumeo', userDate)
37
- // console.log(results)
38
- // })
39
-
40
- test.skip('learningPathCompletion', async () => {
41
- const learningPathId = 435527
42
- await contentStatusReset(learningPathId)
43
- await resetAllLearningPaths()
44
- await startLearningPath('drumeo', learningPathId)
45
- const collection = { type: 'learning-path-v2', id: learningPathId }
46
- const learningPath = await getEnrichedLearningPath(learningPathId)
47
-
48
- // Complete each child one by one
49
- for (const child of learningPath.children) {
50
- await contentStatusReset(child.id)
51
- await contentStatusCompleted(child.id, collection)
52
-
53
- // Check child status
54
- const childProgress = await getProgressDataByIds([child.id], collection)
55
-
56
- // Check parent status after each child
57
- const parentProgress = await getProgressDataByIds([learningPathId], collection)
58
- }
59
-
60
- // Final check - parent should be completed
61
- const finalParentProgress = await getProgressDataByIds([learningPathId], collection)
62
- console.log('\n--- Final parent progress:', finalParentProgress)
63
- expect(finalParentProgress[learningPathId]?.status).toBe('completed')
64
-
65
- await new Promise((resolve) => setTimeout(resolve, 5000))
66
-
67
- const activePath = await getActivePath('drumeo')
68
- expect(activePath.active_learning_path_id).toBe(435563)
69
- }, 15000)
70
- })
@@ -1,7 +0,0 @@
1
- import { initializeTestService } from '../initializeTests'
2
-
3
- describe('railcontentLive', function () {
4
- beforeEach(async () => {
5
- await initializeTestService(true)
6
- }, 1000000)
7
- })
@@ -1,32 +0,0 @@
1
- import { initializeTestService } from '../initializeTests.js'
2
- import { getRecommendedForYou } from '../../src/index.js'
3
-
4
- describe('Recommended System', function() {
5
- beforeAll(async () => {
6
- await initializeTestService(true)
7
- })
8
-
9
- test('getRecommendedForYou', async () => {
10
- const results = await getRecommendedForYou('drumeo')
11
- log(results)
12
- expect(results.id).toBeDefined()
13
- expect(results.title).toBeDefined()
14
- expect(results.items).toBeDefined()
15
- expect(results.items.length).toBeGreaterThanOrEqual(1)
16
- })
17
-
18
- test('getRecommendedForYou-SeeAll', async () => {
19
- const results = await getRecommendedForYou('drumeo', 'recommended', { page: 1, limit: 20 })
20
- log(results)
21
- expect(results.type).toBeDefined()
22
- expect(results.data).toBeDefined()
23
- expect(results.meta).toBeDefined()
24
- expect(results.data.length).toBeGreaterThanOrEqual(1)
25
- })
26
-
27
- test('fetchMetadata', async () => {
28
- const response = await fetchMetadata('singeo', 'recent-activities')
29
- log(response)
30
- expect(response.tabs.length).toBeGreaterThan(0)
31
- })
32
- })