musora-content-services 2.94.8 → 2.95.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 (68) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CLAUDE.md +408 -0
  3. package/babel.config.cjs +10 -0
  4. package/jsdoc.json +2 -1
  5. package/package.json +2 -2
  6. package/src/constants/award-assets.js +35 -0
  7. package/src/filterBuilder.js +7 -2
  8. package/src/index.d.ts +26 -5
  9. package/src/index.js +26 -5
  10. package/src/services/awards/award-callbacks.js +126 -0
  11. package/src/services/awards/award-query.js +327 -0
  12. package/src/services/awards/internal/.indexignore +1 -0
  13. package/src/services/awards/internal/award-definitions.js +239 -0
  14. package/src/services/awards/internal/award-events.js +102 -0
  15. package/src/services/awards/internal/award-manager.js +162 -0
  16. package/src/services/awards/internal/certificate-builder.js +66 -0
  17. package/src/services/awards/internal/completion-data-generator.js +84 -0
  18. package/src/services/awards/internal/content-progress-observer.js +137 -0
  19. package/src/services/awards/internal/image-utils.js +62 -0
  20. package/src/services/awards/internal/message-generator.js +17 -0
  21. package/src/services/awards/internal/types.js +5 -0
  22. package/src/services/awards/types.d.ts +79 -0
  23. package/src/services/awards/types.js +101 -0
  24. package/src/services/config.js +24 -4
  25. package/src/services/content-org/learning-paths.ts +19 -15
  26. package/src/services/gamification/awards.ts +114 -83
  27. package/src/services/progress-events.js +58 -0
  28. package/src/services/progress-row/method-card.js +20 -5
  29. package/src/services/sanity.js +1 -1
  30. package/src/services/sync/fetch.ts +10 -2
  31. package/src/services/sync/manager.ts +6 -0
  32. package/src/services/sync/models/ContentProgress.ts +5 -6
  33. package/src/services/sync/models/UserAwardProgress.ts +55 -0
  34. package/src/services/sync/models/index.ts +1 -0
  35. package/src/services/sync/repositories/content-progress.ts +47 -25
  36. package/src/services/sync/repositories/index.ts +1 -0
  37. package/src/services/sync/repositories/practices.ts +16 -1
  38. package/src/services/sync/repositories/user-award-progress.ts +133 -0
  39. package/src/services/sync/repository-proxy.ts +6 -0
  40. package/src/services/sync/retry.ts +12 -11
  41. package/src/services/sync/schema/index.ts +18 -3
  42. package/src/services/sync/store/index.ts +53 -8
  43. package/src/services/sync/store/push-coalescer.ts +3 -3
  44. package/src/services/sync/store-configs.ts +7 -1
  45. package/src/services/userActivity.js +0 -1
  46. package/test/HttpClient.test.js +6 -6
  47. package/test/awards/award-alacarte-observer.test.js +196 -0
  48. package/test/awards/award-auto-refresh.test.js +83 -0
  49. package/test/awards/award-calculations.test.js +33 -0
  50. package/test/awards/award-certificate-display.test.js +328 -0
  51. package/test/awards/award-collection-edge-cases.test.js +210 -0
  52. package/test/awards/award-collection-filtering.test.js +285 -0
  53. package/test/awards/award-completion-flow.test.js +213 -0
  54. package/test/awards/award-exclusion-handling.test.js +273 -0
  55. package/test/awards/award-multi-lesson.test.js +241 -0
  56. package/test/awards/award-observer-integration.test.js +325 -0
  57. package/test/awards/award-query-messages.test.js +438 -0
  58. package/test/awards/award-user-collection.test.js +412 -0
  59. package/test/awards/duplicate-prevention.test.js +118 -0
  60. package/test/awards/helpers/completion-mock.js +54 -0
  61. package/test/awards/helpers/index.js +3 -0
  62. package/test/awards/helpers/mock-setup.js +69 -0
  63. package/test/awards/helpers/progress-emitter.js +39 -0
  64. package/test/awards/message-generator.test.js +162 -0
  65. package/test/initializeTests.js +6 -0
  66. package/test/mockData/award-definitions.js +171 -0
  67. package/test/sync/models/award-database-integration.test.js +519 -0
  68. package/tools/generate-index.cjs +9 -0
