musora-content-services 2.167.2 → 2.167.3

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 (131) 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/CHANGELOG.md +8 -0
  4. package/package.json +2 -1
  5. package/src/contentTypeConfig.js +1 -0
  6. package/src/lib/sanity/query.ts +5 -0
  7. package/src/services/config.js +15 -0
  8. package/src/services/content/artist.ts +1 -3
  9. package/src/services/content/counts.ts +47 -0
  10. package/src/services/content/genre.ts +1 -3
  11. package/src/services/content/instructor.ts +1 -3
  12. package/src/services/userActivity.js +2 -0
  13. package/src/version-info.js +1 -0
  14. package/.claude/settings.local.json +0 -53
  15. package/test/initializeTests.js +0 -60
  16. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  17. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  18. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  19. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  20. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  21. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  22. package/test/integration/content-progress/getters.test.ts +0 -334
  23. package/test/integration/content-progress/helpers.test.ts +0 -263
  24. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  25. package/test/integration/forums.test.ts +0 -209
  26. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  27. package/test/integration/initializeTestDB.ts +0 -80
  28. package/test/integration/learning-paths.test.ts +0 -854
  29. package/test/integration/notifications.test.ts +0 -178
  30. package/test/integration/progress-row/content-card.test.ts +0 -283
  31. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  32. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  33. package/test/integration/sync/fetch.test.ts +0 -224
  34. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  35. package/test/integration/sync/repositories/practices.test.ts +0 -179
  36. package/test/integration/sync/repositories/progress.test.ts +0 -245
  37. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  38. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  39. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  40. package/test/integration/sync/store/store-idb.test.ts +0 -180
  41. package/test/integration/sync/store/store.test.ts +0 -361
  42. package/test/integration/user/permissions.test.js +0 -20
  43. package/test/integration/userActivity.test.ts +0 -102
  44. package/test/live/README.md +0 -29
  45. package/test/live/content.test.js +0 -116
  46. package/test/localStorageMock.js +0 -21
  47. package/test/log.js +0 -5
  48. package/test/mockData/award-definitions.js +0 -171
  49. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  50. package/test/mockData/mockData_progress_content.json +0 -182
  51. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  52. package/test/mockData/mockData_user_practices.json +0 -9
  53. package/test/setupConsole.js +0 -6
  54. package/test/setupNetworkGuard.js +0 -3
  55. package/test/setupTimers.js +0 -13
  56. package/test/unit/HttpClient.test.ts +0 -267
  57. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  58. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  59. package/test/unit/awards/award-calculations.test.ts +0 -34
  60. package/test/unit/awards/award-callbacks.test.ts +0 -144
  61. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  62. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  63. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  64. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  65. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  66. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  67. package/test/unit/awards/award-query-messages.test.ts +0 -448
  68. package/test/unit/awards/award-user-collection.test.ts +0 -416
  69. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  70. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  71. package/test/unit/awards/helpers/index.ts +0 -3
  72. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  73. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  74. package/test/unit/awards/message-generator.test.ts +0 -162
  75. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  76. package/test/unit/content-progress/helpers.test.ts +0 -329
  77. package/test/unit/content-progress/navigateTo.test.ts +0 -392
  78. package/test/unit/contentLikes.test.ts +0 -62
  79. package/test/unit/contentMetaData.test.ts +0 -58
  80. package/test/unit/dataContext.test.ts +0 -38
  81. package/test/unit/dateUtils.test.ts +0 -188
  82. package/test/unit/endScreen.test.js +0 -712
  83. package/test/unit/eventsAPI.test.ts +0 -159
  84. package/test/unit/imageSRCBuilder.test.ts +0 -37
  85. package/test/unit/imageSRCVerify.test.ts +0 -155
  86. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  87. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  88. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  89. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  90. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  91. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  92. package/test/unit/lib/filter.test.ts +0 -1154
  93. package/test/unit/lib/lastUpdated.test.ts +0 -21
  94. package/test/unit/lib/query.test.ts +0 -1469
  95. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  96. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  97. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  98. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  99. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  100. package/test/unit/playerSettings.test.ts +0 -73
  101. package/test/unit/progress-row/content-card.test.ts +0 -314
  102. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  103. package/test/unit/progressRows.test.ts +0 -243
  104. package/test/unit/recommendations.test.js +0 -47
  105. package/test/unit/sanityQueryService.test.ts +0 -200
  106. package/test/unit/search.test.ts +0 -44
  107. package/test/unit/sentry.test.ts +0 -62
  108. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  109. package/test/unit/services/progress.test.ts +0 -396
  110. package/test/unit/streakMessage.test.ts +0 -251
  111. package/test/unit/sync/context.test.ts +0 -51
  112. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  113. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  114. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  115. package/test/unit/sync/errors/validators.test.ts +0 -61
  116. package/test/unit/sync/helpers/TestModel.ts +0 -44
  117. package/test/unit/sync/helpers/index.ts +0 -172
  118. package/test/unit/sync/manager.test.ts +0 -303
  119. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  120. package/test/unit/sync/resolver.test.ts +0 -229
  121. package/test/unit/sync/retry.test.ts +0 -314
  122. package/test/unit/sync/run-scope.test.ts +0 -23
  123. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  124. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  125. package/test/unit/sync/store-configs.test.ts +0 -37
  126. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  127. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  128. package/test/unit/sync/utils/throttle.test.ts +0 -245
  129. package/test/unit/url-builder.test.ts +0 -72
  130. package/test/unit/user/account.test.ts +0 -66
  131. package/test/unit/user/permissions.test.js +0 -191
