test(backend): Testen voor DatabaseLearningPathProvider.fetchLearningPaths afgewerkt

Hierbij optredende problemen opgelost.
This commit is contained in:
Gerald Schmittinger 2025-03-10 21:14:40 +01:00
parent 1f9e9ed70a
commit 7018a8822d
10 changed files with 139 additions and 32 deletions

View file

@ -7,21 +7,30 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
public findByIdentifier( public findByIdentifier(
identifier: LearningObjectIdentifier identifier: LearningObjectIdentifier
): Promise<LearningObject | null> { ): Promise<LearningObject | null> {
return this.findOne({ return this.findOne(
hruid: identifier.hruid, {
language: identifier.language, hruid: identifier.hruid,
version: identifier.version, language: identifier.language,
}); version: identifier.version,
},
{
populate: ["keywords"]
}
);
} }
public findLatestByHruidAndLanguage(hruid: string, language: Language) { public findLatestByHruidAndLanguage(hruid: string, language: Language) {
return this.findOne({ return this.findOne(
hruid: hruid, {
language: language hruid: hruid,
}, { language: language
orderBy: { },
version: "DESC" {
populate: ["keywords"],
orderBy: {
version: "DESC"
}
} }
}); );
} }
} }

View file

@ -7,7 +7,10 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
hruid: string, hruid: string,
language: Language language: Language
): Promise<LearningPath | null> { ): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }); return this.findOne(
{ hruid: hruid, language: language },
{ populate: ["nodes", "nodes.transitions"] }
);
} }
/** /**
@ -25,8 +28,8 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
{ title: { $like: `%${query}%`} }, { title: { $like: `%${query}%`} },
{ description: { $like: `%${query}%`} } { description: { $like: `%${query}%`} }
] ]
} },
populate: ["nodes", "nodes.transitions"]
}); });
} }
// This repository is read-only for now since creating own learning object is an extension feature.
} }

View file

@ -33,6 +33,8 @@ import { LearningPath } from '../entities/content/learning-path.entity.js';
import { LearningPathRepository } from './content/learning-path-repository.js'; import { LearningPathRepository } from './content/learning-path-repository.js';
import { AttachmentRepository } from './content/attachment-repository.js'; import { AttachmentRepository } from './content/attachment-repository.js';
import { Attachment } from '../entities/content/attachment.entity.js'; import { Attachment } from '../entities/content/attachment.entity.js';
import {LearningPathNode} from "../entities/content/learning-path-node.entity";
import {LearningPathTransition} from "../entities/content/learning-path-transition.entity";
let entityManager: EntityManager | undefined; let entityManager: EntityManager | undefined;
@ -113,6 +115,8 @@ export const getLearningPathRepository = repositoryGetter<
LearningPath, LearningPath,
LearningPathRepository LearningPathRepository
>(LearningPath); >(LearningPath);
export const getLearningPathNodeRepository = repositoryGetter(LearningPathNode);
export const getLearningPathTransitionRepository = repositoryGetter(LearningPathTransition);
export const getAttachmentRepository = repositoryGetter< export const getAttachmentRepository = repositoryGetter<
Attachment, Attachment,
AttachmentRepository AttachmentRepository

View file

@ -5,10 +5,11 @@ import {LearningPathTransition} from "./learning-path-transition.entity";
@Entity() @Entity()
export class LearningPathNode { export class LearningPathNode {
@ManyToOne({ entity: () => LearningPath, primary: true }) @ManyToOne({ entity: () => LearningPath, primary: true })
learningPath!: LearningPath; learningPath!: LearningPath;
@PrimaryKey({ type: "numeric", autoincrement: true }) @PrimaryKey({ type: "integer", autoincrement: true })
nodeNumber!: number; nodeNumber!: number;
@Property({ type: 'string' }) @Property({ type: 'string' })

View file

@ -3,7 +3,7 @@ import {LearningPathNode} from "./learning-path-node.entity";
@Entity() @Entity()
export class LearningPathTransition { export class LearningPathTransition {
@ManyToOne({entity: () => LearningPathNode }) @ManyToOne({entity: () => LearningPathNode, primary: true })
node!: LearningPathNode; node!: LearningPathNode;
@PrimaryKey({ type: 'numeric' }) @PrimaryKey({ type: 'numeric' })

View file

@ -21,7 +21,7 @@ export interface LearningObjectNode {
_id: string; _id: string;
learningobject_hruid: string; learningobject_hruid: string;
version: number; version: number;
language: string; language: Language;
start_node?: boolean; start_node?: boolean;
transitions: Transition[]; transitions: Transition[];
created_at: string; created_at: string;
@ -88,7 +88,7 @@ export interface FilteredLearningObject {
version: number; version: number;
title: string; title: string;
htmlUrl: string; htmlUrl: string;
language: string; language: Language;
difficulty: number; difficulty: number;
estimatedTime: number; estimatedTime: number;
available: boolean; available: boolean;

View file

@ -6,6 +6,10 @@ import {getLearningObjectRepository, getLearningPathRepository} from "../../../s
import learningObjectExample from "../../test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example"; import learningObjectExample from "../../test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example";
import learningPathExample from "../../test-assets/learning-paths/pn-werking-example" import learningPathExample from "../../test-assets/learning-paths/pn-werking-example"
import databaseLearningPathProvider from "../../../src/services/learning-paths/database-learning-path-provider"; import databaseLearningPathProvider from "../../../src/services/learning-paths/database-learning-path-provider";
import {expectToBeCorrectLearningPath} from "../../test-utils/expectations";
import {LearningObjectRepository} from "../../../src/data/content/learning-object-repository";
import learningObjectService from "../../../src/services/learning-objects/learning-object-service";
import {Language} from "../../../src/entities/content/language";
async function initExampleData(): Promise<{ learningObject: LearningObject, learningPath: LearningPath }> { async function initExampleData(): Promise<{ learningObject: LearningObject, learningPath: LearningPath }> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
@ -18,15 +22,17 @@ async function initExampleData(): Promise<{ learningObject: LearningObject, lear
} }
describe("DatabaseLearningPathProvider", () => { describe("DatabaseLearningPathProvider", () => {
let learningObjectRepo: LearningObjectRepository;
let example: {learningObject: LearningObject, learningPath: LearningPath}; let example: {learningObject: LearningObject, learningPath: LearningPath};
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
example = await initExampleData(); example = await initExampleData();
learningObjectRepo = getLearningObjectRepository();
}); });
describe("fetchLearningPaths", () => { describe("fetchLearningPaths", () => {
it("returns the learning path correctly", () => { it("returns the learning path correctly", async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths( const result = await databaseLearningPathProvider.fetchLearningPaths(
[example.learningPath.hruid], [example.learningPath.hruid],
example.learningPath.language, example.learningPath.language,
@ -34,7 +40,26 @@ describe("DatabaseLearningPathProvider", () => {
); );
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
expect(result.data)
}) const learningObjectsOnPath = (await Promise.all(
example.learningPath.nodes.map(node =>
learningObjectService.getLearningObjectById({
hruid: node.learningObjectHruid,
version: node.version,
language: node.language
}))
)).filter(it => it !== null);
expectToBeCorrectLearningPath(result.data![0], example.learningPath, learningObjectsOnPath)
});
it("returns a non-successful response if a non-existing learning path is queried", async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths(
[example.learningPath.hruid],
Language.Abkhazian, // wrong language
"the source"
);
expect(result.success).toBe(false);
});
}); });
}); });

View file

@ -1,21 +1,28 @@
import {Language} from "../../../src/entities/content/language"; import {Language} from "../../../src/entities/content/language";
import {LearningPathTransition} from "../../../src/entities/content/learning-path-transition.entity"; import {LearningPathTransition} from "../../../src/entities/content/learning-path-transition.entity";
import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity"; import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity";
import {LearningPath} from "../../../src/entities/content/learning-path.entity";
export function createLearningPathTransition(condition: string | null, to: LearningPathNode) { export function createLearningPathTransition(node: LearningPathNode, transitionNumber: number, condition: string | null, to: LearningPathNode) {
let trans = new LearningPathTransition(); let trans = new LearningPathTransition();
trans.node = node;
trans.transitionNumber = transitionNumber;
trans.condition = condition || "true"; trans.condition = condition || "true";
trans.next = to; trans.next = to;
return trans; return trans;
} }
export function createLearningPathNode( export function createLearningPathNode(
learningPath: LearningPath,
nodeNumber: number,
learningObjectHruid: string, learningObjectHruid: string,
version: number, version: number,
language: Language, language: Language,
startNode: boolean startNode: boolean
) { ) {
let node = new LearningPathNode(); let node = new LearningPathNode();
node.learningPath = learningPath;
node.nodeNumber = nodeNumber;
node.learningObjectHruid = learningObjectHruid; node.learningObjectHruid = learningObjectHruid;
node.version = version; node.version = version;
node.language = language; node.language = language;

View file

@ -1,16 +1,17 @@
import {LearningPath, LearningPathNode} from "../../../src/entities/content/learning-path.entity"; import {LearningPath} from "../../../src/entities/content/learning-path.entity";
import {Language} from "../../../src/entities/content/language"; import {Language} from "../../../src/entities/content/language";
import {EnvVars, getEnvVar} from "../../../src/util/envvars"; import {EnvVars, getEnvVar} from "../../../src/util/envvars";
import {createLearningPathNode, createLearningPathTransition} from "./learning-path-utils"; import {createLearningPathNode, createLearningPathTransition} from "./learning-path-utils";
import {LearningPathNode} from "../../../src/entities/content/learning-path-node.entity";
function createNodes(): LearningPathNode[] { function createNodes(learningPath: LearningPath): LearningPathNode[] {
let nodes = [ let nodes = [
createLearningPathNode("u_pn_werkingnotebooks", 3, Language.Dutch, true), createLearningPathNode(learningPath, 0, "u_pn_werkingnotebooks", 3, Language.Dutch, true),
createLearningPathNode("pn_werkingnotebooks2", 3, Language.Dutch, false), createLearningPathNode(learningPath, 1, "pn_werkingnotebooks2", 3, Language.Dutch, false),
createLearningPathNode("pn_werkingnotebooks3", 3, Language.Dutch, false), createLearningPathNode(learningPath, 2, "pn_werkingnotebooks3", 3, Language.Dutch, false),
]; ];
nodes[0].transitions.push(createLearningPathTransition("true", nodes[1])); nodes[0].transitions.push(createLearningPathTransition(nodes[0], 0, "true", nodes[1]));
nodes[1].transitions.push(createLearningPathTransition("true", nodes[2])); nodes[1].transitions.push(createLearningPathTransition(nodes[1], 0, "true", nodes[2]));
return nodes; return nodes;
} }
@ -21,7 +22,7 @@ const example: LearningPathExample = {
path.hruid = `${getEnvVar(EnvVars.UserContentPrefix)}pn_werking`; path.hruid = `${getEnvVar(EnvVars.UserContentPrefix)}pn_werking`;
path.title = "Werken met notebooks"; path.title = "Werken met notebooks";
path.description = "Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?"; path.description = "Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?";
path.nodes = createNodes(); path.nodes = createNodes(path);
return path; return path;
} }
} }

View file

@ -99,9 +99,66 @@ export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearni
* *
* @param learningPath The learning path returned by the retriever, service or endpoint * @param learningPath The learning path returned by the retriever, service or endpoint
* @param expectedEntity The expected entity * @param expectedEntity The expected entity
* @param learningObjectsOnPath The learning objects on LearningPath. Necessary since some information in
* the learning path returned from the API endpoint
*/ */
export function expectToBeCorrectLearningPath(learningPath: LearningPath, expectedEntity: LearningPathEntity) { export function expectToBeCorrectLearningPath(
learningPath: LearningPath,
expectedEntity: LearningPathEntity,
learningObjectsOnPath: FilteredLearningObject[]
) {
expect(learningPath.hruid).toEqual(expectedEntity.hruid); expect(learningPath.hruid).toEqual(expectedEntity.hruid);
expect(learningPath.language).toEqual(expectedEntity.language); expect(learningPath.language).toEqual(expectedEntity.language);
expect(learningPath.description).toEqual(expectedEntity.description); expect(learningPath.description).toEqual(expectedEntity.description);
expect(learningPath.title).toEqual(expectedEntity.title);
const keywords = new Set(learningObjectsOnPath.flatMap(it => it.keywords || []));
expect(new Set(learningPath.keywords.split(' '))).toEqual(keywords)
const targetAges = new Set(learningObjectsOnPath.flatMap(it => it.targetAges || []));
expect(new Set(learningPath.target_ages)).toEqual(targetAges);
expect(learningPath.min_age).toEqual(Math.min(...targetAges));
expect(learningPath.max_age).toEqual(Math.max(...targetAges));
expect(learningPath.num_nodes).toEqual(expectedEntity.nodes.length);
expect(learningPath.image || null).toEqual(expectedEntity.image);
let expectedLearningPathNodes = new Map(
expectedEntity.nodes.map(node => [
{learningObjectHruid: node.learningObjectHruid, language: node.language, version: node.version},
{startNode: node.startNode, transitions: node.transitions}
])
);
for (let node of learningPath.nodes) {
const nodeKey = {
learningObjectHruid: node.learningobject_hruid,
language: node.language,
version: node.version
};
expect(expectedLearningPathNodes.keys()).toContainEqual(nodeKey);
let expectedNode = [...expectedLearningPathNodes.entries()]
.filter(([key, _]) =>
key.learningObjectHruid === nodeKey.learningObjectHruid
&& key.language === node.language
&& key.version === node.version
)[0][1]
expect(node.start_node).toEqual(expectedNode?.startNode);
expect(
new Set(node.transitions.map(it => it.next.hruid))
).toEqual(
new Set(expectedNode.transitions.map(it => it.next.learningObjectHruid))
);
expect(
new Set(node.transitions.map(it => it.next.language))
).toEqual(
new Set(expectedNode.transitions.map(it => it.next.language))
);
expect(
new Set(node.transitions.map(it => it.next.version))
).toEqual(
new Set(expectedNode.transitions.map(it => it.next.version))
);
}
} }