UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

351 lines (297 loc) 12.7 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 { UpdateUserAction } from '../providers/learnworlds/actions/update-user.action'; import { UserInfo } from '@memberjunction/core'; import { UpdateUserParams, LWApiUser } 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 API user response */ function mockLWApiUserResponse(overrides: Partial<LWApiUser> = {}): LWApiUser { return { id: 'lw-user-1', email: 'updated@example.com', username: 'updateduser', first_name: 'Updated', last_name: 'User', full_name: 'Updated User', status: 'active', role: 'student', is_active: true, created: '2024-01-01T00:00:00Z', created_at: '2024-01-01T00:00:00Z', tags: ['tag1'], custom_fields: { level: 'senior' }, ...overrides, }; } describe('UpdateUserAction', () => { let action: UpdateUserAction; const contextUser = mockContextUser(); beforeEach(() => { action = new UpdateUserAction(); vi.restoreAllMocks(); }); describe('UpdateUser() typed method', () => { it('should update user successfully', async () => { const apiResponse = mockLWApiUserResponse(); vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', Email: 'updated@example.com', FirstName: 'Updated', }; const result = await action.UpdateUser(params, contextUser); expect(result.UserDetails.id).toBe('lw-user-1'); expect(result.UserDetails.email).toBe('updated@example.com'); expect(result.UserDetails.firstName).toBe('Updated'); expect(result.UserDetails.status).toBe('active'); expect(result.Summary.userId).toBe('lw-user-1'); expect(result.Summary.email).toBe('updated@example.com'); }); it('should only send non-undefined fields', async () => { const apiResponse = mockLWApiUserResponse(); const requestSpy = vi .spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest') .mockResolvedValue(apiResponse); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', FirstName: 'OnlyFirst', // Email, Username, Password, LastName, Role, IsActive, Tags, CustomFields all undefined }; await action.UpdateUser(params, contextUser); expect(requestSpy).toHaveBeenCalledTimes(1); const body = requestSpy.mock.calls[0][2] as Record<string, unknown>; expect(body.first_name).toBe('OnlyFirst'); // These should NOT be in the body expect(body.email).toBeUndefined(); expect(body.username).toBeUndefined(); expect(body.password).toBeUndefined(); expect(body.last_name).toBeUndefined(); expect(body.role).toBeUndefined(); expect(body.is_active).toBeUndefined(); expect(body.tags).toBeUndefined(); expect(body.custom_fields).toBeUndefined(); }); it('should track fields updated in summary', async () => { const apiResponse = mockLWApiUserResponse(); vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', Email: 'new@example.com', FirstName: 'Jane', LastName: 'Smith', Role: 'instructor', }; const result = await action.UpdateUser(params, contextUser); expect(result.Summary.fieldsUpdated).toContain('Email'); expect(result.Summary.fieldsUpdated).toContain('FirstName'); expect(result.Summary.fieldsUpdated).toContain('LastName'); expect(result.Summary.fieldsUpdated).toContain('Role'); expect(result.Summary.fieldsUpdated).toHaveLength(4); }); it('should throw when UserID missing', async () => { // buildUpdateBody needs at least one field to not throw "No fields" // but the caller needs UserID - let's verify the API call uses UserID const apiResponse = mockLWApiUserResponse(); const requestSpy = vi .spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest') .mockResolvedValue(apiResponse); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', Email: 'new@example.com', }; await action.UpdateUser(params, contextUser); // Verify the endpoint included the UserID expect(requestSpy.mock.calls[0][0]).toBe('users/lw-user-1'); expect(requestSpy.mock.calls[0][1]).toBe('PUT'); }); it('should throw when no fields provided to update', async () => { const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', // No updatable fields provided }; await expect(action.UpdateUser(params, contextUser)).rejects.toThrow('No fields provided to update'); }); it('should map all update fields correctly to the API body', async () => { const apiResponse = mockLWApiUserResponse(); const requestSpy = vi .spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest') .mockResolvedValue(apiResponse); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', Email: 'all@example.com', Username: 'allfields', Password: 'newpass', FirstName: 'All', LastName: 'Fields', Role: 'Instructor', IsActive: false, Tags: ['new-tag', 'another-tag'], CustomFields: { department: 'Sales', region: 'NA' }, }; await action.UpdateUser(params, contextUser); const body = requestSpy.mock.calls[0][2] as Record<string, unknown>; expect(body.email).toBe('all@example.com'); expect(body.username).toBe('allfields'); expect(body.password).toBe('newpass'); expect(body.first_name).toBe('All'); expect(body.last_name).toBe('Fields'); expect(body.role).toBe('instructor'); expect(body.is_active).toBe(false); expect(body.tags).toEqual(['new-tag', 'another-tag']); expect(body.custom_fields).toEqual({ department: 'Sales', region: 'NA' }); }); it('should handle API user response with _id instead of id', async () => { const apiResponse = mockLWApiUserResponse({ id: undefined, _id: 'mongo-id-123' }); vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockResolvedValue( apiResponse, ); const params: UpdateUserParams = { CompanyID: 'comp-1', UserID: 'lw-user-1', Email: 'mongo@example.com', }; const result = await action.UpdateUser(params, contextUser); expect(result.UserDetails.id).toBe('mongo-id-123'); }); }); describe('InternalRunAction()', () => { it('should work via framework path', async () => { const apiResponse = mockLWApiUserResponse(); 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: 'UserID', Value: 'lw-user-1', Type: 'Input' as const }, { Name: 'Email', Value: 'updated@example.com', Type: 'Input' as const }, { Name: 'FirstName', Value: 'Updated', 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 updated user'); expect(result.Message).toContain('updated@example.com'); expect(result.Message).toContain('field(s) updated'); }); it('should return validation error when UserID is missing', async () => { const runParams = { Params: [ { Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const }, // No UserID { Name: 'Email', Value: 'new@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("'UserID'"); }); it('should return error when API call fails', async () => { vi.spyOn(action as unknown as { makeLearnWorldsRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsRequest').mockRejectedValue( new Error('Server unavailable'), ); const runParams = { Params: [ { Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const }, { Name: 'UserID', Value: 'lw-user-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('Server unavailable'); }); it('should set output params on success', async () => { const apiResponse = mockLWApiUserResponse(); 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: 'UserID', Value: 'lw-user-1', Type: 'Input' as const }, { Name: 'FirstName', Value: 'Updated', Type: 'Input' as const }, ]; const runParams = { Params: params, ContextUser: contextUser, Action: {}, Filters: [], }; await action['InternalRunAction'](runParams as unknown as Parameters<(typeof action)['InternalRunAction']>[0]); const userDetailsParam = params.find((p) => p.Name === 'UserDetails'); const summaryParam = params.find((p) => p.Name === 'Summary'); expect(userDetailsParam).toBeDefined(); expect(summaryParam).toBeDefined(); }); }); });