Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions tests/services/Permission.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const httpStub = {
doGet: jest.fn(),
doPost: jest.fn(),
doDelete: jest.fn(),
};

const appConfigStub = {
webAPIRoot: '/api/',
userAuthenticationEnabled: true,
};

const authApiStub = {
subject: jest.fn(() => 'owner'),
};

const defineOrReplace = (name, factory) => {
if (requirejsInstance.defined(name)) {
requirejsInstance.undef(name);
}
requirejsInstance.define(name, [], factory);
};

let permissionService;
let ko;

beforeAll(() => {
defineOrReplace('appConfig', () => appConfigStub);
defineOrReplace('services/http', () => httpStub);
defineOrReplace('services/AuthAPI', () => authApiStub);
});

beforeEach(() => {
jest.clearAllMocks();
});

beforeAll(async () => {
[permissionService, ko] = await requireAmd(['services/Permission', 'knockout']);
});

describe('Permission service', () => {
test('loadRoleSuggestions forwards role search term to backend', async () => {
httpStub.doGet.mockResolvedValue({ data: ['admin'] });

const data = await permissionService.loadRoleSuggestions('adm');

expect(httpStub.doGet).toHaveBeenCalledWith('/api/permission/access/suggest', { roleSearch: 'adm' });
expect(data).toEqual(['admin']);
});

test('loadEntityAccessList defaults perm_type to WRITE', async () => {
httpStub.doGet.mockResolvedValue({ data: [{ id: 1 }] });

const data = await permissionService.loadEntityAccessList('cohort', 42);

expect(httpStub.doGet).toHaveBeenCalledWith('/api/permission/access/cohort/42/WRITE');
expect(data).toEqual([{ id: 1 }]);
});

test('grantEntityAccess posts access request payload', () => {
permissionService.grantEntityAccess('cohort', 42, 99, 'READ');

expect(httpStub.doPost).toHaveBeenCalledWith(
'/api/permission/access/cohort/42/role/99',
{ accessType: 'READ' }
);
});

test('revokeEntityAccess sends delete with chosen access type', () => {
permissionService.revokeEntityAccess('cohort', 42, 99, 'CUSTOM');

expect(httpStub.doDelete).toHaveBeenCalledWith(
'/api/permission/access/cohort/42/role/99',
{ accessType: 'CUSTOM' }
);
});

test('decorateComponent wires owner checks and proxy methods', async () => {
httpStub.doGet.mockResolvedValue({ data: [] });
const component = {};
const getters = {
entityTypeGetter: () => 'cohort',
entityIdGetter: () => 12,
createdByUsernameGetter: () => 'owner',
};

permissionService.decorateComponent(component, getters);

expect(component.isAccessModalShown()).toBe(false);
expect(component.isOwner()).toBe(true);

await component.loadAccessList('READ');
expect(httpStub.doGet).toHaveBeenCalledWith('/api/permission/access/cohort/12/READ');

component.grantAccess(5, 'READ');
expect(httpStub.doPost).toHaveBeenLastCalledWith(
'/api/permission/access/cohort/12/role/5',
{ accessType: 'READ' }
);

component.revokeAccess(5, 'READ');
expect(httpStub.doDelete).toHaveBeenLastCalledWith(
'/api/permission/access/cohort/12/role/5',
{ accessType: 'READ' }
);

await component.loadAccessRoleSuggestions('adm');
expect(httpStub.doGet).toHaveBeenLastCalledWith('/api/permission/access/suggest', { roleSearch: 'adm' });
});
});
38 changes: 38 additions & 0 deletions tests/services/Source.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const httpStub = {
doGet: jest.fn(),
};

const appConfigStub = {
webAPIRoot: '/api/',
};

const defineOrReplace = (name, factory) => {
if (requirejsInstance.defined(name)) {
requirejsInstance.undef(name);
}
requirejsInstance.define(name, [], factory);
};

let sourceService;

beforeAll(() => {
defineOrReplace('services/http', () => httpStub);
defineOrReplace('appConfig', () => appConfigStub);
});

beforeEach(() => {
jest.clearAllMocks();
});

beforeAll(async () => {
sourceService = await requireAmd(['services/Source']);
});

test('loadSourceList returns sources from API response', async () => {
httpStub.doGet.mockResolvedValue({ data: [{ sourceId: 1 }] });

const sources = await sourceService.loadSourceList();

expect(httpStub.doGet).toHaveBeenCalledWith('/api/source/sources');
expect(sources).toEqual([{ sourceId: 1 }]);
});
129 changes: 129 additions & 0 deletions tests/utils/ChartUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const ensureModule = (name, factory) => {
if (!requirejsInstance.defined(name)) {
requirejsInstance.define(name, [], factory);
}
};

let ChartUtils;
const createModuleArray = () => {
// Use ChartUtils itself to create an Array instance from the AMD context so instanceof checks succeed.
const [series] = ChartUtils.mapMonthYearDataToSeries({ x: [], y: [], p: [] });
series.values.length = 0;
return series.values;
};

beforeAll(() => {
ensureModule('d3', () => ({
format: () => (value) => value,
keys: Object.keys,
}));

ensureModule('lodash', () => ({
isFinite: Number.isFinite,
}));

ensureModule('html2canvas', () => jest.fn(() => Promise.resolve()));
ensureModule('file-saver', () => ({}));
ensureModule('svgsaver', () => class MockSvgSaver {});
});

beforeAll(async () => {
ChartUtils = await requireAmd(['utils/ChartUtils']);
});

