Skip to content

Commit 2326596

Browse files
Merge pull request #145 from UniversityOfHelsinkiCS/unittest
Unittest
2 parents 279b2ce + 724e282 commit 2326596

10 files changed

+1117
-54
lines changed

src/server/util/recommender.ts

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import type { AnswerData, CourseData, CourseRecommendation, CourseRecommendation
22
import { uniqueVals } from './misc.ts'
33
import type { OrganisationRecommendation } from './organisationCourseRecommmendations.ts'
44
import {challegeCourseCodes, codesInOrganisations, courseHasAnyOfCodes, courseHasAnyRealisationCodeUrn, courseHasCustomCodeUrn, courseMatches, finmuMentroingCourseCodes, getUserOrganisationRecommendations, languageSpesificCodes, languageToStudy, mentoringCourseCodes, readOrganisationRecommendationData } from './organisationCourseRecommmendations.ts'
5-
import { dateObjToPeriod, getStudyPeriod, parseDate, getStudyYear } from './studyPeriods.ts'
6-
import studyPeriods from './studyPeriods.ts'
5+
import { dateObjToPeriod, getCoursePeriod, getStudyPeriod, parseDate, getStudyYear } from './studyPeriods.ts'
76
import { curcusWithUnitIdOf, curWithIdOf, cuWithCourseCodeOf, organisationWithGroupIdOf } from './dbActions.ts'
87
import pointRecommendedCourses from './pointRecommendCourses.ts'
98
import { allowedStudyPlaces, collaborationOrganisationNames, collaborationOrganisationCourseNameIncludes, correctValue, incorrectValue, notAnsweredValue, organisationCodeToUrn } from './constants.ts'
109

11-
async function recommendCourses(answerData: AnswerData, strictFields: string[]) {
10+
async function recommendCourses(
11+
answerData: AnswerData,
12+
strictFields: string[] = []
13+
) {
1214
const userCoordinates: UserCoordinates = calculateUserCoordinates(answerData)
1315
const recommendations = await getRecommendations(userCoordinates, answerData, strictFields)
1416

1517
return recommendations
1618
}
1719

18-
function commonCoordinateFromAnswerData(value: string, yesValue: number, noValue: number, neutralValue: number | null){
20+
export function commonCoordinateFromAnswerData(value: string, yesValue: number, noValue: number, neutralValue: number | null){
1921
switch(value){
2022
case '1':
2123
return yesValue
@@ -34,7 +36,7 @@ export function readAnswer(answerData: AnswerData, key: string){
3436
return value
3537
}
3638

37-
function readAsStringArr(variable: string[] | string): string[]{
39+
export function readAsStringArr(variable: string[] | string): string[]{
3840
return Array.isArray(variable) ? variable : [variable]
3941
}
4042

@@ -45,7 +47,7 @@ function getDateFromUserInput(answerData: AnswerData){
4547
}
4648

4749

48-
function readStudyPlaceCoordinate (answerData: AnswerData){
50+
export function readStudyPlaceCoordinate (answerData: AnswerData){
4951
const value = readAnswer(answerData, 'study-place')
5052
if(value === 'neutral'){
5153
return notAnsweredValue
@@ -81,15 +83,15 @@ function calculateUserCoordinates(answerData: AnswerData) {
8183
}
8284

8385
//generic courses have many cases where they are considered to be for the users organisation
84-
async function courseInSameOrganisationAsUser(course: CourseData, organisationCode: string, codesInOrganisation: string[]){
86+
export async function courseInSameOrganisationAsUser(course: CourseData, organisationCode: string, codesInOrganisation: string[]){
8587
const isSpesificForUserOrg = courseIsSpesificForUserOrg(course, organisationCode)
8688
if(isSpesificForUserOrg){
8789
return isSpesificForUserOrg
8890
}
8991

9092
const organisations = await organisationWithGroupIdOf(course.groupIds)
9193
const orgCodes = organisations.map(o => o.code)
92-
if( organisationCode in orgCodes){
94+
if(orgCodes.includes(organisationCode)){
9395
return true
9496
}
9597

@@ -103,15 +105,13 @@ async function courseInSameOrganisationAsUser(course: CourseData, organisationCo
103105
}
104106

105107

106-
function courseIsSpesificForUserOrg(course: CourseData, organisationCode: string){
108+
export function courseIsSpesificForUserOrg(course: CourseData, organisationCode: string){
107109
const codes = [organisationCode]
108110
for(const code of codes){
109111
const urnHit = organisationCodeToUrn[code]
110112
if(urnHit){
111113
const hasCustomCodeUrn = courseHasCustomCodeUrn(course, urnHit)
112114
if(hasCustomCodeUrn){
113-
console.log('is spesific course!')
114-
console.log(course)
115115
return true
116116
}
117117
}
@@ -120,7 +120,7 @@ function courseIsSpesificForUserOrg(course: CourseData, organisationCode: string
120120
return false
121121
}
122122

123-
function readArrOrSingleValue(val: string | string[]){
123+
export function readArrOrSingleValue(val: string | string[]){
124124
const value = val ? val : []
125125
if(Array.isArray(value)){
126126
return value
@@ -130,7 +130,7 @@ function readArrOrSingleValue(val: string | string[]){
130130
}
131131
}
132132

133-
function courseStudyPlaceCoordinate(course: CourseData, answerData: AnswerData){
133+
export function courseStudyPlaceCoordinate(course: CourseData, answerData: AnswerData){
134134

135135
const userStudyPlaces = readArrOrSingleValue(answerData['study-place'])
136136
const lookups = userStudyPlaces.filter((p) => allowedStudyPlaces.includes(p)).map((p) => p)
@@ -142,14 +142,14 @@ function courseStudyPlaceCoordinate(course: CourseData, answerData: AnswerData){
142142
return incorrectValue
143143
}
144144

145-
function isIndependentCourse(course: CourseData){
145+
export function isIndependentCourse(course: CourseData){
146146
const hasIndependentCodeUrn = courseHasCustomCodeUrn(course, 'kks-alm')
147147
const hasIndependentInName = course.name.fi?.toLowerCase().includes('itsenäinen')
148148

149149
return hasIndependentCodeUrn || hasIndependentInName
150150
}
151151

152-
function localeNameIncludesAny(localizedName: { fi?: string; en?: string; sv?: string } | undefined, patterns: string[]): boolean {
152+
export function localeNameIncludesAny(localizedName: { fi?: string; en?: string; sv?: string } | undefined, patterns: string[]): boolean {
153153
const nameFi = localizedName?.fi?.toLowerCase() || ''
154154
const nameEn = localizedName?.en?.toLowerCase() || ''
155155
const nameSv = localizedName?.sv?.toLowerCase() || ''
@@ -166,7 +166,7 @@ function localeNameIncludesAny(localizedName: { fi?: string; en?: string; sv?: s
166166
return false
167167
}
168168

169-
async function courseIsCollaboration(course: CourseData): Promise<boolean> {
169+
export async function courseIsCollaboration(course: CourseData): Promise<boolean> {
170170
// Check if course name contains any collaboration indicators
171171
if (localeNameIncludesAny(course.name, collaborationOrganisationCourseNameIncludes)) {
172172
return true
@@ -282,7 +282,7 @@ export async function getRealisationsWithCourseUnitCodes(courseCodeStrings: stri
282282
(cur) => {
283283
return {
284284
...cur,
285-
period: getPeriodForCourse(cur),
285+
period: getCoursePeriod(cur),
286286
courseCodes: uniqueVals(courseUnitsWithCodes
287287
.filter((cu) => cur.unitIds.includes(cu.id))
288288
.map((cu) => cu.courseCode)),
@@ -299,32 +299,7 @@ export async function getRealisationsWithCourseUnitCodes(courseCodeStrings: stri
299299
return courseRealisationsWithCodes
300300
}
301301

302-
const getPeriodForCourse = (cur): Period[] | null => {
303-
const courseStart = cur.startDate
304-
const courseEnd = cur.endDate
305-
306-
const overlappingPeriods = studyPeriods.periods.filter(periodData => {
307-
const periodStart = parseDate(periodData.start_date)
308-
const periodEnd = parseDate(periodData.end_date)
309-
return periodStart <= courseEnd && periodEnd >= courseStart
310-
})
311-
312-
if (overlappingPeriods.length === 0) {
313-
return null
314-
}
315-
316-
const periods: Period[] = overlappingPeriods.map(periodData => ({
317-
name: periodData.name,
318-
startDate: parseDate(periodData.start_date),
319-
endDate: parseDate(periodData.end_date),
320-
startYear: periodData.start_year,
321-
endYear: periodData.end_year
322-
}))
323-
324-
return periods
325-
}
326-
327-
function courseSpansMultiplePeriods(course: CourseData): boolean {
302+
export function courseSpansMultiplePeriods(course: CourseData): boolean {
328303
return (course.period?.length ?? 0) > 1
329304
}
330305

src/server/util/studyPeriods.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Period } from '../../common/types.ts'
2+
13
//source: https://studies.helsinki.fi/ohjeet/artikkeli/lukuvuosi-ja-opetusperiodit?check_logged_in=1#degree_students and https://studies.helsinki.fi/ohjeet/node/314
24
//end dates of intensive_3 is changed to be the next years period I start date in order to prevent courses falling to 'no period'
35
import { getCurrentDate } from './testUtils.ts'
@@ -56,6 +58,28 @@ export const dateObjToPeriod = (dateObj: Date, debug = false) => {
5658
return hits
5759
}
5860

61+
export const getCoursePeriod = (course: { startDate: Date; endDate: Date }): Period[] | null => {
62+
const overlappingPeriods = studyPeriods.periods.filter((periodData) => {
63+
const periodStart = parseDate(periodData.start_date)
64+
const periodEnd = parseDate(periodData.end_date)
65+
return periodStart <= course.endDate && periodEnd >= course.startDate
66+
})
67+
68+
if (overlappingPeriods.length === 0) {
69+
return null
70+
}
71+
72+
const periods: Period[] = overlappingPeriods.map((periodData) => ({
73+
name: periodData.name,
74+
startDate: parseDate(periodData.start_date),
75+
endDate: parseDate(periodData.end_date),
76+
startYear: periodData.start_year,
77+
endYear: periodData.end_year,
78+
}))
79+
80+
return periods
81+
}
82+
5983
//returns closest period in the future given the string
6084
export const closestPeriod = (name: string = '') => {
6185
const date = getCurrentDate()
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
vi.mock('../../server/util/organisationCourseRecommmendations.ts', () => ({
4+
courseHasCustomCodeUrn: vi.fn(),
5+
courseHasAnyOfCodes: vi.fn(),
6+
courseHasAnyRealisationCodeUrn: vi.fn(),
7+
}))
8+
9+
vi.mock('../../server/util/dbActions.ts', () => ({
10+
organisationWithGroupIdOf: vi.fn(),
11+
}))
12+
13+
import {
14+
commonCoordinateFromAnswerData,
15+
readAsStringArr,
16+
readArrOrSingleValue,
17+
} from '../../server/util/recommender.ts'
18+
19+
describe('commonCoordinateFromAnswerData', () => {
20+
it('maps 1 to yesValue', () => {
21+
expect(commonCoordinateFromAnswerData('1', 10, 20, null)).toBe(10)
22+
})
23+
24+
it('maps 0 to noValue', () => {
25+
expect(commonCoordinateFromAnswerData('0', 10, 20, null)).toBe(20)
26+
})
27+
28+
it('maps neutral to neutralValue', () => {
29+
expect(commonCoordinateFromAnswerData('neutral', 10, 20, null)).toBe(null)
30+
expect(commonCoordinateFromAnswerData('neutral', 10, 20, 5)).toBe(5)
31+
})
32+
33+
it('returns undefined for unexpected input', () => {
34+
expect(commonCoordinateFromAnswerData('unknown', 10, 20, null)).toBeUndefined()
35+
})
36+
})
37+
38+
describe('readAsStringArr', () => {
39+
it('wraps single string in array', () => {
40+
expect(readAsStringArr('hello')).toEqual(['hello'])
41+
})
42+
43+
it('leaves array unchanged', () => {
44+
expect(readAsStringArr(['a', 'b'])).toEqual(['a', 'b'])
45+
})
46+
})
47+
48+
describe('readArrOrSingleValue', () => {
49+
it('wraps single string in array', () => {
50+
expect(readArrOrSingleValue('test')).toEqual(['test'])
51+
})
52+
53+
it('leaves array unchanged', () => {
54+
expect(readArrOrSingleValue(['x', 'y'])).toEqual(['x', 'y'])
55+
})
56+
57+
it('returns empty array for falsy input', () => {
58+
expect(readArrOrSingleValue('')).toEqual([])
59+
expect(readArrOrSingleValue(null as any)).toEqual([])
60+
})
61+
})

0 commit comments

Comments
 (0)