@memberjunction/actions-bizapps-lms
Version:
LMS system integration actions for MemberJunction
257 lines (212 loc) • 9.42 kB
text/typescript
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 { SSOLoginAction } from '../providers/learnworlds/actions/sso-login.action';
import { UserInfo } from '@memberjunction/core';
import { SSOLoginParams } 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;
}
describe('SSOLoginAction', () => {
let action: SSOLoginAction;
const contextUser = mockContextUser();
beforeEach(() => {
action = new SSOLoginAction();
vi.restoreAllMocks();
});
describe('GenerateSSOUrl() typed method', () => {
it('should generate SSO URL with email', async () => {
const requestSpy = vi
.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest')
.mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=abc123',
user_id: 'lw-user-42',
});
const params: SSOLoginParams = {
CompanyID: 'comp-1',
Email: 'student@example.com',
};
const result = await action.GenerateSSOUrl(params, contextUser);
expect(result.LoginURL).toBe('https://school.learnworlds.com/sso?token=abc123');
expect(result.LearnWorldsUserID).toBe('lw-user-42');
// Verify the request body contained the email
expect(requestSpy).toHaveBeenCalledTimes(1);
expect(requestSpy.mock.calls[0][0]).toBe('sso');
expect(requestSpy.mock.calls[0][1]).toBe('POST');
const body = requestSpy.mock.calls[0][2] as Record<string, unknown>;
expect(body.email).toBe('student@example.com');
expect(body.user_id).toBeUndefined();
});
it('should generate SSO URL with userId', async () => {
const requestSpy = vi
.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest')
.mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=def456',
user_id: 'lw-user-99',
});
const params: SSOLoginParams = {
CompanyID: 'comp-1',
UserID: 'lw-user-99',
};
const result = await action.GenerateSSOUrl(params, contextUser);
expect(result.LoginURL).toBe('https://school.learnworlds.com/sso?token=def456');
expect(result.LearnWorldsUserID).toBe('lw-user-99');
const body = requestSpy.mock.calls[0][2] as Record<string, unknown>;
expect(body.user_id).toBe('lw-user-99');
expect(body.email).toBeUndefined();
});
it('should throw when neither email nor userId provided', async () => {
const params: SSOLoginParams = {
CompanyID: 'comp-1',
};
await expect(action.GenerateSSOUrl(params, contextUser)).rejects.toThrow('Either Email or UserID is required for SSO login');
});
it('should include RedirectTo in the request body when provided', async () => {
const requestSpy = vi
.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest')
.mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=xyz',
});
const params: SSOLoginParams = {
CompanyID: 'comp-1',
Email: 'student@example.com',
RedirectTo: '/courses/my-course',
};
await action.GenerateSSOUrl(params, contextUser);
const body = requestSpy.mock.calls[0][2] as Record<string, unknown>;
expect(body.redirect_to).toBe('/courses/my-course');
});
it('should prefer email over userId when both are provided', async () => {
const requestSpy = vi
.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest')
.mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=both',
});
const params: SSOLoginParams = {
CompanyID: 'comp-1',
Email: 'both@example.com',
UserID: 'lw-user-both',
};
await action.GenerateSSOUrl(params, contextUser);
// Email should be set, and since the code checks `if (params.Email)` first,
// user_id should NOT be set (it's in the else-if branch)
const body = requestSpy.mock.calls[0][2] as Record<string, unknown>;
expect(body.email).toBe('both@example.com');
expect(body.user_id).toBeUndefined();
});
it('should propagate API errors', async () => {
vi.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest').mockRejectedValue(
new Error('LearnWorlds API error: 401 Unauthorized'),
);
const params: SSOLoginParams = {
CompanyID: 'comp-1',
Email: 'student@example.com',
};
await expect(action.GenerateSSOUrl(params, contextUser)).rejects.toThrow('LearnWorlds API error: 401 Unauthorized');
});
});
describe('InternalRunAction()', () => {
it('should work via framework path', async () => {
vi.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest').mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=framework',
user_id: 'lw-user-fw',
});
const runParams = {
Params: [
{ Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const },
{ Name: 'Email', Value: 'student@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(true);
expect(result.ResultCode).toBe('SUCCESS');
expect(result.Message).toContain('SSO URL generated successfully');
});
it('should set output params on success', async () => {
vi.spyOn(action as unknown as { makeLearnWorldsNonVersionedRequest: (...args: unknown[]) => Promise<unknown> }, 'makeLearnWorldsNonVersionedRequest').mockResolvedValue({
url: 'https://school.learnworlds.com/sso?token=output',
user_id: 'lw-user-out',
});
const params = [
{ Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const },
{ Name: 'Email', Value: 'student@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]);
const loginUrlParam = params.find((p) => p.Name === 'LoginURL');
const userIdParam = params.find((p) => p.Name === 'LearnWorldsUserID');
expect(loginUrlParam).toBeDefined();
expect(loginUrlParam?.Value).toBe('https://school.learnworlds.com/sso?token=output');
expect(userIdParam).toBeDefined();
expect(userIdParam?.Value).toBe('lw-user-out');
});
it('should return error when validation fails', async () => {
const runParams = {
Params: [
{ Name: 'CompanyID', Value: 'comp-1', Type: 'Input' as const },
// Neither Email nor UserID provided
],
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('Either Email or UserID is required');
});
});
});