fix(backend): Testen DatabaseLearningObjectProvider gerepareerd na refactoring.

This commit is contained in:
Gerald Schmittinger 2025-04-16 07:58:55 +02:00
parent ee9afab6ca
commit 51268af79c
20 changed files with 72 additions and 210 deletions

View file

@ -1,4 +1,4 @@
import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import {ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property} from '@mikro-orm/core';
import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
@ -42,7 +42,7 @@ export class LearningObject {
@Property({ type: 'array' })
keywords: string[] = [];
@Property({ type: 'array', nullable: true })
@Property({ type: new ArrayType(i => +i), nullable: true })
targetAges?: number[] = [];
@Property({ type: 'bool' })

View file

@ -9,7 +9,7 @@ export class LearningPathNode {
learningPath!: Rel<LearningPath>;
@PrimaryKey({ type: 'integer', autoincrement: true })
nodeNumber!: number;
nodeNumber?: number;
@Property({ type: 'string' })
learningObjectHruid!: string;

View file

@ -8,12 +8,13 @@ import {
LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content';
import { getLogger } from '../logging/initalize.js';
import {v4} from "uuid";
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return {
key: data.hruid, // Hruid learningObject (not path)
_id: data._id,
uuid: data.uuid,
uuid: data.uuid || v4(),
version: data.version,
title: data.title,
htmlUrl, // Url to fetch html content

View file

@ -32,7 +32,8 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
educationalGoals: learningObject.educationalGoals,
returnValue: {
callback_url: learningObject.returnValue.callbackUrl,
callback_schema: JSON.parse(learningObject.returnValue.callbackSchema),
callback_schema: learningObject.returnValue.callbackSchema === "" ? ""
: JSON.parse(learningObject.returnValue.callbackSchema),
},
skosConcepts: learningObject.skosConcepts,
targetAges: learningObject.targetAges || [],

View file

@ -11,6 +11,7 @@ import {
LearningPathIdentifier,
LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content';
import {v4} from "uuid";
const logger: Logger = getLogger();
@ -23,7 +24,7 @@ function filterData(data: LearningObjectMetadata): FilteredLearningObject {
return {
key: data.hruid, // Hruid learningObject (not path)
_id: data._id,
uuid: data.uuid,
uuid: data.uuid ?? v4(),
version: data.version,
title: data.title,
htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content

View file

@ -15,6 +15,7 @@ import {
import { Language } from '@dwengo-1/common/util/language';
import {Group} from "../../entities/assignments/group.entity";
import {Collection} from "@mikro-orm/core";
import {v4} from "uuid";
/**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
@ -163,7 +164,7 @@ function convertTransition(
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
default: false, // We don't work with default transitions but retain this for backwards compatibility.
next: {
_id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
_id: nextNode._id ? (nextNode._id + index) : v4(), // Construct a unique ID for the transition for backwards compatibility.
hruid: transition.next.learningObjectHruid,
language: nextNode.language,
version: nextNode.version,

View file

@ -29,14 +29,13 @@ export function mapToLearningPath(
admins,
image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null
});
const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) =>
const nodes = dto.nodes.map((nodeDto: LearningObjectNode) =>
repo.createNode({
learningPath: path,
learningObjectHruid: nodeDto.learningobject_hruid,
language: nodeDto.language,
version: nodeDto.version,
startNode: nodeDto.start_node ?? false,
nodeNumber: i,
createdAt: new Date(),
updatedAt: new Date()
})
@ -66,10 +65,10 @@ export function mapToLearningPath(
}
}).filter(it => it).map(it => it!);
fromNode.transitions = new Collection<LearningPathTransition>(transitions);
fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions);
});
path.nodes = new Collection<LearningPathNode>(nodes);
path.nodes = new Collection<LearningPathNode>(path, nodes);
return path;
}

View file

@ -1,43 +1,44 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests';
import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories';
import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider';
import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations';
import { Language } from '@dwengo-1/common/util/language';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example';
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { FilteredLearningObject } from '@dwengo-1/common/interfaces/learning-content';
async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> {
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
const learningObject = learningObjectExample.createLearningObject();
const learningPath = learningPathExample.createLearningPath();
await learningObjectRepo.save(learningObject);
await learningPathRepo.save(learningPath);
return { learningObject, learningPath };
}
import {
FilteredLearningObject,
LearningObjectNode,
LearningPathIdentifier
} from '@dwengo-1/common/interfaces/learning-content';
import {
testPartiallyDatabaseAndPartiallyDwengoApiLearningPath
} from "../../test_assets/content/learning-paths.testdata";
import {testLearningObjectPnNotebooks} from "../../test_assets/content/learning-objects.testdata";
import { LearningPath } from '@dwengo-1/common/dist/interfaces/learning-content';
import {RequiredEntityData} from "@mikro-orm/core";
import {getHtmlRenderingForTestLearningObject} from "../../test-utils/get-html-rendering";
const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan';
describe('DatabaseLearningObjectProvider', () => {
let exampleLearningObject: LearningObject;
let exampleLearningObject: RequiredEntityData<LearningObject>;
let exampleLearningPath: LearningPath;
let exampleLearningPathId: LearningPathIdentifier;
beforeAll(async () => {
await setupTestApp();
const exampleData = await initExampleData();
exampleLearningObject = exampleData.learningObject;
exampleLearningPath = exampleData.learningPath;
exampleLearningObject = testLearningObjectPnNotebooks;
exampleLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath;
exampleLearningPathId = {
hruid: exampleLearningPath.hruid,
language: exampleLearningPath.language as Language
};
});
describe('getLearningObjectById', () => {
it('should return the learning object when it is queried by its id', async () => {
const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById(exampleLearningObject);
expect(result).toBeTruthy();
expectToBeCorrectFilteredLearningObject(result, exampleLearningObject);
expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject);
});
it('should return the learning object when it is queried by only hruid and language (but not version)', async () => {
@ -46,7 +47,7 @@ describe('DatabaseLearningObjectProvider', () => {
language: exampleLearningObject.language,
});
expect(result).toBeTruthy();
expectToBeCorrectFilteredLearningObject(result, exampleLearningObject);
expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject);
});
it('should return null when queried with an id that does not exist', async () => {
@ -61,7 +62,7 @@ describe('DatabaseLearningObjectProvider', () => {
it('should return the correct rendering of the learning object', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject);
// Set newlines so your tests are platform-independent.
expect(result).toEqual(example.getHTMLRendering().replace(/\r\n/g, '\n'));
expect(result).toEqual(getHtmlRenderingForTestLearningObject(exampleLearningObject).replace(/\r\n/g, '\n'));
});
it('should return null for a non-existing learning object', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectHTML({
@ -73,8 +74,10 @@ describe('DatabaseLearningObjectProvider', () => {
});
describe('getLearningObjectIdsFromPath', () => {
it('should return all learning object IDs from a path', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPath);
expect(new Set(result)).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)));
const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPathId);
expect(new Set(result)).toEqual(
new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid))
);
});
it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect(
@ -89,9 +92,11 @@ describe('DatabaseLearningObjectProvider', () => {
});
describe('getLearningObjectsFromPath', () => {
it('should correctly return all learning objects which are on the path, even those who are not in the database', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPath);
const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPathId);
expect(result.length).toBe(exampleLearningPath.nodes.length);
expect(new Set(result.map((it) => it.key))).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)));
expect(new Set(result.map((it) => it.key))).toEqual(
new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid))
);
expect(result.map((it) => it.title)).toContainEqual(EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT);
});

View file

@ -3,6 +3,7 @@ import { LearningObject } from '../../src/entities/content/learning-object.entit
import { LearningPath as LearningPathEntity } from '../../src/entities/content/learning-path.entity';
import { expect } from 'vitest';
import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content';
import {RequiredEntityData} from "@mikro-orm/core";
// Ignored properties because they belang for example to the class, not to the entity itself.
const IGNORE_PROPERTIES = ['parent'];
@ -60,9 +61,9 @@ export function expectToBeCorrectEntity<T extends object>(
/**
* Checks that filtered is the correct representation of original as FilteredLearningObject.
* @param filtered the representation as FilteredLearningObject
* @param original the original entity added to the database
* @param original the data of the entity in the database that was filtered.
*/
export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: LearningObject): void {
export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: RequiredEntityData<LearningObject>): void {
expect(filtered.uuid).toEqual(original.uuid);
expect(filtered.version).toEqual(original.version);
expect(filtered.language).toEqual(original.language);

View file

@ -0,0 +1,12 @@
import {RequiredEntityData} from "@mikro-orm/core";
import {loadTestAsset} from "./load-test-asset";
import {LearningObject} from "../../src/entities/content/learning-object.entity";
import {envVars, getEnvVar} from "../../src/util/envVars";
export function getHtmlRenderingForTestLearningObject(learningObject: RequiredEntityData<LearningObject>): string {
const userPrefix = getEnvVar(envVars.UserContentPrefix);
const cleanedHruid = learningObject.hruid.startsWith(userPrefix)
? learningObject.hruid.substring(userPrefix.length)
: learningObject.hruid;
return loadTestAsset(`/content/learning-object-resources/${cleanedHruid}/rendering.txt`).toString();
}

View file

@ -1,32 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../../src/entities/content/learning-object.entity';
import { Language } from '@dwengo-1/common/dist/util/language';
import { loadTestAsset } from '../../../../test-utils/load-test-asset';
import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type';
import { envVars, getEnvVar } from '../../../../../src/util/envVars';
/**
* Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use
* on a path), but where the precise contents of the learning object are not important.
*/
export function dummyLearningObject(hruid: string, language: Language, title: string): LearningObjectExample {
return {
createLearningObject: (): LearningObject => {
const learningObject = new LearningObject();
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid;
learningObject.language = language;
learningObject.version = 1;
learningObject.title = title;
learningObject.description = 'Just a dummy learning object for testing purposes';
learningObject.contentType = DwengoContentType.TEXT_PLAIN;
learningObject.content = Buffer.from('Dummy content');
learningObject.returnValue = {
callbackUrl: `/learningObject/${hruid}/submissions`,
callbackSchema: '[]',
};
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/dummy/rendering.txt').toString(),
};
}

View file

@ -1,74 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { Language } from '@dwengo-1/common/dist/util/language';
import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type';
import { loadTestAsset } from '../../../../test-utils/load-test-asset';
import { LearningObject } from '../../../../../src/entities/content/learning-object.entity';
import { Attachment } from '../../../../../src/entities/content/attachment.entity';
import { envVars, getEnvVar } from '../../../../../src/util/envVars';
import { EducationalGoal } from '../../../../../src/entities/content/educational-goal.entity';
import { ReturnValue } from '../../../../../src/entities/content/return-value.entity';
const ASSETS_PREFIX = 'learning-objects/pn-werkingnotebooks/';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`;
learningObject.version = 3;
learningObject.language = Language.Dutch;
learningObject.title = 'Werken met notebooks';
learningObject.description = 'Leren werken met notebooks';
learningObject.keywords = ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI'];
const educationalGoal1 = new EducationalGoal();
educationalGoal1.source = 'Source';
educationalGoal1.id = 'id';
const educationalGoal2 = new EducationalGoal();
educationalGoal2.source = 'Source2';
educationalGoal2.id = 'id2';
learningObject.educationalGoals = [educationalGoal1, educationalGoal2];
learningObject.admins = [];
learningObject.contentType = DwengoContentType.TEXT_MARKDOWN;
learningObject.teacherExclusive = false;
learningObject.skosConcepts = [
'http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen',
'http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen',
];
learningObject.copyright = 'dwengo';
learningObject.license = 'dwengo';
learningObject.estimatedTime = 10;
const returnValue = new ReturnValue();
returnValue.callbackUrl = 'callback_url_example';
returnValue.callbackSchema = '{"att": "test", "att2": "test2"}';
learningObject.returnValue = returnValue;
learningObject.available = true;
learningObject.content = loadTestAsset(`${ASSETS_PREFIX}/content.md`);
return learningObject;
},
createAttachment: {
dwengoLogo: (learningObject) => {
const att = new Attachment();
att.learningObject = learningObject;
att.name = 'dwengo.png';
att.mimeType = 'image/png';
att.content = loadTestAsset(`${ASSETS_PREFIX}/dwengo.png`);
return att;
},
knop: (learningObject) => {
const att = new Attachment();
att.learningObject = learningObject;
att.name = 'Knop.png';
att.mimeType = 'image/png';
att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`);
return att;
},
},
getHTMLRendering: () => loadTestAsset(`${ASSETS_PREFIX}/rendering.txt`).toString(),
};
export default example;

View file

@ -1,28 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../../src/entities/content/learning-object.entity';
import { loadTestAsset } from '../../../../test-utils/load-test-asset';
import { envVars, getEnvVar } from '../../../../../src/util/envVars';
import { Language } from '@dwengo-1/common/dist/util/language';
import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_essay`;
learningObject.language = Language.English;
learningObject.version = 1;
learningObject.title = 'Essay question for testing';
learningObject.description = 'This essay question was only created for testing purposes.';
learningObject.contentType = DwengoContentType.GIFT;
learningObject.returnValue = {
callbackUrl: `/learningObject/${learningObject.hruid}/submissions`,
callbackSchema: '["antwoord vraag 1"]',
};
learningObject.content = loadTestAsset('learning-objects/test-essay/content.txt');
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/test-essay/rendering.txt').toString(),
};
export default example;

View file

@ -1,28 +0,0 @@
import { LearningObjectExample } from '../learning-object-example';
import { LearningObject } from '../../../../../src/entities/content/learning-object.entity';
import { loadTestAsset } from '../../../../test-utils/load-test-asset';
import { envVars, getEnvVar } from '../../../../../src/util/envVars';
import { DwengoContentType } from '../../../../../src/services/learning-objects/processing/content-type';
import { Language } from '@dwengo-1/common/dist/util/language';
const example: LearningObjectExample = {
createLearningObject: () => {
const learningObject = new LearningObject();
learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`;
learningObject.language = Language.English;
learningObject.version = 1;
learningObject.title = 'Multiple choice question for testing';
learningObject.description = 'This multiple choice question was only created for testing purposes.';
learningObject.contentType = DwengoContentType.GIFT;
learningObject.returnValue = {
callbackUrl: `/learningObject/${learningObject.hruid}/submissions`,
callbackSchema: '["antwoord vraag 1"]',
};
learningObject.content = loadTestAsset('learning-objects/test-multiple-choice/content.txt');
return learningObject;
},
createAttachment: {},
getHTMLRendering: () => loadTestAsset('learning-objects/test-multiple-choice/rendering.txt').toString(),
};
export default example;

View file

@ -5,6 +5,7 @@ import {DwengoContentType} from '../../../src/services/learning-objects/processi
import {ReturnValue} from '../../../src/entities/content/return-value.entity';
import {envVars, getEnvVar} from "../../../src/util/envVars";
import {loadTestAsset} from "../../test-utils/load-test-asset";
import {v4} from "uuid";
export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
const returnValue: ReturnValue = new ReturnValue();
@ -30,8 +31,8 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
export function createReturnValue(): ReturnValue {
const returnValue: ReturnValue = new ReturnValue();
returnValue.callbackSchema = '';
returnValue.callbackUrl = '';
returnValue.callbackSchema = '[]';
returnValue.callbackUrl = '%SUBMISSION%';
return returnValue;
}
@ -44,6 +45,8 @@ export const testLearningObject01: RequiredEntityData<LearningObject> = {
description: 'debute',
contentType: DwengoContentType.TEXT_MARKDOWN,
keywords: [],
uuid: v4(),
targetAges: [16, 17, 18],
teacherExclusive: false,
skosConcepts: [],
educationalGoals: [],
@ -235,16 +238,16 @@ export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> =
{
name: "dwengo.png",
mimeType: "image/png",
content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/dwengo.png")
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/dwengo.png")
},
{
name: "Knop.png",
mimeType: "image/png",
content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/Knop.png")
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/Knop.png")
}
],
available: false,
content: loadTestAsset("/content/learning-object-resources/pn-werkingnotebooks/content.md"),
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/content.md"),
returnValue: {
callbackUrl: "%SUBMISSION%",
callbackSchema: "[]"

View file

@ -1,10 +1,10 @@
import { Language } from '../util/language';
export interface Transition {
default: boolean;
_id: string;
default?: boolean;
_id?: string;
next: {
_id: string;
_id?: string;
hruid: string;
version: number;
language: string;