From c16ad8347714b8d22c86f5c40cbc15ebe2080ea5 Mon Sep 17 00:00:00 2001 From: Aaro Koivukangas Date: Tue, 18 Aug 2026 10:25:40 +0300 Subject: [PATCH] [Tests] Add api and unit tests for Custom population related features --- ...pulationStatisticsByProgrammeCodes.test.ts | 50 ++++++ ...pulationStatisticsByStudentNumbers.test.ts | 49 ++++++ .../populationStatisticsByStudentNumbers.ts | 23 --- .../frontend/tests/unit/courseMatrix.test.ts | 152 ++++++++++++++++++ 4 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 services/backend/tests/api/population/populationStatisticsByProgrammeCodes.test.ts create mode 100644 services/backend/tests/api/population/populationStatisticsByStudentNumbers.test.ts delete mode 100644 services/backend/tests/api/population/populationStatisticsByStudentNumbers.ts create mode 100644 services/frontend/tests/unit/courseMatrix.test.ts diff --git a/services/backend/tests/api/population/populationStatisticsByProgrammeCodes.test.ts b/services/backend/tests/api/population/populationStatisticsByProgrammeCodes.test.ts new file mode 100644 index 000000000..7169a3672 --- /dev/null +++ b/services/backend/tests/api/population/populationStatisticsByProgrammeCodes.test.ts @@ -0,0 +1,50 @@ +import { Express } from 'express' +import request from 'supertest' +import { describe, it, beforeAll, assert } from 'vitest' + +import { CanError } from '@oodikone/shared/routes' +import { CustomPopulationByProgrammesResBody } from '@oodikone/shared/routes/populations' +import { initTests, ResponseWithBody } from '../../utils' + +void describe('Population statistics by programme codes', () => { + let app: Express + beforeAll(async () => { + app = await initTests() + }) + + it('should not return anything with missing years', async () => { + const res = (await request(app) + .post('/populationstatisticsbyprogrammecodes') + .send({ programmes: ['KH50_001'] }) + .set('shib-session-id', 'test') + .set('uid', 'basic') + .set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody> + + assert.deepEqual(res.status, 400) + assert.deepEqual(res.body, { error: 'Programme based populations require years to be defined' }) + }) + + it('should not return any data to unauthorized user', async () => { + const res = (await request(app) + .post('/populationstatisticsbyprogrammecodes') + .send({ programmes: ['MH50_001'], years: ['2021'] }) + .set('shib-session-id', 'test') + .set('uid', 'onlyiamrights') + .set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody> + + assert.deepEqual(res.status, 403) + assert.deepEqual(res.body, { error: 'Trying to request unauthorized students data' }) + }) + + it('should return students in the programmes for authorized user', async () => { + const res = (await request(app) + .post('/populationstatisticsbyprogrammecodes') + .send({ programmes: ['KH50_001'], years: ['2021'] }) + .set('shib-session-id', 'test') + .set('uid', 'basic') + .set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody + + assert.strictEqual(res.status, 200) + assert.notStrictEqual(res.body.students.length, 0) + }) +}) diff --git a/services/backend/tests/api/population/populationStatisticsByStudentNumbers.test.ts b/services/backend/tests/api/population/populationStatisticsByStudentNumbers.test.ts new file mode 100644 index 000000000..b6c31ac9a --- /dev/null +++ b/services/backend/tests/api/population/populationStatisticsByStudentNumbers.test.ts @@ -0,0 +1,49 @@ +import { Express } from 'express' +import request from 'supertest' +import { describe, it, beforeAll, assert } from 'vitest' + +import { CustomPopulationByStudentNumbersResBody } from '@oodikone/shared/routes/populations' +import { initTests, ResponseWithBody } from '../../utils' + +void describe('Population statistics by student number', () => { + let app: Express + beforeAll(async () => { + app = await initTests() + }) + + it('should not return anything with missing parameters', async () => { + const res = await request(app) + .post('/populationstatisticsbystudentnumbers') + .set('shib-session-id', 'test') + .set('uid', 'basic') + .set('hygroupcn', 'grp-oodikone-basic-users') + + assert.deepEqual(res.status, 422) + assert.deepEqual(res.body.error, 'Body should include student numbers') + }) + + it('should not return anything to unauthorized user', async () => { + const res = await request(app) + .post('/populationstatisticsbystudentnumbers') + .send({ studentNumbers: ['433237', '457144'] }) + .set('shib-session-id', 'test') + .set('uid', 'norights') + .set('hygroupcn', 'grp-oodikone-basic-users') + + assert.deepEqual(res.status, 200) + assert.deepEqual(res.body.students, []) + }) + + it('should return students for authorized user', async () => { + const res = (await request(app) + .post('/populationstatisticsbystudentnumbers') + .send({ studentNumbers: ['433237', '457144'] }) + .set('shib-session-id', 'test') + .set('uid', 'basic') + .set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody + + assert.strictEqual(res.status, 200) + assert.strictEqual(res.body.students.length, 2) + assert.deepEqual(res.body.discardedStudentNumbers, []) + }) +}) diff --git a/services/backend/tests/api/population/populationStatisticsByStudentNumbers.ts b/services/backend/tests/api/population/populationStatisticsByStudentNumbers.ts deleted file mode 100644 index d32af1d17..000000000 --- a/services/backend/tests/api/population/populationStatisticsByStudentNumbers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Express } from 'express' -import request from 'supertest' -import { describe, it, beforeAll, assert } from 'vitest' - -import { initTests } from '../../utils' - -void describe('Population statistics by student number', () => { - let app: Express - beforeAll(async () => { - app = await initTests() - }) - - it('should not return anything with missing parameters', async () => { - const res = await request(app) - .post('/populationstatisticsbystudentnumbers') - .set('shib-session-id', 'test') - .set('uid', 'basic') - .set('hygroupcn', 'grp-oodikone-basic-users') - - assert.deepEqual(res.status, 422) - assert.deepEqual(res.body.error, 'Body should include student numbers') - }) -}) diff --git a/services/frontend/tests/unit/courseMatrix.test.ts b/services/frontend/tests/unit/courseMatrix.test.ts new file mode 100644 index 000000000..d1d675a22 --- /dev/null +++ b/services/frontend/tests/unit/courseMatrix.test.ts @@ -0,0 +1,152 @@ +import { assert, describe, it } from 'vitest' + +import { calculateExcelData, getHopsCourses } from '@/components/CustomPopulation/courseMatrix' +import { CreditTypeCode } from '@oodikone/shared/types' +import { FormattedStudent, StudentCourse, StudentStudyPlan } from '@oodikone/shared/types/studentData' + +const createCourse = ( + course_code: string, + credits: number, + passed: boolean, + date: Date, + credittypecode: CreditTypeCode = passed ? CreditTypeCode.PASSED : CreditTypeCode.FAILED +): StudentCourse => ({ + course_code, + date, + passed, + grade: passed ? '5' : 'Hyl.', + credits, + isStudyModuleCredit: false, + credittypecode, + language: 'fi', + studyright_id: 'sr-1', +}) + +const createHops = (includedCourses: string[]): StudentStudyPlan => ({ + included_courses: includedCourses, + programme_code: 'TEST', + includedModules: [], + completed_credits: 0, + curriculum_period_id: 'cp-1', + sis_study_right_id: 'sr-1', +}) + +const createStudent = (overrides: Partial): FormattedStudent => ({ + firstnames: 'Testi', + lastname: 'Opiskelija', + started: new Date('2024-08-01'), + studentNumber: '123456789', + credits: 0, + hopsCredits: 0, + name: 'Testi Opiskelija', + gender_code: '1', + email: 'testi@example.com', + secondaryEmail: '', + phoneNumber: '', + updatedAt: new Date(), + tags: [], + studyrightStart: '2024-08-01', + option: null, + birthdate: new Date('2000-01-01'), + sis_person_id: '1', + citizenships: [], + criteriaProgress: {}, + curriculumVersion: null, + hasPersonalIdentityCode: false, + transferredStudyright: false, + transferSource: undefined, + studyRights: [], + studyplans: [], + courses: [], + enrollments: [], + ...overrides, +}) + +void describe('getHopsCourses', () => { + void it('returns only passed courses included in the student HOPS', () => { + const student = createStudent({ + courses: [ + createCourse('A', 5, true, new Date('2024-09-01')), + createCourse('B', 5, true, new Date('2024-10-01')), + createCourse('C', 5, false, new Date('2024-11-01')), + ], + studyplans: [createHops(['A', 'C'])], + }) + + const result = getHopsCourses(student) + + assert.equal(result.length, 1) + assert.equal(result[0].course_code, 'A') + }) + + void it('returns an empty list when the student has no HOPS', () => { + const student = createStudent({ + courses: [createCourse('A', 5, true, new Date('2024-09-01'))], + studyplans: [], + }) + + assert.deepEqual(getHopsCourses(student), []) + }) + + void it('dedupes multiple completions keeping the latest', () => { + const student = createStudent({ + courses: [createCourse('A', 5, true, new Date('2024-09-01')), createCourse('A', 6, true, new Date('2025-01-15'))], + studyplans: [createHops(['A'])], + }) + + const result = getHopsCourses(student) + + assert.equal(result.length, 1) + assert.equal(result[0].credits, 6) + }) +}) + +void describe('calculateExcelData', () => { + void it('builds completed-course rows and aggregates credits per course', () => { + const students = [ + createStudent({ + studentNumber: '1', + courses: [createCourse('A', 5, true, new Date('2024-09-01'))], + studyplans: [createHops(['A'])], + }), + createStudent({ + studentNumber: '2', + courses: [ + createCourse('A', 5, true, new Date('2024-09-01')), + createCourse('B', 3, true, new Date('2024-10-01')), + ], + studyplans: [createHops(['A', 'B'])], + }), + ] + const courseNameByCode = new Map([ + ['A', 'Course A'], + ['B', 'Course B'], + ]) + + const data = calculateExcelData(students, courseNameByCode) + + assert.deepEqual(data.completedCoursesRows, [ + ['1', 'Testi Opiskelija', 'Course A (A)'], + ['2', 'Testi Opiskelija', 'Course A (A)', 'Course B (B)'], + ]) + assert.deepEqual(data.courseCounterRows, [ + ['A', 'Course A', '2', '10'], + ['B', 'Course B', '1', '3'], + ]) + }) + + void it('uses an empty name for courses missing from the course map', () => { + const students = [ + createStudent({ + studentNumber: '1', + courses: [createCourse('RANDOMCODE', 5, true, new Date('2024-09-01'))], + studyplans: [createHops(['RANDOMCODE'])], + }), + ] + + const data = calculateExcelData(students, new Map()) + + assert.deepEqual(data.completedCoursesRows, [['1', 'Testi Opiskelija', ' (RANDOMCODE)']]) + assert.deepEqual(data.courseCounterRows, [['RANDOMCODE', '', '1', '5']]) + }) +})