describe('ChartUtils', () => {
describe('mapConceptData', () => {
test('transforms simple concept array and sorts alphabetically', () => {
const dataset = createModuleArray();
dataset.push(
{ conceptId: '2', conceptName: 'Beta', countValue: '15' },
{ conceptId: '1', conceptName: 'Alpha', countValue: '10' },
{ conceptName: null, countValue: '5' },
);

const result = ChartUtils.mapConceptData(dataset);

expect(result).toEqual([
{ id: 1, label: 'Alpha', value: 10 },
{ id: 2, label: 'Beta', value: 15 },
{ id: null, label: 'NULL (empty)', value: 5 },
]);
});

test('creates rows from single-value datasets', () => {
const dataset = {
conceptId: 7,
conceptName: 'Solo',
countValue: 3,
};

const result = ChartUtils.mapConceptData(dataset);

expect(result).toEqual([
{ id: 7, label: 'Solo', value: 3 },
]);
});
});

test('normalizeArray numerifies string values when requested', () => {
const table = createModuleArray();
table.push(
{ a: '1', b: '2' },
{ a: '3', b: '4' },
);

const normalized = ChartUtils.normalizeArray(table, true);

expect(normalized).toEqual({
a: [1, 3],
b: [2, 4],
});
});

test('mapMonthYearDataToSeries converts integers to chronological series', () => {
const data = {
x: [202201, 202112],
y: [10, 5],
p: [0.5, 0.25],
};

const [series] = ChartUtils.mapMonthYearDataToSeries(data);

expect(series.name).toBe('All Time');
expect(series.values).toHaveLength(2);
expect(series.values[0].xValue).toEqual(new Date(2021, 11, 1));
expect(series.values[0].yValue).toBe(5);
expect(series.values[0].yPercent).toBe(0.25);
expect(series.values[1].xValue).toEqual(new Date(2022, 0, 1));
});

test('buildHierarchyFromJSON constructs nested nodes honoring threshold', () => {
const data = {
conceptPath: ['Root||ChildA', 'Root||ChildB'],
percentPersons: [70, 30],
numPersons: [7, 3],
conceptId: [1, 2],
agg: [100, 50],
};

const hierarchy = ChartUtils.buildHierarchyFromJSON(data, 0.2, { name: 'agg' });

expect(hierarchy.name).toBe('root');
expect(hierarchy.children).toHaveLength(1);
const root = hierarchy.children[0];
expect(root.name).toBe('Root');
expect(root.children).toHaveLength(2);
expect(root.children[0]).toMatchObject({
name: 'ChildA',
num_persons: 7,
agg_value: 100,
});
});

test('filterByConcept returns predicate matching concept ids', () => {
const predicate = ChartUtils.filterByConcept(99);

expect(predicate({ conceptId: 99 })).toBe(true);
expect(predicate({ conceptId: 1 })).toBe(false);
});
});
62 changes: 62 additions & 0 deletions tests/utils/ExecutionUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const constStub = {
generationStatuses: {
STARTED: 'STARTED',
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
},
executionStatuses: {
PENDING: 'PENDING',
STARTED: 'STARTED',
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
},
};

let executionUtils;
let ko;

beforeAll(() => {
if (requirejsInstance.defined('const')) {
requirejsInstance.undef('const');
}

requirejsInstance.define('const', [], () => constStub);
});

beforeAll(async () => {
[executionUtils, ko] = await requireAmd(['utils/ExecutionUtils', 'knockout']);
ko.i18n = jest.fn(() => () => 'Start a new execution?');
});

describe('ExecutionUtils', () => {
describe('StartExecution', () => {
test('rejects when no execution group is provided', async () => {
await expect(executionUtils.StartExecution()).rejects.toBeUndefined();
});

test('resolves immediately when generation is not running', async () => {
const executionGroup = { status: () => constStub.generationStatuses.COMPLETED };

await expect(executionUtils.StartExecution(executionGroup)).resolves.toBeUndefined();
});
});

describe('getExecutionGroupStatus', () => {
test('prioritizes pending over other statuses', () => {
const submissions = ko.observableArray([
{ status: constStub.executionStatuses.RUNNING },
{ status: constStub.executionStatuses.PENDING },
]);

expect(executionUtils.getExecutionGroupStatus(submissions)).toBe(constStub.executionStatuses.PENDING);
});

test('falls back to completed when no active submissions remain', () => {
const submissions = ko.observableArray([
{ status: constStub.executionStatuses.COMPLETED },
]);

expect(executionUtils.getExecutionGroupStatus(submissions)).toBe(constStub.executionStatuses.COMPLETED);
});
});
});
40 changes: 40 additions & 0 deletions tests/utils/Renderers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
let renderers;
let ko;

beforeAll(async () => {
[renderers, ko] = await requireAmd(['utils/Renderers', 'knockout']);
});

describe('Renderers', () => {
describe('renderCheckbox', () => {
test('includes click binding when checkbox is interactive', () => {
const template = renderers.renderCheckbox('isSelected');

expect(template).toContain('click: function(d) { d.isSelected(!d.isSelected()); },');
expect(template).toContain('css: { selected: isSelected }');
});

test('omits click binding when checkbox is read-only', () => {
const template = renderers.renderCheckbox('isActive', false);

expect(template).not.toContain('click: function');
expect(template).toContain('css: { selected: isActive }');
});
});

describe('renderConceptSetCheckbox', () => {
test('returns interactive markup when permissions are granted', () => {
const template = renderers.renderConceptSetCheckbox(ko.observable(true), 'isConceptSelected');

expect(template).toContain('click: function(d) { d.isConceptSelected(!d.isConceptSelected()); },');
expect(template).not.toContain('readonly');
});

test('returns readonly markup when permissions are missing or readonly flag is true', () => {
const template = renderers.renderConceptSetCheckbox(ko.observable(false), 'isConceptSelected', true);

expect(template).toContain('readonly');
expect(template).not.toContain('click: function');
});
});
});
Loading