[UnitTests][Updater] Add basic unit tests to updater

This commit is contained in:
Aaro Koivukangas
2026-09-21 15:24:21 +03:00
committed by Aaro Koivukangas
parent 3a1bee9b79
commit f5229ee678
20 changed files with 1879 additions and 5 deletions
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -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()
})
})
@@ -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)
})
})
@@ -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()
})
})
@@ -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,
},
})