UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

323 lines (270 loc) 12.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies (same pattern as lms.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'; }, ActionResultSimple: class ActionResultSimple { Success: boolean = false; ResultCode: string = ''; Message: string = ''; Params?: Array<{ Name: string; Value: unknown; Type: string }>; }, RunActionParams: class RunActionParams { Params: Array<{ Name: string; Value: unknown; Type: string }> = []; ContextUser: unknown = {}; Action: unknown = {}; Filters: unknown[] = []; }, })); import { CreateUserAction } from '../providers/learnworlds/actions/create-user.action'; import { UserInfo } from '@memberjunction/core'; import { CreateUserParams } from '../providers/learnworlds/interfaces'; /** Helper to build a mock UserInfo */ function mockContextUser(): UserInfo { return { ID: 'user-1', Name: 'Test User', Email: 'test@example.com' } as unknown as UserInfo; } /** Helper to build a mock LW create-user response */ function mockLWCreateUserResponse(overrides: Record<string, unknown> = {}): Record<string, unknown> { return { id: 'lw-user-123', email: 'newuser@example.com', username: 'newuser', first_name: 'New', last_name: 'User', role: 'student', is_active: true, tags: [], custom_fields: {}, created_at: '2024-06-15T10:00:00Z', login_url: 'https://school.learnworlds.com/login/lw-user-123', reset_password_url: 'https://school.learnworlds.com/reset/lw-user-123', ...overrides, }; } describe('CreateUserAction', () => { let action: CreateUserAction; const contextUser = mockContextUser(); beforeEach(() => { action = new CreateUserAction(); vi.restoreAllMocks(); }); describe('CreateUser() typed method', () => { it('should create user successfully', async () => { const apiResponse = mockLWCreateUserResponse(); const requestSpy = vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const params: CreateUserParams = { CompanyID: 'comp-1', Email: 'newuser@example.com', FirstName: 'New', LastName: 'User', }; const result = await action.CreateUser(params, contextUser); expect(result.UserDetails.id).toBe('lw-user-123'); expect(result.UserDetails.email).toBe('newuser@example.com'); expect(result.UserDetails.fullName).toBe('New User'); expect(result.UserDetails.role).toBe('student'); expect(result.UserDetails.status).toBe('active'); expect(result.Summary.userId).toBe('lw-user-123'); expect(result.Summary.email).toBe('newuser@example.com'); expect(result.Summary.welcomeEmailSent).toBe(true); expect(result.EnrollmentResults).toEqual([]); // username should always be present (defaults to email) const body = requestSpy.mock.calls[0][2] as Record<string, unknown>; expect(body.username).toBe('newuser@example.com'); }); it('should throw when email is missing', async () => { const params: CreateUserParams = { CompanyID: 'comp-1', Email: '', }; await expect(action.CreateUser(params, contextUser)).rejects.toThrow('Email is required'); }); it('should handle enrollment in courses', async () => { const createResponse = mockLWCreateUserResponse(); const enrollResponse = { id: 'enrollment-1' }; const requestSpy = vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest'); // First call: create user; Second call: enroll in course requestSpy.mockResolvedValueOnce(createResponse).mockResolvedValueOnce(enrollResponse); const params: CreateUserParams = { CompanyID: 'comp-1', Email: 'newuser@example.com', EnrollInCourses: ['course-abc'], }; const result = await action.CreateUser(params, contextUser); expect(result.EnrollmentResults).toHaveLength(1); expect(result.EnrollmentResults[0].courseId).toBe('course-abc'); expect(result.EnrollmentResults[0].success).toBe(true); expect(result.EnrollmentResults[0].enrollmentId).toBe('enrollment-1'); expect(result.Summary.coursesEnrolled).toBe(1); expect(result.Summary.totalCoursesRequested).toBe(1); // Verify enrollment used the unified endpoint and new body format expect(requestSpy.mock.calls[1][0]).toBe('users/lw-user-123/enrollment'); const enrollBody = requestSpy.mock.calls[1][2] as Record<string, unknown>; expect(enrollBody.productId).toBe('course-abc'); expect(enrollBody.productType).toBe('course'); expect(enrollBody.send_enrollment_email).toBe(false); }); it('should handle enrollment failure gracefully', async () => { const createResponse = mockLWCreateUserResponse(); const requestSpy = vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest'); // First call: create user succeeds; Second call: enrollment fails requestSpy.mockResolvedValueOnce(createResponse).mockRejectedValueOnce(new Error('Course not found')); const params: CreateUserParams = { CompanyID: 'comp-1', Email: 'newuser@example.com', EnrollInCourses: 'course-xyz', }; const result = await action.CreateUser(params, contextUser); expect(result.EnrollmentResults).toHaveLength(1); expect(result.EnrollmentResults[0].success).toBe(false); expect(result.EnrollmentResults[0].error).toBe('Course not found'); expect(result.Summary.coursesEnrolled).toBe(0); expect(result.Summary.totalCoursesRequested).toBe(1); }); it('should pass optional fields to the API', async () => { const apiResponse = mockLWCreateUserResponse({ role: 'instructor', tags: ['vip', 'beta'] }); const requestSpy = vi .spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest') .mockResolvedValue(apiResponse); const params: CreateUserParams = { CompanyID: 'comp-1', Email: 'instructor@example.com', Username: 'instructor1', Password: 'secret123', FirstName: 'Jane', LastName: 'Doe', Role: 'instructor', IsActive: true, SendWelcomeEmail: false, Tags: 'vip,beta', CustomFields: { department: 'Engineering' }, }; await action.CreateUser(params, contextUser); // Verify the API was called with the correct body expect(requestSpy).toHaveBeenCalledTimes(1); const callArgs = requestSpy.mock.calls[0]; expect(callArgs[0]).toBe('users'); expect(callArgs[1]).toBe('POST'); const body = callArgs[2] as Record<string, unknown>; expect(body.email).toBe('instructor@example.com'); expect(body.username).toBe('instructor1'); expect(body.password).toBe('secret123'); expect(body.first_name).toBe('Jane'); expect(body.last_name).toBe('Doe'); expect(body.role).toBe('instructor'); expect(body.tags).toEqual(['vip', 'beta']); expect(body.custom_fields).toEqual({ department: 'Engineering' }); }); it('should handle tags as an array', async () => { const apiResponse = mockLWCreateUserResponse({ tags: ['alpha', 'omega'] }); const requestSpy = vi .spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest') .mockResolvedValue(apiResponse); const params: CreateUserParams = { CompanyID: 'comp-1', Email: 'arrayuser@example.com', Tags: ['alpha', 'omega'], }; await action.CreateUser(params, contextUser); const body = requestSpy.mock.calls[0][2] as Record<string, unknown>; expect(body.tags).toEqual(['alpha', 'omega']); }); }); describe('InternalRunAction()', () => { it('should return success ActionResultSimple', async () => { const apiResponse = mockLWCreateUserResponse(); vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const runParams = { Params: [ { Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const }, { Name: 'Email', Value: 'newuser@example.com', Type: 'Input' as const }, { Name: 'FirstName', Value: 'New', Type: 'Input' as const }, { Name: 'LastName', Value: 'User', Type: 'Input' as const }, ], ContextUser: contextUser, Action: {}, Filters: [], }; const result = await action['InternalRunAction'](runParams as unknown as Parameters<(typeof action)['InternalRunAction']>[0]); expect(result.Success).toBe(true); expect(result.ResultCode).toBe('SUCCESS'); expect(result.Message).toContain('Successfully created user'); expect(result.Message).toContain('newuser@example.com'); }); it('should return error on failure', async () => { vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockRejectedValue( new Error('API connection failed'), ); const runParams = { Params: [ { Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const }, { Name: 'Email', Value: 'fail@example.com', Type: 'Input' as const }, ], ContextUser: contextUser, Action: {}, Filters: [], }; const result = await action['InternalRunAction'](runParams as unknown as Parameters<(typeof action)['InternalRunAction']>[0]); expect(result.Success).toBe(false); expect(result.ResultCode).toBe('ERROR'); expect(result.Message).toContain('API connection failed'); }); it('should set output params on success', async () => { const apiResponse = mockLWCreateUserResponse(); vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const params = [ { Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const }, { Name: 'Email', Value: 'newuser@example.com', Type: 'Input' as const }, ]; const runParams = { Params: params, ContextUser: contextUser, Action: {}, Filters: [], }; await action['InternalRunAction'](runParams as unknown as Parameters<(typeof action)['InternalRunAction']>[0]); // Output params should have been added const userDetailsParam = params.find((p) => p.Name === 'UserDetails'); const summaryParam = params.find((p) => p.Name === 'Summary'); const enrollmentResultsParam = params.find((p) => p.Name === 'EnrollmentResults'); expect(userDetailsParam).toBeDefined(); expect(summaryParam).toBeDefined(); expect(enrollmentResultsParam).toBeDefined(); }); }); });