Skip to content

Commit 1b50893

Browse files
authored
BC-10853 - Entfernen des h5p Inhaltstypen Twitter User Feed in der initialen Liste der Inhaltstypen (#6008)
1 parent ccc8a99 commit 1b50893

File tree

14 files changed

+305
-15
lines changed

14 files changed

+305
-15
lines changed

apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class H5PEditorController {
142142
@Query() query: AjaxGetQueryParams,
143143
@CurrentUser() currentUser: ICurrentUser
144144
): Promise<IHubInfo | ILibraryDetailedDataForClient | IAjaxResponse | undefined> {
145-
const response = this.h5pEditorUc.getAjax(query, currentUser.userId);
145+
const response = await this.h5pEditorUc.getAjax(query, currentUser.userId);
146146

147147
return response;
148148
}

apps/server/src/modules/h5p-editor/h5p-editor.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { AuthorizationClientConfig } from '@infra/authorization-client';
55
import { S3Config } from '@infra/s3-client';
66
import { LanguageType } from '@shared/domain/interface';
77
import { Algorithm } from 'jsonwebtoken';
8+
import { getLibraryWhiteList } from './helper';
89

910
export interface H5PEditorCoreConfig extends CoreModuleConfig, AuthorizationClientConfig {
1011
NEST_LOG_LEVEL: string;
1112
INCOMING_REQUEST_TIMEOUT: number;
1213
}
1314

14-
export interface H5PEditorConfig extends H5PEditorCoreConfig, JwtAuthGuardConfig {}
15+
export interface H5PEditorConfig extends H5PEditorCoreConfig, JwtAuthGuardConfig {
16+
H5P_EDITOR__LIBRARY_WHITE_LIST: string[];
17+
}
1518

1619
export const authorizationClientConfig: AuthorizationClientConfig = {
1720
basePath: `${Configuration.get('API_HOST') as string}/v3/`,
@@ -26,8 +29,12 @@ const h5pEditorCoreConfig: H5PEditorCoreConfig = {
2629

2730
// Lazy-load the full config to avoid eager evaluation of JWT values
2831
const getH5pEditorConfig = (): H5PEditorConfig => {
32+
const filePath = Configuration.get('H5P_EDITOR__LIBRARY_LIST_PATH') as string;
33+
const libraryWhiteList = getLibraryWhiteList(filePath);
34+
2935
return {
3036
...h5pEditorCoreConfig,
37+
H5P_EDITOR__LIBRARY_WHITE_LIST: libraryWhiteList,
3138
// Node's process.env escapes newlines. We need to reverse it for the keys to work.
3239
// See: https://stackoverflow.com/questions/30400341/environment-variables-containing-newlines-in-node
3340
JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'),
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { InternalServerErrorException } from '@nestjs/common';
2+
import * as fs from 'fs';
3+
import { getLibraryWhiteList, resetLibraryWhiteList } from './h5p-libraries.helper';
4+
5+
jest.mock('fs', (): unknown => {
6+
return {
7+
...jest.requireActual('fs'),
8+
readFileSync: jest.fn(),
9+
};
10+
});
11+
12+
describe('getLibraryWhiteList', () => {
13+
afterEach(() => {
14+
jest.resetAllMocks();
15+
resetLibraryWhiteList();
16+
});
17+
18+
describe('when filePath does exist', () => {
19+
describe('when file contains valid YAML content', () => {
20+
const setup = () => {
21+
const filePath = 'config/h5p-libraries.yaml';
22+
const validYamlContent = 'h5p_libraries:\r\n - H5P.Accordion\r\n';
23+
24+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(validYamlContent);
25+
26+
return { filePath, readFileSyncSpy };
27+
};
28+
29+
it('should return correct white list', () => {
30+
const { filePath, readFileSyncSpy } = setup();
31+
32+
const libraryWhiteList = getLibraryWhiteList(filePath);
33+
34+
expect(readFileSyncSpy).toHaveBeenCalledWith(filePath, { encoding: 'utf-8' });
35+
expect(libraryWhiteList).toEqual(['H5P.Accordion']);
36+
});
37+
});
38+
39+
describe('when file contains invalid YAML content', () => {
40+
const setup = () => {
41+
const filePath = 'config/h5p-libraries.yaml';
42+
const invalidYamlContent = 'No valid YAML content';
43+
44+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(invalidYamlContent);
45+
46+
return { filePath, readFileSyncSpy };
47+
};
48+
49+
it('should throw InternalServerErrorException', () => {
50+
const { filePath, readFileSyncSpy } = setup();
51+
52+
expect(() => getLibraryWhiteList(filePath)).toThrow(InternalServerErrorException);
53+
expect(readFileSyncSpy).toHaveBeenCalledWith(filePath, { encoding: 'utf-8' });
54+
});
55+
});
56+
});
57+
58+
describe('when filePath does not exist', () => {
59+
const setup = () => {
60+
const filePath = 'config/h5p-libraries.yaml';
61+
62+
const error = new Error('File not found');
63+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockImplementationOnce(() => {
64+
throw error;
65+
});
66+
67+
return { filePath, readFileSyncSpy };
68+
};
69+
70+
it('should throw an error', () => {
71+
const { filePath, readFileSyncSpy } = setup();
72+
73+
expect(() => getLibraryWhiteList(filePath)).toThrowError('File not found');
74+
expect(readFileSyncSpy).toHaveBeenCalledWith(filePath, { encoding: 'utf-8' });
75+
});
76+
});
77+
78+
describe('singleton functionality', () => {
79+
describe('when getLibraryWhiteList is called multiple times', () => {
80+
const setup = () => {
81+
const filePath = 'config/h5p-libraries.yaml';
82+
const validYamlContent = 'h5p_libraries:\r\n - H5P.Accordion\r\n - H5P.InteractiveVideo\r\n';
83+
84+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(validYamlContent);
85+
86+
return { filePath, readFileSyncSpy };
87+
};
88+
89+
it('should only read the file once and cache the result', () => {
90+
const { filePath, readFileSyncSpy } = setup();
91+
92+
const firstResult = getLibraryWhiteList(filePath);
93+
const secondResult = getLibraryWhiteList(filePath);
94+
const thirdResult = getLibraryWhiteList(filePath);
95+
96+
expect(readFileSyncSpy).toHaveBeenCalledTimes(1);
97+
expect(readFileSyncSpy).toHaveBeenCalledWith(filePath, { encoding: 'utf-8' });
98+
99+
expect(firstResult).toEqual(['H5P.Accordion', 'H5P.InteractiveVideo']);
100+
expect(secondResult).toEqual(['H5P.Accordion', 'H5P.InteractiveVideo']);
101+
expect(thirdResult).toEqual(['H5P.Accordion', 'H5P.InteractiveVideo']);
102+
103+
expect(firstResult).toBe(secondResult); // Should be the same reference
104+
expect(secondResult).toBe(thirdResult); // Should be the same reference
105+
});
106+
});
107+
108+
describe('when getLibraryWhiteList is called with different file paths', () => {
109+
const setup = () => {
110+
const filePath1 = 'config/h5p-libraries.yaml';
111+
const filePath2 = 'config/different-h5p-libraries.yaml';
112+
const validYamlContent = 'h5p_libraries:\r\n - H5P.Accordion\r\n';
113+
114+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(validYamlContent);
115+
116+
return { filePath1, filePath2, readFileSyncSpy };
117+
};
118+
119+
it('should still only read once and ignore subsequent different file paths', () => {
120+
const { filePath1, filePath2, readFileSyncSpy } = setup();
121+
122+
const firstResult = getLibraryWhiteList(filePath1);
123+
const secondResult = getLibraryWhiteList(filePath2); // Different path, but should still use cached value
124+
125+
expect(readFileSyncSpy).toHaveBeenCalledTimes(1);
126+
expect(readFileSyncSpy).toHaveBeenCalledWith(filePath1, { encoding: 'utf-8' });
127+
expect(readFileSyncSpy).not.toHaveBeenCalledWith(filePath2, { encoding: 'utf-8' });
128+
129+
expect(firstResult).toEqual(['H5P.Accordion']);
130+
expect(secondResult).toEqual(['H5P.Accordion']);
131+
expect(firstResult).toBe(secondResult); // Should be the same cached reference
132+
});
133+
});
134+
});
135+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { InternalServerErrorException } from '@nestjs/common';
2+
import { readFileSync } from 'fs';
3+
import { parse } from 'yaml';
4+
5+
interface LibrariesContentType {
6+
h5p_libraries: string[];
7+
}
8+
9+
let libraryWhiteList: string[] | null = null;
10+
11+
const readWhitelistFromConfig = (filePath: string): string[] => {
12+
const librariesYamlContent = readFileSync(filePath, { encoding: 'utf-8' });
13+
const librariesContentType = castToLibrariesContentType(parse(librariesYamlContent));
14+
const libraryWhiteList = librariesContentType.h5p_libraries;
15+
16+
return libraryWhiteList;
17+
};
18+
19+
const castToLibrariesContentType = (object: unknown): LibrariesContentType => {
20+
if (!isLibrariesContentType(object)) {
21+
throw new InternalServerErrorException('Invalid input type for castToLibrariesContentType');
22+
}
23+
24+
return object;
25+
};
26+
27+
const isLibrariesContentType = (object: unknown): object is LibrariesContentType => {
28+
const isType =
29+
typeof object === 'object' &&
30+
!Array.isArray(object) &&
31+
object !== null &&
32+
'h5p_libraries' in object &&
33+
Array.isArray(object.h5p_libraries);
34+
35+
return isType;
36+
};
37+
38+
export const getLibraryWhiteList = (filePath: string): string[] => {
39+
if (libraryWhiteList === null) {
40+
libraryWhiteList = readWhitelistFromConfig(filePath);
41+
}
42+
43+
return libraryWhiteList;
44+
};
45+
46+
export const resetLibraryWhiteList = (): void => {
47+
libraryWhiteList = null;
48+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './h5p-libraries.helper';

apps/server/src/modules/h5p-editor/uc/h5p-ajax.uc.spec.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { IHubInfo, IUser as LumiIUser } from '@lumieducation/h5p-server/build/sr
66
import { UserService } from '@modules/user';
77
import { userDoFactory } from '@modules/user/testing';
88
import { InternalServerErrorException } from '@nestjs/common';
9+
import { ConfigService } from '@nestjs/config';
910
import { Test, TestingModule } from '@nestjs/testing';
1011
import { LanguageType } from '@shared/domain/interface';
1112
import { currentUserFactory } from '@testing/factory/currentuser.factory';
1213
import * as fs from 'fs';
1314
import * as fsPromises from 'fs/promises';
15+
import { H5PEditorConfig } from '../h5p-editor.config';
1416
import { H5PContentRepo } from '../repo';
1517
import { LibraryStorage } from '../service';
1618
import { H5PUploadFile } from '../types';
@@ -42,6 +44,12 @@ describe(`${H5PEditorUc.name} Ajax`, () => {
4244
module = await Test.createTestingModule({
4345
providers: [
4446
H5PEditorUc,
47+
{
48+
provide: ConfigService,
49+
useValue: createMock<ConfigService<H5PEditorConfig, true>>({
50+
get: () => ['H5P.Accordion'],
51+
}),
52+
},
4553
{
4654
provide: H5PEditor,
4755
useValue: createMock<H5PEditor>(),
@@ -100,28 +108,68 @@ describe(`${H5PEditorUc.name} Ajax`, () => {
100108
const mockedResponse: IHubInfo = {
101109
apiVersion: { major: 1, minor: 1 },
102110
details: [],
103-
libraries: [],
111+
libraries: [
112+
{
113+
machineName: 'LibraryToBeFilteredOut',
114+
canInstall: false,
115+
installed: false,
116+
isUpToDate: false,
117+
localMajorVersion: 0,
118+
localMinorVersion: 0,
119+
localPatchVersion: 0,
120+
restricted: false,
121+
description: '',
122+
icon: '',
123+
majorVersion: 0,
124+
minorVersion: 0,
125+
owner: '',
126+
patchVersion: 0,
127+
title: '',
128+
},
129+
{
130+
machineName: 'H5P.Accordion',
131+
canInstall: false,
132+
installed: false,
133+
isUpToDate: false,
134+
localMajorVersion: 0,
135+
localMinorVersion: 0,
136+
localPatchVersion: 0,
137+
restricted: false,
138+
description: '',
139+
icon: '',
140+
majorVersion: 0,
141+
minorVersion: 0,
142+
owner: '',
143+
patchVersion: 0,
144+
title: '',
145+
},
146+
],
104147
outdated: false,
105148
recentlyUsed: [],
106149
user: 'DummyUser',
107150
};
108151

152+
const expectedResponse = { ...mockedResponse };
153+
expectedResponse.libraries = expectedResponse.libraries.filter(
154+
(library) => library.machineName === 'H5P.Accordion'
155+
);
156+
109157
ajaxEndpoint.getAjax.mockResolvedValueOnce(mockedResponse);
110158
userService.findById.mockResolvedValueOnce(userDo);
111159

112160
return {
113161
user,
114162
language,
115-
mockedResponse,
163+
expectedResponse,
116164
};
117165
};
118166

119-
it('should call H5PAjaxEndpoint.getAjax and return the result', async () => {
120-
const { user, language, mockedResponse } = setup();
167+
it('should call H5PAjaxEndpoint.getAjax, filter out unwanted library and return the result', async () => {
168+
const { user, language, expectedResponse } = setup();
121169

122170
const result = await uc.getAjax({ action: 'content-type-cache' }, user.userId);
123171

124-
expect(result).toBe(mockedResponse);
172+
expect(result).toStrictEqual(expectedResponse);
125173
expect(ajaxEndpoint.getAjax).toHaveBeenCalledWith(
126174
'content-type-cache',
127175
undefined, // MachineName

apps/server/src/modules/h5p-editor/uc/h5p-delete.uc.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
import { H5PEditor, H5PPlayer } from '@lumieducation/h5p-server';
1010
import { UserService } from '@modules/user';
1111
import { ForbiddenException, NotFoundException } from '@nestjs/common';
12+
import { ConfigService } from '@nestjs/config';
1213
import { Test, TestingModule } from '@nestjs/testing';
14+
import { H5PEditorConfig } from '../h5p-editor.config';
1315
import { H5PAjaxEndpointProvider } from '../provider';
1416
import { H5PContentRepo } from '../repo';
1517
import { LibraryStorage } from '../service';
@@ -42,6 +44,12 @@ describe('save or create H5P content', () => {
4244
module = await Test.createTestingModule({
4345
providers: [
4446
H5PEditorUc,
47+
{
48+
provide: ConfigService,
49+
useValue: createMock<ConfigService<H5PEditorConfig, true>>({
50+
get: () => ['H5P.Accordion'],
51+
}),
52+
},
4553
H5PAjaxEndpointProvider,
4654
{
4755
provide: H5PEditor,

apps/server/src/modules/h5p-editor/uc/h5p-files.uc.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import {
99
import { H5PAjaxEndpoint, H5PEditor, IPlayerModel } from '@lumieducation/h5p-server';
1010
import { UserService } from '@modules/user';
1111
import { ForbiddenException, NotFoundException } from '@nestjs/common';
12+
import { ConfigService } from '@nestjs/config';
1213
import { Test, TestingModule } from '@nestjs/testing';
1314
import { Request } from 'express';
1415
import { Readable } from 'stream';
16+
import { H5PEditorConfig } from '../h5p-editor.config';
1517
import { H5P_CACHE_PROVIDER_TOKEN, H5PEditorProvider, H5PPlayerProvider } from '../provider';
1618
import { H5PContentRepo } from '../repo';
1719
import { ContentStorage, LibraryStorage } from '../service';
@@ -59,6 +61,12 @@ describe('H5P Files', () => {
5961
module = await Test.createTestingModule({
6062
providers: [
6163
H5PEditorUc,
64+
{
65+
provide: ConfigService,
66+
useValue: createMock<ConfigService<H5PEditorConfig, true>>({
67+
get: () => ['H5P.Accordion'],
68+
}),
69+
},
6270
H5PEditorProvider,
6371
H5PPlayerProvider,
6472
{

apps/server/src/modules/h5p-editor/uc/h5p-get-editor.uc.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { H5PContentRepo } from '../repo';
1616
import { LibraryStorage } from '../service';
1717
import { h5pContentFactory } from '../testing';
1818
import { H5PEditorUc } from './h5p.uc';
19+
import { ConfigService } from '@nestjs/config';
20+
import { H5PEditorConfig } from '../h5p-editor.config';
1921

2022
const createParams = () => {
2123
const content = h5pContentFactory.build();
@@ -55,6 +57,12 @@ describe('get H5P editor', () => {
5557
module = await Test.createTestingModule({
5658
providers: [
5759
H5PEditorUc,
60+
{
61+
provide: ConfigService,
62+
useValue: createMock<ConfigService<H5PEditorConfig, true>>({
63+
get: () => ['H5P.Accordion'],
64+
}),
65+
},
5866
H5PAjaxEndpointProvider,
5967
{
6068
provide: H5PEditor,

0 commit comments

Comments
 (0)