musora-content-services 2.167.2 → 2.167.4

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 (140) hide show
  1. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  2. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  3. package/.idea/codeStyles/Project.xml +61 -0
  4. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/musora-content-services.iml +8 -0
  7. package/.idea/php.xml +19 -0
  8. package/.idea/prettier.xml +6 -0
  9. package/.idea/vcs.xml +7 -0
  10. package/.yarn/install-state.gz +0 -0
  11. package/CHANGELOG.md +10 -0
  12. package/package.json +2 -1
  13. package/src/contentTypeConfig.js +1 -0
  14. package/src/lib/sanity/query.ts +5 -0
  15. package/src/services/config.js +15 -0
  16. package/src/services/content/artist.ts +1 -3
  17. package/src/services/content/counts.ts +47 -0
  18. package/src/services/content/genre.ts +1 -3
  19. package/src/services/content/instructor.ts +1 -3
  20. package/src/services/multi-user-accounts/multi-user-accounts.ts +1 -0
  21. package/src/services/userActivity.js +2 -0
  22. package/src/version-info.js +1 -0
  23. package/.claude/settings.local.json +0 -53
  24. package/test/initializeTests.js +0 -60
  25. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  26. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  27. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  28. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  29. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  30. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  31. package/test/integration/content-progress/getters.test.ts +0 -334
  32. package/test/integration/content-progress/helpers.test.ts +0 -263
  33. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  34. package/test/integration/forums.test.ts +0 -209
  35. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  36. package/test/integration/initializeTestDB.ts +0 -80
  37. package/test/integration/learning-paths.test.ts +0 -854
  38. package/test/integration/notifications.test.ts +0 -178
  39. package/test/integration/progress-row/content-card.test.ts +0 -283
  40. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  41. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  42. package/test/integration/sync/fetch.test.ts +0 -224
  43. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  44. package/test/integration/sync/repositories/practices.test.ts +0 -179
  45. package/test/integration/sync/repositories/progress.test.ts +0 -245
  46. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  47. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  48. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  49. package/test/integration/sync/store/store-idb.test.ts +0 -180
  50. package/test/integration/sync/store/store.test.ts +0 -361
  51. package/test/integration/user/permissions.test.js +0 -20
  52. package/test/integration/userActivity.test.ts +0 -102
  53. package/test/live/README.md +0 -29
  54. package/test/live/content.test.js +0 -116
  55. package/test/localStorageMock.js +0 -21
  56. package/test/log.js +0 -5
  57. package/test/mockData/award-definitions.js +0 -171
  58. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  59. package/test/mockData/mockData_progress_content.json +0 -182
  60. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  61. package/test/mockData/mockData_user_practices.json +0 -9
  62. package/test/setupConsole.js +0 -6
  63. package/test/setupNetworkGuard.js +0 -3
  64. package/test/setupTimers.js +0 -13
  65. package/test/unit/HttpClient.test.ts +0 -267
  66. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  67. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  68. package/test/unit/awards/award-calculations.test.ts +0 -34
  69. package/test/unit/awards/award-callbacks.test.ts +0 -144
  70. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  71. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  72. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  73. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  74. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  75. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  76. package/test/unit/awards/award-query-messages.test.ts +0 -448
  77. package/test/unit/awards/award-user-collection.test.ts +0 -416
  78. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  79. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  80. package/test/unit/awards/helpers/index.ts +0 -3
  81. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  82. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  83. package/test/unit/awards/message-generator.test.ts +0 -162
  84. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  85. package/test/unit/content-progress/helpers.test.ts +0 -329
  86. package/test/unit/content-progress/navigateTo.test.ts +0 -392
  87. package/test/unit/contentLikes.test.ts +0 -62
  88. package/test/unit/contentMetaData.test.ts +0 -58
  89. package/test/unit/dataContext.test.ts +0 -38
  90. package/test/unit/dateUtils.test.ts +0 -188
  91. package/test/unit/endScreen.test.js +0 -712
  92. package/test/unit/eventsAPI.test.ts +0 -159
  93. package/test/unit/imageSRCBuilder.test.ts +0 -37
  94. package/test/unit/imageSRCVerify.test.ts +0 -155
  95. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  96. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  97. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  98. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  99. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  100. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  101. package/test/unit/lib/filter.test.ts +0 -1154
  102. package/test/unit/lib/lastUpdated.test.ts +0 -21
  103. package/test/unit/lib/query.test.ts +0 -1469
  104. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  105. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  106. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  107. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  108. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  109. package/test/unit/playerSettings.test.ts +0 -73
  110. package/test/unit/progress-row/content-card.test.ts +0 -314
  111. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  112. package/test/unit/progressRows.test.ts +0 -243
  113. package/test/unit/recommendations.test.js +0 -47
  114. package/test/unit/sanityQueryService.test.ts +0 -200
  115. package/test/unit/search.test.ts +0 -44
  116. package/test/unit/sentry.test.ts +0 -62
  117. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  118. package/test/unit/services/progress.test.ts +0 -396
  119. package/test/unit/streakMessage.test.ts +0 -251
  120. package/test/unit/sync/context.test.ts +0 -51
  121. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  122. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  123. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  124. package/test/unit/sync/errors/validators.test.ts +0 -61
  125. package/test/unit/sync/helpers/TestModel.ts +0 -44
  126. package/test/unit/sync/helpers/index.ts +0 -172
  127. package/test/unit/sync/manager.test.ts +0 -303
  128. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  129. package/test/unit/sync/resolver.test.ts +0 -229
  130. package/test/unit/sync/retry.test.ts +0 -314
  131. package/test/unit/sync/run-scope.test.ts +0 -23
  132. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  133. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  134. package/test/unit/sync/store-configs.test.ts +0 -37
  135. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  136. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  137. package/test/unit/sync/utils/throttle.test.ts +0 -245
  138. package/test/unit/url-builder.test.ts +0 -72
  139. package/test/unit/user/account.test.ts +0 -66
  140. package/test/unit/user/permissions.test.js +0 -191
