UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

376 lines (311 loc) 13.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies 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 { BaseLMSAction } from '../base/base-lms.action'; import { LearnWorldsBaseAction } from '../providers/learnworlds/learnworlds-base.action'; // Concrete subclass for testing BaseLMSAction class TestLMSAction extends BaseLMSAction { protected lmsProvider = 'TestLMS'; protected integrationName = 'TestLMS'; protected async InternalRunAction(): Promise<{ Success: boolean; ResultCode: string }> { return { Success: true, ResultCode: 'SUCCESS' }; } } describe('BaseLMSAction', () => { let action: TestLMSAction; beforeEach(() => { action = new TestLMSAction(); }); describe('calculateProgressPercentage', () => { it('should calculate percentage correctly', () => { expect(action['calculateProgressPercentage'](75, 100)).toBe(75); expect(action['calculateProgressPercentage'](1, 3)).toBe(33); }); it('should return 0 when total is 0', () => { expect(action['calculateProgressPercentage'](0, 0)).toBe(0); }); it('should return 100 when all completed', () => { expect(action['calculateProgressPercentage'](50, 50)).toBe(100); }); }); describe('formatDuration', () => { it('should format seconds only', () => { expect(action['formatDuration'](30)).toBe('30s'); }); it('should format minutes and seconds', () => { expect(action['formatDuration'](90)).toBe('1m 30s'); }); it('should format hours, minutes, and seconds', () => { expect(action['formatDuration'](3661)).toBe('1h 1m 1s'); }); }); describe('mapEnrollmentStatus', () => { it('should map active statuses', () => { expect(action['mapEnrollmentStatus']('active')).toBe('active'); }); it('should map completed statuses', () => { expect(action['mapEnrollmentStatus']('completed')).toBe('completed'); expect(action['mapEnrollmentStatus']('finished')).toBe('completed'); }); it('should map expired statuses', () => { expect(action['mapEnrollmentStatus']('expired')).toBe('expired'); }); it('should map suspended statuses', () => { expect(action['mapEnrollmentStatus']('suspended')).toBe('suspended'); expect(action['mapEnrollmentStatus']('paused')).toBe('suspended'); expect(action['mapEnrollmentStatus']('inactive')).toBe('suspended'); }); it('should return unknown for unrecognized statuses', () => { expect(action['mapEnrollmentStatus']('pending')).toBe('unknown'); expect(action['mapEnrollmentStatus']('review')).toBe('unknown'); }); }); describe('formatLMSDate', () => { it('should format date as ISO string', () => { const date = new Date('2024-06-15T10:30:00Z'); expect(action['formatLMSDate'](date)).toBe('2024-06-15T10:30:00.000Z'); }); }); describe('parseLMSDate', () => { it('should parse ISO date string', () => { const result = action['parseLMSDate']('2024-06-15T10:30:00Z'); expect(result.toISOString()).toBe('2024-06-15T10:30:00.000Z'); }); }); describe('buildLMSErrorMessage', () => { it('should build error message without system error', () => { expect(action['buildLMSErrorMessage']('GetCourse', 'Not found')).toBe('LMS operation failed: GetCourse. Not found'); }); it('should include system error details', () => { const result = action['buildLMSErrorMessage']('GetCourse', 'Failed', new Error('timeout')); expect(result).toContain('System error: timeout'); }); }); describe('getCommonLMSParams', () => { it('should return CompanyID param', () => { const params = action['getCommonLMSParams'](); expect(params).toHaveLength(1); expect(params[0].Name).toBe('CompanyID'); }); }); describe('getCredentialFromEnv', () => { it('should build correct env key', () => { process.env['BIZAPPS_TESTLMS_COMP1_API_KEY'] = 'key123'; expect(action['getCredentialFromEnv']('COMP1', 'API_KEY')).toBe('key123'); delete process.env['BIZAPPS_TESTLMS_COMP1_API_KEY']; }); it('should return undefined for missing env var', () => { const result = action['getCredentialFromEnv']('COMP1', 'MISSING_KEY'); expect(result).toBeUndefined(); }); it('should fallback to lowercase companyId', () => { process.env['BIZAPPS_TESTLMS_abc-123_API_KEY'] = 'lower-key'; expect(action['getCredentialFromEnv']('ABC-123', 'API_KEY')).toBe('lower-key'); delete process.env['BIZAPPS_TESTLMS_abc-123_API_KEY']; }); it('should prefer original casing over lowercase fallback', () => { process.env['BIZAPPS_TESTLMS_COMP1_API_KEY'] = 'original'; process.env['BIZAPPS_TESTLMS_comp1_API_KEY'] = 'lower'; expect(action['getCredentialFromEnv']('COMP1', 'API_KEY')).toBe('original'); delete process.env['BIZAPPS_TESTLMS_COMP1_API_KEY']; delete process.env['BIZAPPS_TESTLMS_comp1_API_KEY']; }); }); describe('getParamValue', () => { it('should find param by name', () => { const params = [{ Name: 'CourseID', Value: 'c1', Type: 'Input' as const }]; expect(action['getParamValue'](params, 'CourseID')).toBe('c1'); }); it('should return undefined for missing param', () => { expect(action['getParamValue']([], 'Missing')).toBeUndefined(); }); }); describe('setOutputParam', () => { it('should create param if it does not exist', () => { const params: { Name: string; Type: string; Value: unknown }[] = []; action['setOutputParam'](params, 'Result', 42); expect(params).toHaveLength(1); expect(params[0].Name).toBe('Result'); expect(params[0].Value).toBe(42); expect(params[0].Type).toBe('Output'); }); it('should update existing param value', () => { const params = [{ Name: 'Result', Type: 'Output', Value: 'old' }]; action['setOutputParam'](params, 'Result', 'new'); expect(params).toHaveLength(1); expect(params[0].Value).toBe('new'); }); }); describe('buildSuccessResult', () => { it('should return a success ActionResultSimple', () => { const result = action['buildSuccessResult']('All good'); expect(result.Success).toBe(true); expect(result.ResultCode).toBe('SUCCESS'); expect(result.Message).toBe('All good'); }); }); describe('buildErrorResult', () => { it('should return an error ActionResultSimple', () => { const result = action['buildErrorResult']('FAIL', 'Bad things'); expect(result.Success).toBe(false); expect(result.ResultCode).toBe('FAIL'); expect(result.Message).toBe('Bad things'); }); }); }); describe('LearnWorldsBaseAction', () => { class TestLearnWorldsAction extends LearnWorldsBaseAction { protected async InternalRunAction(): Promise<{ Success: boolean; ResultCode: string }> { return { Success: true, ResultCode: 'SUCCESS' }; } } let action: TestLearnWorldsAction; beforeEach(() => { action = new TestLearnWorldsAction(); }); describe('parseLearnWorldsDate', () => { it('should parse ISO date string', () => { const result = action['parseLearnWorldsDate']('2024-06-15T10:30:00Z'); expect(result.toISOString()).toBe('2024-06-15T10:30:00.000Z'); }); it('should parse unix timestamp (seconds)', () => { const result = action['parseLearnWorldsDate'](1718444400); expect(result).toBeInstanceOf(Date); expect(result.getTime()).toBe(1718444400000); }); }); describe('formatLearnWorldsDate', () => { it('should format date as ISO string', () => { const date = new Date('2024-06-15T10:30:00Z'); expect(action['formatLearnWorldsDate'](date)).toBe('2024-06-15T10:30:00.000Z'); }); }); describe('mapUserStatus', () => { it('should map active status', () => { expect(action['mapUserStatus']('active')).toBe('active'); }); it('should map inactive status', () => { expect(action['mapUserStatus']('inactive')).toBe('inactive'); }); it('should map suspended/blocked statuses', () => { expect(action['mapUserStatus']('suspended')).toBe('suspended'); expect(action['mapUserStatus']('blocked')).toBe('suspended'); }); it('should return inactive for unknown statuses', () => { expect(action['mapUserStatus']('unknown')).toBe('inactive'); }); }); describe('mapLearnWorldsEnrollmentStatus', () => { it('should return completed when completed is true', () => { expect(action['mapLearnWorldsEnrollmentStatus']({ completed: true, active: true })).toBe('completed'); }); it('should return expired when expired is true', () => { expect(action['mapLearnWorldsEnrollmentStatus']({ expired: true, active: true })).toBe('expired'); }); it('should return suspended when suspended is true', () => { expect(action['mapLearnWorldsEnrollmentStatus']({ suspended: true, active: false })).toBe('suspended'); }); it('should return suspended when not active', () => { expect(action['mapLearnWorldsEnrollmentStatus']({ active: false })).toBe('suspended'); }); it('should return active when active and not completed/expired/suspended', () => { expect(action['mapLearnWorldsEnrollmentStatus']({ active: true })).toBe('active'); }); }); describe('calculateProgress', () => { it('should extract progress data', () => { const result = action['calculateProgress']({ percentage: 75, completed_units: 15, total_units: 20, time_spent: 3600, }); expect(result.percentage).toBe(75); expect(result.completedUnits).toBe(15); expect(result.totalUnits).toBe(20); expect(result.timeSpent).toBe(3600); }); it('should default missing values to 0', () => { const result = action['calculateProgress']({}); expect(result.percentage).toBe(0); expect(result.completedUnits).toBe(0); expect(result.totalUnits).toBe(0); expect(result.timeSpent).toBe(0); }); }); describe('SetCompanyContext', () => { it('should set params with CompanyID', () => { action.SetCompanyContext('comp-123'); expect(action['params']).toHaveLength(1); expect(action['params'][0].Name).toBe('CompanyID'); expect(action['params'][0].Value).toBe('comp-123'); }); }); describe('FindUserByEmail', () => { it('should return user when found', async () => { const mockUser = { id: 'u1', email: 'test@example.com', username: 'testuser', first_name: 'Test', last_name: 'User', status: 'active', role: 'student', created: '2024-01-01T00:00:00Z', tags: ['tag1'], }; action.SetCompanyContext('comp-123'); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ data: [mockUser], meta: { page: 1, totalPages: 1 } } as never); const mockCtx = { ID: 'ctx-user' } as unknown as (typeof import('@memberjunction/core'))['UserInfo']; const result = await action.FindUserByEmail('test@example.com', mockCtx as never); expect(result).not.toBeNull(); expect(result!.id).toBe('u1'); expect(result!.email).toBe('test@example.com'); }); it('should return null when not found', async () => { action.SetCompanyContext('comp-123'); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ data: [], meta: { page: 1, totalPages: 1 } } as never); const mockCtx = { ID: 'ctx-user' } as unknown as (typeof import('@memberjunction/core'))['UserInfo']; const result = await action.FindUserByEmail('nobody@example.com', mockCtx as never); expect(result).toBeNull(); }); it('should re-throw on API error', async () => { action.SetCompanyContext('comp-123'); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockRejectedValue(new Error('API error') as never); const mockCtx = { ID: 'ctx-user' } as unknown as (typeof import('@memberjunction/core'))['UserInfo']; await expect(action.FindUserByEmail('test@example.com', mockCtx as never)).rejects.toThrow('API error'); }); }); });