fix(backend): Foute entity-structuur van leerpaden verbeterd.

Ook testen geschreven voor LearningPathRepository en LearningObjectRepository.
This commit is contained in:
Gerald Schmittinger 2025-03-09 08:50:39 +01:00
parent 4d999c78ba
commit 1417907933
24 changed files with 474 additions and 64 deletions

View file

@ -24,5 +24,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
}
});
}
// This repository is read-only for now since creating own learning object is an extension feature.
}

View file

@ -1,6 +1,6 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js';
import {DwengoEntityRepository} from '../dwengo-entity-repository.js';
import {LearningPath} from '../../entities/content/learning-path.entity.js';
import {Language} from '../../entities/content/language.js';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage(
@ -17,7 +17,7 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
* @param query The query string we want to seach for in the title or description.
* @param language The language of the learning paths we want to find.
*/
public findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> {
public async findByQueryStringAndLanguage(query: string, language: Language): Promise<LearningPath[]> {
return this.findAll({
where: {
language: language,

View file

@ -45,7 +45,7 @@ export class LearningObject {
keywords: string[] = [];
@Property({ type: 'array', nullable: true })
targetAges?: number[];
targetAges?: number[] = [];
@Property({ type: 'bool' })
teacherExclusive: boolean = false;

View file

@ -0,0 +1,37 @@
import {Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property} from "@mikro-orm/core";
import {Language} from "./language";
import {LearningPath} from "./learning-path.entity";
import {LearningPathTransition} from "./learning-path-transition.entity";
@Entity()
export class LearningPathNode {
@ManyToOne({ entity: () => LearningPath, primary: true })
learningPath!: LearningPath;
@PrimaryKey({ type: "numeric", autoincrement: true })
nodeNumber!: number;
@Property({ type: 'string' })
learningObjectHruid!: string;
@Enum({ items: () => Language })
language!: Language;
@Property({ type: 'number' })
version!: number;
@Property({ type: 'text', nullable: true })
instruction?: string;
@Property({ type: 'bool' })
startNode!: boolean;
@OneToMany({ entity: () => LearningPathTransition, mappedBy: "node" })
transitions: LearningPathTransition[] = [];
@Property({ length: 3 })
createdAt: Date = new Date();
@Property({ length: 3, onUpdate: () => new Date() })
updatedAt: Date = new Date();
}

View file

@ -0,0 +1,17 @@
import {Entity, ManyToOne, PrimaryKey, Property} from "@mikro-orm/core";
import {LearningPathNode} from "./learning-path-node.entity";
@Entity()
export class LearningPathTransition {
@ManyToOne({entity: () => LearningPathNode })
node!: LearningPathNode;
@PrimaryKey({ type: 'numeric' })
transitionNumber!: number;
@Property({ type: 'string' })
condition!: string;
@ManyToOne({ entity: () => LearningPathNode })
next!: LearningPathNode;
}

View file

@ -1,16 +1,14 @@
import {
Embeddable,
Embedded,
Entity,
Enum,
ManyToMany,
OneToOne,
ManyToMany, OneToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Language } from './language.js';
import { Teacher } from '../users/teacher.entity.js';
import {LearningPathRepository} from "../../data/content/learning-path-repository";
import {LearningPathNode} from "./learning-path-node.entity";
@Entity({repository: () => LearningPathRepository})
export class LearningPath {
@ -29,45 +27,9 @@ export class LearningPath {
@Property({ type: 'text' })
description!: string;
@Property({ type: 'blob' })
image!: string;
@Property({ type: 'blob', nullable: true })
image: Buffer | null = null;
@Embedded({ entity: () => LearningPathNode, array: true })
@OneToMany({ entity: () => LearningPathNode, mappedBy: "learningPath" })
nodes: LearningPathNode[] = [];
}
@Embeddable()
export class LearningPathNode {
@Property({ type: 'string' })
learningObjectHruid!: string;
@Enum({ items: () => Language })
language!: Language;
@Property({ type: 'number' })
version!: number;
@Property({ type: 'longtext' })
instruction!: string;
@Property({ type: 'bool' })
startNode!: boolean;
@Embedded({ entity: () => LearningPathTransition, array: true })
transitions!: LearningPathTransition[];
@Property({ length: 3 })
createdAt: Date = new Date();
@Property({ length: 3, onUpdate: () => new Date() })
updatedAt: Date = new Date();
}
@Embeddable()
export class LearningPathTransition {
@Property({ type: 'string' })
condition!: string;
@OneToOne({ entity: () => LearningPathNode })
next!: LearningPathNode;
}

View file

@ -2,10 +2,10 @@ import {getAttachmentRepository} from "../../data/repositories";
import {Attachment} from "../../entities/content/attachment.entity";
import {LearningObjectIdentifier} from "../../interfaces/learning-content";
const attachmentRepo = getAttachmentRepository();
const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) {
return attachmentRepo.findByLearningObjectIdAndName({
hruid: learningObjectId.hruid,

View file

@ -11,8 +11,6 @@ import {getUrlStringForLearningObject} from "../../util/links";
import processingService from "./processing/processing-service";
import {NotFoundError} from "@mikro-orm/core";
const learningObjectRepo = getLearningObjectRepository();
const learningPathRepo = getLearningPathRepository();
function convertLearningObject(learningObject: LearningObject | null): FilteredLearningObject | null {
if (!learningObject) {
@ -45,6 +43,8 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
}
function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(
id.hruid, id.language as Language
);
@ -66,6 +66,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
* Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(
id.hruid, id.language as Language
);
@ -82,6 +84,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
* Fetch the HRUIDs of all learning objects on this path.
*/
async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
const learningPathRepo = getLearningPathRepository();
const learningPath = await learningPathRepo.findByHruidAndLanguage(id.hruid, id.language);
if (!learningPath) {
throw new NotFoundError("The learning path with the given ID could not be found.");
@ -93,6 +97,8 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
* Fetch the full metadata of all learning objects on this path.
*/
async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
const learningPathRepo = getLearningPathRepository();
const learningPath = await learningPathRepo.findByHruidAndLanguage(id.hruid, id.language);
if (!learningPath) {
throw new NotFoundError("The learning path with the given ID could not be found.");

View file

@ -1,15 +1,20 @@
/**
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/learing_object_markdown_renderer.js [sic!]
*/
import PdfProcessor from "../pdf/pdf-processor.js";
import AudioProcessor from "../audio/audio-processor.js";
import ExternProcessor from "../extern/extern-processor.js";
import InlineImageProcessor from "../image/inline-image-processor.js";
import {RendererObject, Tokens} from "marked";
import * as marked from "marked";
import {getUrlStringForLearningObjectHTML, isValidHttpUrl} from "../../../../util/links";
import {ProcessingError} from "../processing-error";
import {LearningObjectIdentifier} from "../../../../interfaces/learning-content";
import {Language} from "../../../../entities/content/language";
import Image = Tokens.Image;
import Heading = Tokens.Heading;
import Link = Tokens.Link;
import Image = marked.Tokens.Image;
import Heading = marked.Tokens.Heading;
import Link = marked.Tokens.Link;
import RendererObject = marked.RendererObject;
const prefixes = {
learningObject: '@learning-object',

View file

@ -1,6 +1,5 @@
/**
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/markdown_processor.js
* and https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/learing_object_markdown_renderer.js [sic!]
*/
import {marked} from 'marked'
@ -8,7 +7,7 @@ import Processor from '../processor.js';
import InlineImageProcessor from '../image/inline-image-processor.js';
import {DwengoContentType} from "../content-type";
import {ProcessingError} from "../processing-error";
import dwengoMarkedRenderer from "./learning-object-markdown-renderer";
import dwengoMarkedRenderer from "./dwengo-marked-renderer";
class MarkdownProcessor extends Processor<string> {
constructor() {

View file

@ -35,9 +35,9 @@ class ProcessingService {
new GiftProcessor()
];
processors.forEach(processor => {
this.processors.set(processor.contentType, processor);
});
this.processors = new Map(
processors.map(processor => [processor.contentType, processor])
)
}
/**

View file

@ -15,8 +15,6 @@ import {getLearningPathRepository} from "../../data/repositories";
import {Language} from "../../entities/content/language";
import learningObjectService from "../learning-objects/learning-object-service";
const learningPathRepo = getLearningPathRepository();
/**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
* corresponding learning object.
@ -138,6 +136,8 @@ const databaseLearningPathProvider: LearningPathProvider = {
* Fetch the learning paths with the given hruids from the database.
*/
async fetchLearningPaths(hruids: string[], language: Language, source: string): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository();
const learningPaths = await Promise.all(
hruids.map(hruid => learningPathRepo.findByHruidAndLanguage(hruid, language))
);
@ -158,6 +158,8 @@ const databaseLearningPathProvider: LearningPathProvider = {
* Search learning paths in the database using the given search string.
*/
async searchLearningPaths(query: string, language: Language): Promise<LearningPath[]> {
const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
return await Promise.all(
searchResults.map((result, index) =>

View file

@ -0,0 +1,71 @@
import {beforeAll, describe, it, expect} from "vitest";
import {LearningObjectRepository} from "../../../src/data/content/learning-object-repository";
import {setupTestApp} from "../../setup-tests";
import {getLearningObjectRepository} from "../../../src/data/repositories";
import example from "../../test-assets/learning-objects/pn_werkingnotebooks/pn-werkingnotebooks-example.js"
import {LearningObject} from "../../../src/entities/content/learning-object.entity";
import {expectToBeCorrectEntity} from "../../test-utils/expect-to-be-correct-entity";
describe("LearningObjectRepository", () => {
let learningObjectRepository: LearningObjectRepository;
let exampleLearningObject: LearningObject;
beforeAll(async () => {
await setupTestApp();
learningObjectRepository = getLearningObjectRepository();
});
it("should be able to add a learning object to it without an error", async () => {
exampleLearningObject = example.createLearningObject();
await learningObjectRepository.insert(exampleLearningObject);
});
it("should return the learning object when queried by id", async () => {
const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid,
language: exampleLearningObject.language,
version: exampleLearningObject.version
});
expect(result).toBeInstanceOf(LearningObject);
console.log(result);
expectToBeCorrectEntity({
name: "actual",
entity: result!
}, {
name: "expected",
entity: exampleLearningObject
});
});
it("should return null when non-existing version is queried", async () => {
const result = await learningObjectRepository.findByIdentifier({
hruid: exampleLearningObject.hruid,
language: exampleLearningObject.language,
version: 100
});
expect(result).toBe(null);
});
let newerExample: LearningObject;
it("should allow a learning object with the same id except a different version to be added", async () => {
newerExample = example.createLearningObject();
newerExample.version = 10;
newerExample.title += " (nieuw)";
await learningObjectRepository.save(newerExample);
});
it("should return the newest version of the learning object when queried by only hruid and language", async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage(newerExample.hruid, newerExample.language);
expect(result).toBeInstanceOf(LearningObject);
expect(result?.version).toBe(10);
expect(result?.title).toContain("(nieuw)");
});
it("should return null when queried by non-existing hruid or language", async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage("something_that_does_not_exist", exampleLearningObject.language);
expect(result).toBe(null);
});
});

View file

@ -0,0 +1,78 @@
import {beforeAll, describe, expect, it} from "vitest";
import {setupTestApp} from "../../setup-tests";
import {getLearningPathRepository} from "../../../src/data/repositories";
import {LearningPathRepository} from "../../../src/data/content/learning-path-repository";
import example from "../../test-assets/learning-paths/pn-werking-example";
import {LearningPath} from "../../../src/entities/content/learning-path.entity";
import {expectToBeCorrectEntity} from "../../test-utils/expect-to-be-correct-entity";
import {Language} from "../../../src/entities/content/language";
function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void {
expect(result).toHaveProperty("length");
expect(result.length).toBe(1);
expectToBeCorrectEntity({ entity: result[0]! }, { entity: expected });
}
function expectToHaveFoundNothing(result: LearningPath[]): void {
expect(result).toHaveProperty("length");
expect(result.length).toBe(0);
}
describe("LearningPathRepository", () => {
let learningPathRepo: LearningPathRepository;
beforeAll(async () => {
await setupTestApp();
learningPathRepo = getLearningPathRepository();
});
let examplePath: LearningPath;
it("should be able to add a learning path without throwing an error", async () => {
examplePath = example.createLearningPath();
await learningPathRepo.insert(examplePath);
});
it("should return the added path when it is queried by hruid and language", async () => {
const result = await learningPathRepo.findByHruidAndLanguage(examplePath.hruid, examplePath.language);
expect(result).toBeInstanceOf(LearningPath);
expectToBeCorrectEntity({ entity: result! }, { entity: examplePath });
});
it("should return null to a query on a non-existing hruid or language", async () => {
const result = await learningPathRepo.findByHruidAndLanguage("not_existing_hruid", examplePath.language);
expect(result).toBe(null);
});
it("should return the learning path when we search for a search term occurring in its title", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
examplePath.title.slice(4, 9),
examplePath.language
);
expectToHaveFoundPrecisely(examplePath, result);
});
it("should return the learning path when we search for a search term occurring in its description", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
examplePath.description.slice(8, 15),
examplePath.language
);
expectToHaveFoundPrecisely(examplePath, result);
});
it("should return null when we search for something not occurring in its title or description", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
"something not occurring in the path",
examplePath.language
);
expectToHaveFoundNothing(result);
});
it("should return null when we search for something occurring in its title, but another language", async () => {
const result = await learningPathRepo.findByQueryStringAndLanguage(
examplePath.description.slice(1, 3),
Language.Kalaallisut
);
expectToHaveFoundNothing(result);
});
});

View file

@ -0,0 +1,7 @@
import {LearningObject} from "../../../src/entities/content/learning-object.entity";
import {Attachment} from "../../../src/entities/content/attachment.entity";
type LearningObjectExample = {
createLearningObject: () => LearningObject,
createAttachment: {[key: string]: (owner: LearningObject) => Attachment}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,25 @@
# Werken met notebooks
Het lesmateriaal van 'Python in wiskunde en STEM' wordt aangeboden in de vorm van interactieve **_notebooks_**. Notebooks zijn _digitale documenten_ die zowel uitvoerbare code bevatten als tekst, afbeeldingen, video, hyperlinks ...
_Nieuwe begrippen_ worden aangebracht via tekstuele uitleg, video en afbeeldingen.
Er zijn uitgewerkte *voorbeelden* met daarnaast ook kleine en grote *opdrachten*. In deze opdrachten zal je aangereikte code kunnen uitvoeren, maar ook zelf code opstellen.
De code die in de notebooks gebruikt wordt, is Python versie 3. We kozen voor Python omdat dit een heel toegankelijke programmeertaal is, die vaak ook intuïtief is.
Python is bovendien bezig aan een opmars en wordt gebruikt door bedrijven, zoals Google, NASA, Netflix, Uber, AstraZeneca, Barco, Instagram en YouTube.
We kozen voor notebooks omdat daar enkele belangrijke voordelen aan verbonden zijn: leerkrachten moeten geen geavanceerde installaties doen om de notebooks te gebruiken, leerkrachten kunnen verschillende soorten van lesinhouden aanbieden via één platform, de notebooks zijn interactief, leerlingen bouwen de oplossing van een probleem stap voor stap op in de notebook waardoor dat proces zichtbaar is voor de leerkracht ([Jeroen Van der Hooft, 2023](https://libstore.ugent.be/fulltxt/RUG01/003/151/437/RUG01-003151437_2023_0001_AC.pdf)).
---
Klik je op onderstaande knop 'Open notebooks', dan word je doorgestuurd naar een andere website waar jouw persoonlijke notebooks ingeladen worden. (Dit kan even duren.)
Links op het scherm vind je er twee bestanden met extensie _.ipynb_.
Dit zijn de twee notebooks waarin je resp. een overzicht krijgt van de opbouw en mogelijkheden en hoe je er mee aan de slag kan. Dubbelklik op de bestandsnaam om een notebook te openen.
Je ziet er ook een map *images* met de afbeeldingen die in de notebooks getoond worden.
In deze eerste twee notebooks leer je hoe de notebooks zijn opgevat en hoe je ermee aan de slag kan.
Na het doorlopen van beide notebooks heb je een goed idee van hoe onze Python notebooks zijn opgevat.
[![](Knop.png "Knop")](https://kiks.ilabt.imec.be/hub/tmplogin?id=0101 "Notebooks Werking")

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,74 @@
import {LearningObjectExample} from "../learning-object-example";
import {Language} from "../../../../src/entities/content/language";
import {DwengoContentType} from "../../../../src/services/learning-objects/processing/content-type";
import {loadTestAsset} from "../../../test-utils/load-test-asset";
import {EducationalGoal, LearningObject, ReturnValue} from "../../../../src/entities/content/learning-object.entity";
import {Attachment} from "../../../../src/entities/content/attachment.entity";
import {EnvVars, getEnvVar} from "../../../../src/util/envvars";
const ASSETS_PREFIX = "learning-objects/pn_werkingnotebooks/";
const example: LearningObjectExample = {
createLearningObject: ()=> {
let 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"]
let educationalGoal1 = new EducationalGoal();
educationalGoal1.source = "Source";
educationalGoal1.id = "id";
let 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;
let 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}/dwengo.png`);
return learningObject
},
createAttachment: {
dwengoLogo: (learningObject) => {
let 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) => {
let att = new Attachment();
att.learningObject = learningObject;
att.name = "Knop.png";
att.mimeType = "image/png";
att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`)
return att;
}
}
}
export default example;

View file

@ -0,0 +1,3 @@
type LearningPathExample = {
createLearningPath: () => LearningPath
};

View file

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

View file

@ -0,0 +1,29 @@
import {LearningPath, LearningPathNode} from "../../../src/entities/content/learning-path.entity";
import {Language} from "../../../src/entities/content/language";
import {EnvVars, getEnvVar} from "../../../src/util/envvars";
import {createLearningPathNode, createLearningPathTransition} from "./learning-path-utils";
function createNodes(): LearningPathNode[] {
let nodes = [
createLearningPathNode("u_pn_werkingnotebooks", 3, Language.Dutch, true),
createLearningPathNode("pn_werkingnotebooks2", 3, Language.Dutch, false),
createLearningPathNode("pn_werkingnotebooks3", 3, Language.Dutch, false),
];
nodes[0].transitions.push(createLearningPathTransition("true", nodes[1]));
nodes[1].transitions.push(createLearningPathTransition("true", nodes[2]));
return nodes;
}
const example: LearningPathExample = {
createLearningPath: () => {
const path = new LearningPath();
path.language = Language.Dutch;
path.hruid = `${getEnvVar(EnvVars.UserContentPrefix)}pn_werking`;
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.nodes = createNodes();
return path;
}
}
export default example;

View file

@ -0,0 +1,62 @@
import {AssertionError} from "node:assert";
// Ignored properties because they belang for example to the class, not to the entity itself.
const IGNORE_PROPERTIES = ["parent"];
/**
* Checks if the actual entity from the database conforms to the entity that was added previously.
* @param actual The actual entity retrieved from the database
* @param expected The (previously added) entity we would expect to retrieve
*/
export function expectToBeCorrectEntity<T extends object>(
actual: {entity: T, name?: string},
expected: {entity: T, name?: string}
): void {
if (!actual.name) {
actual.name = "actual";
}
if (!expected.name) {
expected.name = "expected";
}
for (let property in expected.entity) {
if (
property !in IGNORE_PROPERTIES
&& expected.entity[property] !== undefined // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
&& typeof expected.entity[property] !== "function" // Functions obviously are not persisted via the database
) {
if (!actual.entity.hasOwnProperty(property)) {
throw new AssertionError({
message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`
});
}
if (typeof expected.entity[property] === "boolean") { // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
if (!!expected.entity[property] !== !!actual.entity[property]) {
throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name},
but ${actual.entity[property]} (${!!expected.entity[property]}) in ${actual.name}`
});
}
} else if (typeof expected.entity[property] !== typeof actual.entity[property]) {
throw new AssertionError({
message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`
});
} else if (typeof expected.entity[property] === "object") {
expectToBeCorrectEntity(
{
name: actual.name + "." + property,
entity: actual.entity[property] as object
}, {
name: expected.name + "." + property,
entity: expected.entity[property] as object
}
);
} else {
if (expected.entity[property] !== actual.entity[property]) {
throw new AssertionError({
message: `${property} was ${expected.entity[property]} in ${expected.name}, but ${actual.entity[property]} in ${actual.name}`
});
}
}
}
}
}

View file

@ -0,0 +1,10 @@
import fs from "fs";
import path from "node:path";
/**
* Load the asset at the given path.
* @param relPath Path of the asset relative to the test-assets folder.
*/
export function loadTestAsset(relPath: string): Buffer {
return fs.readFileSync(path.resolve(__dirname, `../test-assets/${relPath}`));
}