@@ -1,172 +0,0 @@
1
- import { Database } from '@nozbe/watermelondb'
2
- import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
3
- import { SyncTelemetry, SeverityLevel } from '@/services/sync/telemetry/index'
4
- import SyncContext from '@/services/sync/context/index'
5
- import {
6
- BaseSessionProvider,
7
- BaseConnectivityProvider,
8
- BaseVisibilityProvider,
9
- NullTabsProvider,
10
- BaseDurabilityProvider,
11
- } from '@/services/sync/context/providers/index'
12
- import SyncRetry from '@/services/sync/retry'
13
- import SyncRunScope from '@/services/sync/run-scope'
14
- import SyncStore, { type SyncStoreConfig } from '@/services/sync/store/index'
15
- import schema from '@/services/sync/schema/index'
16
- import * as modelClasses from '@/services/sync/models/index'
17
- import type BaseModel from '@/services/sync/models/Base'
18
- import type { ModelClass, SyncUserScope } from '@/services/sync/index'
19
- import type { SyncPullResponse, SyncPushResponse } from '@/services/sync/fetch'
20
-
21
- // --- Sentry / Telemetry ---
22
-
23
- function makeDummySentry() {
24
- return {
25
- captureException: jest.fn(),
26
- captureMessage: jest.fn(),
27
- addBreadcrumb: jest.fn(),
28
- startSpan: jest.fn((options: any, callback: any) => {
29
- const mockSpan = {
30
- _spanId: 'mock-span-id-0000',
31
- _parentSpanId: undefined as string | undefined,
32
- end: jest.fn(),
33
- setStatus: jest.fn(),
34
- setData: jest.fn(),
35
- setAttribute: jest.fn(),
36
- }
37
- return callback ? callback(mockSpan) : mockSpan
38
- }),
39
- logger: {
40
- debug: jest.fn(),
41
- info: jest.fn(),
42
- warn: jest.fn(),
43
- error: jest.fn(),
44
- trace: jest.fn(),
45
- fatal: jest.fn(),
46
- fmt: jest.fn(),
47
- },
48
- }
49
- }
50
-
51
- export function makeTelemetry(userScope: SyncUserScope) {
52
- return new SyncTelemetry(userScope, {
53
- Sentry: makeDummySentry(),
54
- level: SeverityLevel.FATAL, // silence output in tests
55
- pretty: false,
56
- })
57
- }
58
-
59
- // --- Context providers ---
60
-
61
- class TestSessionProvider extends BaseSessionProvider {
62
- getClientId() { return 'test-client-id' }
63
- }
64
-
65
- class TestConnectivityProvider extends BaseConnectivityProvider {
66
- private online = true
67
- getValue() { return this.online }
68
- setOnline(v: boolean) { this.online = v; this['notifyListeners']() }
69
- }
70
-
71
- class TestVisibilityProvider extends BaseVisibilityProvider {
72
- getValue() { return true }
73
- }
74
-
75
- class TestDurabilityProvider extends BaseDurabilityProvider {
76
- getValue() { return true }
77
- failed() {}
78
- }
79
-
80
- export function makeContext() {
81
- return new SyncContext({
82
- session: new TestSessionProvider(),
83
- connectivity: new TestConnectivityProvider(),
84
- visibility: new TestVisibilityProvider(),
85
- tabs: new NullTabsProvider(),
86
- durability: new TestDurabilityProvider(),
87
- })
88
- }
89
-
90
- export const TEST_USER_ID = 1
91
-
92
- export function makeUserScope(): SyncUserScope {
93
- return {
94
- initialId: TEST_USER_ID,
95
- getCurrentId: () => TEST_USER_ID,
96
- }
97
- }
98
-
99
- // --- Database ---
100
-
101
- export function makeDatabase(opts: { useIncrementalIndexedDB?: boolean } = {}) {
102
- const adapter = new LokiJSAdapter({
103
- schema,
104
- useWebWorker: false,
105
- useIncrementalIndexedDB: opts.useIncrementalIndexedDB ?? false,
106
- dbName: `test_${Date.now()}_${Math.random()}`,
107
- extraLokiOptions: { autosave: false },
108
- })
109
-
110
- return new Database({
111
- adapter,
112
- modelClasses: Object.values(modelClasses),
113
- })
114
- }
115
-
116
- // --- Pull / Push mocks ---
117
-
118
- export function makePullMock(overrides: Partial<Extract<SyncPullResponse, { ok: true }>> = {}) {
119
- return jest.fn().mockResolvedValue({
120
- ok: true,
121
- entries: [],
122
- token: Date.now(),
123
- intendedUserId: TEST_USER_ID,
124
- previousToken: null,
125
- ...overrides,
126
- } as Extract<SyncPullResponse, { ok: true }>)
127
- }
128
-
129
- export function makePushMock(overrides: Partial<Extract<SyncPushResponse, { ok: true }>> = {}) {
130
- return jest.fn().mockResolvedValue({
131
- ok: true,
132
- results: [],
133
- ...overrides,
134
- } as Extract<SyncPushResponse, { ok: true }>)
135
- }
136
-
137
- // --- SyncStore ---
138
-
139
- export function makeStore<TModel extends BaseModel>(
140
- model: ModelClass<TModel>,
141
- db: Database,
142
- config: Partial<SyncStoreConfig<TModel>> = {}
143
- ) {
144
- const userScope = makeUserScope()
145
- const context = makeContext()
146
- const telemetry = makeTelemetry(userScope)
147
- const retry = new SyncRetry(context, telemetry)
148
- const runScope = new SyncRunScope()
149
-
150
- const store = new SyncStore<TModel>(
151
- {
152
- model,
153
- pull: makePullMock(),
154
- push: makePushMock(),
155
- ...config,
156
- },
157
- userScope,
158
- context,
159
- db,
160
- retry,
161
- runScope,
162
- telemetry
163
- )
164
-
165
- return { store, userScope, context, telemetry, retry, runScope }
166
- }
167
-
168
- export async function resetDatabase(db: Database) {
169
- await db.write(async () => {
170
- await db.unsafeResetDatabase()
171
- })
172
- }
@@ -1,303 +0,0 @@
1
- import SyncManager from '@/services/sync/manager'
2
- import { SyncTelemetry } from '@/services/sync/telemetry/index'
3
- import { SyncError } from '@/services/sync/errors/index'
4
- import BaseModel from '@/services/sync/models/Base'
5
- import createStoreConfigs from '@/services/sync/store-configs'
6
- import { makeTelemetry, makeContext, makeUserScope, makeDatabase } from './helpers/index'
7
- import type { ModelClass, SyncUserScope } from '@/services/sync/index'
8
-
9
- class DummyModelA extends BaseModel { static table = 'dummy_a' }
10
- class DummyModelB extends BaseModel { static table = 'dummy_b' }
11
- class UnknownModel extends BaseModel { static table = 'not_a_real_table' }
12
-
13
- jest.mock('../../../src/services/awards/internal/content-progress-observer', () => ({
14
- contentProgressObserver: {
15
- start: jest.fn().mockResolvedValue(undefined),
16
- stop: jest.fn(),
17
- },
18
- }))
19
-
20
- // ---
21
-
22
- function makeMockStrategy() {
23
- return {
24
- start: jest.fn(),
25
- stop: jest.fn(),
26
- onTrigger: jest.fn().mockReturnThis(),
27
- }
28
- }
29
-
30
- function makeMockEffect() {
31
- const teardown = jest.fn()
32
- const effect = jest.fn().mockReturnValue(teardown)
33
- return { effect, teardown }
34
- }
35
-
36
- function buildManager(opts: {
37
- userScope?: SyncUserScope
38
- destroy?: jest.Mock
39
- abort?: jest.Mock
40
- } = {}) {
41
- const userScope = opts.userScope ?? makeUserScope()
42
- const context = makeContext()
43
- const teardownDatabase = {
44
- ...(opts.destroy && { destroy: opts.destroy }),
45
- ...(opts.abort && { abort: opts.abort }),
46
- }
47
-
48
- return new SyncManager(userScope, context, () => makeDatabase(), teardownDatabase)
49
- }
50
-
51
- function assignManager(manager: SyncManager) {
52
- return SyncManager.assignAndSetupInstance(manager)
53
- }
54
-
55
- function setupManager(opts: Parameters<typeof buildManager>[0] = {}) {
56
- const manager = buildManager(opts)
57
- const teardown = assignManager(manager)
58
- return { manager, teardown }
59
- }
60
-
61
- // ---
62
-
63
- beforeEach(() => {
64
- SyncTelemetry.setInstance(makeTelemetry(makeUserScope()))
65
- })
66
-
67
- afterEach(() => {
68
- SyncTelemetry.clearInstance()
69
- ;(SyncManager as any).instance = null
70
- })
71
-
72
- // ---
73
-
74
- describe('singleton lifecycle', () => {
75
- test('getInstance throws before any setup', () => {
76
- expect(() => SyncManager.getInstance()).toThrow(SyncError)
77
- })
78
-
79
- test('getInstanceOrNull returns null before any setup', () => {
80
- expect(SyncManager.getInstanceOrNull()).toBeNull()
81
- })
82
-
83
- test('getInstance returns manager after setup', async () => {
84
- const { manager, teardown } = setupManager()
85
- expect(SyncManager.getInstance()).toBe(manager)
86
- await teardown()
87
- })
88
-
89
- test('getInstanceOrNull returns manager after setup', async () => {
90
- const { manager, teardown } = setupManager()
91
- expect(SyncManager.getInstanceOrNull()).toBe(manager)
92
- await teardown()
93
- })
94
-
95
- test('assignAndSetupInstance throws if called while instance exists', async () => {
96
- const { teardown } = setupManager()
97
- expect(() => assignManager(buildManager())).toThrow(SyncError)
98
- await teardown()
99
- })
100
-
101
- test('getInstance throws after teardown', async () => {
102
- const { teardown } = setupManager()
103
- await teardown()
104
- expect(() => SyncManager.getInstance()).toThrow(SyncError)
105
- })
106
-
107
- test('getInstanceOrNull returns null after teardown', async () => {
108
- const { teardown } = setupManager()
109
- await teardown()
110
- expect(SyncManager.getInstanceOrNull()).toBeNull()
111
- })
112
-
113
- test('new instance can be set up after teardown', async () => {
114
- const { teardown: t1 } = setupManager()
115
- await t1()
116
-
117
- const { manager: m2, teardown: t2 } = setupManager()
118
- expect(SyncManager.getInstance()).toBe(m2)
119
- await t2()
120
- })
121
- })
122
-
123
- // ---
124
-
125
- describe('store registry', () => {
126
- test('all configured model tables have stores', async () => {
127
- const { manager, teardown } = setupManager()
128
- const configuredTables = createStoreConfigs().map(c => c.model.table)
129
- const registeredTables = Object.keys(manager.getAllStores())
130
-
131
- expect(registeredTables.sort()).toEqual(configuredTables.sort())
132
- await teardown()
133
- })
134
-
135
- test('getStore returns store for a registered model', async () => {
136
- const { manager, teardown } = setupManager()
137
- const anyConfiguredModel = createStoreConfigs()[0].model as unknown as ModelClass
138
- expect(manager.getStore(anyConfiguredModel)).toBeDefined()
139
- await teardown()
140
- })
141
-
142
- test('getStore throws SyncError for unregistered model', async () => {
143
- const { manager, teardown } = setupManager()
144
- expect(() => manager.getStore(UnknownModel)).toThrow(SyncError)
145
- await teardown()
146
- })
147
- })
148
-
149
- // ---
150
-
151
- describe('getId', () => {
152
- test('successive instances get different ids', async () => {
153
- const { manager: m1, teardown: t1 } = setupManager()
154
- const id1 = m1.getId()
155
- await t1()
156
-
157
- const { manager: m2, teardown: t2 } = setupManager()
158
- const id2 = m2.getId()
159
- await t2()
160
-
161
- expect(id1).not.toBe(id2)
162
- })
163
- })
164
-
165
- // ---
166
-
167
- describe('strategy registration', () => {
168
- test('strategy.start called during setup', async () => {
169
- const manager = buildManager()
170
- const strategy = makeMockStrategy()
171
- manager.registerStrategies([DummyModelA], [strategy])
172
- const teardown = assignManager(manager)
173
-
174
- expect(strategy.start).toHaveBeenCalledTimes(1)
175
- await teardown()
176
- })
177
-
178
- test('strategy.stop called during teardown', async () => {
179
- const manager = buildManager()
180
- const strategy = makeMockStrategy()
181
- manager.registerStrategies([DummyModelA], [strategy])
182
- const teardown = assignManager(manager)
183
- await teardown()
184
-
185
- expect(strategy.stop).toHaveBeenCalledTimes(1)
186
- })
187
-
188
- test('onTrigger called once per registered model', async () => {
189
- const manager = buildManager()
190
- const strategy = makeMockStrategy()
191
- manager.registerStrategies([DummyModelA, DummyModelB], [strategy])
192
- const teardown = assignManager(manager)
193
-
194
- expect(strategy.onTrigger).toHaveBeenCalledTimes(2)
195
- await teardown()
196
- })
197
-
198
- test('multiple strategies all started and stopped', async () => {
199
- const manager = buildManager()
200
- const s1 = makeMockStrategy()
201
- const s2 = makeMockStrategy()
202
- manager.registerStrategies([DummyModelA], [s1, s2])
203
- const teardown = assignManager(manager)
204
- await teardown()
205
-
206
- expect(s1.start).toHaveBeenCalledTimes(1)
207
- expect(s2.start).toHaveBeenCalledTimes(1)
208
- expect(s1.stop).toHaveBeenCalledTimes(1)
209
- expect(s2.stop).toHaveBeenCalledTimes(1)
210
- })
211
- })
212
-
213
- // ---
214
-
215
- describe('effect registration', () => {
216
- test('effect called with context and stores during setup', async () => {
217
- const manager = buildManager()
218
- const { effect, teardown: effectTeardown } = makeMockEffect()
219
- manager.registerEffects([DummyModelA], [effect])
220
- const teardown = assignManager(manager)
221
-
222
- expect(effect).toHaveBeenCalledTimes(1)
223
- expect(effect).toHaveBeenCalledWith(
224
- expect.anything(),
225
- expect.any(Array)
226
- )
227
-
228
- await teardown()
229
- })
230
-
231
- test('effect receives one store per registered model', async () => {
232
- const manager = buildManager()
233
- const { effect } = makeMockEffect()
234
- manager.registerEffects([DummyModelA, DummyModelB], [effect])
235
- const teardown = assignManager(manager)
236
-
237
- const [, stores] = effect.mock.calls[0]
238
- expect(stores).toHaveLength(2)
239
-
240
- await teardown()
241
- })
242
-
243
- test('effect teardown called during manager teardown', async () => {
244
- const manager = buildManager()
245
- const { effect, teardown: effectTeardown } = makeMockEffect()
246
- manager.registerEffects([DummyModelA], [effect])
247
- const teardown = assignManager(manager)
248
- await teardown()
249
-
250
- expect(effectTeardown).toHaveBeenCalledTimes(1)
251
- })
252
- })
253
-
254
- // ---
255
-
256
- describe('teardown modes', () => {
257
- test('reset mode resolves', async () => {
258
- const { teardown } = setupManager()
259
- await expect(teardown('reset')).resolves.toBeUndefined()
260
- })
261
-
262
- test('default mode (no arg) resolves', async () => {
263
- const { teardown } = setupManager()
264
- await expect(teardown()).resolves.toBeUndefined()
265
- })
266
-
267
- test('destroyOrReset calls destroy with dbName and adapter', async () => {
268
- const destroy = jest.fn().mockResolvedValue(undefined)
269
- const { teardown } = setupManager({ destroy })
270
- await expect(teardown('destroyOrReset')).resolves.toBeUndefined()
271
- expect(destroy).toHaveBeenCalledTimes(1)
272
- expect(destroy).toHaveBeenCalledWith(expect.any(String), expect.anything())
273
- })
274
-
275
- test('abortWrites rejects when no implementation provided', async () => {
276
- const { teardown } = setupManager()
277
- await expect(teardown('abortWrites')).rejects.toThrow(
278
- 'Cannot abort writes to database - implementation not provided'
279
- )
280
- })
281
-
282
- test('abortWrites calls abort with underlying adapter', async () => {
283
- const abort = jest.fn().mockResolvedValue(undefined)
284
- const { teardown } = setupManager({ abort })
285
- await expect(teardown('abortWrites')).resolves.toBeUndefined()
286
- expect(abort).toHaveBeenCalledTimes(1)
287
- })
288
-
289
- test('concurrent teardown calls both resolve', async () => {
290
- const { teardown } = setupManager()
291
- await expect(Promise.all([teardown(), teardown()])).resolves.toBeDefined()
292
- })
293
- })
294
-
295
- // ---
296
-
297
- describe('abort', () => {
298
- test('does not throw', async () => {
299
- const { manager, teardown } = setupManager()
300
- expect(() => manager.abort('test reason')).not.toThrow()
301
- await teardown()
302
- })
303
- })
@@ -1,82 +0,0 @@
1
- jest.mock('../../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
- jest.mock('../../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
-
4
- import { Database } from '@nozbe/watermelondb'
5
- import { makeDatabase, makeStore, resetDatabase } from '../helpers/index'
6
- import UserAwardProgress from '../../../../src/services/sync/models/UserAwardProgress'
7
- import UserAwardProgressRepository from '../../../../src/services/sync/repositories/user-award-progress'
8
- let db: Database
9
- let repo: UserAwardProgressRepository
10
- beforeEach(() => {
11
- db = makeDatabase()
12
- const { store } = makeStore(UserAwardProgress, db)
13
- repo = new UserAwardProgressRepository(store)
14
- })
15
- afterEach(async () => {
16
- await resetDatabase(db)
17
- })
18
- describe('UserAwardProgress model', () => {
19
- describe('getters', () => {
20
- test('award_id returns raw string value', async () => {
21
- await repo.recordAwardProgress('test-award-id', 50)
22
- const result = await repo.getByAwardId('test-award-id')
23
- expect(result.data?.award_id).toBe('test-award-id')
24
- })
25
- test('progress_percentage returns raw number value', async () => {
26
- await repo.recordAwardProgress('test-award-id', 75)
27
- const result = await repo.getByAwardId('test-award-id')
28
- expect(result.data?.progress_percentage).toBe(75)
29
- })
30
- test('completed_at returns null when not set', async () => {
31
- await repo.recordAwardProgress('test-award-id', 50)
32
- const result = await repo.getByAwardId('test-award-id')
33
- expect(result.data?.completed_at).toBeNull()
34
- })
35
- test('progress_data parses JSON when set', async () => {
36
- const progressData = { step: 1, total: 5 }
37
- await repo.recordAwardProgress('test-award-id', 50, { progressData })
38
- const result = await repo.getByAwardId('test-award-id')
39
- expect(result.data?.progress_data).toEqual(progressData)
40
- })
41
- test('progress_data returns null when not set', async () => {
42
- await repo.recordAwardProgress('test-award-id', 50)
43
- const result = await repo.getByAwardId('test-award-id')
44
- expect(result.data?.progress_data).toBeNull()
45
- })
46
- test('completion_data parses JSON when set', async () => {
47
- const completionData = {
48
- content_title: 'Blues Foundations',
49
- completed_at: '2024-01-01T00:00:00Z',
50
- days_user_practiced: 14,
51
- practice_minutes: 180,
52
- }
53
- await repo.recordAwardProgress('test-award-id', 100, { completionData })
54
- const result = await repo.getByAwardId('test-award-id')
55
- expect(result.data?.completion_data).toEqual(completionData)
56
- })
57
- test('completion_data returns null when not set', async () => {
58
- await repo.recordAwardProgress('test-award-id', 50)
59
- const result = await repo.getByAwardId('test-award-id')
60
- expect(result.data?.completion_data).toBeNull()
61
- })
62
- })
63
- describe('setters via validators', () => {
64
- test.todo('progress_percentage throws SyncValidationError when value exceeds 100')
65
- test('completed_at accepts null', async () => {
66
- await repo.recordAwardProgress('test-award-id', 50, { completedAt: null })
67
- const result = await repo.getByAwardId('test-award-id')
68
- expect(result.data?.completed_at).toBeNull()
69
- })
70
- test('completion_data serialises to JSON when set', async () => {
71
- const completionData = {
72
- content_title: 'Test',
73
- completed_at: '2024-01-01T00:00:00Z',
74
- days_user_practiced: 5,
75
- practice_minutes: 60,
76
- }
77
- await repo.recordAwardProgress('test-award-id', 100, { completionData })
78
- const result = await repo.getByAwardId('test-award-id')
79
- expect(result.data?.completion_data).toEqual(completionData)
80
- })
81
- })
82
- })