@@ -1,51 +0,0 @@
1
- jest.mock('../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
- jest.mock('../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
- import { makeContext } from './helpers/index'
4
- describe('SyncContext', () => {
5
- test('start calls start on all providers', () => {
6
- const context = makeContext() as any
7
- const providers = [
8
- context.session,
9
- context.connectivity,
10
- context.visibility,
11
- context.tabs,
12
- context.durability,
13
- ]
14
- providers.forEach(p => jest.spyOn(p, 'start'))
15
- context.start()
16
- providers.forEach(p => expect(p.start).toHaveBeenCalled())
17
- })
18
- test('stop calls stop on all providers', () => {
19
- const context = makeContext() as any
20
- const providers = [
21
- context.session,
22
- context.connectivity,
23
- context.visibility,
24
- context.tabs,
25
- context.durability,
26
- ]
27
- providers.forEach(p => jest.spyOn(p, 'stop'))
28
- context.stop()
29
- providers.forEach(p => expect(p.stop).toHaveBeenCalled())
30
- })
31
- test('session getter returns session provider', () => {
32
- const context = makeContext()
33
- expect(context.session).toBeDefined()
34
- })
35
- test('connectivity getter returns connectivity provider', () => {
36
- const context = makeContext()
37
- expect(context.connectivity).toBeDefined()
38
- })
39
- test('visibility getter returns visibility provider', () => {
40
- const context = makeContext()
41
- expect(context.visibility).toBeDefined()
42
- })
43
- test('tabs getter returns tabs provider', () => {
44
- const context = makeContext()
45
- expect(context.tabs).toBeDefined()
46
- })
47
- test('durability getter returns durability provider', () => {
48
- const context = makeContext()
49
- expect(context.durability).toBeDefined()
50
- })
51
- })
@@ -1,158 +0,0 @@
1
- import createLogoutWarningEffect from '@/services/sync/effects/logout-warning'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- function makeMockStore(model: any = { table: 'test' }) {
8
- let handler: ((records: any[]) => void) | null = null
9
-
10
- const store = {
11
- model,
12
- collection: {
13
- query: jest.fn().mockReturnValue({
14
- observe: jest.fn().mockReturnValue({
15
- subscribe: (h: (records: any[]) => void) => {
16
- handler = h
17
- return { unsubscribe: () => { handler = null } }
18
- },
19
- }),
20
- }),
21
- },
22
- } as unknown as SyncStore
23
-
24
- const push = (records: any[]) => handler?.(records)
25
-
26
- return { store, push }
27
- }
28
-
29
- // ---
30
-
31
- describe('notification on unsynced records', () => {
32
- test('notifyCallback called when unsynced records appear', () => {
33
- const notify = jest.fn()
34
- const { store, push } = makeMockStore()
35
-
36
- createLogoutWarningEffect(notify)(makeContext(), [store])
37
- push([{ id: 'rec-1' }])
38
-
39
- expect(notify).toHaveBeenCalledTimes(1)
40
- })
41
-
42
- test('notifyCallback called with the model class of the store', () => {
43
- const notify = jest.fn()
44
- const model = { table: 'content_progress' }
45
- const { store, push } = makeMockStore(model)
46
-
47
- createLogoutWarningEffect(notify)(makeContext(), [store])
48
- push([{ id: 'rec-1' }])
49
-
50
- expect(notify.mock.calls[0][0]).toContain(model)
51
- })
52
-
53
- test('notifyCallback called with empty array when records go back to zero', () => {
54
- const notify = jest.fn()
55
- const { store, push } = makeMockStore()
56
-
57
- createLogoutWarningEffect(notify)(makeContext(), [store])
58
- push([{ id: 'rec-1' }])
59
- push([])
60
-
61
- expect(notify).toHaveBeenCalledTimes(2)
62
- expect(notify.mock.calls[1][0]).toHaveLength(0)
63
- })
64
-
65
- test('model removed from unsyncedModels when records cleared', () => {
66
- const notify = jest.fn()
67
- const model = { table: 'content_progress' }
68
- const { store, push } = makeMockStore(model)
69
-
70
- createLogoutWarningEffect(notify)(makeContext(), [store])
71
- push([{ id: 'rec-1' }])
72
- push([])
73
-
74
- expect(notify.mock.calls[1][0]).not.toContain(model)
75
- })
76
- })
77
-
78
- // ---
79
-
80
- describe('multiple stores', () => {
81
- test('tracks unsynced models across stores independently', () => {
82
- const notify = jest.fn()
83
- const modelA = { table: 'content_progress' }
84
- const modelB = { table: 'content_likes' }
85
- const storeA = makeMockStore(modelA)
86
- const storeB = makeMockStore(modelB)
87
-
88
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
89
-
90
- storeA.push([{ id: 'rec-1' }])
91
- storeB.push([{ id: 'rec-2' }])
92
-
93
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
94
- expect(lastCall).toContain(modelA)
95
- expect(lastCall).toContain(modelB)
96
- })
97
-
98
- test('clears only the model whose records were resolved', () => {
99
- const notify = jest.fn()
100
- const modelA = { table: 'content_progress' }
101
- const modelB = { table: 'content_likes' }
102
- const storeA = makeMockStore(modelA)
103
- const storeB = makeMockStore(modelB)
104
-
105
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
106
-
107
- storeA.push([{ id: 'rec-1' }])
108
- storeB.push([{ id: 'rec-2' }])
109
- storeA.push([]) // storeA resolved
110
-
111
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
112
- expect(lastCall).not.toContain(modelA)
113
- expect(lastCall).toContain(modelB)
114
- })
115
-
116
- test('notifies once per store event regardless of other stores', () => {
117
- const notify = jest.fn()
118
- const storeA = makeMockStore({ table: 'a' })
119
- const storeB = makeMockStore({ table: 'b' })
120
-
121
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
122
-
123
- storeA.push([{ id: '1' }])
124
- storeA.push([{ id: '1' }])
125
- storeB.push([{ id: '2' }])
126
-
127
- expect(notify).toHaveBeenCalledTimes(3)
128
- })
129
- })
130
-
131
- // ---
132
-
133
- describe('teardown', () => {
134
- test('teardown unsubscribes all store subscriptions', () => {
135
- const notify = jest.fn()
136
- const { store, push } = makeMockStore()
137
-
138
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [store])
139
- teardown()
140
- push([{ id: 'rec-1' }])
141
-
142
- expect(notify).not.toHaveBeenCalled()
143
- })
144
-
145
- test('teardown unsubscribes all stores', () => {
146
- const notify = jest.fn()
147
- const storeA = makeMockStore({ table: 'a' })
148
- const storeB = makeMockStore({ table: 'b' })
149
-
150
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
151
- teardown()
152
-
153
- storeA.push([{ id: '1' }])
154
- storeB.push([{ id: '2' }])
155
-
156
- expect(notify).not.toHaveBeenCalled()
157
- })
158
- })
@@ -1,196 +0,0 @@
1
- import createPushFailureNotificationEffect, { NOTIFICATION_COOLDOWN, MUTE_PERIOD } from '@/services/sync/effects/push-failure-notification'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- beforeEach(() => {
8
- jest.useFakeTimers()
9
- })
10
-
11
- afterEach(() => {
12
- jest.useRealTimers()
13
- })
14
-
15
- // ---
16
-
17
- function makeMockStore() {
18
- const listeners = new Map<string, (() => void)[]>()
19
-
20
- const store = {
21
- on: jest.fn().mockImplementation((event: string, handler: () => void) => {
22
- const list = listeners.get(event) ?? []
23
- list.push(handler)
24
- listeners.set(event, list)
25
- return () => {
26
- const idx = list.indexOf(handler)
27
- if (idx >= 0) list.splice(idx, 1)
28
- }
29
- }),
30
- } as unknown as SyncStore
31
-
32
- const emit = (event: string) => listeners.get(event)?.forEach(h => h())
33
-
34
- return { store, emit }
35
- }
36
-
37
- function makeEffect(notify = jest.fn()) {
38
- const effect = createPushFailureNotificationEffect(notify)
39
- return { effect, notify }
40
- }
41
-
42
- // ---
43
-
44
- describe('notification triggering', () => {
45
- test('notifyCallback called when failedPush fires', () => {
46
- const { store, emit } = makeMockStore()
47
- const { effect, notify } = makeEffect()
48
-
49
- effect(makeContext(), [store])
50
- emit('failedPush')
51
-
52
- expect(notify).toHaveBeenCalledTimes(1)
53
- })
54
-
55
- test('notifyCallback called with mute function', () => {
56
- const { store, emit } = makeMockStore()
57
- const { effect, notify } = makeEffect()
58
-
59
- effect(makeContext(), [store])
60
- emit('failedPush')
61
-
62
- expect(notify.mock.calls[0][0]).toHaveProperty('mute')
63
- expect(typeof notify.mock.calls[0][0].mute).toBe('function')
64
- })
65
-
66
- test('failedPush on any store triggers notification', () => {
67
- const s1 = makeMockStore()
68
- const s2 = makeMockStore()
69
- const { effect, notify } = makeEffect()
70
-
71
- effect(makeContext(), [s1.store, s2.store])
72
-
73
- s2.emit('failedPush')
74
-
75
- expect(notify).toHaveBeenCalledTimes(1)
76
- })
77
- })
78
-
79
- // ---
80
-
81
- describe('cooldown', () => {
82
- test('second failedPush within cooldown does not notify again', () => {
83
- const { store, emit } = makeMockStore()
84
- const { effect, notify } = makeEffect()
85
-
86
- effect(makeContext(), [store])
87
- emit('failedPush')
88
- emit('failedPush')
89
-
90
- expect(notify).toHaveBeenCalledTimes(1)
91
- })
92
-
93
- test('notifies again after cooldown expires', () => {
94
- const { store, emit } = makeMockStore()
95
- const { effect, notify } = makeEffect()
96
-
97
- effect(makeContext(), [store])
98
- emit('failedPush')
99
-
100
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
101
- emit('failedPush')
102
-
103
- expect(notify).toHaveBeenCalledTimes(2)
104
- })
105
-
106
- test('does not notify just before cooldown expires', () => {
107
- const { store, emit } = makeMockStore()
108
- const { effect, notify } = makeEffect()
109
-
110
- effect(makeContext(), [store])
111
- emit('failedPush')
112
-
113
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN - 1)
114
- emit('failedPush')
115
-
116
- expect(notify).toHaveBeenCalledTimes(1)
117
- })
118
- })
119
-
120
- // ---
121
-
122
- describe('mute', () => {
123
- test('mute() prevents further notifications', () => {
124
- const { store, emit } = makeMockStore()
125
- const { effect, notify } = makeEffect()
126
-
127
- effect(makeContext(), [store])
128
- emit('failedPush')
129
-
130
- const { mute } = notify.mock.calls[0][0]
131
- mute()
132
-
133
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
134
- emit('failedPush')
135
-
136
- expect(notify).toHaveBeenCalledTimes(1)
137
- })
138
-
139
- test('notifications resume after mute period expires', () => {
140
- const { store, emit } = makeMockStore()
141
- const { effect, notify } = makeEffect()
142
-
143
- effect(makeContext(), [store])
144
- emit('failedPush')
145
-
146
- const { mute } = notify.mock.calls[0][0]
147
- mute()
148
-
149
- jest.advanceTimersByTime(MUTE_PERIOD + NOTIFICATION_COOLDOWN)
150
- emit('failedPush')
151
-
152
- expect(notify).toHaveBeenCalledTimes(2)
153
- })
154
-
155
- test('mute does not suppress notification already in progress', () => {
156
- const { store, emit } = makeMockStore()
157
- const { effect, notify } = makeEffect()
158
-
159
- effect(makeContext(), [store])
160
- emit('failedPush')
161
-
162
- expect(notify).toHaveBeenCalledTimes(1)
163
- })
164
- })
165
-
166
- // ---
167
-
168
- describe('teardown', () => {
169
- test('teardown prevents further notifications', () => {
170
- const { store, emit } = makeMockStore()
171
- const { effect, notify } = makeEffect()
172
-
173
- const teardown = effect(makeContext(), [store])
174
- teardown()
175
-
176
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
177
- emit('failedPush')
178
-
179
- expect(notify).not.toHaveBeenCalled()
180
- })
181
-
182
- test('teardown unsubscribes all stores', () => {
183
- const s1 = makeMockStore()
184
- const s2 = makeMockStore()
185
- const { effect, notify } = makeEffect()
186
-
187
- const teardown = effect(makeContext(), [s1.store, s2.store])
188
- teardown()
189
-
190
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
191
- s1.emit('failedPush')
192
- s2.emit('failedPush')
193
-
194
- expect(notify).not.toHaveBeenCalled()
195
- })
196
- })
@@ -1,106 +0,0 @@
1
- import { SyncError, SyncValidationError, SyncAbortError, SyncInitError, SyncSetupError, SyncUnexpectedError } from '../../../../src/services/sync/errors/index'
2
- import { inBoundary } from '../../../../src/services/sync/errors/boundary'
3
- import { SyncTelemetry } from '../../../../src/services/sync/telemetry/index'
4
-
5
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
6
- capture: jest.fn()
7
- } as any)
8
-
9
- describe('SyncError', () => {
10
- test('has correct name property', () => {
11
- const error = new SyncError('test message')
12
- expect(error.name).toBe('SyncError')
13
- })
14
- test('isReported() returns false by default', () => {
15
- const error = new SyncError('test message')
16
- expect(error.isReported()).toBe(false)
17
- })
18
- test('markReported() sets isReported() to true', () => {
19
- const error = new SyncError('test message')
20
- error.markReported()
21
- expect(error.isReported()).toBe(true)
22
- })
23
- test('getDetails() returns details passed to constructor', () => {
24
- const details = { table: 'content_progress', userId: 1 }
25
- const error = new SyncError('test message', details)
26
- expect(error.getDetails()).toEqual(details)
27
- })
28
- })
29
- describe('SyncValidationError', () => {
30
- test('has correct name property', () => {
31
- const error = new SyncValidationError('invalid value', {} as any)
32
- expect(error.name).toBe('SyncValidationError')
33
- })
34
- test('is instanceof SyncError', () => {
35
- const error = new SyncValidationError('invalid value', {} as any)
36
- expect(error).toBeInstanceOf(SyncError)
37
- })
38
- })
39
- describe('SyncAbortError', () => {
40
- test('has default message when none provided', () => {
41
- const error = new SyncAbortError()
42
- expect(error.message).toBe('Sync operation was aborted')
43
- })
44
- test('accepts custom message', () => {
45
- const error = new SyncAbortError('custom abort message')
46
- expect(error.message).toBe('custom abort message')
47
- })
48
- })
49
- describe('SyncInitError', () => {
50
- test('has correct name and stores cause in details', () => {
51
- const cause = new Error('original error')
52
- const error = new SyncInitError(cause)
53
- expect(error.name).toBe('SyncInitError')
54
- expect(error.getDetails()).toEqual({ cause })
55
- })
56
- })
57
- describe('SyncSetupError', () => {
58
- test('is instanceof SyncError', () => {
59
- const error = new SyncSetupError(new Error('setup failed'))
60
- expect(error).toBeInstanceOf(SyncError)
61
- })
62
- })
63
- describe('SyncUnexpectedError', () => {
64
- test('is instanceof SyncError', () => {
65
- const error = new SyncUnexpectedError('unexpected')
66
- expect(error).toBeInstanceOf(SyncError)
67
- })
68
- })
69
-
70
- describe('inBoundary', () => {
71
- let mockCapture: jest.Mock
72
-
73
- beforeEach(() => {
74
- mockCapture = jest.fn()
75
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
76
- capture: mockCapture
77
- } as any)
78
- })
79
- test('returns result of sync function when no error', () => {
80
- const result = inBoundary(() => 'hello')
81
- expect(result).toBe('hello')
82
- expect(mockCapture).not.toHaveBeenCalled()
83
- })
84
- test('throws and calls capture when sync function throws', () => {
85
- const error = new Error('sync error')
86
- expect(() => inBoundary(() => { throw error })).toThrow('sync error')
87
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
88
- })
89
- test('returns resolved value of async function when no error', async () => {
90
- const result = await inBoundary(async () => 'async hello')
91
- expect(result).toBe('async hello')
92
- expect(mockCapture).not.toHaveBeenCalled()
93
- })
94
- test('re-throws and calls capture when async function rejects', async () => {
95
- const error = new Error('async error')
96
- await expect(inBoundary(async () => { throw error })).rejects.toThrow('async error')
97
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
98
- })
99
- test('passes context to fn and to capture on error', () => {
100
- const context = { table: 'content_progress' }
101
- const error = new Error('context error')
102
- expect(() => inBoundary((ctx) => { throw error }, context)).toThrow('context error')
103
- expect(mockCapture).toHaveBeenCalledWith(error, context)
104
- })
105
- })
106
-
@@ -1,61 +0,0 @@
1
- import { percent, nullableString, string, number, varchar, numberInRange, enumValue } from '../../../../src/services/sync/errors/validators'
2
- import { SyncValidationError } from '../../../../src/services/sync/errors/index'
3
- describe('validators', () => {
4
- describe('percent', () => {
5
- test('returns value when within 0-100', () => {
6
- expect(percent(50)).toBe(50)
7
- })
8
- })
9
- test('returns 0 at minimum boundary', () => {
10
- expect(percent(0)).toBe(0)
11
- })
12
- test('returns 100 at maximum boundary', () => {
13
- expect(percent(100)).toBe(100)
14
- })
15
- test('throws SyncValidationError when value exceeds 100', () => {
16
- expect(() => percent(101)).toThrow(SyncValidationError)
17
- })
18
- test('throws SyncValidationError when value is negative', () => {
19
- expect(() => percent(-1)).toThrow(SyncValidationError)
20
- })
21
- })
22
- describe('nullableString', () => {
23
- test('returns null when value is null', () => {
24
- expect(nullableString(null)).toBeNull()
25
- })
26
- test('returns string when value is a string', () => {
27
- expect(nullableString('hello')).toBe('hello')
28
- })
29
- })
30
- describe('string', () => {
31
- test('throws SyncValidationError when value is not a string', () => {
32
- expect(() => string(123)).toThrow(SyncValidationError)
33
- })
34
- })
35
- describe('number', () => {
36
- test('throws SyncValidationError when value is not a number', () => {
37
- expect(() => number('abc')).toThrow(SyncValidationError)
38
- })
39
- })
40
- describe('varchar', () => {
41
- test('returns string when within max length', () => {
42
- expect(varchar(10)('hello')).toBe('hello')
43
- })
44
- test('throws SyncValidationError when string exceeds max length', () => {
45
- expect(() => varchar(5)('toolong')).toThrow(SyncValidationError)
46
- })
47
- })
48
- describe('numberInRange', () => {
49
- test('returns value when within range', () => {
50
- expect(numberInRange(0, 10)(5)).toBe(5)
51
- })
52
- test('throws SyncValidationError when value exceeds max', () => {
53
- expect(() => numberInRange(0, 10)(11)).toThrow(SyncValidationError)
54
- })
55
- })
56
- describe('enumValue', () => {
57
- test('throws SyncValidationError when value is not in enum', () => {
58
- enum TestEnum { A = 'a', B = 'b' }
59
- expect(() => enumValue(TestEnum)('c')).toThrow(SyncValidationError)
60
- })
61
- })
@@ -1,44 +0,0 @@
1
- import { appSchema, tableSchema } from '@nozbe/watermelondb'
2
- import { Database } from '@nozbe/watermelondb'
3
- import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
4
- import BaseModel from '@/services/sync/models/Base'
5
-
6
- export const TEST_TABLE = 'test_items'
7
-
8
- export const testSchema = appSchema({
9
- version: 1,
10
- tables: [
11
- tableSchema({
12
- name: TEST_TABLE,
13
- columns: [
14
- { name: 'server_record_id', type: 'number' },
15
- { name: 'value', type: 'string' },
16
- { name: 'score', type: 'number' },
17
- { name: 'created_at', type: 'number' },
18
- { name: 'updated_at', type: 'number' },
19
- ],
20
- }),
21
- ],
22
- })
23
-
24
- export default class TestModel extends BaseModel {
25
- static table = TEST_TABLE
26
-
27
- get value() { return this._getRaw('value') as string }
28
- get score() { return this._getRaw('score') as number }
29
-
30
- set value(v: string) { this._setRaw('value', v) }
31
- set score(v: number) { this._setRaw('score', v) }
32
- }
33
-
34
- export function makeTestDatabase(opts: { useIncrementalIndexedDB?: boolean; dbName?: string } = {}) {
35
- const adapter = new LokiJSAdapter({
36
- schema: testSchema,
37
- useWebWorker: false,
38
- useIncrementalIndexedDB: opts.useIncrementalIndexedDB ?? false,
39
- dbName: opts.dbName ?? `test_store_${Date.now()}_${Math.random()}`,
40
- extraLokiOptions: { autosave: false },
41
- })
42
-
43
- return new Database({ adapter, modelClasses: [TestModel] })
44
- }