UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

468 lines (386 loc) 16.9 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 { GetCertificatesAction } from '../providers/learnworlds/actions/get-certificates.action'; import { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { FormattedCertificate } from '../providers/learnworlds/interfaces'; /** * 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; } /** * Helper to build a raw LW API certificate object matching the LWRawCertificate interface */ function createRawApiCertificate(overrides: Record<string, unknown> = {}): Record<string, unknown> { return { id: 'cert-1', user_id: 'user-1', course_id: 'course-1', certificate_number: 'CERT-2024-001', issued_at: '2024-06-15T10:00:00Z', expires_at: '2025-06-15T10:00:00Z', status: 'active', grade: 95, score: 92, completion_percentage: 100, user: { id: 'user-1', email: 'student@example.com', first_name: 'Jane', last_name: 'Doe', }, course: { id: 'course-1', title: 'Intro to Testing', duration: 3600, }, pdf_url: 'https://example.com/cert.pdf', image_url: 'https://example.com/cert.png', public_url: 'https://example.com/cert/public', verification_url: 'https://example.com/verify/CERT-2024-001', verification_code: 'VERIFY-001', qr_code_url: 'https://example.com/qr/CERT-2024-001', ...overrides, }; } describe('GetCertificatesAction', () => { let action: GetCertificatesAction; let contextUser: UserInfo; beforeEach(() => { action = new GetCertificatesAction(); contextUser = createMockContextUser(); }); describe('GetCertificates() typed method', () => { it('should get certificates successfully with full data', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.TotalCount).toBe(1); expect(result.Certificates).toHaveLength(1); const cert: FormattedCertificate = result.Certificates[0]; expect(cert.id).toBe('cert-1'); expect(cert.userId).toBe('user-1'); expect(cert.courseId).toBe('course-1'); expect(cert.certificateNumber).toBe('CERT-2024-001'); expect(cert.issuedAt).toBe('2024-06-15T10:00:00Z'); expect(cert.expiresAt).toBe('2025-06-15T10:00:00Z'); expect(cert.status).toBe('active'); expect(cert.grade).toBe(95); expect(cert.score).toBe(92); expect(cert.completionPercentage).toBe(100); }); it('should format certificate with inline user and course info', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); const cert = result.Certificates[0]; expect(cert.user).toBeDefined(); expect(cert.user?.id).toBe('user-1'); expect(cert.user?.email).toBe('student@example.com'); expect(cert.user?.name).toBe('Jane Doe'); expect(cert.course).toBeDefined(); expect(cert.course?.id).toBe('course-1'); expect(cert.course?.title).toBe('Intro to Testing'); expect(cert.course?.duration).toBe(3600); }); it('should include download links by default', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); const cert = result.Certificates[0]; expect(cert.downloadLinks).toBeDefined(); expect(cert.downloadLinks?.pdf).toBe('https://example.com/cert.pdf'); expect(cert.downloadLinks?.image).toBe('https://example.com/cert.png'); expect(cert.downloadLinks?.publicUrl).toBe('https://example.com/cert/public'); }); it('should omit download links when IncludeDownloadLinks is false', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates( { CompanyID: 'comp-1', UserID: 'user-1', IncludeDownloadLinks: false }, contextUser, ); const cert = result.Certificates[0]; expect(cert.downloadLinks).toBeUndefined(); }); it('should include verification info', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); const cert = result.Certificates[0]; expect(cert.verification).toBeDefined(); expect(cert.verification.url).toBe('https://example.com/verify/CERT-2024-001'); expect(cert.verification.code).toBe('VERIFY-001'); expect(cert.verification.qrCode).toBe('https://example.com/qr/CERT-2024-001'); }); it('should build summary with correct counts and grouping by course when filtering by user', async () => { const cert1 = createRawApiCertificate({ id: 'cert-1', course_id: 'course-1', course: { id: 'course-1', title: 'Course A' }, status: 'active', expires_at: '2099-12-31T00:00:00Z', }); const cert2 = createRawApiCertificate({ id: 'cert-2', course_id: 'course-2', course: { id: 'course-2', title: 'Course B' }, status: 'active', expires_at: '2000-01-01T00:00:00Z', // expired }); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [cert1, cert2], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.Summary.totalCertificates).toBe(2); expect(result.Summary.activeCertificates).toBe(1); expect(result.Summary.expiredCertificates).toBe(1); expect(result.Summary.filterType).toBe('user'); // When filtering by UserID only, certs are grouped by course title expect(result.Summary.groupedData).not.toBeNull(); expect(result.Summary.groupedData?.['Course A']).toHaveLength(1); expect(result.Summary.groupedData?.['Course B']).toHaveLength(1); }); it('should build summary grouped by user when filtering by course', async () => { const cert1 = createRawApiCertificate({ id: 'cert-1', user_id: 'user-1', user: { id: 'user-1', email: 'alice@example.com', first_name: 'Alice', last_name: 'Smith' }, }); const cert2 = createRawApiCertificate({ id: 'cert-2', user_id: 'user-2', user: { id: 'user-2', email: 'bob@example.com', first_name: 'Bob', last_name: 'Jones' }, }); // Mock once for the certificates endpoint, no extra user/course lookups needed (inline data present) vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [cert1, cert2], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', CourseID: 'course-1' }, contextUser); expect(result.Summary.filterType).toBe('course'); expect(result.Summary.groupedData).not.toBeNull(); expect(result.Summary.groupedData?.['Alice Smith']).toHaveLength(1); expect(result.Summary.groupedData?.['Bob Jones']).toHaveLength(1); }); it('should set filterType to user-course when both UserID and CourseID are provided', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1' }, contextUser, ); expect(result.Summary.filterType).toBe('user-course'); // No groupedData when both filters are provided expect(result.Summary.groupedData).toBeNull(); }); it('should handle empty certificates', async () => { vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.TotalCount).toBe(0); expect(result.Certificates).toEqual([]); expect(result.Summary.totalCertificates).toBe(0); expect(result.Summary.activeCertificates).toBe(0); expect(result.Summary.expiredCertificates).toBe(0); }); it('should throw when neither UserID nor CourseID is provided', async () => { await expect(action.GetCertificates({ CompanyID: 'comp-1' }, contextUser)).rejects.toThrow( 'Either UserID or CourseID is required', ); }); it('should propagate API errors', async () => { vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: false, message: 'Unauthorized', } as never); await expect(action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser)).rejects.toThrow( 'Unauthorized', ); }); it('should propagate network errors', async () => { vi.spyOn(action as never, 'makeLearnWorldsRequest').mockRejectedValue(new Error('Network timeout') as never); await expect(action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser)).rejects.toThrow( 'Network timeout', ); }); it('should use download_url as fallback for pdf when pdf_url is missing', async () => { const rawCert = createRawApiCertificate({ pdf_url: undefined, download_url: 'https://example.com/download/cert.pdf', }); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.Certificates[0].downloadLinks?.pdf).toBe('https://example.com/download/cert.pdf'); }); it('should use certificate_url as fallback for publicUrl when public_url is missing', async () => { const rawCert = createRawApiCertificate({ public_url: undefined, certificate_url: 'https://example.com/certificate/view', }); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.Certificates[0].downloadLinks?.publicUrl).toBe('https://example.com/certificate/view'); }); it('should use certificate_id as fallback when id is missing', async () => { const rawCert = createRawApiCertificate({ id: undefined, certificate_id: 'alt-cert-id' }); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: [rawCert], } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.Certificates[0].id).toBe('alt-cert-id'); }); it('should handle nested data shape from API response', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: { data: [rawCert] }, } as never); const result = await action.GetCertificates({ CompanyID: 'comp-1', UserID: 'user-1' }, contextUser); expect(result.TotalCount).toBe(1); expect(result.Certificates[0].id).toBe('cert-1'); }); it('should handle single-certificate object response', async () => { const rawCert = createRawApiCertificate(); vi.spyOn(action as never, 'makeLearnWorldsRequest').mockResolvedValue({ success: true, data: rawCert, // single object, not array } as never); const result = await action.GetCertificates( { CompanyID: 'comp-1', UserID: 'user-1', CourseID: 'course-1' }, contextUser, ); expect(result.TotalCount).toBe(1); expect(result.Certificates[0].id).toBe('cert-1'); }); }); describe('InternalRunAction()', () => { it('should return success when GetCertificates succeeds', async () => { const mockCertificates: FormattedCertificate[] = [ { id: 'cert-1', userId: 'user-1', courseId: 'course-1', certificateNumber: 'CERT-001', issuedAt: '2024-06-15T10:00:00Z', status: 'active', completionPercentage: 100, verification: { url: undefined, code: undefined, qrCode: undefined }, }, { id: 'cert-2', userId: 'user-1', courseId: 'course-2', certificateNumber: 'CERT-002', issuedAt: '2024-07-01T10:00:00Z', status: 'active', completionPercentage: 100, verification: { url: undefined, code: undefined, qrCode: undefined }, }, ]; vi.spyOn(action, 'GetCertificates').mockResolvedValue({ Certificates: mockCertificates, TotalCount: 2, Summary: { totalCertificates: 2, activeCertificates: 2, expiredCertificates: 0, dateRange: { from: 'all-time', to: 'current' }, filterType: 'user', groupedData: null, }, }); const runParams: RunActionParams = { Params: [ { Name: 'CompanyID', Type: 'Input', Value: 'comp-1' }, { Name: 'UserID', Type: 'Input', Value: 'user-1' }, { Name: 'CourseID', Type: 'Input', Value: undefined }, { Name: 'IncludeDownloadLinks', Type: 'Input', Value: true }, ], 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('Retrieved 2 certificate(s)'); }); it('should return error result when GetCertificates throws', async () => { vi.spyOn(action, 'GetCertificates').mockRejectedValue(new Error('API connection failed')); const runParams: RunActionParams = { Params: [ { Name: 'CompanyID', Type: 'Input', Value: 'comp-1' }, { Name: 'UserID', Type: 'Input', Value: 'user-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 retrieving certificates'); expect(result.Message).toContain('API connection failed'); }); }); });