@@ -1,4 +1,5 @@
1
1
  export { default as ContentLikesRepository } from './content-likes'
2
2
  export { default as ContentProgressRepository } from './content-progress'
3
3
  export { default as PracticesRepository } from './practices'
4
+ export { default as UserAwardProgressRepository } from './user-award-progress'
4
5
  export { default as PracticeDayNotesRepository } from './practice-day-notes'
@@ -1,8 +1,23 @@
1
- import SyncRepository from "./base";
1
+ import SyncRepository, { Q } from "./base";
2
2
  import Practice from "../models/Practice";
3
3
  import { RecordId } from "@nozbe/watermelondb";
4
4
 
5
5
  export default class PracticesRepository extends SyncRepository<Practice> {
6
+ async sumPracticeMinutesForContent(contentIds: number[]): Promise<number> {
7
+ if (contentIds.length === 0) return 0
8
+
9
+ const practices = await this.queryAll(
10
+ Q.where('content_id', Q.oneOf(contentIds))
11
+ )
12
+
13
+ const totalSeconds = practices.data.reduce(
14
+ (sum, practice) => sum + practice.duration_seconds,
15
+ 0
16
+ )
17
+
18
+ return Math.round(totalSeconds / 60)
19
+ }
20
+
6
21
  async trackAutoPractice(contentId: number, date: string, incrementalDurationSeconds: number) {
7
22
  return await this.upsertOne(PracticesRepository.generateAutoId(contentId, date), r => {
8
23
  r._raw.id = PracticesRepository.generateAutoId(contentId, date);
@@ -0,0 +1,133 @@
1
+ import { Q } from '@nozbe/watermelondb'
2
+ import UserAwardProgress from '../models/UserAwardProgress'
3
+ import SyncRepository from './base'
4
+ import type { AwardDefinition, CompletionData } from '../../awards/types'
5
+ import type { ModelSerialized } from '../serializers'
6
+
7
+ type AwardProgressData = {
8
+ completed_at: number | null
9
+ progress_percentage: number
10
+ }
11
+
12
+ export default class UserAwardProgressRepository extends SyncRepository<UserAwardProgress> {
13
+ static isCompleted(progress: AwardProgressData): boolean {
14
+ return progress.completed_at !== null && progress.progress_percentage === 100
15
+ }
16
+
17
+ static isInProgress(progress: AwardProgressData): boolean {
18
+ return progress.progress_percentage > 0 && !UserAwardProgressRepository.isCompleted(progress)
19
+ }
20
+
21
+ static completedAtDate(progress: { completed_at: number | null }): Date | null {
22
+ return progress.completed_at ? new Date(progress.completed_at) : null
23
+ }
24
+
25
+ async getAll(options?: {
26
+ limit?: number
27
+ onlyCompleted?: boolean
28
+ }) {
29
+ const clauses = []
30
+
31
+ if (options?.onlyCompleted) {
32
+ clauses.push(Q.where('completed_at', Q.notEq(null)))
33
+ }
34
+
35
+ clauses.push(Q.sortBy('updated_at', Q.desc))
36
+
37
+ if (options?.limit) {
38
+ clauses.push(Q.take(options.limit))
39
+ }
40
+
41
+ return this.queryAll(...clauses as any)
42
+ }
43
+
44
+ async getCompleted(limit?: number) {
45
+ return this.getAll({ onlyCompleted: true, limit })
46
+ }
47
+
48
+ async getInProgress(limit?: number) {
49
+ const clauses: any[] = [
50
+ Q.where('progress_percentage', Q.gt(0)),
51
+ Q.where('completed_at', Q.eq(null)),
52
+ Q.sortBy('progress_percentage', Q.desc)
53
+ ]
54
+
55
+ if (limit) {
56
+ clauses.push(Q.take(limit))
57
+ }
58
+
59
+ return this.queryAll(...clauses)
60
+ }
61
+
62
+ async getByAwardId(awardId: string) {
63
+ return this.readOne(awardId)
64
+ }
65
+
66
+ async hasCompletedAward(awardId: string): Promise<boolean> {
67
+ const result = await this.readOne(awardId)
68
+ if (!result.data) return false
69
+ return UserAwardProgressRepository.isCompleted(result.data)
70
+ }
71
+
72
+ async recordAwardProgress(
73
+ awardId: string,
74
+ progressPercentage: number,
75
+ options?: {
76
+ completedAt?: number | null
77
+ progressData?: any
78
+ completionData?: CompletionData | null
79
+ immediate?: boolean
80
+ }
81
+ ) {
82
+ const builder = (record: UserAwardProgress) => {
83
+ record.award_id = awardId
84
+ record.progress_percentage = progressPercentage
85
+
86
+ if (options?.completedAt !== undefined) {
87
+ record.completed_at = options.completedAt
88
+ }
89
+
90
+ if (options?.progressData !== undefined) {
91
+ record.progress_data = options.progressData
92
+ }
93
+
94
+ if (options?.completionData !== undefined) {
95
+ record.completion_data = options.completionData
96
+ }
97
+ }
98
+
99
+ return this.upsertOne(awardId, builder)
100
+ }
101
+
102
+ async completeAward(
103
+ awardId: string,
104
+ completionData: CompletionData
105
+ ) {
106
+ return this.recordAwardProgress(awardId, 100, {
107
+ completedAt: Date.now(),
108
+ completionData,
109
+ immediate: true
110
+ })
111
+ }
112
+
113
+ async getAwardsForContent(contentId: number): Promise<{
114
+ definitions: AwardDefinition[]
115
+ progress: Map<string, ModelSerialized<UserAwardProgress>>
116
+ }> {
117
+ const { awardDefinitions } = await import('../../awards/internal/award-definitions')
118
+
119
+ const definitions = await awardDefinitions.getByContentId(contentId)
120
+
121
+ const awardIds = definitions.map(d => d._id)
122
+ const progressMap = new Map<string, ModelSerialized<UserAwardProgress>>()
123
+
124
+ for (const awardId of awardIds) {
125
+ const result = await this.getByAwardId(awardId)
126
+ if (result.data) {
127
+ progressMap.set(awardId, result.data)
128
+ }
129
+ }
130
+
131
+ return { definitions, progress: progressMap }
132
+ }
133
+ }
@@ -7,10 +7,12 @@ import {
7
7
  PracticesRepository,
8
8
  PracticeDayNotesRepository
9
9
  } from "./repositories"
10
+ import UserAwardProgressRepository from "./repositories/user-award-progress"
10
11
  import {
11
12
  ContentLike,
12
13
  ContentProgress,
13
14
  Practice,
15
+ UserAwardProgress,
14
16
  PracticeDayNote
15
17
  } from "./models"
16
18
 
@@ -19,6 +21,7 @@ interface SyncRepositories {
19
21
  likes: ContentLikesRepository;
20
22
  contentProgress: ContentProgressRepository;
21
23
  practices: PracticesRepository;
24
+ userAwardProgress: UserAwardProgressRepository;
22
25
  practiceDayNotes: PracticeDayNotesRepository;
23
26
  }
24
27
 
@@ -47,6 +50,9 @@ const proxy = new Proxy({} as SyncRepositories, {
47
50
  case 'practices':
48
51
  cache.practices = new PracticesRepository(manager.getStore(Practice));
49
52
  break;
53
+ case 'userAwardProgress':
54
+ cache.userAwardProgress = new UserAwardProgressRepository(manager.getStore(UserAwardProgress));
55
+ break;
50
56
  case 'practiceDayNotes':
51
57
  cache.practiceDayNotes = new PracticeDayNotesRepository(manager.getStore(PracticeDayNote));
52
58
  break;
@@ -32,16 +32,10 @@ export default class SyncRetry {
32
32
  * Runs the given syncFn with automatic retries.
33
33
  * Returns the first successful result or the last failed result after retries.
34
34
  */
35
- async request<T extends SyncResponse>(spanOpts: StartSpanOptions, syncFn: (span: Span) => Promise<T>) {
35
+ async request<T extends SyncResponse>(spanOpts: StartSpanOptions, syncFn: (span: Span) => Promise<T | void>) {
36
36
  let attempt = 0
37
37
 
38
38
  while (true) {
39
- if (!this.context.connectivity.getValue()) {
40
- this.telemetry.debug('[Retry] No connectivity - skipping')
41
- this.paused = true
42
- return { ok: false } as T
43
- }
44
-
45
39
  const now = Date.now()
46
40
  if (now < this.backoffUntil) {
47
41
  await this.sleep(this.backoffUntil - now)
@@ -50,15 +44,22 @@ export default class SyncRetry {
50
44
  attempt++
51
45
 
52
46
  const spanOptions = { ...spanOpts, name: `${spanOpts.name}:attempt:${attempt}/${this.MAX_ATTEMPTS}`, op: `${spanOpts.op}:attempt` }
53
- const result = await this.telemetry.trace(spanOptions, span => syncFn(span))
47
+ const result = await this.telemetry.trace(spanOptions, span => {
48
+ if (!this.context.connectivity.getValue()) {
49
+ this.telemetry.debug('[Retry] No connectivity - skipping')
50
+ return { ok: false } as T
51
+ }
52
+
53
+ return syncFn(span)
54
+ })
55
+
56
+ if (!result) return result
54
57
 
55
58
  if (result.ok) {
56
59
  this.resetBackoff()
57
60
  return result
58
61
  } else {
59
- const isRetryable = 'isRetryable' in result ? result.isRetryable : false
60
-
61
- if (isRetryable) {
62
+ if (result.failureType === 'fetch' && result.isRetryable) {
62
63
  this.scheduleBackoff()
63
64
  if (attempt >= this.MAX_ATTEMPTS) return result
64
65
  } else {
@@ -4,7 +4,8 @@ export const SYNC_TABLES = {
4
4
  CONTENT_LIKES: 'content_likes',
5
5
  CONTENT_PROGRESS: 'progress',
6
6
  PRACTICES: 'practices',
7
- PRACTICE_DAY_NOTES: 'practice_day_notes'
7
+ PRACTICE_DAY_NOTES: 'practice_day_notes',
8
+ USER_AWARD_PROGRESS: 'user_award_progress'
8
9
  }
9
10
 
10
11
  const contentLikesTable = tableSchema({
@@ -24,7 +25,7 @@ const contentProgressTable = tableSchema({
24
25
  { name: 'collection_id', type: 'number', isOptional: true, isIndexed: true },
25
26
  { name: 'state', type: 'string', isIndexed: true },
26
27
  { name: 'progress_percent', type: 'number' },
27
- { name: 'resume_time_seconds', type: 'number' },
28
+ { name: 'resume_time_seconds', type: 'number', isOptional: true },
28
29
  { name: 'created_at', type: 'number' },
29
30
  { name: 'updated_at', type: 'number', isIndexed: true }
30
31
  ]
@@ -55,12 +56,26 @@ const practiceDayNotesTable = tableSchema({
55
56
  ]
56
57
  })
57
58
 
59
+ const userAwardProgressTable = tableSchema({
60
+ name: SYNC_TABLES.USER_AWARD_PROGRESS,
61
+ columns: [
62
+ { name: 'award_id', type: 'string', isIndexed: true },
63
+ { name: 'progress_percentage', type: 'number' },
64
+ { name: 'completed_at', type: 'number', isOptional: true, isIndexed: true },
65
+ { name: 'progress_data', type: 'string', isOptional: true },
66
+ { name: 'completion_data', type: 'string', isOptional: true },
67
+ { name: 'created_at', type: 'number' },
68
+ { name: 'updated_at', type: 'number', isIndexed: true }
69
+ ]
70
+ })
71
+
58
72
  export default appSchema({
59
73
  version: 1,
60
74
  tables: [
61
75
  contentLikesTable,
62
76
  contentProgressTable,
63
77
  practicesTable,
64
- practiceDayNotesTable
78
+ practiceDayNotesTable,
79
+ userAwardProgressTable
65
80
  ]
66
81
  })
@@ -1,7 +1,7 @@
1
1
  import { Database, Q, type Collection, type RecordId } from '@nozbe/watermelondb'
2
2
  import { RawSerializer, ModelSerializer } from '../serializers'
3
3
  import { ModelClass, SyncToken, SyncEntry, SyncContext, EpochMs } from '..'
4
- import { SyncPullResponse, SyncPushResponse, PushPayload } from '../fetch'
4
+ import { SyncPullResponse, SyncPushResponse, SyncPushFailureResponse, PushPayload } from '../fetch'
5
5
  import type SyncRetry from '../retry'
6
6
  import type SyncRunScope from '../run-scope'
7
7
  import EventEmitter from '../utils/event-emitter'
@@ -17,6 +17,7 @@ import { type WriterInterface } from '@nozbe/watermelondb/Database/WorkQueue'
17
17
  import type LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
18
18
  import { SyncError } from '../errors'
19
19
 
20
+
20
21
  type SyncPull = (
21
22
  session: BaseSessionProvider,
22
23
  previousFetchToken: SyncToken | null,
@@ -244,15 +245,39 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
244
245
  const existing = await writer.callReader(() => this.queryMaybeDeletedRecords(Q.where('id', Q.oneOf(ids))))
245
246
  const existingMap = existing.reduce((map, record) => map.set(record.id, record), new Map<RecordId, TModel>())
246
247
 
247
- const destroyedBuilds = existing.filter(record => record._raw._status === 'deleted').map(record => {
248
- return new this.model(this.collection, { id: record.id }).prepareDestroyPermanently()
248
+ const destroyedBuilds = []
249
+ const recreateBuilds: Array<{ id: RecordId; created_at: EpochMs; builder: (record: TModel) => void }> = []
250
+
251
+ existing.forEach(record => {
252
+ if (record._raw._status === 'deleted') {
253
+ destroyedBuilds.push(new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
254
+ } else if (record._raw._status === 'created' && builders[record.id]) {
255
+ // Workaround for WatermelonDB bug: prepareUpdate() doesn't commit field changes
256
+ // for records with _status='created'. Destroy and recreate to ensure updates persist.
257
+ destroyedBuilds.push(new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
258
+ recreateBuilds.push({
259
+ id: record.id,
260
+ created_at: record._raw.created_at,
261
+ builder: builders[record.id]
262
+ })
263
+ }
249
264
  })
265
+
250
266
  const newBuilds = Object.entries(builders).map(([id, builder]) => {
251
267
  const existing = existingMap.get(id)
268
+ const recreate = recreateBuilds.find(r => r.id === id)
252
269
 
253
- if (existing && existing._raw._status !== 'deleted') {
270
+ if (recreate) {
271
+ return this.collection.prepareCreate(record => {
272
+ record._raw.id = id
273
+ record._raw.created_at = recreate.created_at
274
+ record._raw.updated_at = this.generateTimestamp()
275
+ record._raw._status = 'created'
276
+ builder(record)
277
+ })
278
+ } else if (existing && existing._raw._status !== 'deleted' && existing._raw._status !== 'created') {
254
279
  return existing.prepareUpdate(builder)
255
- } else {
280
+ } else if (!existing || existing._raw._status === 'deleted') {
256
281
  return this.collection.prepareCreate(record => {
257
282
  const now = this.generateTimestamp()
258
283
 
@@ -262,7 +287,8 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
262
287
  builder(record)
263
288
  })
264
289
  }
265
- })
290
+ return null
291
+ }).filter((build): build is ReturnType<typeof this.collection.prepareCreate> => build !== null)
266
292
 
267
293
  await writer.batch(...destroyedBuilds)
268
294
  await writer.batch(...newBuilds)
@@ -455,12 +481,26 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
455
481
  const records = await this.queryMaybeDeletedRecords(Q.where('_status', Q.notEq('synced')))
456
482
 
457
483
  if (records.length) {
484
+ const recordIds = records.map(r => r.id)
485
+ const updatedAtMap = new Map<RecordId, EpochMs>()
486
+ records.forEach(record => {
487
+ updatedAtMap.set(record.id, record._raw.updated_at)
488
+ })
489
+
458
490
  this.pushCoalescer.push(
459
491
  records,
460
492
  queueThrottle({ state: this.pushThrottleState }, () => {
461
- return this.retry.request(
493
+ return this.retry.request<SyncPushResponse>(
462
494
  { name: `push:${this.model.table}`, op: 'push', parentSpan: span },
463
- (span) => this.executePush(records, span)
495
+ async (span) => {
496
+ // re-query records since this fn may be deferred due to throttling/retries
497
+ const currentRecords = await this.queryMaybeDeletedRecords(Q.where('id', Q.oneOf(recordIds)))
498
+ const recordsToPush = currentRecords.filter(r => r._raw.updated_at <= (updatedAtMap.get(r.id) || 0))
499
+
500
+ if (recordsToPush.length) {
501
+ return this.executePush(recordsToPush, span)
502
+ }
503
+ }
464
504
  )
465
505
  })
466
506
  )
@@ -468,6 +508,11 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
468
508
  }
469
509
 
470
510
  private async executePull(span?: Span) {
511
+ if (!this.context.connectivity.getValue()) {
512
+ this.telemetry.debug('[Retry] No connectivity - skipping')
513
+ return { ok: false } as SyncPushFailureResponse
514
+ }
515
+
471
516
  return this.telemetry.trace(
472
517
  {
473
518
  name: `pull:${this.model.table}:run`,
@@ -4,7 +4,7 @@ import { EpochMs } from ".."
4
4
  import { SyncPushResponse } from "../fetch"
5
5
 
6
6
  type PushIntent = {
7
- promise: Promise<SyncPushResponse>
7
+ promise: Promise<void | SyncPushResponse>
8
8
  records: {
9
9
  id: RecordId
10
10
  updatedAt: EpochMs
@@ -18,7 +18,7 @@ export default class PushCoalescer {
18
18
  this.intents = []
19
19
  }
20
20
 
21
- push(records: BaseModel[], pusher: (records: BaseModel[]) => Promise<SyncPushResponse>) {
21
+ push(records: BaseModel[], pusher: (records: BaseModel[]) => Promise<void | SyncPushResponse>) {
22
22
  const found = this.find(records)
23
23
 
24
24
  if (found) {
@@ -28,7 +28,7 @@ export default class PushCoalescer {
28
28
  return this.add(pusher(records), records)
29
29
  }
30
30
 
31
- private add(promise: Promise<SyncPushResponse>, records: BaseModel[]) {
31
+ private add(promise: Promise<void | SyncPushResponse>, records: BaseModel[]) {
32
32
  const intent = {
33
33
  promise,
34
34
  records: records.map(record => ({
@@ -1,5 +1,5 @@
1
1
  import { SyncStoreConfig } from "./store"
2
- import { ContentLike, ContentProgress, Practice, PracticeDayNote } from "./models"
2
+ import { ContentLike, ContentProgress, Practice, UserAwardProgress, PracticeDayNote } from "./models"
3
3
  import { handlePull, handlePush, makeFetchRequest } from "./fetch"
4
4
 
5
5
  import type SyncStore from "./store"
@@ -36,6 +36,12 @@ export default function createStoresFromConfig(createStore: <TModel extends Base
36
36
  model: PracticeDayNote,
37
37
  pull: handlePull(makeFetchRequest('/api/user/practices/v1/notes')),
38
38
  push: handlePush(makeFetchRequest('/api/user/practices/v1/notes', { method: 'POST' })),
39
+ }),
40
+
41
+ createStore({
42
+ model: UserAwardProgress,
43
+ pull: handlePull(makeFetchRequest('/api/content/v1/user/awards')),
44
+ push: handlePush(makeFetchRequest('/api/content/v1/user/awards', { method: 'POST' })),
39
45
  })
40
46
  ] as unknown as SyncStore<BaseModel>[]
41
47
  }
@@ -17,7 +17,6 @@ import {
17
17
  fetchShows,
18
18
  } from './sanity'
19
19
  import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
20
- import { guidedCourses } from './content-org/guided-courses'
21
20
  import {
22
21
  getMonday,
23
22
  getWeekNumber,
@@ -65,7 +65,7 @@ describe('HttpClient', () => {
65
65
  expect(mockRequestExecutor.execute).toHaveBeenCalledWith(
66
66
  `${baseUrl}${url}`,
67
67
  expect.objectContaining({
68
- method: 'get',
68
+ method: 'GET',
69
69
  headers: expect.objectContaining({
70
70
  Authorization: `Bearer ${token}`,
71
71
  }),
@@ -82,7 +82,7 @@ describe('HttpClient', () => {
82
82
  expect(mockRequestExecutor.execute).toHaveBeenCalledWith(
83
83
  `${baseUrl}${url}`,
84
84
  expect.objectContaining({
85
- method: 'post',
85
+ method: 'POST',
86
86
  headers: expect.objectContaining({
87
87
  Authorization: `Bearer ${token}`,
88
88
  }),
@@ -100,7 +100,7 @@ describe('HttpClient', () => {
100
100
  expect(mockRequestExecutor.execute).toHaveBeenCalledWith(
101
101
  `${baseUrl}${url}`,
102
102
  expect.objectContaining({
103
- method: 'put',
103
+ method: 'PUT',
104
104
  headers: expect.objectContaining({
105
105
  Authorization: `Bearer ${token}`,
106
106
  }),
@@ -118,7 +118,7 @@ describe('HttpClient', () => {
118
118
  expect(mockRequestExecutor.execute).toHaveBeenCalledWith(
119
119
  `${baseUrl}${url}`,
120
120
  expect.objectContaining({
121
- method: 'patch',
121
+ method: 'PATCH',
122
122
  headers: expect.objectContaining({
123
123
  Authorization: `Bearer ${token}`,
124
124
  }),
@@ -135,7 +135,7 @@ describe('HttpClient', () => {
135
135
  expect(mockRequestExecutor.execute).toHaveBeenCalledWith(
136
136
  `${baseUrl}${url}`,
137
137
  expect.objectContaining({
138
- method: 'delete',
138
+ method: 'DELETE',
139
139
  headers: expect.objectContaining({
140
140
  Authorization: `Bearer ${token}`,
141
141
  }),
@@ -249,7 +249,7 @@ describe('HttpClient', () => {
249
249
  await expect(httpClient.get('/test')).rejects.toMatchObject({
250
250
  message: 'Network error',
251
251
  url: '/test',
252
- method: 'get',
252
+ method: 'GET',
253
253
  originalError: networkError,
254
254
  })
255
255
  })