[Tests][Backend] Fix remaining tests and use better courseCodeToGroupId format

This commit is contained in:
Aaro Koivukangas
2026-09-21 15:24:21 +03:00
committed by Aaro Koivukangas
parent 793bf317e8
commit 7b772e188a
9 changed files with 147 additions and 115 deletions
@@ -5,9 +5,7 @@ import { yearToYearCode } from '@oodikone/shared/util'
import { initTests } from '../../../../utils' import { initTests } from '../../../../utils'
import { getCourseYearlyStats, CourseYearlyStats } from '../helpers' import { getCourseYearlyStats, CourseYearlyStats } from '../helpers'
const courseCodeToGroupId = { const MAT21003 = 'hy-CU-117375829'
MAT21003: 'hy-CU-117375829',
}
void describe('Course yearly attempt statistics - MAT21003 (hy-CU-117375394, no substitutions)', () => { void describe('Course yearly attempt statistics - MAT21003 (hy-CU-117375394, no substitutions)', () => {
let app: Express let app: Express
@@ -18,7 +16,7 @@ void describe('Course yearly attempt statistics - MAT21003 (hy-CU-117375394, no
describe('duplicate failed attempts across a large timespan are counted separately', () => { describe('duplicate failed attempts across a large timespan are counted separately', () => {
let body: CourseYearlyStats let body: CourseYearlyStats
beforeAll(async () => { beforeAll(async () => {
body = await getCourseYearlyStats(app, `courses=${courseCodeToGroupId.MAT21003}&substitutions=false`) body = await getCourseYearlyStats(app, `courses=${MAT21003}&substitutions=false`)
}) })
it('2017-2018 should include a single failed attempt (539036)', () => { it('2017-2018 should include a single failed attempt (539036)', () => {
@@ -65,7 +63,7 @@ void describe('Course yearly attempt statistics - MAT21003 (hy-CU-117375394, no
it('2017-2018 should include a failed attempt (539036)', async () => { it('2017-2018 should include a failed attempt (539036)', async () => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2017)}&toYearCode=${yearToYearCode(2017)}` `courses=${MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2017)}&toYearCode=${yearToYearCode(2017)}`
) )
const year = body.unifyStats.statistics.find(year => year.name === '2017-2018') const year = body.unifyStats.statistics.find(year => year.name === '2017-2018')
assert(year && 'enrollments' in year, 'Missing stats for 2017-2018') assert(year && 'enrollments' in year, 'Missing stats for 2017-2018')
@@ -79,7 +77,7 @@ void describe('Course yearly attempt statistics - MAT21003 (hy-CU-117375394, no
it('2018-2019 should include a failed attempt (539036)', async () => { it('2018-2019 should include a failed attempt (539036)', async () => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2018)}&toYearCode=${yearToYearCode(2018)}` `courses=${MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2018)}&toYearCode=${yearToYearCode(2018)}`
) )
const year = body.unifyStats.statistics.find(year => year.name === '2018-2019') const year = body.unifyStats.statistics.find(year => year.name === '2018-2019')
assert(year && 'enrollments' in year, 'Missing stats for 2018-2019') assert(year && 'enrollments' in year, 'Missing stats for 2018-2019')
@@ -4,16 +4,14 @@ import { describe, it, beforeAll, assert } from 'vitest'
import { initTests } from '../../../../utils' import { initTests } from '../../../../utils'
import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers' import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers'
const courseCodeToGroupId = { const MAT11002 = 'hy-CU-117375394'
MAT11002: 'hy-CU-117375394',
}
void describe('Course yearly statistics - MAT11002 (hy-CU-117375394, no substitutions)', () => { void describe('Course yearly statistics - MAT11002 (hy-CU-117375394, no substitutions)', () => {
let app: Express let app: Express
let body: CourseYearlyStats let body: CourseYearlyStats
beforeAll(async () => { beforeAll(async () => {
app = await initTests() app = await initTests()
body = await getCourseYearlyStats(app, `courses=${courseCodeToGroupId.MAT11002}&combineSubstitutions=false`) body = await getCourseYearlyStats(app, `courses=${MAT11002}&combineSubstitutions=false`)
}) })
it('should not include students for AY code', () => { it('should not include students for AY code', () => {
@@ -5,9 +5,7 @@ import { yearToYearCode } from '@oodikone/shared/util'
import { initTests } from '../../../../utils' import { initTests } from '../../../../utils'
import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers' import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers'
const courseCodeToGroupId = { const MAT21003 = 'hy-CU-117375829'
MAT21003: 'hy-CU-117375829',
}
void describe('Course yearly statistics - MAT21003 (no substitutions)', () => { void describe('Course yearly statistics - MAT21003 (no substitutions)', () => {
let app: Express let app: Express
@@ -18,7 +16,7 @@ void describe('Course yearly statistics - MAT21003 (no substitutions)', () => {
describe('duplicate failed grades across a large timespan are not double-counted', () => { describe('duplicate failed grades across a large timespan are not double-counted', () => {
let body: CourseYearlyStats let body: CourseYearlyStats
beforeAll(async () => { beforeAll(async () => {
body = await getCourseYearlyStats(app, `courses=${courseCodeToGroupId.MAT21003}&substitutions=false`) body = await getCourseYearlyStats(app, `courses=${MAT21003}&substitutions=false`)
}) })
it('2017-2018 should not include a failed grade', () => { it('2017-2018 should not include a failed grade', () => {
@@ -46,7 +44,7 @@ void describe('Course yearly statistics - MAT21003 (no substitutions)', () => {
])('should include correct years for %s', async (name, from, to) => { ])('should include correct years for %s', async (name, from, to) => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(from)}&toYearCode=${yearToYearCode(to)}` `courses=${MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(from)}&toYearCode=${yearToYearCode(to)}`
) )
const years: string[] = [] const years: string[] = []
@@ -65,7 +63,7 @@ void describe('Course yearly statistics - MAT21003 (no substitutions)', () => {
it('2017-2018 should include a failed grade', async () => { it('2017-2018 should include a failed grade', async () => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2017)}&toYearCode=${yearToYearCode(2017)}` `courses=${MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2017)}&toYearCode=${yearToYearCode(2017)}`
) )
const year = body.unifyStats.statistics.find(year => year.name === '2017-2018') const year = body.unifyStats.statistics.find(year => year.name === '2017-2018')
assert(year && 'enrollments' in year, 'Missing stats for 2017-2018') assert(year && 'enrollments' in year, 'Missing stats for 2017-2018')
@@ -78,7 +76,7 @@ void describe('Course yearly statistics - MAT21003 (no substitutions)', () => {
it('2018-2019 should include a failed grade', async () => { it('2018-2019 should include a failed grade', async () => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2018)}&toYearCode=${yearToYearCode(2018)}` `courses=${MAT21003}&substitutions=false&fromYearCode=${yearToYearCode(2018)}&toYearCode=${yearToYearCode(2018)}`
) )
const year = body.unifyStats.statistics.find(year => year.name === '2018-2019') const year = body.unifyStats.statistics.find(year => year.name === '2018-2019')
assert(year && 'enrollments' in year, 'Missing stats for 2018-2019') assert(year && 'enrollments' in year, 'Missing stats for 2018-2019')
@@ -5,9 +5,7 @@ import { yearToYearCode } from '@oodikone/shared/util'
import { initTests } from '../../../../utils' import { initTests } from '../../../../utils'
import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers' import { calculatePassedAndFailed, getCourseYearlyStats, CourseYearlyStats } from '../helpers'
const courseCodeToGroupId = { const TKT10002 = 'hy-CU-118023867'
TKT10002: 'hy-CU-118023867',
}
void describe('Course yearly statistics - TKT10002 (no substitutions)', () => { void describe('Course yearly statistics - TKT10002 (no substitutions)', () => {
let app: Express let app: Express
@@ -20,7 +18,7 @@ void describe('Course yearly statistics - TKT10002 (no substitutions)', () => {
beforeAll(async () => { beforeAll(async () => {
body = await getCourseYearlyStats( body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.TKT10002}&substitutions=false&fromYearCode=${yearToYearCode(2016)}&toYearCode=${yearToYearCode(2023)}` `courses=${TKT10002}&substitutions=false&fromYearCode=${yearToYearCode(2016)}&toYearCode=${yearToYearCode(2023)}`
) )
}) })
@@ -236,7 +234,7 @@ void describe('Course yearly statistics - TKT10002 (no substitutions)', () => {
])('should include correct stats for one academic year ($0)', async (year, categories, grades) => { ])('should include correct stats for one academic year ($0)', async (year, categories, grades) => {
const body = await getCourseYearlyStats( const body = await getCourseYearlyStats(
app, app,
`courses=${courseCodeToGroupId.TKT10002}&substitutions=false&fromYearCode=${yearToYearCode(year.split('-').at(0))}&toYearCode=${yearToYearCode(parseInt(year.split('-').at(1)!)) - 1}` `courses=${TKT10002}&substitutions=false&fromYearCode=${yearToYearCode(year.split('-').at(0))}&toYearCode=${yearToYearCode(parseInt(year.split('-').at(1)!)) - 1}`
) )
const stats = body.unifyStats.statistics.find(yearStats => yearStats.name === year)! const stats = body.unifyStats.statistics.find(yearStats => yearStats.name === year)!
assert('enrollments' in stats, 'Missing field enrollment in statsitics') assert('enrollments' in stats, 'Missing field enrollment in statsitics')
@@ -8,10 +8,8 @@ import { PopulationstatisticsbycourseResBody } from '@oodikone/shared/routes/pop
import { CreditModel } from '@/models' import { CreditModel } from '@/models'
import { EnrollmentState } from '@oodikone/shared/types' import { EnrollmentState } from '@oodikone/shared/types'
const courseCodeToGroupId = { const MAT21003 = 'hy-CU-117375829'
MAT21003: 'hy-CU-117375829', const MAT21005 = 'hy-CU-117376344'
MAT21005: 'hy-CU-117376344',
}
void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375829 + hy-CU-117376344)', () => { void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375829 + hy-CU-117376344)', () => {
let app: Express let app: Express
@@ -45,9 +43,7 @@ void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375
[2023, 21, 1, 0, 36], [2023, 21, 1, 0, 36],
])('should return correct amount of students for single year ($0)', async (year, total, passed, failed, enrolled) => { ])('should return correct amount of students for single year ($0)', async (year, total, passed, failed, enrolled) => {
const res = await request(app) const res = await request(app)
.get( .get(`/populationstatisticsbycourse?courses=${MAT21003}&from=${yearToYearCode(year)}&to=${yearToYearCode(year)}`)
`/populationstatisticsbycourse?courses=${courseCodeToGroupId.MAT21003}&from=${yearToYearCode(year)}&to=${yearToYearCode(year)}`
)
.set('shib-session-id', 'test') .set('shib-session-id', 'test')
.set('uid', 'basic') .set('uid', 'basic')
.set('hygroupcn', 'grp-oodikone-basic-users') .set('hygroupcn', 'grp-oodikone-basic-users')
@@ -60,14 +56,14 @@ void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375
assert.strictEqual(body.students.length, total, 'Incorrect amount of students') assert.strictEqual(body.students.length, total, 'Incorrect amount of students')
assert.strictEqual( assert.strictEqual(
body.coursestatistics.credits.filter( body.coursestatistics.credits.filter(
credit => body.idToGroupIdMap[credit.course_id] === courseCodeToGroupId.MAT21003 && CreditModel.passed(credit) credit => body.idToGroupIdMap[credit.course_id] === MAT21003 && CreditModel.passed(credit)
).length, ).length,
passed, passed,
'Incorrect amount of passed credits' 'Incorrect amount of passed credits'
) )
assert.strictEqual( assert.strictEqual(
body.coursestatistics.credits.filter( body.coursestatistics.credits.filter(
credit => body.idToGroupIdMap[credit.course_id] === courseCodeToGroupId.MAT21003 && CreditModel.failed(credit) credit => body.idToGroupIdMap[credit.course_id] === MAT21003 && CreditModel.failed(credit)
).length, ).length,
failed, failed,
'Incorrect amount of failed credits' 'Incorrect amount of failed credits'
@@ -75,8 +71,7 @@ void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375
assert.strictEqual( assert.strictEqual(
body.coursestatistics.enrollments.filter( body.coursestatistics.enrollments.filter(
enrollment => enrollment =>
body.idToGroupIdMap[enrollment.course_id] === courseCodeToGroupId.MAT21003 && body.idToGroupIdMap[enrollment.course_id] === MAT21003 && enrollment.state === EnrollmentState.ENROLLED
enrollment.state === EnrollmentState.ENROLLED
).length, ).length,
enrolled, enrolled,
'Incorrect amount of enrollments' 'Incorrect amount of enrollments'
@@ -85,7 +80,7 @@ void describe('Population statistics by course MAT21003 + MAT21005 (hy-CU-117375
it('should return correct amount of students for a course', async () => { it('should return correct amount of students for a course', async () => {
const res = await request(app) const res = await request(app)
.get(`/populationstatisticsbycourse?courses=${courseCodeToGroupId.MAT21005}&from=70&to=70`) .get(`/populationstatisticsbycourse?courses=${MAT21005}&from=70&to=70`)
.set('shib-session-id', 'test') .set('shib-session-id', 'test')
.set('uid', 'basic') .set('uid', 'basic')
.set('hygroupcn', 'grp-oodikone-basic-users') .set('hygroupcn', 'grp-oodikone-basic-users')
@@ -5,11 +5,8 @@ import { yearToYearCode } from '@oodikone/shared/util'
import { Unification } from '@oodikone/shared/types' import { Unification } from '@oodikone/shared/types'
import { getRelevantCourseIdMap } from '@/services/courses' import { getRelevantCourseIdMap } from '@/services/courses'
const courseCodeToGroupId = { const CSM14204 = 'hy-CU-119366777'
MAT11001: 'hy-CU-117375151', const MAT21018 = 'hy-CU-117377764'
CSM14204: 'hy-CU-119366777',
MAT21018: 'hy-CU-117377764',
}
void describe.concurrent('Search and complete substitution groups to codes', () => { void describe.concurrent('Search and complete substitution groups to codes', () => {
beforeAll(async () => { beforeAll(async () => {
@@ -29,7 +26,7 @@ void describe.concurrent('Search and complete substitution groups to codes', ()
] as TestList)( ] as TestList)(
'should return correct student numbers for a short course (CSM14204, 2019-2023) by $0', 'should return correct student numbers for a short course (CSM14204, 2019-2023) by $0',
async (separate, to, from) => { async (separate, to, from) => {
const courseIds = await getRelevantCourseIdMap([courseCodeToGroupId.CSM14204], false) const courseIds = await getRelevantCourseIdMap([CSM14204], false)
const res = await findByCourseAndSemesters( const res = await findByCourseAndSemesters(
Object.keys(courseIds), Object.keys(courseIds),
to, to,
@@ -46,7 +43,7 @@ void describe.concurrent('Search and complete substitution groups to codes', ()
['semester', semesterToSemesterCode(2020, 'fall'), semesterToSemesterCode(2023, 'spring')], ['semester', semesterToSemesterCode(2020, 'fall'), semesterToSemesterCode(2023, 'spring')],
['year', yearToYearCode(2020), yearToYearCode(2023)], ['year', yearToYearCode(2020), yearToYearCode(2023)],
] as TestList)('should filter out students outside of 2020-2023 by $0', async (separate, to, from) => { ] as TestList)('should filter out students outside of 2020-2023 by $0', async (separate, to, from) => {
const courseIds = await getRelevantCourseIdMap([courseCodeToGroupId.CSM14204], false) const courseIds = await getRelevantCourseIdMap([CSM14204], false)
const res = await findByCourseAndSemesters( const res = await findByCourseAndSemesters(
Object.keys(courseIds), Object.keys(courseIds),
to, to,
@@ -63,7 +60,7 @@ void describe.concurrent('Search and complete substitution groups to codes', ()
] as TestList)( ] as TestList)(
'should return correct student numbers for a longer course (MAT21018, 2017-2021) by $0', 'should return correct student numbers for a longer course (MAT21018, 2017-2021) by $0',
async (separate, to, from) => { async (separate, to, from) => {
const courseIds = await getRelevantCourseIdMap([courseCodeToGroupId.MAT21018], false) const courseIds = await getRelevantCourseIdMap([MAT21018], false)
const res = await findByCourseAndSemesters( const res = await findByCourseAndSemesters(
Object.keys(courseIds), Object.keys(courseIds),
to, to,
@@ -94,7 +91,7 @@ void describe.concurrent('Search and complete substitution groups to codes', ()
] as TestList)( ] as TestList)(
'should return correct student numbers for a longer course (MAT21018, 2021-2023) by $0', 'should return correct student numbers for a longer course (MAT21018, 2021-2023) by $0',
async (separate, to, from) => { async (separate, to, from) => {
const courseIds = await getRelevantCourseIdMap([courseCodeToGroupId.MAT21018], false) const courseIds = await getRelevantCourseIdMap([MAT21018], false)
const res = await findByCourseAndSemesters( const res = await findByCourseAndSemesters(
Object.keys(courseIds), Object.keys(courseIds),
to, to,
@@ -133,7 +130,7 @@ void describe.concurrent('Search and complete substitution groups to codes', ()
describe('should work with specific cases', () => { describe('should work with specific cases', () => {
it('Student with enrollment in -21 and passed grade in -23 should be included in both stats when querying one year at a time (522321)', async () => { it('Student with enrollment in -21 and passed grade in -23 should be included in both stats when querying one year at a time (522321)', async () => {
const courseIds = await getRelevantCourseIdMap([courseCodeToGroupId.MAT21018], false) const courseIds = await getRelevantCourseIdMap([MAT21018], false)
const res21 = await findByCourseAndSemesters( const res21 = await findByCourseAndSemesters(
Object.keys(courseIds), Object.keys(courseIds),
yearToYearCode(2021), yearToYearCode(2021),
@@ -2,125 +2,153 @@ import { describe, it, assert, vi, afterEach } from 'vitest'
import { Op } from 'sequelize' import { Op } from 'sequelize'
import { CreditTypeCode, Unification } from '@oodikone/shared/types' import { CreditTypeCode, Unification } from '@oodikone/shared/types'
import { Credit } from '@oodikone/shared/models'
import { createCredit as credit } from '@oodikone/shared/test/utils' import { createCredit as credit } from '@oodikone/shared/test/utils'
import { StudentModel } from '@/models' import { CreditModel } from '@/models'
import { getCreditsForCourses } from '@/services/courses/creditsAndEnrollmentsOfCourse' import { getCreditsForCourses } from '@/services/courses/creditsAndEnrollmentsOfCourse'
const mockStudents = (students: Array<{ studentnumber: string; credits: Array<Record<string, unknown>> }>) => const MAT11002 = 'hy-CU-117375394'
vi.spyOn(StudentModel, 'findAll').mockResolvedValue(students as any) const MAT21001 = 'hy-CU-117375754'
const MAT21002 = 'hy-CU-117375793'
const OTHER101 = 'hy-CU-000000000'
const mockCredits = (credits: Array<Record<string, unknown>>) =>
vi.spyOn(CreditModel, 'findAll').mockResolvedValue(credits as any)
const from = new Date(2000, 0, 1) const from = new Date(2000, 0, 1)
const to = new Date(2030, 0, 1) const to = new Date(2030, 0, 1)
// Tests share a spy on the same StudentModel.findAll, so they must not run concurrently // Tests share a spy on the same CreditModel.findAll, so they must not run concurrently
// (the suite config default is sequence.concurrent: true). // (the suite config default is sequence.concurrent: true).
void describe('Get credits for courses (unit, mocked db)', { concurrent: false }, () => { void describe('Get credits for courses (unit, mocked db)', { concurrent: false }, () => {
afterEach(() => { afterEach(() => {
vi.restoreAllMocks() vi.restoreAllMocks()
}) })
it('should return no groups without any course codes, without querying credits by course', async () => { it('should return no groups when there are no groupId groups to match', async () => {
const findAll = mockStudents([{ studentnumber: '111', credits: [credit()] }]) const findAll = mockCredits([credit()])
const res = await getCreditsForCourses([], Unification.UNIFY, from, to) const res = await getCreditsForCourses([], [], Unification.UNIFY, from, to)
assert.deepStrictEqual(res, []) assert.deepStrictEqual(res, [])
assert.strictEqual(findAll.mock.calls.length, 1) assert.strictEqual(findAll.mock.calls.length, 1)
}) })
it('should query StudentModel with course codes, date range and openness filters', async () => { it('should query CreditModel with course ids, date range and openness filters', async () => {
const findAll = mockStudents([]) const findAll = mockCredits([])
await getCreditsForCourses([['MAT11002'], ['MAT21001', 'MAT21002']], Unification.OPEN, from, to) await getCreditsForCourses([[MAT11002], [MAT21001, MAT21002]], ['course-1', 'course-2'], Unification.OPEN, from, to)
const call = findAll.mock.calls[0][0] as any const call = findAll.mock.calls[0][0] as any
const creditInclude = call.include[0]
assert.deepStrictEqual(creditInclude.where.course_code, { [Op.in]: ['MAT11002', 'MAT21001', 'MAT21002'] }) assert.deepStrictEqual(call.where.course_id, { [Op.in]: ['course-1', 'course-2'] })
assert.deepStrictEqual(creditInclude.where.attainment_date, { [Op.between]: [from, to] }) assert.deepStrictEqual(call.where.attainment_date, { [Op.between]: [from, to] })
assert.deepStrictEqual(creditInclude.where.is_open, { [Op.eq]: true }) assert.deepStrictEqual(call.where.is_open, { [Op.eq]: true })
}) })
it('should include both passed and failed credits for a single-code group as one group', async () => { it('should include both passed and failed credits for a single-code group as one group', async () => {
mockStudents([ mockCredits([credit({ credittypecode: CreditTypeCode.FAILED }), credit({ credittypecode: CreditTypeCode.PASSED })])
{
studentnumber: '111',
credits: [credit({ credittypecode: CreditTypeCode.FAILED }), credit({ credittypecode: CreditTypeCode.PASSED })],
},
])
const res = await getCreditsForCourses([['MAT11002']], Unification.UNIFY, from, to) const res = await getCreditsForCourses([[MAT11002]], ['course-1'], Unification.UNIFY, from, to)
assert.strictEqual(res.length, 1) assert.strictEqual(res.length, 1)
assert.strictEqual(res.flat().length, 2) assert.strictEqual(res.flat().length, 2)
}) })
it('should create a separate group per student for a single-code group', async () => { it('should create a separate group per student for a single-code group', async () => {
mockStudents([ mockCredits([credit({ student_studentnumber: '111' }), credit({ student_studentnumber: '222' })])
{ studentnumber: '111', credits: [credit({ student_studentnumber: '111' })] },
{ studentnumber: '222', credits: [credit({ student_studentnumber: '222' })] },
])
const res = await getCreditsForCourses([['MAT11002']], Unification.UNIFY, from, to) const res = await getCreditsForCourses([[MAT11002]], ['course-1'], Unification.UNIFY, from, to)
assert.strictEqual(res.length, 2) assert.strictEqual(res.length, 2)
assert.deepStrictEqual(res.map(group => group.map(c => c.student_studentnumber)).sort(), [['111'], ['222']]) assert.deepStrictEqual(res.map(group => group.map(c => c.student_studentnumber)).sort(), [['111'], ['222']])
}) })
it('should not create a group for a single course code the student has no credit for', async () => { it('should not create a group for a single course code the student has no credit for', async () => {
mockStudents([{ studentnumber: '111', credits: [credit({ course_code: 'OTHER101' })] }]) mockCredits([credit({ course_code: 'OTHER101', course: { groupId: OTHER101 } as Credit['course'] })])
const res = await getCreditsForCourses([['MAT11002']], Unification.UNIFY, from, to) const res = await getCreditsForCourses([[MAT11002]], ['course-1'], Unification.UNIFY, from, to)
assert.deepStrictEqual(res, []) assert.deepStrictEqual(res, [])
}) })
it('should only include the passed credits of a substitution group when every code was passed', async () => { it('should only include the passed credits of a substitution group when every code was passed', async () => {
mockStudents([ mockCredits([
{ credit({
studentnumber: '111', course_code: 'MAT11002',
credits: [ course: { groupId: MAT11002 } as Credit['course'],
credit({ course_code: 'MAT11002', credittypecode: CreditTypeCode.PASSED }), credittypecode: CreditTypeCode.PASSED,
credit({ course_code: 'MAT21001', credittypecode: CreditTypeCode.APPROVED }), }),
], credit({
}, course_code: 'MAT21001',
course: { groupId: MAT21001 } as Credit['course'],
credittypecode: CreditTypeCode.APPROVED,
}),
]) ])
const res = await getCreditsForCourses([['MAT11002', 'MAT21001']], Unification.UNIFY, from, to) const res = await getCreditsForCourses(
[[MAT11002, MAT21001]],
['course-1', 'course-2'],
Unification.UNIFY,
from,
to
)
assert.strictEqual(res.length, 1) assert.strictEqual(res.length, 1)
assert.deepStrictEqual(res[0].map(c => c.course_code).sort(), ['MAT11002', 'MAT21001']) assert.deepStrictEqual(res[0].map(c => c.course_code).sort(), ['MAT11002', 'MAT21001'])
}) })
it('should exclude a partially completed substitution group entirely', async () => { it('should exclude a partially completed substitution group entirely', async () => {
mockStudents([ mockCredits([
{ credit({
studentnumber: '111', course_code: 'MAT11002',
credits: [ course: { groupId: MAT11002 } as Credit['course'],
credit({ course_code: 'MAT11002', credittypecode: CreditTypeCode.PASSED }), credittypecode: CreditTypeCode.PASSED,
credit({ course_code: 'MAT21001', credittypecode: CreditTypeCode.FAILED }), }),
], credit({
}, course_code: 'MAT21001',
course: { groupId: MAT21001 } as Credit['course'],
credittypecode: CreditTypeCode.FAILED,
}),
]) ])
const res = await getCreditsForCourses([['MAT11002', 'MAT21001']], Unification.UNIFY, from, to) const res = await getCreditsForCourses(
[[MAT11002, MAT21001]],
['course-1', 'course-2'],
Unification.UNIFY,
from,
to
)
assert.deepStrictEqual(res, []) assert.deepStrictEqual(res, [])
}) })
it('should exclude an earlier failed attempt from a substitution group even when it later passed', async () => { it('should exclude an earlier failed attempt from a substitution group even when it later passed', async () => {
mockStudents([ mockCredits([
{ credit({
studentnumber: '111', course_code: 'MAT11002',
credits: [ course: { groupId: MAT11002 } as Credit['course'],
credit({ course_code: 'MAT11002', credittypecode: CreditTypeCode.FAILED }), credittypecode: CreditTypeCode.FAILED,
credit({ course_code: 'MAT11002', credittypecode: CreditTypeCode.PASSED }), }),
credit({ course_code: 'MAT21001', credittypecode: CreditTypeCode.PASSED }), credit({
], course_code: 'MAT11002',
}, course: { groupId: MAT11002 } as Credit['course'],
credittypecode: CreditTypeCode.PASSED,
}),
credit({
course_code: 'MAT21001',
course: { groupId: MAT21001 } as Credit['course'],
credittypecode: CreditTypeCode.PASSED,
}),
]) ])
const res = await getCreditsForCourses([['MAT11002', 'MAT21001']], Unification.UNIFY, from, to) const res = await getCreditsForCourses(
[[MAT11002, MAT21001]],
['course-1', 'course-2'],
Unification.UNIFY,
from,
to
)
assert.strictEqual(res.length, 1) assert.strictEqual(res.length, 1)
assert.strictEqual(res.flat().length, 2) assert.strictEqual(res.flat().length, 2)
@@ -131,18 +159,33 @@ void describe('Get credits for courses (unit, mocked db)', { concurrent: false }
}) })
it('should handle independent groups and students together', async () => { it('should handle independent groups and students together', async () => {
mockStudents([ mockCredits([
{ studentnumber: '111', credits: [credit({ student_studentnumber: '111', course_code: 'MAT11002' })] }, credit({
{ student_studentnumber: '111',
studentnumber: '222', course_code: 'MAT11002',
credits: [ course: { groupId: MAT11002 } as Credit['course'],
credit({ student_studentnumber: '222', course_code: 'MAT21001', credittypecode: CreditTypeCode.PASSED }), }),
credit({ student_studentnumber: '222', course_code: 'MAT21002', credittypecode: CreditTypeCode.PASSED }), credit({
], student_studentnumber: '222',
}, course_code: 'MAT21001',
course: { groupId: MAT21001 } as Credit['course'],
credittypecode: CreditTypeCode.PASSED,
}),
credit({
student_studentnumber: '222',
course_code: 'MAT21002',
course: { groupId: MAT21002 } as Credit['course'],
credittypecode: CreditTypeCode.PASSED,
}),
]) ])
const res = await getCreditsForCourses([['MAT11002'], ['MAT21001', 'MAT21002']], Unification.UNIFY, from, to) const res = await getCreditsForCourses(
[[MAT11002], [MAT21001, MAT21002]],
['course-1', 'course-2', 'course-3'],
Unification.UNIFY,
from,
to
)
assert.strictEqual(res.length, 2) assert.strictEqual(res.length, 2)
}) })
@@ -1,6 +1,7 @@
import { describe, it, assert } from 'vitest' import { describe, it, assert } from 'vitest'
import { CreditTypeCode } from '@oodikone/shared/types' import { CreditTypeCode } from '@oodikone/shared/types'
import { Credit } from '@oodikone/shared/models'
import { createCredit as credit, createStudyRightElementModel } from '@oodikone/shared/test/utils' import { createCredit as credit, createStudyRightElementModel } from '@oodikone/shared/test/utils'
import { parseCredit } from '@/services/courses' import { parseCredit } from '@/services/courses'
import { SISStudyRightElementModel } from '@/models' import { SISStudyRightElementModel } from '@/models'
@@ -30,7 +31,10 @@ void describe('parseCredit', () => {
it('should combine a substitution group into one credit with summed credits', () => { it('should combine a substitution group into one credit with summed credits', () => {
const res = parseCredit( const res = parseCredit(
[credit({ course_code: 'MAT11002', credits: 5 }), credit({ course_code: 'MAT21001', credits: 5 })], [
credit({ course_code: 'MAT11002', course: { groupId: 'hy-CU-117375394' } as Credit['course'], credits: 5 }),
credit({ course_code: 'MAT21001', course: { groupId: 'hy-CU-117375754' } as Credit['course'], credits: 5 }),
],
null, null,
'MAT11002', 'MAT11002',
[studyRightElement()] [studyRightElement()]
+5 -4
View File
@@ -69,8 +69,8 @@ export const createStudyRight = (overrides: Partial<StudentStudyRight> = {}): St
}) })
export const createCourse = (overrides: Partial<StudentCourse> = {}): StudentCourse => ({ export const createCourse = (overrides: Partial<StudentCourse> = {}): StudentCourse => ({
course_code: 'MAT001', course_id: 'hy-CU-000000001-2024-08-01',
date: new Date('2024-09-01'), date: new Date('2024-08-01'),
passed: true, passed: true,
grade: '5', grade: '5',
credits: 5, credits: 5,
@@ -94,9 +94,9 @@ export const createStudyPlan = (overrides: Partial<StudentStudyPlan> = {}): Stud
export const createEnrollment = ( export const createEnrollment = (
overrides: Partial<FormattedStudent['enrollments'][number]> = {} overrides: Partial<FormattedStudent['enrollments'][number]> = {}
): FormattedStudent['enrollments'][number] => ({ ): FormattedStudent['enrollments'][number] => ({
course_code: 'MAT001', course_id: 'hy-CU-000000001-2024-08-01',
state: EnrollmentState.ENROLLED, state: EnrollmentState.ENROLLED,
enrollment_date_time: new Date('2024-09-01'), enrollment_date_time: new Date('2024-08-01'),
semestercode: 149, semestercode: 149,
studyright_id: 'sr-1', studyright_id: 'sr-1',
...overrides, ...overrides,
@@ -113,6 +113,7 @@ export const createCredit = (overrides: Partial<Credit> = {}) =>
({ ({
grade: '5', grade: '5',
course_code: 'MAT11002', course_code: 'MAT11002',
course: { groupId: 'hy-CU-117375394' } as Credit['course'],
credits: 5, credits: 5,
attainment_date: new Date(2020, 0, 1), attainment_date: new Date(2020, 0, 1),
student_studentnumber: '111', student_studentnumber: '111',