musora-content-services 2.157.2 → 2.158.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.
Files changed (194) hide show
  1. package/.claude/settings.local.json +17 -5
  2. package/.coderabbit.yaml +0 -0
  3. package/.editorconfig +0 -0
  4. package/.github/pull_request_template.md +0 -0
  5. package/.github/workflows/conventional-commits.yaml +0 -0
  6. package/.github/workflows/docs.js.yml +1 -1
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +30 -0
  10. package/CLAUDE.md +0 -0
  11. package/README.md +0 -0
  12. package/babel.config.cjs +0 -0
  13. package/jest.live.config.js +10 -0
  14. package/package.json +1 -1
  15. package/src/constants/award-assets.js +0 -0
  16. package/src/constants/membership-permissions.ts +0 -0
  17. package/src/contentMetaData.js +0 -0
  18. package/src/contentTypeConfig.js +1 -1
  19. package/src/filterBuilder.js +0 -0
  20. package/src/index.d.ts +5 -0
  21. package/src/index.js +5 -0
  22. package/src/infrastructure/http/HttpClient.ts +0 -0
  23. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  24. package/src/infrastructure/http/index.ts +0 -0
  25. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  26. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  27. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  28. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  29. package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
  30. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  31. package/src/lib/ads/monoid.ts +0 -0
  32. package/src/lib/ads/semigroup.ts +0 -0
  33. package/src/lib/brands.ts +0 -0
  34. package/src/lib/lastUpdated.js +0 -0
  35. package/src/lib/sanity/field-access.ts +0 -0
  36. package/src/lib/sanity/query.ts +0 -0
  37. package/src/services/api/types.js +0 -0
  38. package/src/services/api/types.ts +0 -0
  39. package/src/services/awards/award-callbacks.js +0 -0
  40. package/src/services/awards/award-query.js +0 -0
  41. package/src/services/awards/internal/.indexignore +0 -0
  42. package/src/services/awards/internal/award-definitions.js +0 -0
  43. package/src/services/awards/internal/award-events.js +0 -0
  44. package/src/services/awards/internal/award-manager.js +0 -0
  45. package/src/services/awards/internal/certificate-builder.js +0 -0
  46. package/src/services/awards/internal/completion-data-generator.js +0 -0
  47. package/src/services/awards/internal/content-progress-observer.js +0 -0
  48. package/src/services/awards/internal/image-utils.js +0 -0
  49. package/src/services/awards/internal/message-generator.js +0 -0
  50. package/src/services/awards/internal/types.js +0 -0
  51. package/src/services/awards/types.d.ts +0 -0
  52. package/src/services/awards/types.js +0 -0
  53. package/src/services/config.js +0 -0
  54. package/src/services/content/artist.ts +0 -0
  55. package/src/services/content/content.ts +0 -0
  56. package/src/services/content/genre.ts +0 -0
  57. package/src/services/content/instructor.ts +0 -0
  58. package/src/services/content-org/content-org.js +0 -0
  59. package/src/services/content-org/playlists-types.js +0 -0
  60. package/src/services/content-org/playlists.js +0 -0
  61. package/src/services/contentAggregator.js +0 -0
  62. package/src/services/contentLikes.js +0 -0
  63. package/src/services/dataContext.js +0 -0
  64. package/src/services/dateUtils.js +0 -0
  65. package/src/services/endScreen/README.md +62 -0
  66. package/src/services/endScreen/endScreen.ts +153 -0
  67. package/src/services/endScreen/types.ts +63 -0
  68. package/src/services/eventsAPI.js +0 -0
  69. package/src/services/forums/categories.ts +0 -0
  70. package/src/services/forums/forums.ts +0 -0
  71. package/src/services/forums/posts.ts +0 -0
  72. package/src/services/forums/threads.ts +13 -2
  73. package/src/services/forums/types.ts +0 -0
  74. package/src/services/gamification/awards.ts +0 -0
  75. package/src/services/gamification/gamification.js +0 -0
  76. package/src/services/imageSRCBuilder.js +0 -0
  77. package/src/services/imageSRCVerify.js +0 -0
  78. package/src/services/liveTesting.ts +0 -0
  79. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  80. package/src/services/permissions/README.md +0 -0
  81. package/src/services/progress-events.js +0 -0
  82. package/src/services/progress-row/base.js +0 -0
  83. package/src/services/progress-row/rows/.indexignore +0 -0
  84. package/src/services/progress-row/rows/content-card.js +0 -0
  85. package/src/services/progress-row/rows/playlist-card.js +0 -0
  86. package/src/services/railcontent.js +0 -0
  87. package/src/services/recommendations.js +3 -0
  88. package/src/services/reporting/README.md +0 -0
  89. package/src/services/reporting/types.ts +0 -0
  90. package/src/services/sanity.js +7 -6
  91. package/src/services/sentry/.indexignore +0 -0
  92. package/src/services/sentry/index.ts +0 -0
  93. package/src/services/state.ts +0 -0
  94. package/src/services/sync/.indexignore +0 -0
  95. package/src/services/sync/adapters/factory.ts +0 -0
  96. package/src/services/sync/adapters/lokijs.ts +1 -0
  97. package/src/services/sync/adapters/sqlite.ts +0 -0
  98. package/src/services/sync/context/providers/base.ts +0 -0
  99. package/src/services/sync/context/providers/connectivity.ts +0 -0
  100. package/src/services/sync/context/providers/durability.ts +0 -0
  101. package/src/services/sync/context/providers/index.ts +0 -0
  102. package/src/services/sync/context/providers/session.ts +0 -0
  103. package/src/services/sync/context/providers/tabs.ts +0 -0
  104. package/src/services/sync/context/providers/visibility.ts +0 -0
  105. package/src/services/sync/database/factory.ts +0 -0
  106. package/src/services/sync/debug.ts +0 -0
  107. package/src/services/sync/effects/index.ts +0 -0
  108. package/src/services/sync/effects/logout-warning.ts +0 -0
  109. package/src/services/sync/errors/boundary.ts +0 -0
  110. package/src/services/sync/errors/index.ts +0 -0
  111. package/src/services/sync/errors/validators.ts +0 -0
  112. package/src/services/sync/fetch.ts +0 -0
  113. package/src/services/sync/index.ts +0 -0
  114. package/src/services/sync/manager.ts +0 -0
  115. package/src/services/sync/models/Base.ts +0 -0
  116. package/src/services/sync/models/ContentLike.ts +0 -0
  117. package/src/services/sync/models/ContentProgress.ts +0 -0
  118. package/src/services/sync/models/Practice.ts +0 -0
  119. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  120. package/src/services/sync/models/UserAwardProgress.ts +0 -0
  121. package/src/services/sync/models/index.ts +0 -0
  122. package/src/services/sync/repositories/content-likes.ts +0 -0
  123. package/src/services/sync/repositories/content-progress.ts +0 -0
  124. package/src/services/sync/repositories/index.ts +0 -0
  125. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  126. package/src/services/sync/repositories/practices.ts +0 -0
  127. package/src/services/sync/repositories/user-award-progress.ts +0 -0
  128. package/src/services/sync/repository-proxy.ts +0 -0
  129. package/src/services/sync/resolver.ts +0 -0
  130. package/src/services/sync/run-scope.ts +0 -0
  131. package/src/services/sync/schema/index.ts +0 -0
  132. package/src/services/sync/serializers/index.ts +0 -0
  133. package/src/services/sync/serializers/model.ts +0 -0
  134. package/src/services/sync/serializers/raw.ts +0 -0
  135. package/src/services/sync/store/index.ts +2 -6
  136. package/src/services/sync/store-configs.ts +0 -0
  137. package/src/services/sync/strategies/base.ts +0 -0
  138. package/src/services/sync/strategies/index.ts +0 -0
  139. package/src/services/sync/strategies/initial.ts +0 -0
  140. package/src/services/sync/strategies/polling.ts +0 -0
  141. package/src/services/sync/telemetry/flood-prevention.ts +0 -0
  142. package/src/services/sync/telemetry/sampling.ts +0 -0
  143. package/src/services/sync/utils/event-emitter.ts +0 -0
  144. package/src/services/sync/utils/index.ts +0 -0
  145. package/src/services/sync/utils/throttle.ts +0 -0
  146. package/src/services/sync/utils/timers.ts +0 -0
  147. package/src/services/types.js +0 -0
  148. package/src/services/urlBuilder.ts +0 -17
  149. package/src/services/user/account.ts +0 -0
  150. package/src/services/user/chat.js +0 -0
  151. package/src/services/user/interests.js +0 -0
  152. package/src/services/user/management.js +0 -0
  153. package/src/services/user/memberships.ts +0 -0
  154. package/src/services/user/notifications.js +0 -0
  155. package/src/services/user/payments.ts +0 -0
  156. package/src/services/user/profile.js +1 -1
  157. package/src/services/user/sessions.js +0 -0
  158. package/src/services/user/streakCalculator.ts +0 -0
  159. package/src/services/user/types.js +0 -0
  160. package/src/services/user/user-management-system.js +0 -0
  161. package/test/SKIPPED_TESTS.md +0 -0
  162. package/test/initializeTests.js +0 -0
  163. package/test/integration/content.test.js +0 -0
  164. package/test/integration/contentProgress.test.js +0 -0
  165. package/test/integration/forum.test.js +0 -0
  166. package/test/integration/sanityQueryService.test.js +0 -0
  167. package/test/localStorageMock.js +0 -0
  168. package/test/log.js +0 -0
  169. package/test/mockData/award-definitions.js +0 -0
  170. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  171. package/test/mockData/mockData_progress_content.json +0 -0
  172. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  173. package/test/mockData/mockData_user_practices.json +0 -0
  174. package/test/setupConsole.js +0 -0
  175. package/test/setupNetworkGuard.js +0 -0
  176. package/test/unit/awards/award-callbacks.test.ts +144 -0
  177. package/test/unit/awards/internal/image-utils.test.ts +86 -0
  178. package/test/unit/endScreen.test.js +712 -0
  179. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +39 -0
  180. package/test/unit/infrastructure/FetchRequestExecutor.test.ts +88 -0
  181. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -0
  182. package/test/unit/lib/query.test.ts +0 -0
  183. package/test/unit/progress-row/playlist-card.test.ts +104 -0
  184. package/test/unit/sentry.test.ts +62 -0
  185. package/test/unit/sync/context.test.ts +51 -0
  186. package/test/unit/sync/errors/sync-errors.test.ts +106 -0
  187. package/test/unit/sync/errors/validators.test.ts +61 -0
  188. package/test/unit/sync/models/user-award-progress.test.ts +82 -0
  189. package/test/unit/sync/repositories/user-award-progress.static.test.ts +68 -0
  190. package/test/unit/sync/run-scope.test.ts +23 -0
  191. package/test/unit/sync/store-configs.test.ts +37 -0
  192. package/test/unit/sync/telemetry/sync-telemetry.test.ts +118 -0
  193. package/test/unit/sync/utils/event-emitter.test.ts +64 -0
  194. package/test/unit/url-builder.test.ts +72 -0
@@ -0,0 +1,23 @@
1
+ import SyncRunScope from '../../../src/services/sync/run-scope'
2
+ import { SyncAbortError } from '../../../src/services/sync/errors/index'
3
+ describe('SyncRunScope', () => {
4
+ let scope: SyncRunScope
5
+ beforeEach(() => {
6
+ scope = new SyncRunScope()
7
+ })
8
+ test('signal returns the AbortSignal', () => {
9
+ expect(scope.signal).toBeInstanceOf(AbortSignal)
10
+ })
11
+ test('abort aborts the signal', () => {
12
+ scope.abort('test reason')
13
+ expect(scope.signal.aborted).toBe(true)
14
+ })
15
+ test('abortable runs fn when not aborted and returns result', async () => {
16
+ const result = await scope.abortable(() => Promise.resolve('hello'))
17
+ expect(result).toBe('hello')
18
+ })
19
+ test('abortable rejects with SyncAbortError when already aborted', async () => {
20
+ scope.abort('cancelled')
21
+ await expect(scope.abortable(() => Promise.resolve('hello'))).rejects.toThrow(SyncAbortError)
22
+ })
23
+ })
@@ -0,0 +1,37 @@
1
+ jest.mock('../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
+ jest.mock('../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
+ jest.mock('../../../src/services/sync/fetch', () => ({
4
+ handlePull: jest.fn(() => jest.fn()),
5
+ handlePush: jest.fn(() => jest.fn()),
6
+ makeFetchRequest: jest.fn(() => jest.fn()),
7
+ }))
8
+ import createStoresFromConfig from '../../../src/services/sync/store-configs'
9
+ import ContentProgress from '../../../src/services/sync/models/ContentProgress'
10
+ describe('createStoresFromConfig', () => {
11
+ const configs = createStoresFromConfig()
12
+ const progressConfig = configs.find(c => c.model === ContentProgress)!
13
+ const comparator = progressConfig.comparator!
14
+ describe('ContentProgress comparator', () => {
15
+ test('returns SERVER when server progress_percent is higher', () => {
16
+ const result = comparator(
17
+ { record: { progress_percent: 80 }, meta: { lifecycle: { updated_at: 1000 } } } as any,
18
+ { progress_percent: 50, updated_at: 2000 } as any
19
+ )
20
+ expect(result).toBe('SERVER')
21
+ })
22
+ test('returns LOCAL when local progress_percent is higher', () => {
23
+ const result = comparator(
24
+ { record: { progress_percent: 50 }, meta: { lifecycle: { updated_at: 2000 } } } as any,
25
+ { progress_percent: 80, updated_at: 1000 } as any
26
+ )
27
+ expect(result).toBe('LOCAL')
28
+ })
29
+ test('returns SERVER when server updated_at is newer and either side is 0', () => {
30
+ const result = comparator(
31
+ { record: { progress_percent: 0 }, meta: { lifecycle: { updated_at: 2000 } } } as any,
32
+ { progress_percent: 50, updated_at: 1000 } as any
33
+ )
34
+ expect(result).toBe('SERVER')
35
+ })
36
+ })
37
+ })
@@ -0,0 +1,118 @@
1
+ import { SyncTelemetry, SeverityLevel } from '../../../../src/services/sync/telemetry/index'
2
+ import { SyncError, SyncUnexpectedError } from '../../../../src/services/sync/errors/index'
3
+ import { makeUserScope } from '../helpers/index'
4
+ const makeSentry = () => ({
5
+ captureException: jest.fn(),
6
+ captureMessage: jest.fn(),
7
+ addBreadcrumb: jest.fn(),
8
+ startSpan: jest.fn(),
9
+ logger: {
10
+ debug: jest.fn(),
11
+ info: jest.fn(),
12
+ },
13
+ })
14
+ const makeTelemetry = (level = SeverityLevel.DEBUG) => {
15
+ const sentry = makeSentry()
16
+ const telemetry = new SyncTelemetry(makeUserScope(), {
17
+ Sentry: sentry,
18
+ level,
19
+ pretty: false,
20
+ })
21
+ return { telemetry, sentry }
22
+ }
23
+ describe('SyncTelemetry', () => {
24
+ afterEach(() => {
25
+ SyncTelemetry.clearInstance()
26
+ })
27
+ describe('static methods', () => {
28
+ test('setInstance and getInstance round-trip', () => {
29
+ const { telemetry } = makeTelemetry()
30
+ SyncTelemetry.setInstance(telemetry)
31
+ expect(SyncTelemetry.getInstance()).toBe(telemetry)
32
+ })
33
+ test('clearInstance sets instance to null', () => {
34
+ const { telemetry } = makeTelemetry()
35
+ SyncTelemetry.setInstance(telemetry)
36
+ SyncTelemetry.clearInstance()
37
+ expect(SyncTelemetry.getInstance()).toBeNull()
38
+ })
39
+ test('isSyncConsoleMessage returns true for sync-prefixed messages', () => {
40
+ expect(SyncTelemetry.isSyncConsoleMessage(['📡 SYNC: something'])).toBe(true)
41
+ })
42
+ test('isSyncConsoleMessage returns false for non-sync messages', () => {
43
+ expect(SyncTelemetry.isSyncConsoleMessage(['regular message'])).toBe(false)
44
+ })
45
+ })
46
+ describe('capture', () => {
47
+ // BUG: condition checks `err instanceof SyncUnexpectedError` but err is the original Error,
48
+ // not the wrapped one. mechanism: { handled: false } is never triggered for plain Errors.
49
+ // Should be checking `wrapped instanceof SyncUnexpectedError` instead.
50
+ test('calls captureException with undefined when err is a plain Error', () => {
51
+ const { telemetry, sentry } = makeTelemetry()
52
+ const error = new Error('raw error')
53
+ telemetry.capture(error)
54
+ expect(sentry.captureException).toHaveBeenCalledWith(error, undefined)
55
+ })
56
+ test('passes mechanism handled:false when err is SyncUnexpectedError', () => {
57
+ const { telemetry, sentry } = makeTelemetry()
58
+ const error = new SyncUnexpectedError('unexpected')
59
+ telemetry.capture(error)
60
+ expect(sentry.captureException).toHaveBeenCalledWith(error, { mechanism: { handled: false } })
61
+ })
62
+ test('passes SyncError directly without wrapping', () => {
63
+ const { telemetry, sentry } = makeTelemetry()
64
+ const error = new SyncError('sync error')
65
+ telemetry.capture(error)
66
+ expect(sentry.captureException).toHaveBeenCalledWith(error, undefined)
67
+ })
68
+ test('marks error as reported after capture', () => {
69
+ const { telemetry } = makeTelemetry()
70
+ const error = new SyncError('sync error')
71
+ telemetry.capture(error)
72
+ expect(error.isReported()).toBe(true)
73
+ })
74
+ })
75
+ describe('ignoreLike and shouldIgnoreMessage', () => {
76
+ test('ignoreLike with string suppresses matching messages', () => {
77
+ const { telemetry, sentry } = makeTelemetry()
78
+ telemetry.ignoreLike('ignored message')
79
+ telemetry.debug('ignored message')
80
+ expect(sentry.addBreadcrumb).not.toHaveBeenCalled()
81
+ })
82
+ test('ignoreLike with RegExp suppresses matching messages', () => {
83
+ const { telemetry, sentry } = makeTelemetry()
84
+ telemetry.ignoreLike(/ignore.*/)
85
+ telemetry.debug('ignore this')
86
+ expect(sentry.addBreadcrumb).not.toHaveBeenCalled()
87
+ })
88
+ test('shouldIgnoreRejection returns true for matching error', () => {
89
+ const { telemetry } = makeTelemetry()
90
+ telemetry.ignoreLike('abort')
91
+ expect(telemetry.shouldIgnoreRejection(new Error('abort error'))).toBe(true)
92
+ })
93
+ test('shouldIgnoreException returns true for matching Error', () => {
94
+ const { telemetry } = makeTelemetry()
95
+ telemetry.ignoreLike('abort')
96
+ expect(telemetry.shouldIgnoreException(new Error('abort error'))).toBe(true)
97
+ })
98
+ })
99
+ describe('level filtering', () => {
100
+ test('debug does not call Sentry when level is above DEBUG', () => {
101
+ const { telemetry, sentry } = makeTelemetry(SeverityLevel.ERROR)
102
+ telemetry.debug('debug message')
103
+ expect(sentry.addBreadcrumb).not.toHaveBeenCalled()
104
+ })
105
+ test('warn calls captureException with correct level', () => {
106
+ const { telemetry, sentry } = makeTelemetry()
107
+ const error = new Error('warn error')
108
+ telemetry.warn(error)
109
+ expect(sentry.captureException).toHaveBeenCalledWith(error, { level: 'warning' })
110
+ })
111
+ test('error calls captureException with correct level', () => {
112
+ const { telemetry, sentry } = makeTelemetry()
113
+ const error = new Error('error message')
114
+ telemetry.error(error)
115
+ expect(sentry.captureException).toHaveBeenCalledWith(error, { level: 'error' })
116
+ })
117
+ })
118
+ })
@@ -0,0 +1,64 @@
1
+ import EventEmitter from '../../../../src/services/sync/utils/event-emitter'
2
+ type TestEvents = {
3
+ data: [string]
4
+ update: [number, boolean]
5
+ }
6
+ describe('EventEmitter', () => {
7
+ let emitter: EventEmitter<TestEvents>
8
+ beforeEach(() => {
9
+ emitter = new EventEmitter()
10
+ })
11
+ test('on registers a listener and returns an unsubscribe function', () => {
12
+ const listener = jest.fn()
13
+ const unsubscribe = emitter.on('data', listener)
14
+ expect(typeof unsubscribe).toBe('function')
15
+ })
16
+ test('emit calls all registered listeners with correct args', () => {
17
+ const listener = jest.fn()
18
+ emitter.on('data', listener)
19
+ emitter.emit('data', 'hello')
20
+ expect(listener).toHaveBeenCalledWith('hello')
21
+ })
22
+ test('off with a specific function removes only that listener', () => {
23
+ const first = jest.fn()
24
+ const second = jest.fn()
25
+ emitter.on('data', first)
26
+ emitter.on('data', second)
27
+ emitter.off('data', first)
28
+ emitter.emit('data', 'hello')
29
+ expect(first).not.toHaveBeenCalled()
30
+ expect(second).toHaveBeenCalledWith('hello')
31
+ })
32
+ test('off without a function removes all listeners for that event', () => {
33
+ const first = jest.fn()
34
+ const second = jest.fn()
35
+ emitter.on('data', first)
36
+ emitter.on('data', second)
37
+ emitter.off('data')
38
+ emitter.emit('data', 'hello')
39
+ expect(first).not.toHaveBeenCalled()
40
+ expect(second).not.toHaveBeenCalled()
41
+ })
42
+ test('unsubscribe function returned by on removes the listener', () => {
43
+ const listener = jest.fn()
44
+ const unsubscribe = emitter.on('data', listener)
45
+ unsubscribe()
46
+ emitter.emit('data', 'hello')
47
+ expect(listener).not.toHaveBeenCalled()
48
+ })
49
+ test('emit on an event with no listeners does nothing', () => {
50
+ expect(() => emitter.emit('data', 'hello')).not.toThrow()
51
+ })
52
+ test('multiple listeners on same event all receive the emit', () => {
53
+ const first = jest.fn()
54
+ const second = jest.fn()
55
+ const third = jest.fn()
56
+ emitter.on('data', first)
57
+ emitter.on('data', second)
58
+ emitter.on('data', third)
59
+ emitter.emit('data', 'hello')
60
+ expect(first).toHaveBeenCalledWith('hello')
61
+ expect(second).toHaveBeenCalledWith('hello')
62
+ expect(third).toHaveBeenCalledWith('hello')
63
+ })
64
+ })
@@ -0,0 +1,72 @@
1
+ import { generateForumPostUrl, generatePlaylistUrl, generateContentUrl } from '../../src/services/urlBuilder'
2
+ jest.mock('../../src/services/config.js', () => ({
3
+ globalConfig: { frontendUrl: 'https://www.musora.com' }
4
+ }))
5
+ jest.mock('../../src/services/sanity.js', () => ({
6
+ ...jest.requireActual('../../src/services/sanity.js'),
7
+ fetchByRailContentIds: jest.fn(),
8
+ }))
9
+ jest.mock('../../src/services/contentProgress.js', () => ({
10
+ getNavigateTo: jest.fn(),
11
+ }))
12
+ describe('generateForumPostUrl', () => {
13
+ test('returns path without domain by default', () => {
14
+ const result = generateForumPostUrl({ brand: 'drumeo' })
15
+ expect(result).toBe('/drumeo/forums/jump-to-post/')
16
+ })
17
+ test('returns full URL with domain when withDomain is true', () => {
18
+ const result = generateForumPostUrl({ brand: 'drumeo' }, true)
19
+ expect(result).toBe('https://www.musora.com/drumeo/forums/jump-to-post/')
20
+ })
21
+ })
22
+
23
+ describe('generatePlaylistUrl', () => {
24
+ test('returns /playlists/{id} without domain', () => {
25
+ const result = generatePlaylistUrl({ id: 123 })
26
+ expect(result).toBe('/playlists/123')
27
+ })
28
+ test('returns full URL with domain when withDomain is true', () => {
29
+ const result = generatePlaylistUrl({ id: 123 }, true)
30
+ expect(result).toBe('https://www.musora.com/playlists/123')
31
+ })
32
+ })
33
+
34
+ describe('generateContentUrl', () => {
35
+ test('returns /{brand}/method for type method', async () => {
36
+ const result = await generateContentUrl({ id: 123, type: 'method', brand: 'drumeo' })
37
+ expect(result).toBe('/drumeo/method')
38
+ })
39
+ test('returns # when id is missing', async () => {
40
+ const result = await generateContentUrl({ id: 0, type: 'song', brand: 'drumeo' })
41
+ expect(result).toBe('#')
42
+ })
43
+ test('returns /{brand}/lessons/{id}/live for type live', async () => {
44
+ const result = await generateContentUrl({ id: 123, type: 'live', brand: 'drumeo' })
45
+ expect(result).toBe('/drumeo/lessons/123/live')
46
+ })
47
+ test('returns /{brand}/lessons/course-collection/overview/{id} for type course-collection', async () => {
48
+ const result = await generateContentUrl({ id: 123, type: 'course-collection', brand: 'drumeo' })
49
+ expect(result).toBe('/drumeo/lessons/course-collection/overview/123')
50
+ })
51
+ test('returns /{brand}/lessons/pack/{id} for type pack', async () => {
52
+ const result = await generateContentUrl({ id: 123, type: 'pack', brand: 'drumeo' })
53
+ expect(result).toBe('/drumeo/lessons/pack/123')
54
+ })
55
+ test('returns /{brand}/lessons/pack-bundle/{id}/{navigateTo.id} for pack-bundle with navigateTo', async () => {
56
+ const result = await generateContentUrl({
57
+ id: 123,
58
+ type: 'pack-bundle',
59
+ brand: 'drumeo',
60
+ navigateTo: { id: 456 }
61
+ })
62
+ expect(result).toBe('/drumeo/lessons/pack-bundle/123/456')
63
+ })
64
+ test('returns /{brand}/lessons/pack-bundle/{id} for pack-bundle without navigateTo', async () => {
65
+ const result = await generateContentUrl({ id: 123, type: 'pack-bundle', brand: 'drumeo' })
66
+ expect(result).toBe('/drumeo/lessons/pack-bundle/123')
67
+ })
68
+ test('returns /{brand}/songs/transcription/{id} for type song', async () => {
69
+ const result = await generateContentUrl({ id: 123, type: 'song', brand: 'drumeo' })
70
+ expect(result).toBe('/drumeo/songs/transcription/123')
71
+ })
72
+ })