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.
- package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
- package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
- package/.idea/codeStyles/Project.xml +61 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/modules.xml +8 -0
- package/.idea/musora-content-services.iml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +7 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +10 -0
- package/package.json +2 -1
- package/src/contentTypeConfig.js +1 -0
- package/src/lib/sanity/query.ts +5 -0
- package/src/services/config.js +15 -0
- package/src/services/content/artist.ts +1 -3
- package/src/services/content/counts.ts +47 -0
- package/src/services/content/genre.ts +1 -3
- package/src/services/content/instructor.ts +1 -3
- package/src/services/multi-user-accounts/multi-user-accounts.ts +1 -0
- package/src/services/userActivity.js +2 -0
- package/src/version-info.js +1 -0
- package/.claude/settings.local.json +0 -53
- package/test/initializeTests.js +0 -60
- package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
- package/test/integration/awards/internal/image-utils.test.ts +0 -86
- package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
- package/test/integration/content-progress/contentProgress.test.ts +0 -335
- package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
- package/test/integration/content-progress/e2eProgress.test.ts +0 -612
- package/test/integration/content-progress/getters.test.ts +0 -334
- package/test/integration/content-progress/helpers.test.ts +0 -263
- package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
- package/test/integration/forums.test.ts +0 -209
- package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
- package/test/integration/initializeTestDB.ts +0 -80
- package/test/integration/learning-paths.test.ts +0 -854
- package/test/integration/notifications.test.ts +0 -178
- package/test/integration/progress-row/content-card.test.ts +0 -283
- package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
- package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
- package/test/integration/sync/fetch.test.ts +0 -224
- package/test/integration/sync/repositories/content-likes.test.ts +0 -99
- package/test/integration/sync/repositories/practices.test.ts +0 -179
- package/test/integration/sync/repositories/progress.test.ts +0 -245
- package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
- package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
- package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
- package/test/integration/sync/store/store-idb.test.ts +0 -180
- package/test/integration/sync/store/store.test.ts +0 -361
- package/test/integration/user/permissions.test.js +0 -20
- package/test/integration/userActivity.test.ts +0 -102
- package/test/live/README.md +0 -29
- package/test/live/content.test.js +0 -116
- package/test/localStorageMock.js +0 -21
- package/test/log.js +0 -5
- package/test/mockData/award-definitions.js +0 -171
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
- package/test/mockData/mockData_progress_content.json +0 -182
- package/test/mockData/mockData_sanity_progress_content.json +0 -1451
- package/test/mockData/mockData_user_practices.json +0 -9
- package/test/setupConsole.js +0 -6
- package/test/setupNetworkGuard.js +0 -3
- package/test/setupTimers.js +0 -13
- package/test/unit/HttpClient.test.ts +0 -267
- package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
- package/test/unit/awards/award-auto-refresh.test.ts +0 -84
- package/test/unit/awards/award-calculations.test.ts +0 -34
- package/test/unit/awards/award-callbacks.test.ts +0 -144
- package/test/unit/awards/award-certificate-display.test.ts +0 -330
- package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
- package/test/unit/awards/award-collection-filtering.test.ts +0 -287
- package/test/unit/awards/award-completion-flow.test.ts +0 -306
- package/test/unit/awards/award-multi-lesson.test.ts +0 -243
- package/test/unit/awards/award-observer-integration.test.ts +0 -327
- package/test/unit/awards/award-query-messages.test.ts +0 -448
- package/test/unit/awards/award-user-collection.test.ts +0 -416
- package/test/unit/awards/duplicate-prevention.test.ts +0 -120
- package/test/unit/awards/helpers/completion-mock.ts +0 -56
- package/test/unit/awards/helpers/index.ts +0 -3
- package/test/unit/awards/helpers/mock-setup.ts +0 -79
- package/test/unit/awards/helpers/progress-emitter.ts +0 -45
- package/test/unit/awards/message-generator.test.ts +0 -162
- package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
- package/test/unit/content-progress/helpers.test.ts +0 -329
- package/test/unit/content-progress/navigateTo.test.ts +0 -392
- package/test/unit/contentLikes.test.ts +0 -62
- package/test/unit/contentMetaData.test.ts +0 -58
- package/test/unit/dataContext.test.ts +0 -38
- package/test/unit/dateUtils.test.ts +0 -188
- package/test/unit/endScreen.test.js +0 -712
- package/test/unit/eventsAPI.test.ts +0 -159
- package/test/unit/imageSRCBuilder.test.ts +0 -37
- package/test/unit/imageSRCVerify.test.ts +0 -155
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
- package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
- package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
- package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
- package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
- package/test/unit/lib/filter.test.ts +0 -1154
- package/test/unit/lib/lastUpdated.test.ts +0 -21
- package/test/unit/lib/query.test.ts +0 -1469
- package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
- package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
- package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
- package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
- package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
- package/test/unit/playerSettings.test.ts +0 -73
- package/test/unit/progress-row/content-card.test.ts +0 -314
- package/test/unit/progress-row/playlist-card.test.ts +0 -104
- package/test/unit/progressRows.test.ts +0 -243
- package/test/unit/recommendations.test.js +0 -47
- package/test/unit/sanityQueryService.test.ts +0 -200
- package/test/unit/search.test.ts +0 -44
- package/test/unit/sentry.test.ts +0 -62
- package/test/unit/services/progress-internal/bubble.test.ts +0 -279
- package/test/unit/services/progress.test.ts +0 -396
- package/test/unit/streakMessage.test.ts +0 -251
- package/test/unit/sync/context.test.ts +0 -51
- package/test/unit/sync/effects/logout-warning.test.ts +0 -158
- package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
- package/test/unit/sync/errors/sync-errors.test.ts +0 -106
- package/test/unit/sync/errors/validators.test.ts +0 -61
- package/test/unit/sync/helpers/TestModel.ts +0 -44
- package/test/unit/sync/helpers/index.ts +0 -172
- package/test/unit/sync/manager.test.ts +0 -303
- package/test/unit/sync/models/user-award-progress.test.ts +0 -82
- package/test/unit/sync/resolver.test.ts +0 -229
- package/test/unit/sync/retry.test.ts +0 -314
- package/test/unit/sync/run-scope.test.ts +0 -23
- package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
- package/test/unit/sync/store/push-coalescer.test.ts +0 -156
- package/test/unit/sync/store-configs.test.ts +0 -37
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
- package/test/unit/sync/utils/event-emitter.test.ts +0 -64
- package/test/unit/sync/utils/throttle.test.ts +0 -245
- package/test/unit/url-builder.test.ts +0 -72
- package/test/unit/user/account.test.ts +0 -66
- 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
|
-
})
|