Merge branch 'dev' into feat/service-layer
This commit is contained in:
commit
e487dfff5c
20 changed files with 647 additions and 74 deletions
|
@ -1,10 +1,16 @@
|
|||
DWENGO_PORT=3000
|
||||
#
|
||||
# Basic configuration
|
||||
#
|
||||
|
||||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
DWENGO_DB_HOST=localhost
|
||||
DWENGO_DB_PORT=5431
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
DWENGO_DB_PASSWORD=postgres
|
||||
DWENGO_DB_UPDATE=true
|
||||
|
||||
# Auth
|
||||
|
||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||
|
@ -14,3 +20,9 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/
|
|||
|
||||
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
|
||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||
|
||||
#
|
||||
# Advanced configuration
|
||||
#
|
||||
|
||||
# LOKI_HOST=http://localhost:9001 # The address of the Loki instance, used for logging
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
DWENGO_DB_HOST=domain-or-ip-of-database
|
||||
DWENGO_DB_PORT=5432
|
||||
DWENGO_DB_PORT=5431
|
||||
|
||||
# Change this to the actual credentials of the user Dwengo should use in the backend
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
|
|
28
backend/.env.production.example
Normal file
28
backend/.env.production.example
Normal file
|
@ -0,0 +1,28 @@
|
|||
DWENGO_PORT=3000 # The port the backend will listen on
|
||||
DWENGO_DB_HOST=db # Name of the database container
|
||||
DWENGO_DB_PORT=5431
|
||||
|
||||
# Change this to the actual credentials of the user Dwengo should use in the backend
|
||||
DWENGO_DB_NAME=postgres
|
||||
DWENGO_DB_USERNAME=postgres
|
||||
DWENGO_DB_PASSWORD=postgres
|
||||
|
||||
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
|
||||
DWENGO_DB_UPDATE=false
|
||||
|
||||
# Data for the identity provider via which the students authenticate.
|
||||
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
|
||||
# Data for the identity provider via which the teachers authenticate.
|
||||
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
|
||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
|
||||
|
||||
#
|
||||
# Advanced configuration
|
||||
#
|
||||
|
||||
# Logging and monitoring
|
||||
|
||||
# LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging
|
35
backend/Dockerfile
Normal file
35
backend/Dockerfile
Normal file
|
@ -0,0 +1,35 @@
|
|||
FROM node:22 AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
|
||||
COPY package*.json ./
|
||||
COPY backend/package.json ./backend/
|
||||
|
||||
RUN npm install --silent
|
||||
|
||||
# Build the backend
|
||||
|
||||
# Root tsconfig.json
|
||||
COPY tsconfig.json ./
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
COPY backend ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22 AS production-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package-lock.json backend/package.json ./
|
||||
|
||||
RUN npm install --silent --only=production
|
||||
|
||||
COPY --from=build-stage /app/backend/dist ./dist/
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "--env-file=.env", "dist/app.js"]
|
|
@ -1,4 +1,4 @@
|
|||
import express, { Express, Response } from 'express';
|
||||
import express, { Express } from 'express';
|
||||
import { initORM } from './orm.js';
|
||||
|
||||
import themeRoutes from './routes/themes.js';
|
||||
|
@ -13,12 +13,14 @@ import submissionRouter from './routes/submissions.js';
|
|||
import classRouter from './routes/classes.js';
|
||||
import questionRouter from './routes/questions.js';
|
||||
import authRouter from './routes/auth.js';
|
||||
|
||||
import { authenticateUser } from './middleware/auth/auth.js';
|
||||
import cors from './middleware/cors.js';
|
||||
import { getLogger, Logger } from './logging/initalize.js';
|
||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||
import responseTime from 'response-time';
|
||||
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
|
||||
import apiRouter from './routes/router.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
|
@ -30,31 +32,13 @@ app.use(express.json());
|
|||
app.use(responseTime(responseTimeLogger));
|
||||
app.use(authenticateUser);
|
||||
|
||||
// TODO Replace with Express routes
|
||||
app.get('/', (_, res: Response) => {
|
||||
logger.debug('GET /');
|
||||
res.json({
|
||||
message: 'Hello Dwengo!🚀',
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/student', studentRouter);
|
||||
app.use('/teacher', teacherRouter);
|
||||
app.use('/group', groupRouter);
|
||||
app.use('/assignment', assignmentRouter);
|
||||
app.use('/submission', submissionRouter);
|
||||
app.use('/class', classRouter);
|
||||
app.use('/question', questionRouter);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/theme', themeRoutes);
|
||||
app.use('/learningPath', learningPathRoutes);
|
||||
app.use('/learningObject', learningObjectRoutes);
|
||||
app.get('/api', apiRouter);
|
||||
|
||||
async function startServer() {
|
||||
await initORM();
|
||||
|
||||
app.listen(port, () => {
|
||||
logger.info(`Server is running at http://localhost:${port}`);
|
||||
logger.info(`Server is running at http://localhost:${port}/api`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
35
backend/src/routes/router.ts
Normal file
35
backend/src/routes/router.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Response, Router } from 'express';
|
||||
import studentRouter from './student';
|
||||
import groupRouter from './group';
|
||||
import assignmentRouter from './assignment';
|
||||
import submissionRouter from './submission';
|
||||
import classRouter from './class';
|
||||
import questionRouter from './question';
|
||||
import authRouter from './auth';
|
||||
import themeRoutes from './themes';
|
||||
import learningPathRoutes from './learning-paths';
|
||||
import learningObjectRoutes from './learning-objects';
|
||||
import { getLogger, Logger } from '../logging/initalize';
|
||||
|
||||
const router = Router();
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
router.get('/', (_, res: Response) => {
|
||||
logger.debug('GET /');
|
||||
res.json({
|
||||
message: 'Hello Dwengo!🚀',
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/student', studentRouter);
|
||||
router.use('/group', groupRouter);
|
||||
router.use('/assignment', assignmentRouter);
|
||||
router.use('/submission', submissionRouter);
|
||||
router.use('/class', classRouter);
|
||||
router.use('/question', questionRouter);
|
||||
router.use('/auth', authRouter);
|
||||
router.use('/theme', themeRoutes);
|
||||
router.use('/learningPath', learningPathRoutes);
|
||||
router.use('/learningObject', learningObjectRoutes);
|
||||
|
||||
export default router;
|
117
backend/tests/service/learning-objects.test.ts
Normal file
117
backend/tests/service/learning-objects.test.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { LearningObjectMetadata, LearningPath } from '../../src/interfaces/learningPath';
|
||||
import { fetchWithLogging } from '../../src/util/apiHelper';
|
||||
import { getLearningObjectById, getLearningObjectsFromPath } from '../../src/services/learningObjects';
|
||||
import { fetchLearningPaths } from '../../src/services/learningPaths';
|
||||
|
||||
// Mock API functions
|
||||
vi.mock('../../src/util/apiHelper', () => ({
|
||||
fetchWithLogging: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/learningPaths', () => ({
|
||||
fetchLearningPaths: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getLearningObjectById', () => {
|
||||
const hruid = 'test-object';
|
||||
const language = 'en';
|
||||
const mockMetadata: LearningObjectMetadata = {
|
||||
hruid,
|
||||
_id: '123',
|
||||
uuid: 'uuid-123',
|
||||
version: 1,
|
||||
title: 'Test Object',
|
||||
language,
|
||||
difficulty: 5,
|
||||
estimated_time: 120,
|
||||
available: true,
|
||||
teacher_exclusive: false,
|
||||
educational_goals: [{ source: 'source', id: 'id' }],
|
||||
keywords: ['robotics'],
|
||||
description: 'A test object',
|
||||
target_ages: [10, 12],
|
||||
content_type: 'markdown',
|
||||
content_location: '',
|
||||
};
|
||||
|
||||
it('✅ Should return a filtered learning object when API provides data', async () => {
|
||||
vi.mocked(fetchWithLogging).mockResolvedValueOnce(mockMetadata);
|
||||
|
||||
const result = await getLearningObjectById(hruid, language);
|
||||
|
||||
expect(result).toEqual({
|
||||
key: hruid,
|
||||
_id: '123',
|
||||
uuid: 'uuid-123',
|
||||
version: 1,
|
||||
title: 'Test Object',
|
||||
htmlUrl: expect.stringContaining('/learningObject/getRaw?hruid=test-object&language=en'),
|
||||
language,
|
||||
difficulty: 5,
|
||||
estimatedTime: 120,
|
||||
available: true,
|
||||
teacherExclusive: false,
|
||||
educationalGoals: [{ source: 'source', id: 'id' }],
|
||||
keywords: ['robotics'],
|
||||
description: 'A test object',
|
||||
targetAges: [10, 12],
|
||||
contentType: 'markdown',
|
||||
contentLocation: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('⚠️ Should return null if API returns no metadata', async () => {
|
||||
vi.mocked(fetchWithLogging).mockResolvedValueOnce(null);
|
||||
const result = await getLearningObjectById(hruid, language);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLearningObjectsFromPath', () => {
|
||||
const hruid = 'test-path';
|
||||
const language = 'en';
|
||||
|
||||
it('✅ Should not give error or warning', async () => {
|
||||
const mockPathResponse: LearningPath[] = [
|
||||
{
|
||||
_id: 'path-1',
|
||||
hruid,
|
||||
language,
|
||||
title: 'Test Path',
|
||||
description: '',
|
||||
num_nodes: 1,
|
||||
num_nodes_left: 0,
|
||||
nodes: [],
|
||||
keywords: '',
|
||||
target_ages: [],
|
||||
min_age: 10,
|
||||
max_age: 12,
|
||||
__order: 1,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(fetchLearningPaths).mockResolvedValueOnce({
|
||||
success: true,
|
||||
source: 'Test Source',
|
||||
data: mockPathResponse,
|
||||
});
|
||||
|
||||
const result = await getLearningObjectsFromPath(hruid, language);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('⚠️ Should give a warning', async () => {
|
||||
vi.mocked(fetchLearningPaths).mockResolvedValueOnce({ success: false, source: 'Test Source', data: [] });
|
||||
|
||||
const result = await getLearningObjectsFromPath(hruid, language);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('❌ Should give an error', async () => {
|
||||
vi.mocked(fetchLearningPaths).mockRejectedValueOnce(new Error('API Error'));
|
||||
|
||||
const result = await getLearningObjectsFromPath(hruid, language);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
90
backend/tests/service/learning-paths.test.ts
Normal file
90
backend/tests/service/learning-paths.test.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fetchLearningPaths, searchLearningPaths } from '../../src/services/learningPaths';
|
||||
import { fetchWithLogging } from '../../src/util/apiHelper';
|
||||
import { LearningPathResponse } from '../../src/interfaces/learningPath';
|
||||
|
||||
// Mock the fetchWithLogging module using vi
|
||||
vi.mock('../../src/util/apiHelper', () => ({
|
||||
fetchWithLogging: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('fetchLearningPaths', () => {
|
||||
// Mock data and response
|
||||
const mockHruids = ['pn_werking', 'art1'];
|
||||
const language = 'en';
|
||||
const source = 'Test Source';
|
||||
const mockResponse = [{ title: 'Test Path', hruids: mockHruids }];
|
||||
|
||||
it('✅ Should return a successful response when HRUIDs are provided', async () => {
|
||||
// Mock the function to return mockResponse
|
||||
vi.mocked(fetchWithLogging).mockResolvedValue(mockResponse);
|
||||
|
||||
const result: LearningPathResponse = await fetchLearningPaths(mockHruids, language, source);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockResponse);
|
||||
expect(result.source).toBe(source);
|
||||
});
|
||||
|
||||
it('⚠️ Should return an error when no HRUIDs are provided', async () => {
|
||||
vi.mocked(fetchWithLogging).mockResolvedValue(mockResponse);
|
||||
|
||||
const result: LearningPathResponse = await fetchLearningPaths([], language, source);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.message).toBe(`No HRUIDs provided for ${source}.`);
|
||||
});
|
||||
|
||||
it('⚠️ Should return a failure response when no learning paths are found', async () => {
|
||||
// Mock fetchWithLogging to return an empty array
|
||||
vi.mocked(fetchWithLogging).mockResolvedValue([]);
|
||||
|
||||
const result: LearningPathResponse = await fetchLearningPaths(mockHruids, language, source);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.data).toEqual([]);
|
||||
expect(result.message).toBe(`No learning paths found for ${source}.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchLearningPaths', () => {
|
||||
const query =
|
||||
'https://dwengo.org/backend/api/learningPath/getPathsFromIdList?pathIdList=%7B%22hruids%22:%5B%22pn_werking%22,%22un_artificiele_intelligentie%22%5D%7D&language=nl';
|
||||
const language = 'nl';
|
||||
|
||||
it('✅ Should return search results when API responds with data', async () => {
|
||||
const mockResults = [
|
||||
{
|
||||
_id: '67b4488c9dadb305c4104618',
|
||||
language: 'nl',
|
||||
hruid: 'pn_werking',
|
||||
title: 'Werken met notebooks',
|
||||
description: 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?',
|
||||
num_nodes: 0,
|
||||
num_nodes_left: 0,
|
||||
nodes: [],
|
||||
keywords: 'Python KIKS Wiskunde STEM AI',
|
||||
target_ages: [14, 15, 16, 17, 18],
|
||||
min_age: 14,
|
||||
max_age: 18,
|
||||
__order: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock fetchWithLogging to return search results
|
||||
vi.mocked(fetchWithLogging).mockResolvedValue(mockResults);
|
||||
|
||||
const result = await searchLearningPaths(query, language);
|
||||
|
||||
expect(result).toEqual(mockResults);
|
||||
});
|
||||
|
||||
it('⚠️ Should return an empty array when API returns no results', async () => {
|
||||
vi.mocked(fetchWithLogging).mockResolvedValue([]);
|
||||
|
||||
const result = await searchLearningPaths(query, language);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue