musora-content-services 2.13.0 → 2.15.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 (92) hide show
  1. package/.github/workflows/node.js.yml +0 -0
  2. package/.prettierignore +0 -0
  3. package/.prettierrc +0 -0
  4. package/.yarnrc.yml +1 -0
  5. package/CHANGELOG.md +17 -0
  6. package/babel.config.cjs +0 -0
  7. package/docs/ContentOrganization.html +2 -2
  8. package/docs/Gamification.html +2 -2
  9. package/docs/UserManagementSystem.html +2 -2
  10. package/docs/api_types.js.html +2 -2
  11. package/docs/config.js.html +2 -2
  12. package/docs/content-org_content-org.js.html +2 -2
  13. package/docs/content-org_playlists-types.js.html +4 -2
  14. package/docs/content-org_playlists.js.html +25 -3
  15. package/docs/content.js.html +3 -3
  16. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  17. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  18. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  20. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  34. package/docs/gamification_awards.js.html +2 -2
  35. package/docs/gamification_gamification.js.html +2 -2
  36. package/docs/gamification_types.js.html +2 -2
  37. package/docs/global.html +51 -3
  38. package/docs/index.html +2 -3
  39. package/docs/module-Awards.html +2 -2
  40. package/docs/module-Config.html +2 -2
  41. package/docs/module-Content-Services-V2.html +2 -2
  42. package/docs/module-Interests.html +2 -2
  43. package/docs/module-Permissions.html +2 -2
  44. package/docs/module-Playlists.html +198 -31
  45. package/docs/module-Railcontent-Services.html +168 -17
  46. package/docs/module-Sanity-Services.html +25 -21
  47. package/docs/module-Sessions.html +2 -2
  48. package/docs/module-UserActivity.html +15 -15
  49. package/docs/module-UserChat.html +410 -0
  50. package/docs/module-UserManagement.html +2 -2
  51. package/docs/module-UserNotifications.html +838 -19
  52. package/docs/module-UserProfile.html +2 -2
  53. package/docs/railcontent.js.html +15 -3
  54. package/docs/sanity.js.html +93 -33
  55. package/docs/scripts/collapse.js +0 -0
  56. package/docs/scripts/commonNav.js +0 -0
  57. package/docs/scripts/linenumber.js +0 -0
  58. package/docs/scripts/nav.js +0 -0
  59. package/docs/scripts/polyfill.js +0 -0
  60. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  61. package/docs/scripts/prettify/lang-css.js +0 -0
  62. package/docs/scripts/prettify/prettify.js +0 -0
  63. package/docs/scripts/search.js +0 -0
  64. package/docs/styles/jsdoc.css +0 -0
  65. package/docs/styles/prettify.css +0 -0
  66. package/docs/userActivity.js.html +27 -17
  67. package/docs/user_chat.js.html +98 -0
  68. package/docs/user_interests.js.html +2 -2
  69. package/docs/user_management.js.html +2 -2
  70. package/docs/user_notifications.js.html +125 -5
  71. package/docs/user_permissions.js.html +2 -2
  72. package/docs/user_profile.js.html +2 -2
  73. package/docs/user_sessions.js.html +2 -2
  74. package/docs/user_types.js.html +2 -2
  75. package/docs/user_user-management-system.js.html +2 -2
  76. package/jest.config.js +0 -0
  77. package/link_mcs.sh +0 -0
  78. package/package.json +1 -1
  79. package/src/index.d.ts +14 -1
  80. package/src/index.js +14 -1
  81. package/src/services/content-org/guided-courses.ts +26 -0
  82. package/src/services/contentProgress.js +2 -2
  83. package/src/services/sanity.js +1 -1
  84. package/src/services/user/notifications.js +97 -0
  85. package/test/dataContext.test.js +0 -0
  86. package/test/imageSRCBuilder.test.js +0 -0
  87. package/test/imageSRCVerify.test.js +0 -0
  88. package/test/live/contentProgressLive.test.js +0 -0
  89. package/test/live/railcontentLive.test.js +0 -0
  90. package/test/localStorageMock.js +0 -0
  91. package/test/log.js +0 -0
  92. package/test/notifications.test.js +178 -0
package/src/index.d.ts CHANGED
@@ -5,6 +5,12 @@ import {
5
5
  initializeService
6
6
  } from './services/config.js';
7
7
 
8
+ import {
9
+ enrollUserInGuidedCourse,
10
+ fetchEnrollmentPageMetadata,
11
+ unEnrollUserInGuidedCourse
12
+ } from './services/content-org/guided-courses.js';
13
+
8
14
  import {
9
15
  addItemToPlaylist,
10
16
  createPlaylist,
@@ -237,11 +243,13 @@ import {
237
243
 
238
244
  import {
239
245
  deleteNotification,
246
+ fetchNotificationSettings,
240
247
  fetchNotifications,
241
248
  fetchUnreadCount,
242
249
  markAllNotificationsAsRead,
243
250
  markNotificationAsRead,
244
- markNotificationAsUnread
251
+ markNotificationAsUnread,
252
+ updateNotificationSetting
245
253
  } from './services/user/notifications.js';
246
254
 
247
255
  import {
@@ -309,6 +317,7 @@ declare module 'musora-content-services' {
309
317
  deleteUserActivity,
310
318
  duplicatePlaylist,
311
319
  editComment,
320
+ enrollUserInGuidedCourse,
312
321
  extractSanityUrl,
313
322
  fetchAll,
314
323
  fetchAllCompletedStates,
@@ -338,6 +347,7 @@ declare module 'musora-content-services' {
338
347
  fetchContentInProgress,
339
348
  fetchContentPageUserData,
340
349
  fetchContentProgress,
350
+ fetchEnrollmentPageMetadata,
341
351
  fetchFoundation,
342
352
  fetchGenreLessons,
343
353
  fetchHandler,
@@ -357,6 +367,7 @@ declare module 'musora-content-services' {
357
367
  fetchNewReleases,
358
368
  fetchNextContentDataForParent,
359
369
  fetchNextPreviousLesson,
370
+ fetchNotificationSettings,
360
371
  fetchNotifications,
361
372
  fetchOtherSongVersions,
362
373
  fetchOwnedChallenges,
@@ -481,6 +492,7 @@ declare module 'musora-content-services' {
481
492
  restoreUserPractice,
482
493
  setStudentViewForUser,
483
494
  togglePlaylistPrivate,
495
+ unEnrollUserInGuidedCourse,
484
496
  unassignModeratorToComment,
485
497
  unblockUser,
486
498
  undeletePlaylist,
@@ -488,6 +500,7 @@ declare module 'musora-content-services' {
488
500
  unlikeContent,
489
501
  unlikePlaylist,
490
502
  unpinProgressRow,
503
+ updateNotificationSetting,
491
504
  updatePlaylist,
492
505
  updatePracticeNotes,
493
506
  updateUserPractice,
package/src/index.js CHANGED
@@ -5,6 +5,12 @@ import {
5
5
  initializeService
6
6
  } from './services/config.js';
7
7
 
8
+ import {
9
+ enrollUserInGuidedCourse,
10
+ fetchEnrollmentPageMetadata,
11
+ unEnrollUserInGuidedCourse
12
+ } from './services/content-org/guided-courses.js';
13
+
8
14
  import {
9
15
  addItemToPlaylist,
10
16
  createPlaylist,
@@ -237,11 +243,13 @@ import {
237
243
 
238
244
  import {
239
245
  deleteNotification,
246
+ fetchNotificationSettings,
240
247
  fetchNotifications,
241
248
  fetchUnreadCount,
242
249
  markAllNotificationsAsRead,
243
250
  markNotificationAsRead,
244
- markNotificationAsUnread
251
+ markNotificationAsUnread,
252
+ updateNotificationSetting
245
253
  } from './services/user/notifications.js';
246
254
 
247
255
  import {
@@ -308,6 +316,7 @@ export {
308
316
  deleteUserActivity,
309
317
  duplicatePlaylist,
310
318
  editComment,
319
+ enrollUserInGuidedCourse,
311
320
  extractSanityUrl,
312
321
  fetchAll,
313
322
  fetchAllCompletedStates,
@@ -337,6 +346,7 @@ export {
337
346
  fetchContentInProgress,
338
347
  fetchContentPageUserData,
339
348
  fetchContentProgress,
349
+ fetchEnrollmentPageMetadata,
340
350
  fetchFoundation,
341
351
  fetchGenreLessons,
342
352
  fetchHandler,
@@ -356,6 +366,7 @@ export {
356
366
  fetchNewReleases,
357
367
  fetchNextContentDataForParent,
358
368
  fetchNextPreviousLesson,
369
+ fetchNotificationSettings,
359
370
  fetchNotifications,
360
371
  fetchOtherSongVersions,
361
372
  fetchOwnedChallenges,
@@ -480,6 +491,7 @@ export {
480
491
  restoreUserPractice,
481
492
  setStudentViewForUser,
482
493
  togglePlaylistPrivate,
494
+ unEnrollUserInGuidedCourse,
483
495
  unassignModeratorToComment,
484
496
  unblockUser,
485
497
  undeletePlaylist,
@@ -487,6 +499,7 @@ export {
487
499
  unlikeContent,
488
500
  unlikePlaylist,
489
501
  unpinProgressRow,
502
+ updateNotificationSetting,
490
503
  updatePlaylist,
491
504
  updatePracticeNotes,
492
505
  updateUserPractice,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @module GuidedCourses
3
+ */
4
+ import { globalConfig } from '../config.js'
5
+ import { fetchHandler } from '../railcontent.js'
6
+ import './playlists-types.js'
7
+
8
+
9
+ const excludeFromGeneratedIndex: string[] = []
10
+
11
+ const BASE_PATH: string = `/api/content-org`
12
+
13
+ export async function enrollUserInGuidedCourse(guidedCourse) {
14
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/enroll-user/${guidedCourse}`
15
+ return await fetchHandler(url, 'POST')
16
+ }
17
+
18
+ export async function unEnrollUserInGuidedCourse(guidedCourse) {
19
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/un-enroll-user/${guidedCourse}`
20
+ return await fetchHandler(url, 'POST')
21
+ }
22
+
23
+ export async function fetchEnrollmentPageMetadata(guidedCourse) {
24
+ const url: string = `${BASE_PATH}/v1/user/guided-courses/enrollment/${guidedCourse}`
25
+ return await fetchHandler(url, 'GET')
26
+ }
@@ -198,7 +198,7 @@ export async function assignmentStatusCompleted(assignmentId, parentContentId) {
198
198
  }
199
199
 
200
200
  export async function contentStatusCompleted(contentId) {
201
- await dataContext.update(
201
+ return await dataContext.update(
202
202
  async function (localContext) {
203
203
  let hierarchy = await fetchHierarchy(contentId)
204
204
  completeStatusInLocalContext(localContext, contentId, hierarchy)
@@ -361,7 +361,7 @@ function getMediaTypeId(mediaType, mediaCategory) {
361
361
  case 'practice_play-alongs':
362
362
  return 4
363
363
  case 'video_soundslice':
364
- return 6
364
+ return 6
365
365
  default:
366
366
  return 5
367
367
  }
@@ -1438,7 +1438,7 @@ export async function fetchRelatedLessons(railContentId, brand) {
1438
1438
  pullFutureContent: true,
1439
1439
  }).buildFilter()
1440
1440
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
1441
- const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, web_url_path, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1441
+ const queryFields = `_id, "id":railcontent_id, published_on, "instructor": instructor[0]->name, title, "thumbnail":thumbnail.asset->url, length_in_seconds, status, "type": _type, difficulty, difficulty_string, railcontent_id, artist->,"permission_id": permission[]->railcontent_id,_type, "genre": genre[]->name`
1442
1442
  const queryFieldsWithSort = queryFields + ', sort'
1443
1443
  const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && (!defined(permission) || references(*[_type=='permission']._id))]{
1444
1444
  _type, parent_type, 'parent_id': parent_content_data[0].id, railcontent_id,
@@ -6,6 +6,12 @@ import './types.js'
6
6
 
7
7
  const baseUrl = `/api/notifications`
8
8
 
9
+ const NotificationChannels = {
10
+ EMAIL: 'email',
11
+ PUSH: 'push',
12
+ BELL: 'bell',
13
+ };
14
+
9
15
  /**
10
16
  * Fetches notifications for a given brand with optional filters for unread status and limit.
11
17
  *
@@ -141,3 +147,94 @@ export async function fetchUnreadCount({ brand = null} = {}) {
141
147
  const url = `${baseUrl}/v1/unread-count?brand=${brand}`
142
148
  return fetchHandler(url, 'get')
143
149
  }
150
+
151
+ /**
152
+ * Fetches the notification settings for the current user grouped by brand.
153
+ *
154
+ * @returns {Promise<Object>} A promise that resolves to an object where keys are brands and values are arrays of settings objects.
155
+ *
156
+ * @example
157
+ * fetchNotificationSetting()
158
+ * .then(settings => {
159
+ * console.log(settings);
160
+ * })
161
+ * .catch(error => {
162
+ * console.error(error);
163
+ * });
164
+ */
165
+ export async function fetchNotificationSettings() {
166
+ const url = `/api/notification-settings/v1`;
167
+ const settings = await fetchHandler(url, 'get');
168
+
169
+ if (!settings || typeof settings !== 'object') return {};
170
+
171
+ const result = {};
172
+
173
+ for (const [brand, brandSettings] of Object.entries(settings)) {
174
+ result[brand] = Object.entries(brandSettings).map(([name, value]) => ({
175
+ name,
176
+ ...value,
177
+ }));
178
+ }
179
+
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Updates notification settings for specified channels within a given brand.
185
+ *
186
+ * @param {Object} options - Options to update notification settings.
187
+ * @param {string} options.brand - The brand context for the notification settings.
188
+ * @param {string} options.settingName - The name of the notification setting to update (required).
189
+ * @param {boolean} [options.email] - Whether email notifications are enabled or disabled.
190
+ * @param {boolean} [options.push] - Whether push notifications are enabled or disabled.
191
+ * @param {boolean} [options.bell] - Whether bell notifications are enabled or disabled.
192
+ * @returns {Promise<Object>} - A promise that resolves to the server response after updating settings.
193
+ *
194
+ * @throws {Error} Throws an error if `settingName` is not provided or if no channels are specified.
195
+ *
196
+ * @example
197
+ * updateNotificationSetting({
198
+ * brand: 'drumeo',
199
+ * settingName: 'new_lesson',
200
+ * email: true,
201
+ * push: false,
202
+ * bell: true
203
+ * })
204
+ * .then(response => console.log(response))
205
+ * .catch(error => console.error(error));
206
+ */
207
+ export async function updateNotificationSetting({ brand, settingName, email, push, bell } = {}) {
208
+ if (!settingName) {
209
+ throw new Error('The "settingName" parameter is required.');
210
+ }
211
+
212
+ const channelValues = {
213
+ [NotificationChannels.EMAIL]: email,
214
+ [NotificationChannels.PUSH]: push,
215
+ [NotificationChannels.BELL]: bell,
216
+ };
217
+
218
+ const settings = Object.entries(channelValues)
219
+ .filter(([, value]) => value !== undefined)
220
+ .map(([channel, value]) => ({
221
+ name: settingName,
222
+ channel,
223
+ value,
224
+ brand,
225
+ }));
226
+
227
+ if (settings.length === 0) {
228
+ throw new Error('At least one channel (email, push, or bell) must be provided.');
229
+ }
230
+
231
+ const payload = { settings };
232
+ const url = '/api/notification-settings/v1';
233
+
234
+ return fetchHandler(url, 'PUT', null, payload);
235
+ }
236
+
237
+
238
+
239
+
240
+
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/log.js CHANGED
File without changes
@@ -0,0 +1,178 @@
1
+ import { initializeTestService } from './initializeTests.js'
2
+ import * as UserNotifications from "../src/services/user/notifications.js";
3
+ import { fetchHandler } from '../src/services/railcontent.js'
4
+
5
+
6
+ jest.mock('../src/services/railcontent.js', () => ({
7
+ fetchUserPermissionsData: jest.fn(() => ({ permissions: [78, 91, 92], isAdmin: false })),
8
+ fetchHandler: jest.fn(),
9
+ }))
10
+
11
+ const baseUrl = `/api/notifications`
12
+
13
+ describe('UserNotifications module', function () {
14
+ beforeEach(() => {
15
+ initializeTestService()
16
+ })
17
+
18
+ describe('fetchNotifications', () => {
19
+ it('throws if brand not provided', async () => {
20
+ await expect(UserNotifications.fetchNotifications()).rejects.toThrow('brand is required')
21
+ })
22
+
23
+ it('calls fetchHandler with correct url and method', async () => {
24
+ fetchHandler.mockResolvedValueOnce([{ id: 1 }])
25
+
26
+ const result = await UserNotifications.fetchNotifications({
27
+ brand: 'drumeo',
28
+ limit: 5,
29
+ onlyUnread: true,
30
+ page: 2,
31
+ })
32
+
33
+ expect(fetchHandler).toHaveBeenCalledWith(
34
+ `${baseUrl}/v1?brand=drumeo&unread=1&limit=5&page=2`,
35
+ 'get'
36
+ )
37
+ expect(result).toEqual([{ id: 1 }])
38
+ })
39
+ })
40
+
41
+ describe('markNotificationAsRead', () => {
42
+ it('throws if notificationId not provided', async () => {
43
+ await expect(UserNotifications.markNotificationAsRead()).rejects.toThrow('notificationId is required')
44
+ })
45
+
46
+ it('calls fetchHandler with correct url and method', async () => {
47
+ fetchHandler.mockResolvedValueOnce({ success: true })
48
+
49
+ const result = await UserNotifications.markNotificationAsRead(123)
50
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/read?id=123`, 'put')
51
+ expect(result).toEqual({ success: true })
52
+ })
53
+ })
54
+
55
+ describe('markAllNotificationsAsRead', () => {
56
+ it('calls fetchHandler with correct url and method', async () => {
57
+ fetchHandler.mockResolvedValueOnce({ success: true })
58
+
59
+ const result = await UserNotifications.markAllNotificationsAsRead('drumeo')
60
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/read?brand=drumeo`, 'put')
61
+ expect(result).toEqual({ success: true })
62
+ })
63
+ })
64
+
65
+ describe('markNotificationAsUnread', () => {
66
+ it('throws if notificationId not provided', async () => {
67
+ await expect(UserNotifications.markNotificationAsUnread()).rejects.toThrow('notificationId is required')
68
+ })
69
+
70
+ it('calls fetchHandler with correct url and method', async () => {
71
+ fetchHandler.mockResolvedValueOnce({ success: true })
72
+
73
+ const result = await UserNotifications.markNotificationAsUnread(456)
74
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/unread?id=456`, 'put')
75
+ expect(result).toEqual({ success: true })
76
+ })
77
+ })
78
+
79
+ describe('deleteNotification', () => {
80
+ it('throws if notificationId not provided', async () => {
81
+ await expect(UserNotifications.deleteNotification()).rejects.toThrow('notificationId is required')
82
+ })
83
+
84
+ it('calls fetchHandler with correct url and method', async () => {
85
+ fetchHandler.mockResolvedValueOnce({ success: true })
86
+
87
+ const result = await UserNotifications.deleteNotification(789)
88
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/789`, 'delete')
89
+ expect(result).toEqual({ success: true })
90
+ })
91
+ })
92
+
93
+ describe('fetchUnreadCount', () => {
94
+ it('throws if brand not provided', async () => {
95
+ await expect(UserNotifications.fetchUnreadCount()).rejects.toThrow('brand is required')
96
+ })
97
+
98
+ it('calls fetchHandler with correct url and method', async () => {
99
+ fetchHandler.mockResolvedValueOnce({ unread_count: 42 })
100
+
101
+ const result = await UserNotifications.fetchUnreadCount({ brand: 'drumeo' })
102
+ expect(fetchHandler).toHaveBeenCalledWith(`${baseUrl}/v1/unread-count?brand=drumeo`, 'get')
103
+ expect(result).toEqual({ unread_count: 42 })
104
+ })
105
+ })
106
+
107
+ describe('fetchNotificationSettings', () => {
108
+ it('returns empty object if settings is falsy or not object', async () => {
109
+ fetchHandler.mockResolvedValueOnce(null)
110
+ expect(await UserNotifications.fetchNotificationSettings()).toEqual({})
111
+
112
+ fetchHandler.mockResolvedValueOnce('string')
113
+ expect(await UserNotifications.fetchNotificationSettings()).toEqual({})
114
+ })
115
+
116
+ it('returns transformed settings grouped by brand', async () => {
117
+ fetchHandler.mockResolvedValueOnce({
118
+ drumeo: {
119
+ new_lessons_and_features: { channel: 'email', value: true, brand: 'drumeo' },
120
+ membership_perks_promotions: { channel: 'push', value: false, brand: 'drumeo' },
121
+ },
122
+ pianote: {
123
+ membership_perks_promotions: { channel: 'email', value: true, brand: 'pianote' },
124
+ },
125
+ })
126
+
127
+ const result = await UserNotifications.fetchNotificationSettings()
128
+
129
+ expect(result).toEqual({
130
+ drumeo: [
131
+ { name: 'new_lessons_and_features', channel: 'email', value: true, brand: 'drumeo' },
132
+ { name: 'membership_perks_promotions', channel: 'push', value: false, brand: 'drumeo' },
133
+ ],
134
+ pianote: [{ name: 'membership_perks_promotions', channel: 'email', value: true, brand: 'pianote' }],
135
+ })
136
+ })
137
+ })
138
+
139
+ describe('updateNotificationSetting', () => {
140
+ it('throws if settingName not provided', async () => {
141
+ await expect(UserNotifications.updateNotificationSetting({ brand: 'drumeo' })).rejects.toThrow(
142
+ 'The "settingName" parameter is required.'
143
+ )
144
+ })
145
+
146
+ it('throws if no channels provided', async () => {
147
+ await expect(
148
+ UserNotifications.updateNotificationSetting({ brand: 'drumeo', settingName: 'new_lesson' })
149
+ ).rejects.toThrow('At least one channel (email, push, or bell) must be provided.')
150
+ })
151
+
152
+ it('calls fetchHandler with correct payload and url', async () => {
153
+ fetchHandler.mockResolvedValueOnce({ success: true })
154
+
155
+ const result = await UserNotifications.updateNotificationSetting({
156
+ brand: 'drumeo',
157
+ settingName: 'membership_perks_promotions',
158
+ email: true,
159
+ push: false,
160
+ })
161
+
162
+ expect(fetchHandler).toHaveBeenCalledWith(
163
+ '/api/notification-settings/v1',
164
+ 'PUT',
165
+ null,
166
+ {
167
+ settings: [
168
+ { name: 'membership_perks_promotions', channel: 'email', value: true, brand: 'drumeo' },
169
+ { name: 'membership_perks_promotions', channel: 'push', value: false, brand: 'drumeo' },
170
+ ],
171
+ }
172
+ )
173
+ expect(result).toEqual({ success: true })
174
+ })
175
+ })
176
+
177
+
178
+ })