UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

313 lines (271 loc) 11.7 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies (same pattern as get-bundles.action.test.ts) vi.mock('@memberjunction/actions', () => ({ BaseAction: class BaseAction { protected async InternalRunAction(): Promise<unknown> { return {}; } }, })); vi.mock('@memberjunction/global', () => ({ RegisterClass: () => (target: unknown) => target, })); vi.mock('@memberjunction/core', () => ({ UserInfo: class UserInfo {}, Metadata: vi.fn(), RunView: vi.fn().mockImplementation(() => ({ RunView: vi.fn().mockResolvedValue({ Success: true, Results: [] }), })), })); vi.mock('@memberjunction/core-entities', () => ({ MJCompanyIntegrationEntity: class MJCompanyIntegrationEntity { CompanyID: string = ''; APIKey: string | null = null; AccessToken: string | null = null; ExternalSystemID: string | null = null; CustomAttribute1: string | null = null; }, })); vi.mock('@memberjunction/actions-base', () => ({ ActionParam: class ActionParam { Name: string = ''; Value: unknown = null; Type: string = 'Input'; }, })); import { UserInfo } from '@memberjunction/core'; import { UpdateUserProgressAction } from '../providers/learnworlds/actions/update-user-progress.action'; import { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; /** * Helper to create a mock UserInfo for test context */ function createMockContextUser(): UserInfo { return { ID: 'test-user-id', Name: 'Test User', Email: 'test@example.com' } as unknown as UserInfo; } /** * Creates a mock enrollment progress object as returned by LW API */ function createMockEnrollment(overrides: Record<string, unknown> = {}): Record<string, unknown> { return { progress_percentage: 40, completed_units: 4, total_units: 10, total_time_spent: 3600, last_accessed_at: '2024-06-20T12:00:00Z', completed: false, completed_at: undefined, ...overrides, }; } describe('UpdateUserProgressAction', () => { let action: UpdateUserProgressAction; let contextUser: UserInfo; beforeEach(() => { action = new UpdateUserProgressAction(); contextUser = createMockContextUser(); }); describe('UpdateProgress() typed method', () => { it('should update lesson progress successfully', async () => { const currentEnrollment = createMockEnrollment(); const lessonUpdateResult = { success: true }; const updatedEnrollment = createMockEnrollment({ progress_percentage: 50, completed_units: 5, total_time_spent: 4200, last_accessed_at: '2024-06-21T10:00:00Z', }); vi.spyOn(action as never, 'makeLearnWorldsRequest') .mockResolvedValueOnce(currentEnrollment as never) // fetchCurrentEnrollment .mockResolvedValueOnce(lessonUpdateResult as never) // updateLessonProgress .mockResolvedValueOnce(updatedEnrollment as never); // fetchUpdatedEnrollment const result = await action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', LessonID: 'lesson-1', ProgressPercentage: 100, Completed: true, TimeSpent: 600, }, contextUser, ); expect(result.ProgressDetails.userId).toBe('user-1'); expect(result.ProgressDetails.courseId).toBe('course-1'); expect(result.ProgressDetails.lessonId).toBe('lesson-1'); expect(result.ProgressDetails.updateType).toBe('lesson'); expect(result.ProgressDetails.previousProgress.percentage).toBe(40); expect(result.ProgressDetails.updatedProgress.percentage).toBe(50); expect(result.ProgressDetails.updatedProgress.completedUnits).toBe(5); }); it('should update course progress successfully', async () => { const currentEnrollment = createMockEnrollment(); const courseUpdateResult = { success: true }; const updatedEnrollment = createMockEnrollment({ progress_percentage: 75, completed_units: 7, total_time_spent: 5400, }); vi.spyOn(action as never, 'makeLearnWorldsRequest') .mockResolvedValueOnce(currentEnrollment as never) // fetchCurrentEnrollment .mockResolvedValueOnce(courseUpdateResult as never) // updateCourseProgress .mockResolvedValueOnce(updatedEnrollment as never); // fetchUpdatedEnrollment const result = await action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', ProgressPercentage: 75, TimeSpent: 1800, }, contextUser, ); expect(result.ProgressDetails.updateType).toBe('course'); expect(result.ProgressDetails.updatedProgress.percentage).toBe(75); expect(result.Summary.progressIncreased).toBe(true); expect(result.Summary.previousPercentage).toBe(40); expect(result.Summary.newPercentage).toBe(75); }); it('should throw error when ProgressPercentage is out of range (negative)', async () => { await expect( action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', ProgressPercentage: -5 }, contextUser, ), ).rejects.toThrow('ProgressPercentage must be between 0 and 100'); }); it('should throw error when ProgressPercentage is out of range (over 100)', async () => { await expect( action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', ProgressPercentage: 150 }, contextUser, ), ).rejects.toThrow('ProgressPercentage must be between 0 and 100'); }); it('should throw error when UserID is missing', async () => { await expect( action.UpdateProgress({ CompanyID: 'comp-1', UserID: '', CourseID: 'course-1' }, contextUser), ).rejects.toThrow('UserID is required'); }); it('should throw error when CourseID is missing', async () => { await expect( action.UpdateProgress({ CompanyID: 'comp-1', UserID: 'user-1', CourseID: '' }, contextUser), ).rejects.toThrow('CourseID is required'); }); it('should throw user not enrolled error including original error message', async () => { vi.spyOn(action as never, 'makeLearnWorldsRequest').mockRejectedValueOnce(new Error('404 Not Found')); await expect( action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1' }, contextUser, ), ).rejects.toThrow('User is not enrolled in this course: 404 Not Found'); }); it('should use fallback enrollment when updated enrollment fetch fails', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const currentEnrollment = createMockEnrollment({ progress_percentage: 30 }); const courseUpdateResult = { success: true }; vi.spyOn(action as never, 'makeLearnWorldsRequest') .mockResolvedValueOnce(currentEnrollment as never) // fetchCurrentEnrollment .mockResolvedValueOnce(courseUpdateResult as never) // updateCourseProgress .mockRejectedValueOnce(new Error('Fetch updated enrollment failed')); // fetchUpdatedEnrollment fails const result = await action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', ProgressPercentage: 50 }, contextUser, ); // Should fall back to the original enrollment data expect(result.ProgressDetails.updatedProgress.percentage).toBe(30); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('Failed to get updated enrollment details'), expect.anything(), ); warnSpy.mockRestore(); }); it('should compute progressIncreased and isCompleted in summary correctly', async () => { const currentEnrollment = createMockEnrollment({ progress_percentage: 90 }); const updatedEnrollment = createMockEnrollment({ progress_percentage: 100, completed: true, completed_at: '2024-06-21T15:00:00Z', }); vi.spyOn(action as never, 'makeLearnWorldsRequest') .mockResolvedValueOnce(currentEnrollment as never) // fetchCurrentEnrollment .mockResolvedValueOnce({ success: true } as never) // updateCourseProgress .mockResolvedValueOnce(updatedEnrollment as never); // fetchUpdatedEnrollment const result = await action.UpdateProgress( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1', ProgressPercentage: 100, Completed: true }, contextUser, ); expect(result.Summary.progressIncreased).toBe(true); expect(result.Summary.previousPercentage).toBe(90); expect(result.Summary.newPercentage).toBe(100); expect(result.Summary.isCompleted).toBe(true); expect(result.ProgressDetails.updatedProgress.completed).toBe(true); expect(result.ProgressDetails.updatedProgress.completedAt).toBe('2024-06-21T15:00:00Z'); }); }); describe('InternalRunAction()', () => { it('should return success when UpdateProgress succeeds', async () => { vi.spyOn(action, 'UpdateProgress').mockResolvedValue({ ProgressDetails: { userId: 'user-1', courseId: 'course-1', lessonId: 'lesson-1', previousProgress: { percentage: 40, completedUnits: 4, totalTimeSpent: 3600 }, updatedProgress: { percentage: 60, completedUnits: 6, totalUnits: 10, totalTimeSpent: 5400, totalTimeSpentText: '1h 30m 0s', lastAccessedAt: '2024-06-21T10:00:00Z', completed: false, }, updateType: 'lesson', updateResult: { lessonProgress: { success: true } }, }, Summary: { userId: 'user-1', courseId: 'course-1', lessonId: 'lesson-1', progressIncreased: true, previousPercentage: 40, newPercentage: 60, timeAdded: 600, totalTimeSpent: 5400, isCompleted: false, updateType: 'lesson', }, }); const runParams: RunActionParams = { Params: [ { Name: 'CompanyID', Type: 'Input', Value: 'comp-1' }, { Name: 'UserID', Type: 'Input', Value: 'user-1' }, { Name: 'CourseID', Type: 'Input', Value: 'course-1' }, { Name: 'LessonID', Type: 'Input', Value: 'lesson-1' }, ], ContextUser: contextUser, } as unknown as RunActionParams; const result = (await action['InternalRunAction'](runParams)) as ActionResultSimple; expect(result.Success).toBe(true); expect(result.ResultCode).toBe('SUCCESS'); expect(result.Message).toContain('Successfully updated lesson progress'); }); it('should return error result when UpdateProgress throws', async () => { vi.spyOn(action, 'UpdateProgress').mockRejectedValue(new Error('User is not enrolled in this course')); const runParams: RunActionParams = { Params: [ { Name: 'CompanyID', Type: 'Input', Value: 'comp-1' }, { Name: 'UserID', Type: 'Input', Value: 'user-1' }, { Name: 'CourseID', Type: 'Input', Value: 'course-1' }, ], ContextUser: contextUser, } as unknown as RunActionParams; const result = (await action['InternalRunAction'](runParams)) as ActionResultSimple; expect(result.Success).toBe(false); expect(result.ResultCode).toBe('ERROR'); expect(result.Message).toContain('Error updating progress'); expect(result.Message).toContain('User is not enrolled in this course'); }); }); });