mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[UnitTests][Updater] Add basic unit tests to updater
This commit is contained in:
committed by
Aaro Koivukangas
parent
3a1bee9b79
commit
f5229ee678
+1161
-2
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,10 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ./src/index.js",
|
||||
"dev": "nodemon ./src/index.js"
|
||||
"dev": "nodemon ./src/index.js",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run ./tests/unit",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^3.2.6",
|
||||
@@ -21,6 +24,7 @@
|
||||
"winston-transport-sentry-node": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "3.1.14"
|
||||
"nodemon": "3.1.14",
|
||||
"vitest": "4.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
isModule,
|
||||
} from '../mapper.js'
|
||||
|
||||
const earlierOf = (a, b) => (new Date(a) < new Date(b) ? a : b)
|
||||
const laterOf = (a, b) => (new Date(a) > new Date(b) ? a : b)
|
||||
|
||||
const updateTeachers = async attainments => {
|
||||
const acceptorPersonIds = flatten(
|
||||
attainments.map(attainment =>
|
||||
@@ -184,6 +187,18 @@ export const updateAttainments = async (
|
||||
coursecode: parsedCourseCode,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// If the course exists, update it
|
||||
coursesToBeCreated.set(parsedCourseCode, {
|
||||
...course,
|
||||
groupId: course.groupId ?? course.id,
|
||||
name: att.name,
|
||||
isPrimary: course.isPrimary ?? true,
|
||||
coursetypecode: att.study_level_urn,
|
||||
courseUnitType: att.course_unit_type_urn,
|
||||
minAttainmentDate: earlierOf(course.minAttainmentDate, att.attainment_date),
|
||||
maxAttainmentDate: laterOf(course.maxAttainmentDate, att.attainment_date),
|
||||
})
|
||||
}
|
||||
|
||||
const courseIdToUse = attIdToCourseCode[att.id]
|
||||
@@ -204,7 +219,6 @@ export const updateAttainments = async (
|
||||
}
|
||||
|
||||
courseUnit = course || { id: courseIdToUse, code: courseIdToUse }
|
||||
courseUnit.group_id = courseUnit.id
|
||||
}
|
||||
|
||||
// Add the CU to the mapping objects for creditMapper to work properly.
|
||||
@@ -230,6 +244,7 @@ export const updateAttainments = async (
|
||||
const attainmentIdCourseCodeMapForCustomCourseUnitAttainments = attainments.reduce(findMissingCourseCodes, {})
|
||||
const missingCodes = Object.values(attainmentIdCourseCodeMapForCustomCourseUnitAttainments)
|
||||
const courses = await selectFromByIds('course_units', missingCodes, 'code')
|
||||
|
||||
return await Promise.all(
|
||||
attainments.map(
|
||||
addCourseUnitToCustomCourseUnitAttainments(courses, attainmentIdCourseCodeMapForCustomCourseUnitAttainments)
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// updateAttainments talks directly to knex, Sequelize models, and the db/index.js helpers. This
|
||||
// establishes the pattern for testing this kind of DB-coupled orchestrator: mock the DB layer at
|
||||
// its boundary and let the real (pure) mapping logic run on top of canned data.
|
||||
//
|
||||
// vi.hoisted is required because vi.mock factories run before the rest of this file, so any mock
|
||||
// state they reference (and that assertions below need to inspect) has to be created up front.
|
||||
const { knexMock, bulkCreateMock, selectFromByIdsMock, selectOneByIdMock } = vi.hoisted(() => {
|
||||
const makeChainableQuery = result => {
|
||||
const builder = {
|
||||
select: () => builder,
|
||||
from: () => builder,
|
||||
where: () => builder,
|
||||
whereIn: () => builder,
|
||||
then: (resolve, reject) => Promise.resolve(result).then(resolve, reject),
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
const tableResults = {
|
||||
course_units: [{ id: 'cu1', code: 'TKT10' }],
|
||||
modules: [],
|
||||
}
|
||||
|
||||
const knexMock = Object.assign(
|
||||
vi.fn(table => makeChainableQuery(tableResults[table] ?? [])),
|
||||
{ select: () => makeChainableQuery([]) }
|
||||
)
|
||||
|
||||
return {
|
||||
knexMock,
|
||||
bulkCreateMock: vi.fn(),
|
||||
selectFromByIdsMock: vi.fn().mockResolvedValue([]),
|
||||
selectOneByIdMock: vi.fn().mockResolvedValue(null),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/db/connection.js', () => ({ dbConnections: { knex: knexMock } }))
|
||||
|
||||
vi.mock('@/db/index.js', () => ({
|
||||
selectFromByIds: selectFromByIdsMock,
|
||||
selectOneById: selectOneByIdMock,
|
||||
bulkCreate: bulkCreateMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/db/models/index.js', () => ({
|
||||
Course: { name: 'Course', findOne: vi.fn() },
|
||||
Teacher: { name: 'Teacher', findOne: vi.fn() },
|
||||
Credit: { name: 'Credit' },
|
||||
CreditTeacher: { name: 'CreditTeacher' },
|
||||
CourseProvider: { name: 'CourseProvider', findOne: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/logger.js', () => ({
|
||||
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
|
||||
}))
|
||||
|
||||
// updateAttainments builds its credit rows via mapper.js's real creditMapper, which in turn reads
|
||||
// from shared.js's Redis-backed lookup maps - mocked here the same way as in mapper.test.js.
|
||||
vi.mock('@/updater/shared.js', () => ({
|
||||
getSemesterByDate: vi.fn(() => ({ semestercode: 100, composite: '2020-2021-fall' })),
|
||||
getGrade: vi.fn(() => ({ value: '5', passed: true })),
|
||||
getUniOrgId: vi.fn(() => 'uni-org1'),
|
||||
getCountry: vi.fn(),
|
||||
getCreditTypeCodeFromAttainment: vi.fn(() => 'PASSED'),
|
||||
educationTypeToExtentcode: {},
|
||||
}))
|
||||
|
||||
import { Course } from '@/db/models/index.js'
|
||||
import { updateAttainments } from '@/updater/updateStudents/attainments.js'
|
||||
|
||||
describe('updateAttainments', () => {
|
||||
beforeEach(() => {
|
||||
bulkCreateMock.mockClear()
|
||||
})
|
||||
|
||||
/** Get what calls were directed to the wanted model by bulkCreate */
|
||||
const findBulkCreateCall = modelName => bulkCreateMock.mock.calls.find(([model]) => model.name === modelName)
|
||||
|
||||
it('maps a plain course unit attainment into a Credit bulkCreate call', async () => {
|
||||
const attainment = {
|
||||
id: 'att1',
|
||||
type: 'CourseUnitAttainment',
|
||||
misregistration: false,
|
||||
credits: 5,
|
||||
person_id: 'p1',
|
||||
registration_date: '2020-01-10',
|
||||
grade_scale_id: 'scale1',
|
||||
grade_id: 'grade1',
|
||||
organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }],
|
||||
attainment_date: '2020-01-15',
|
||||
course_unit_id: 'cu1',
|
||||
module_id: null,
|
||||
module_group_id: null,
|
||||
study_right_id: null,
|
||||
attainment_language_urn: 'urn:code:language:fi',
|
||||
acceptor_persons: [],
|
||||
}
|
||||
|
||||
await updateAttainments([attainment], { p1: '014123456' }, new Set(), {})
|
||||
|
||||
const [, credits] = findBulkCreateCall('Credit')
|
||||
expect(credits).toHaveLength(1)
|
||||
expect(credits[0]).toMatchObject({
|
||||
id: 'att1',
|
||||
student_studentnumber: '014123456',
|
||||
course_id: 'cu1',
|
||||
course_code: 'TKT10',
|
||||
credits: 5,
|
||||
is_open: false,
|
||||
})
|
||||
|
||||
// No custom-attainment repairs or acceptor persons were involved, so these come back empty.
|
||||
expect(findBulkCreateCall('Teacher')[1]).toEqual([])
|
||||
expect(findBulkCreateCall('Course')[1]).toEqual([])
|
||||
expect(findBulkCreateCall('CreditTeacher')[1]).toEqual([])
|
||||
expect(findBulkCreateCall('CourseProvider')[1]).toEqual([])
|
||||
})
|
||||
|
||||
it('creates new custom course unit from an attainment', async () => {
|
||||
const attainment = {
|
||||
id: 'att-custom-1',
|
||||
type: 'CustomCourseUnitAttainment',
|
||||
code: 'XYZ-123',
|
||||
misregistration: false,
|
||||
credits: 5,
|
||||
person_id: 'p1',
|
||||
registration_date: '2020-01-10',
|
||||
grade_scale_id: 'scale1',
|
||||
grade_id: 'grade1',
|
||||
organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }],
|
||||
attainment_date: '2020-01-15',
|
||||
course_unit_id: null,
|
||||
module_id: null,
|
||||
module_group_id: null,
|
||||
study_right_id: null,
|
||||
attainment_language_urn: 'urn:code:language:fi',
|
||||
acceptor_persons: [],
|
||||
name: { fi: 'Testikurssi' },
|
||||
study_level_urn: 'lvl1',
|
||||
course_unit_type_urn: 'type1',
|
||||
}
|
||||
|
||||
await updateAttainments([attainment], { p1: '014123456' }, new Set(), {})
|
||||
|
||||
const [, courses] = findBulkCreateCall('Course')
|
||||
expect(courses).toHaveLength(1)
|
||||
expect(courses[0].groupId).toBe(courses[0].id)
|
||||
})
|
||||
|
||||
it('updates custom course unit with fields from a new attainment', async () => {
|
||||
Course.findOne.mockResolvedValueOnce({
|
||||
id: 'XYZ-123',
|
||||
groupId: null, // Group id missing
|
||||
code: 'XYZ-123',
|
||||
name: { fi: 'Vanha nimi' }, // Incorrect name
|
||||
isStudyModule: false,
|
||||
isPrimary: true,
|
||||
coursetypecode: 'lvl1',
|
||||
maxAttainmentDate: '2019-06-01',
|
||||
minAttainmentDate: '2019-01-01',
|
||||
substitutionGroups: [],
|
||||
courseUnitType: 'type1',
|
||||
})
|
||||
|
||||
const attainment = {
|
||||
id: 'att-custom-2',
|
||||
type: 'CustomCourseUnitAttainment',
|
||||
code: 'XYZ-123',
|
||||
misregistration: false,
|
||||
credits: 5,
|
||||
person_id: 'p1',
|
||||
registration_date: '2020-01-10',
|
||||
grade_scale_id: 'scale1',
|
||||
grade_id: 'grade1',
|
||||
organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }],
|
||||
attainment_date: '2020-01-15',
|
||||
course_unit_id: null,
|
||||
module_id: null,
|
||||
module_group_id: null,
|
||||
study_right_id: null,
|
||||
attainment_language_urn: 'urn:code:language:fi',
|
||||
acceptor_persons: [],
|
||||
name: { fi: 'Uusi nimi' },
|
||||
study_level_urn: 'lvl1',
|
||||
course_unit_type_urn: 'type1',
|
||||
}
|
||||
|
||||
await updateAttainments([attainment], { p1: '014123456' }, new Set(), {})
|
||||
|
||||
const [, courses] = findBulkCreateCall('Course')
|
||||
expect(courses).toHaveLength(1)
|
||||
expect(courses[0].id).toBe('XYZ-123')
|
||||
expect(courses[0].groupId).toBe('XYZ-123')
|
||||
expect(courses[0].name.fi).toBe('Uusi nimi')
|
||||
expect(courses[0].maxAttainmentDate).toBe('2020-01-15')
|
||||
expect(courses[0].minAttainmentDate).toBe('2019-01-01')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { courseProviderMapper } from '@/updater/mapper.js'
|
||||
|
||||
describe('courseProviderMapper', () => {
|
||||
it('maps organisation shares to a course provider row', () => {
|
||||
const mapProvider = courseProviderMapper('CG1')
|
||||
expect(mapProvider({ organisationId: 'org1', shares: 50 })).toEqual({
|
||||
coursecode: 'CG1',
|
||||
organizationcode: 'org1',
|
||||
shares: 50,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { creditMapper } from '@/updater/mapper.js'
|
||||
import { getCreditTypeCodeFromAttainment, getGrade, getSemesterByDate, getUniOrgId } from '@/updater/shared.js'
|
||||
|
||||
describe('creditMapper', () => {
|
||||
const baseAttainment = {
|
||||
id: 'att1',
|
||||
credits: 5,
|
||||
person_id: 'p1',
|
||||
registration_date: '2020-01-10',
|
||||
grade_scale_id: 'scale1',
|
||||
grade_id: 'grade1',
|
||||
organisations: [{ organisationId: 'org1', roleUrn: 'urn:code:organisation-role:responsible-organisation' }],
|
||||
attainment_date: '2020-01-15',
|
||||
type: 'CourseUnitAttainment',
|
||||
course_unit_id: 'cu1',
|
||||
module_id: null,
|
||||
module_group_id: null,
|
||||
study_right_id: null,
|
||||
attainment_language_urn: 'urn:code:language:fi',
|
||||
}
|
||||
|
||||
const lookupMaps = (overrides = {}) => ({
|
||||
personIdToStudentNumber: { p1: '014123456' },
|
||||
courseUnitIdToCourseCode: { cu1: 'TKT10' },
|
||||
moduleGroupIdToModuleCode: {},
|
||||
studyRightIdToEducationType: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const mapCredit = maps =>
|
||||
creditMapper(
|
||||
maps.personIdToStudentNumber,
|
||||
maps.courseUnitIdToCourseCode,
|
||||
maps.moduleGroupIdToModuleCode,
|
||||
maps.studyRightIdToEducationType
|
||||
)
|
||||
|
||||
it('maps attainment correctly into credit', () => {
|
||||
getUniOrgId.mockReturnValue('uni-org1')
|
||||
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
|
||||
getGrade.mockReturnValue({ value: '5', passed: true })
|
||||
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
|
||||
|
||||
const result = mapCredit(lookupMaps())(baseAttainment)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'att1',
|
||||
grade: '5',
|
||||
student_studentnumber: '014123456',
|
||||
credits: 5,
|
||||
createdate: '2020-01-10',
|
||||
credittypecode: 'PASSED',
|
||||
attainment_date: '2020-01-15',
|
||||
course_id: 'cu1',
|
||||
course_code: 'TKT10',
|
||||
semestercode: 100,
|
||||
semester_composite: '2020-2021-fall',
|
||||
isStudyModule: false,
|
||||
org: 'uni-org1',
|
||||
language: 'fi',
|
||||
is_open: false,
|
||||
studyright_id: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when there is no target semester for the attainment date', () => {
|
||||
getUniOrgId.mockReturnValue('uni-org1')
|
||||
getSemesterByDate.mockReturnValue(undefined)
|
||||
|
||||
expect(mapCredit(lookupMaps())(baseAttainment)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when the course code cannot be resolved', () => {
|
||||
getUniOrgId.mockReturnValue('uni-org1')
|
||||
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
|
||||
|
||||
const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: {} }))(baseAttainment)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('marks open university attainments via the AY course code prefix', () => {
|
||||
getUniOrgId.mockReturnValue('uni-org1')
|
||||
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
|
||||
getGrade.mockReturnValue({ value: '5', passed: true })
|
||||
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
|
||||
|
||||
const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: { cu1: 'AY123' } }))(baseAttainment)
|
||||
|
||||
expect(result.is_open).toBe(true)
|
||||
})
|
||||
|
||||
it('marks open university attainments via the study right education type', () => {
|
||||
getUniOrgId.mockReturnValue('uni-org1')
|
||||
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
|
||||
getGrade.mockReturnValue({ value: '5', passed: true })
|
||||
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
|
||||
|
||||
const attainment = { ...baseAttainment, study_right_id: 'sr1' }
|
||||
const result = mapCredit(
|
||||
lookupMaps({
|
||||
studyRightIdToEducationType: { sr1: 'urn:code:education-type:non-degree-education:open-university-studies' },
|
||||
})
|
||||
)(attainment)
|
||||
|
||||
expect(result.is_open).toBe(true)
|
||||
})
|
||||
|
||||
it('returns null with missing organization', () => {
|
||||
const attainment = { ...baseAttainment, organisations: undefined }
|
||||
|
||||
expect(mapCredit(lookupMaps())(attainment)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { enrollmentMapper } from '@/updater/mapper.js'
|
||||
import { getSemesterByDate } from '@/updater/shared.js'
|
||||
|
||||
describe('enrollmentMapper', () => {
|
||||
const baseEnrollment = {
|
||||
id: 'e1',
|
||||
person_id: 'p1',
|
||||
course_unit_id: 'cu1',
|
||||
course_unit_realisation_id: 'real1',
|
||||
enrolment_date_time: '2020-01-01T00:00:00Z',
|
||||
state: 'ACTIVE',
|
||||
study_right_id: null,
|
||||
}
|
||||
|
||||
const mapEnrollment = (overrides = {}) =>
|
||||
enrollmentMapper(
|
||||
overrides.personIdToStudentNumber ?? { p1: '014111111' },
|
||||
overrides.courseUnitIdToCourseUnit ?? { cu1: { code: 'TKT10', group_id: 'grp1' } },
|
||||
overrides.realisationIdToActivityPeriod ?? {},
|
||||
overrides.studyRightIdToEducationType ?? {}
|
||||
)
|
||||
|
||||
it('returns null when the student number cannot be resolved', () => {
|
||||
expect(mapEnrollment({ personIdToStudentNumber: {} })(baseEnrollment)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when the course unit cannot be resolved', () => {
|
||||
expect(mapEnrollment({ courseUnitIdToCourseUnit: {} })(baseEnrollment)).toBeNull()
|
||||
})
|
||||
|
||||
it('maps an enrollment correctly', () => {
|
||||
getSemesterByDate.mockReturnValue({ semestercode: 5, composite: 'sem5' })
|
||||
|
||||
expect(mapEnrollment()(baseEnrollment)).toEqual({
|
||||
id: 'e1',
|
||||
studentnumber: '014111111',
|
||||
state: 'ACTIVE',
|
||||
course_code: 'TKT10',
|
||||
semestercode: 5,
|
||||
semester_composite: 'sem5',
|
||||
enrollment_date_time: '2020-01-01T00:00:00Z',
|
||||
is_open: false,
|
||||
course_id: 'cu1',
|
||||
course_group_id: 'grp1',
|
||||
studyright_id: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// mapper.js imports logger.js and shared.js at module load time. shared.js transitively
|
||||
// connects to Redis and instantiates Sequelize models as a side effect of being imported, so both
|
||||
// are mocked here to keep these unit tests pure and fast.
|
||||
//
|
||||
// NOTE: Import this module first - before importing anything from mapper.js - in every test file in
|
||||
// this directory. Tests that need per-case control over a shared.js lookup (e.g. creditMapper,
|
||||
// enrollmentMapper) can still `import { getSemesterByDate, ... } from '.../updater/shared.js'`
|
||||
// afterwards and call `.mockReturnValue(...)` on it - it's the same mocked function instance.
|
||||
vi.mock('@/utils/logger.js', () => ({
|
||||
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/updater/shared.js', () => ({
|
||||
getSemesterByDate: vi.fn(),
|
||||
getGrade: vi.fn(),
|
||||
getUniOrgId: vi.fn(),
|
||||
getCountry: vi.fn(),
|
||||
getCreditTypeCodeFromAttainment: vi.fn(),
|
||||
educationTypeToExtentcode: { 'edu-type-1': 1 },
|
||||
}))
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { isModule } from '@/updater/mapper.js'
|
||||
|
||||
describe('isModule', () => {
|
||||
it('is true for module attainment types', () => {
|
||||
expect(isModule('ModuleAttainment')).toBe(true)
|
||||
expect(isModule('CustomModuleAttainment')).toBe(true)
|
||||
expect(isModule('DegreeProgrammeAttainment')).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for course unit attainment types', () => {
|
||||
expect(isModule('CourseUnitAttainment')).toBe(false)
|
||||
expect(isModule('CustomCourseUnitAttainment')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { mapCourseType } from '@/updater/mapper.js'
|
||||
|
||||
describe('mapCourseType', () => {
|
||||
it('maps a study level to a course type row', () => {
|
||||
expect(mapCourseType({ id: 'lvl1', name: { fi: 'Taso' } })).toEqual({
|
||||
coursetypecode: 'lvl1',
|
||||
name: { fi: 'Taso' },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { mapCurriculumPeriod } from '@/updater/mapper.js'
|
||||
|
||||
describe('mapCurriculumPeriod', () => {
|
||||
it('maps a curriculum period correctly', () => {
|
||||
expect(
|
||||
mapCurriculumPeriod({
|
||||
id: 'cp1',
|
||||
name: { fi: 'Kausi' },
|
||||
university_org_id: 'org1',
|
||||
active_period: { startDate: '2020-01-01', endDate: '2020-12-31' },
|
||||
})
|
||||
).toEqual({
|
||||
id: 'cp1',
|
||||
name: { fi: 'Kausi' },
|
||||
universityOrgId: 'org1',
|
||||
startDate: new Date('2020-01-01'),
|
||||
endDate: new Date('2020-12-31'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { mapStudyrightExtent } from '@/updater/mapper.js'
|
||||
|
||||
describe('mapStudyrightExtent', () => {
|
||||
it('resolves the extent code from educationTypeToExtentcode', () => {
|
||||
expect(mapStudyrightExtent({ id: 'edu-type-1', name: { fi: 'Koulutus' } })).toEqual({
|
||||
extentcode: 1,
|
||||
name: { fi: 'Koulutus' },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { mapTeacher } from '@/updater/mapper.js'
|
||||
|
||||
describe('mapTeacher', () => {
|
||||
it('combines last and first names when present', () => {
|
||||
expect(mapTeacher({ id: 'p1', last_name: 'Smith', first_names: 'John' })).toEqual({
|
||||
id: 'p1',
|
||||
name: 'Smith John',
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to just the last name when first names are missing', () => {
|
||||
expect(mapTeacher({ id: 'p2', last_name: 'Doe' })).toEqual({ id: 'p2', name: 'Doe' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { sanitizeCourseCode } from '@/updater/mapper.js'
|
||||
|
||||
describe('sanitizeCourseCode', () => {
|
||||
it('returns null for falsy input', () => {
|
||||
expect(sanitizeCourseCode(null)).toBeNull()
|
||||
expect(sanitizeCourseCode('')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the code as-is when there is no surrogate suffix', () => {
|
||||
expect(sanitizeCourseCode('TKT10')).toBe('TKT10')
|
||||
})
|
||||
|
||||
it('keeps a short second segment', () => {
|
||||
expect(sanitizeCourseCode('a1b2-c3d4')).toBe('a1b2-c3d4')
|
||||
})
|
||||
|
||||
it('drops a long (Oodi surrogate) second segment', () => {
|
||||
expect(sanitizeCourseCode('TKT10-123456789')).toBe('TKT10')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { studentMapper } from '@/updater/mapper.js'
|
||||
|
||||
describe('studentMapper', () => {
|
||||
const baseStudent = {
|
||||
last_name: 'Doe',
|
||||
first_names: 'Jane',
|
||||
student_number: '014999999',
|
||||
primary_email: 'jane@example.com',
|
||||
secondary_email: null,
|
||||
phone_number: null,
|
||||
gender_urn: 'urn:code:gender:female',
|
||||
oppija_id: 'oppija1',
|
||||
date_of_birth: '2000-01-01',
|
||||
id: 'person1',
|
||||
has_personal_identity_code: true,
|
||||
preferred_language_urn: 'urn:code:language:fi',
|
||||
citizenships: [],
|
||||
}
|
||||
|
||||
it('filters out the Oodi test student', () => {
|
||||
const testStudent = { ...baseStudent, student_number: '012023965' }
|
||||
expect(studentMapper([], [], new Set())(testStudent)).toBeNull()
|
||||
})
|
||||
|
||||
it('sums credits from valid, primary attainments of the student', () => {
|
||||
const attainments = [
|
||||
{
|
||||
id: 'a1',
|
||||
person_id: 'person1',
|
||||
credits: 5,
|
||||
primary: true,
|
||||
misregistration: false,
|
||||
expiryDate: new Date('2099-01-01'),
|
||||
type: 'CourseUnitAttainment',
|
||||
state: 'ATTAINED',
|
||||
},
|
||||
{
|
||||
id: 'b1',
|
||||
person_id: 'person1',
|
||||
credits: 5,
|
||||
primary: true,
|
||||
misregistration: false,
|
||||
expiryDate: new Date('2099-01-01'),
|
||||
type: 'CourseUnitAttainment',
|
||||
state: 'ATTAINED',
|
||||
},
|
||||
{
|
||||
// improved (non-primary) attainment must not be counted
|
||||
id: 'a2',
|
||||
person_id: 'person1',
|
||||
credits: 3,
|
||||
primary: false,
|
||||
misregistration: false,
|
||||
expiryDate: new Date('2099-01-01'),
|
||||
type: 'CourseUnitAttainment',
|
||||
state: 'ATTAINED',
|
||||
},
|
||||
{
|
||||
// belongs to a different student
|
||||
id: 'a3',
|
||||
person_id: 'other',
|
||||
credits: 100,
|
||||
primary: true,
|
||||
misregistration: false,
|
||||
expiryDate: new Date('2099-01-01'),
|
||||
type: 'CourseUnitAttainment',
|
||||
state: 'ATTAINED',
|
||||
},
|
||||
]
|
||||
|
||||
const result = studentMapper(attainments, [], new Set())(baseStudent)
|
||||
|
||||
expect(result.creditcount).toBe(10)
|
||||
expect(result.studentnumber).toBe('014999999')
|
||||
expect(result.abbreviatedname).toBe('Doe Jane')
|
||||
expect(result.dateofuniversityenrollment).toBeNull()
|
||||
})
|
||||
})
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { termRegistrationTypeToEnrollmenttype } from '@/updater/mapper.js'
|
||||
|
||||
describe('termRegistrationTypeToEnrollmenttype', () => {
|
||||
it('maps known types and falls back to 3', () => {
|
||||
expect(termRegistrationTypeToEnrollmenttype('ATTENDING')).toBe(1)
|
||||
expect(termRegistrationTypeToEnrollmenttype('NONATTENDING')).toBe(2)
|
||||
expect(termRegistrationTypeToEnrollmenttype('SOMETHING_ELSE')).toBe(3)
|
||||
})
|
||||
})
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { educationTypeToExtentcode } from '@/updater/shared.js'
|
||||
|
||||
describe('educationTypeToExtentcode', () => {
|
||||
it('maps known education types to their extent code', () => {
|
||||
expect(educationTypeToExtentcode['urn:code:education-type:degree-education:bachelors-degree']).toBe(1)
|
||||
expect(educationTypeToExtentcode['urn:code:education-type:degree-education:masters-degree']).toBe(2)
|
||||
})
|
||||
|
||||
it('maps education types without their own extent code to null', () => {
|
||||
expect(
|
||||
educationTypeToExtentcode['urn:code:education-type:non-degree-education:agreement-studies:joo-studies']
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import './initTests.js'
|
||||
|
||||
import { getCreditTypeCodeFromAttainment } from '@/updater/shared.js'
|
||||
|
||||
describe('getCreditTypeCodeFromAttainment', () => {
|
||||
it('is FAILED when the grade did not pass', () => {
|
||||
expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'ATTAINED' }, false)).toBe(10)
|
||||
})
|
||||
|
||||
it('is FAILED when the state is FAILED, even if passed is true', () => {
|
||||
expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'FAILED' }, true)).toBe(10)
|
||||
})
|
||||
|
||||
it('is IMPROVED for a passed, non-primary attainment', () => {
|
||||
expect(getCreditTypeCodeFromAttainment({ primary: false, state: 'ATTAINED' }, true)).toBe(7)
|
||||
})
|
||||
|
||||
it('is PASSED for a passed, primary, ATTAINED attainment', () => {
|
||||
expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'ATTAINED' }, true)).toBe(4)
|
||||
})
|
||||
|
||||
it('is APPROVED for a passed, primary attainment in any other state', () => {
|
||||
expect(getCreditTypeCodeFromAttainment({ primary: true, state: 'INCLUDED' }, true)).toBe(9)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// shared.js connects to Redis and loads every Sequelize model as a side effect of being imported.
|
||||
// Mock those three dependencies so importing its pure exports doesn't require a live Redis/DB.
|
||||
//
|
||||
// Import this module first - before importing anything from shared.js - in every test file in
|
||||
// this directory.
|
||||
vi.mock('@/utils/redis.js', () => ({
|
||||
redisClient: { get: vi.fn(), set: vi.fn(), connect: vi.fn(), on: vi.fn() },
|
||||
lock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/db/index.js', () => ({
|
||||
selectAllFrom: vi.fn(),
|
||||
selectAllFromSnapshots: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/db/models/index.js', () => ({
|
||||
Semester: {},
|
||||
Organization: {},
|
||||
CREDIT_TYPE_CODES: { PASSED: 4, FAILED: 10, IMPROVED: 7, APPROVED: 9 },
|
||||
}))
|
||||
@@ -0,0 +1,14 @@
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(import.meta.dirname, './src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
exclude: ['node_modules'],
|
||||
testTimeout: 10_000,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user