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.
- package/.github/workflows/automated-testing.yml +5 -1
- package/CHANGELOG.md +14 -0
- package/codecov.yml +5 -0
- package/jest.config.js +28 -6
- package/package.json +1 -1
- package/src/index.d.ts +8 -2
- package/src/index.js +8 -2
- package/src/services/content-org/guided-courses.ts +1 -1
- package/src/services/contentProgress.js +34 -34
- package/src/services/multi-user-accounts/multi-user-accounts.ts +43 -7
- package/src/services/sync/adapters/lokijs.ts +2 -2
- package/test/live/README.md +29 -0
- package/test/unit/progressRows.test.js +4 -0
- package/test/live/contentProgressLive.test.js +0 -110
- package/test/live/learningPaths.test.js +0 -70
- package/test/live/railcontentLive.test.js +0 -7
- package/test/live/sanityQueryService.test.js +0 -32
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
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
|
-
//
|
|
26
|
-
//
|
|
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
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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
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
|
-
|
|
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
|
|
579
|
-
|
|
580
|
-
...await bubbleProgress(hierarchy, contentId, collection)
|
|
581
|
-
}
|
|
580
|
+
let allProgresses = {}
|
|
581
|
+
allProgresses[contentId] = progress
|
|
582
582
|
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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 (
|
|
593
|
+
if (isLP) {
|
|
594
|
+
await duplicateProgressToALaCarte(allProgresses, collection, {skipPush: true})
|
|
595
|
+
}
|
|
593
596
|
|
|
594
|
-
for (const [id,
|
|
595
|
-
if (
|
|
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
|
-
|
|
634
|
+
Object.assign(allProgresses, progresses)
|
|
635
|
+
|
|
630
636
|
await bubbleAndTrickleProgressesSafely(progresses, collection, metadata, false)
|
|
631
637
|
|
|
632
638
|
if (isLP) {
|
|
633
|
-
|
|
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
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
})
|