From b67fd1e7a19e5a2aea5c43ef4c016bc81f9677e2 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 10 Nov 2023 14:00:44 +0100 Subject: [PATCH 01/12] initial implementation --- .../update-element-content.body.params.ts | 4 +- .../dto/meta-tag-extractor.response.ts | 18 ++++++- .../controller/post-link-url.body.params.ts | 4 +- .../interface/url-handler.ts | 6 +++ .../meta-tag-extractor.module.ts | 19 ++++++- .../service/meta-tag-extractor.service.ts | 20 ++++---- .../service/meta-tag-internal-url.service.ts | 50 +++++++++++++++++++ .../url-handler/abstract-url-handler.ts | 35 +++++++++++++ .../service/url-handler/board-url-handler.ts | 37 ++++++++++++++ .../service/url-handler/course-url-handler.ts | 29 +++++++++++ .../service/url-handler/index.ts | 4 ++ .../service/url-handler/lesson-url-handler.ts | 29 +++++++++++ .../service/url-handler/task-url-handler.ts | 29 +++++++++++ .../modules/meta-tag-extractor/types/index.ts | 1 + .../types/meta-data.type.ts | 13 +++++ .../uc/meta-tag-extractor.uc.ts | 3 +- 16 files changed, 285 insertions(+), 16 deletions(-) create mode 100644 apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/types/index.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 23ce88b904c..7d0314208c6 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger import { ContentElementType } from '@shared/domain'; import { InputFormat } from '@shared/domain/types'; import { Type } from 'class-transformer'; -import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; +import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator'; export abstract class ElementContentBody { @IsEnum(ContentElementType) @@ -34,7 +34,7 @@ export class FileElementContentBody extends ElementContentBody { } export class LinkContentBody { - @IsUrl() + @IsString() @ApiProperty({}) url!: string; diff --git a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts index a2f5acd8465..16863f0e16a 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.ts @@ -1,13 +1,17 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; import { IsString, IsUrl } from 'class-validator'; +import { MetaDataEntityType } from '../../types'; export class MetaTagExtractorResponse { - constructor({ url, title, description, imageUrl }: MetaTagExtractorResponse) { + constructor({ url, title, description, imageUrl, type, parentTitle, parentType }: MetaTagExtractorResponse) { this.url = url; this.title = title; this.description = description; this.imageUrl = imageUrl; + this.type = type; + this.parentTitle = parentTitle; + this.parentType = parentType; } @ApiProperty() @@ -25,4 +29,16 @@ export class MetaTagExtractorResponse { @ApiProperty() @IsString() imageUrl?: string; + + @ApiProperty() + @IsString() + type: MetaDataEntityType; + + @ApiProperty() + @DecodeHtmlEntities() + parentTitle?: string; + + @ApiProperty() + @IsString() + parentType?: MetaDataEntityType; } diff --git a/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts b/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts index 1e9cd1f7f34..ac6baeebbe8 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/post-link-url.body.params.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsUrl } from 'class-validator'; +import { IsString } from 'class-validator'; export class GetMetaTagDataBody { - @IsUrl() + @IsString() @ApiProperty({ required: true, nullable: false, diff --git a/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts b/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts new file mode 100644 index 00000000000..fc09d2cd40e --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/interface/url-handler.ts @@ -0,0 +1,6 @@ +import { MetaData } from '../types'; + +export interface UrlHandler { + doesUrlMatch(url: string): boolean; + getMetaData(url: string): Promise; +} diff --git a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts index 817d7257330..9350942e1e5 100644 --- a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts +++ b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor.module.ts @@ -5,20 +5,37 @@ import { ConsoleWriterModule } from '@shared/infra/console'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '../authentication/authentication.module'; +import { BoardModule } from '../board'; +import { LearnroomModule } from '../learnroom'; +import { LessonModule } from '../lesson'; +import { TaskModule } from '../task'; import { UserModule } from '../user'; import metaTagExtractorConfig from './meta-tag-extractor.config'; import { MetaTagExtractorService } from './service'; +import { MetaTagInternalUrlService } from './service/meta-tag-internal-url.service'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './service/url-handler'; @Module({ imports: [ AuthenticationModule, + BoardModule, ConsoleWriterModule, HttpModule, + LearnroomModule, + LessonModule, LoggerModule, + TaskModule, UserModule, ConfigModule.forRoot(createConfigModuleOptions(metaTagExtractorConfig)), ], - providers: [MetaTagExtractorService], + providers: [ + MetaTagExtractorService, + MetaTagInternalUrlService, + TaskUrlHandler, + LessonUrlHandler, + CourseUrlHandler, + BoardUrlHandler, + ], exports: [MetaTagExtractorService], }) export class MetaTagExtractorModule {} diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts index 46c30c17702..93b1cc3a465 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts @@ -2,24 +2,24 @@ import { Injectable } from '@nestjs/common'; import ogs from 'open-graph-scraper'; import { ImageObject } from 'open-graph-scraper/dist/lib/types'; import { basename } from 'path'; - -export type MetaData = { - title: string; - description: string; - url: string; - image?: ImageObject; -}; +import type { MetaData } from '../types'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; @Injectable() export class MetaTagExtractorService { + constructor(private readonly internalLinkMataTagService: MetaTagInternalUrlService) {} + async fetchMetaData(url: string): Promise { if (url.length === 0) { throw new Error(`MetaTagExtractorService requires a valid URL. Given URL: ${url}`); } - const metaData = (await this.tryExtractMetaTags(url)) ?? this.tryFilenameAsFallback(url); + const metaData = + (await this.internalLinkMataTagService.tryInternalLinkMetaTags(url)) ?? + (await this.tryExtractMetaTags(url)) ?? + this.tryFilenameAsFallback(url); - return metaData ?? { url, title: '', description: '' }; + return metaData ?? { url, title: '', description: '', type: 'unknown' }; } private async tryExtractMetaTags(url: string): Promise { @@ -35,6 +35,7 @@ export class MetaTagExtractorService { description, image, url, + type: 'external', }; } catch (error) { return undefined; @@ -49,6 +50,7 @@ export class MetaTagExtractorService { title, description: '', url, + type: 'unknown', }; } catch (error) { return undefined; diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts new file mode 100644 index 00000000000..c3bad97001c --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts @@ -0,0 +1,50 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Injectable } from '@nestjs/common'; +import type { UrlHandler } from '../interface/url-handler'; +import { MetaData } from '../types'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler'; + +@Injectable() +export class MetaTagInternalUrlService { + private handlers: UrlHandler[] = []; + + constructor( + private readonly taskUrlHandler: TaskUrlHandler, + private readonly lessonUrlHandler: LessonUrlHandler, + private readonly courseUrlHandler: CourseUrlHandler, + private readonly boardUrlHandler: BoardUrlHandler + ) { + this.handlers = [this.taskUrlHandler, this.lessonUrlHandler, this.courseUrlHandler, this.boardUrlHandler]; + } + + async tryInternalLinkMetaTags(url: string): Promise { + if (this.isInternalLink(url)) { + return this.composeMetaTags(url); + } + return Promise.resolve(undefined); + } + + isInternalLink(url: string) { + const domain = (Configuration.get('SC_DOMAIN') as string) ?? 'nothing-configured-for-internal-links.de'; + const isInternal = url.toLowerCase().includes(domain.toLowerCase()); + return isInternal; + } + + private async composeMetaTags(url: string): Promise { + const urlObject = new URL(url); + + const handler = this.handlers.find((h) => h.doesUrlMatch(url)); + if (handler) { + const result = await handler.getMetaData(url); + return result; + } + + const title = urlObject.pathname; + return Promise.resolve({ + title, + description: '', + url, + type: 'unknown', + }); + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts new file mode 100644 index 00000000000..ea6653ca326 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts @@ -0,0 +1,35 @@ +import { basename } from 'node:path'; +import { MetaData } from '../../types'; + +export abstract class AbstractUrlHandler { + protected abstract patterns: RegExp[]; + + protected extractId(url: string): string | undefined { + const results: RegExpMatchArray = this.patterns + .map((pattern: RegExp) => pattern.exec(url)) + .filter((result) => result !== null) + .find((result) => (result?.length ?? 0) >= 2) as RegExpMatchArray; + + if (results[1]) { + return results[1]; + } + return undefined; + } + + doesUrlMatch(url: string): boolean { + const doesMatch = this.patterns.some((pattern) => pattern.test(url)); + return doesMatch; + } + + getDefaultMetaData(url: string, partial: Partial = {}): MetaData { + const urlObject = new URL(url); + const title = basename(urlObject.pathname); + return { + title, + description: '', + url, + type: 'unknown', + ...partial, + }; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts new file mode 100644 index 00000000000..3052f65518b --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { BoardExternalReferenceType } from '@shared/domain'; +import { ColumnBoardService } from '@src/modules/board'; +import { CourseService } from '@src/modules/learnroom'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class BoardUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/rooms\/(.*?)\/board\/?$/i]; + + constructor(private readonly columnBoardService: ColumnBoardService, private readonly courseService: CourseService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'board' }); + + const columnBoard = await this.columnBoardService.findById(id); + if (columnBoard) { + metaData.title = columnBoard.title; + if (columnBoard.context.type === BoardExternalReferenceType.Course) { + const course = await this.courseService.findById(columnBoard.context.id); + metaData.parentType = 'course'; + metaData.parentTitle = course.name; + } + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts new file mode 100644 index 00000000000..08580b42bb9 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { CourseService } from '@src/modules/learnroom'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class CourseUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/rooms\/([0-9a-z]+)$/i]; + + constructor(private readonly courseService: CourseService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'course' }); + const course = await this.courseService.findById(id); + if (course) { + metaData.title = course.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts new file mode 100644 index 00000000000..a29b8401da2 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/index.ts @@ -0,0 +1,4 @@ +export * from './board-url-handler'; +export * from './course-url-handler'; +export * from './lesson-url-handler'; +export * from './task-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts new file mode 100644 index 00000000000..6ef2689a1cd --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { LessonService } from '@src/modules/lesson/service'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class LessonUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/topics\/([0-9a-z]+)$/i]; + + constructor(private readonly lessonService: LessonService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'lesson' }); + const lesson = await this.lessonService.findById(id); + if (lesson) { + metaData.title = lesson.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts new file mode 100644 index 00000000000..d16eba9f73e --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { TaskService } from '@src/modules/task/service'; +import type { UrlHandler } from '../../interface/url-handler'; +import { MetaData } from '../../types'; +import { AbstractUrlHandler } from './abstract-url-handler'; + +@Injectable() +export class TaskUrlHandler extends AbstractUrlHandler implements UrlHandler { + patterns: RegExp[] = [/\/homework\/([0-9a-z]+)$/i]; + + constructor(private readonly taskService: TaskService) { + super(); + } + + async getMetaData(url: string): Promise { + const id = this.extractId(url); + if (id === undefined) { + return undefined; + } + + const metaData = this.getDefaultMetaData(url, { type: 'task' }); + const task = await this.taskService.findById(id); + if (task) { + metaData.title = task.name; + } + + return metaData; + } +} diff --git a/apps/server/src/modules/meta-tag-extractor/types/index.ts b/apps/server/src/modules/meta-tag-extractor/types/index.ts new file mode 100644 index 00000000000..776e417867e --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/types/index.ts @@ -0,0 +1 @@ +export * from './meta-data.type'; diff --git a/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts b/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts new file mode 100644 index 00000000000..b4da460d6e9 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/types/meta-data.type.ts @@ -0,0 +1,13 @@ +import { ImageObject } from 'open-graph-scraper/dist/lib/types'; + +export type MetaDataEntityType = 'external' | 'course' | 'board' | 'task' | 'lesson' | 'unknown'; + +export type MetaData = { + title: string; + description: string; + url: string; + image?: ImageObject; + type: MetaDataEntityType; + parentTitle?: string; + parentType?: MetaDataEntityType; +}; diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts index 5daca6c962d..45c1e1dadb2 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts @@ -1,7 +1,8 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { AuthorizationService } from '@src/modules/authorization'; -import { MetaData, MetaTagExtractorService } from '../service'; +import { MetaTagExtractorService } from '../service'; +import { MetaData } from '../types'; @Injectable() export class MetaTagExtractorUc { From a7903827b3c8eab189e7153f1d01a26bb9f77d2d Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 14:03:42 +0100 Subject: [PATCH 02/12] chore: improve tests for meta tag extractor service --- .../meta-tag-extractor-get-data.api.spec.ts | 4 +- .../dto/meta-tag-extractor.response.spec.ts | 16 +++-- .../meta-tag-extractor.controller.ts | 2 +- .../meta-tag-extractor.service.spec.ts | 69 ++++++++++++------- .../service/meta-tag-extractor.service.ts | 2 + .../service/meta-tag-internal-url.service.ts | 7 +- 6 files changed, 65 insertions(+), 35 deletions(-) diff --git a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts index c80d47df66d..5a6bdc062f8 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { MetaTagExtractorService } from '../../service'; -const URL = 'https://test.de'; +const URL = 'https://best-example.de/my-article'; const mockedResponse = { url: URL, @@ -13,7 +13,7 @@ const mockedResponse = { description: 'with great description', }; -describe(`get data (api)`, () => { +describe(`get meta tags (api)`, () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; diff --git a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts index 29dfbd94c72..0f527d1139d 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/dto/meta-tag-extractor.response.spec.ts @@ -8,13 +8,19 @@ describe(MetaTagExtractorResponse.name, () => { title: 'Testbild', description: 'Here we describe what this page is about.', imageUrl: 'https://www.abc.de/test.png', + type: 'unknown', + parentTitle: 'Math', + parentType: 'course', }; - const errorResponse = new MetaTagExtractorResponse(properties); - expect(errorResponse.url).toEqual(properties.url); - expect(errorResponse.title).toEqual(properties.title); - expect(errorResponse.description).toEqual(properties.description); - expect(errorResponse.imageUrl).toEqual(properties.imageUrl); + const response = new MetaTagExtractorResponse(properties); + expect(response.url).toEqual(properties.url); + expect(response.title).toEqual(properties.title); + expect(response.description).toEqual(properties.description); + expect(response.imageUrl).toEqual(properties.imageUrl); + expect(response.type).toEqual(properties.type); + expect(response.parentTitle).toEqual(properties.parentTitle); + expect(response.parentType).toEqual(properties.parentType); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts index 8133c4c0b83..a141444f8ee 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts @@ -17,7 +17,7 @@ export class MetaTagExtractorController { @ApiResponse({ status: 401, type: UnauthorizedException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) @Post('') - async getData( + async getMetaTags( @CurrentUser() currentUser: ICurrentUser, @Body() bodyParams: GetMetaTagDataBody ): Promise { diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts index af1a256d121..8a76f96a56d 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts @@ -1,35 +1,52 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; +import ogs from 'open-graph-scraper'; import { ImageObject } from 'open-graph-scraper/dist/lib/types'; import { MetaTagExtractorService } from './meta-tag-extractor.service'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; -let ogsResponseMock = {}; -let ogsRejectMock: Error | undefined; - -jest.mock('open-graph-scraper', () => () => { - if (ogsRejectMock) { - return Promise.reject(ogsRejectMock); - } +jest.mock('open-graph-scraper', () => { + return { + __esModule: true, + default: jest.fn(), + }; +}); - return Promise.resolve({ +const mockOgsResolve = (result: Record) => { + const mockedOgs = ogs as jest.Mock; + mockedOgs.mockResolvedValueOnce({ error: false, html: '', response: {}, - result: ogsResponseMock, + result, }); -}); +}; + +const mockOgsReject = (error: Error) => { + const mockedOgs = ogs as jest.Mock; + mockedOgs.mockRejectedValueOnce(error); +}; describe(MetaTagExtractorService.name, () => { let module: TestingModule; + let metaTagInternalUrlService: DeepMocked; let service: MetaTagExtractorService; beforeAll(async () => { module = await Test.createTestingModule({ - providers: [MetaTagExtractorService], + providers: [ + MetaTagExtractorService, + { + provide: MetaTagInternalUrlService, + useValue: createMock(), + }, + ], }).compile(); + metaTagInternalUrlService = module.get(MetaTagInternalUrlService); service = module.get(MetaTagExtractorService); - await setupEntities(); }); @@ -38,8 +55,8 @@ describe(MetaTagExtractorService.name, () => { }); beforeEach(() => { - ogsResponseMock = {}; - ogsRejectMock = undefined; + Configuration.set('SC_DOMAIN', 'localhost'); + metaTagInternalUrlService.tryInternalLinkMetaTags.mockResolvedValue(undefined); }); afterEach(() => { @@ -48,24 +65,26 @@ describe(MetaTagExtractorService.name, () => { describe('create', () => { describe('when url points to webpage', () => { + it('should thrown an error if url is an empty string', async () => { + const url = ''; + + await expect(service.fetchMetaData(url)).rejects.toThrow(); + }); + it('should return also the original url', async () => { + const ogTitle = 'My Title'; const url = 'https://de.wikipedia.org'; + mockOgsResolve({ url, ogTitle }); const result = await service.fetchMetaData(url); expect(result).toEqual(expect.objectContaining({ url })); }); - it('should thrown an error if url is an empty string', async () => { - const url = ''; - - await expect(service.fetchMetaData(url)).rejects.toThrow(); - }); - it('should return ogTitle as title', async () => { const ogTitle = 'My Title'; const url = 'https://de.wikipedia.org'; - ogsResponseMock = { ogTitle }; + mockOgsResolve({ ogTitle }); const result = await service.fetchMetaData(url); @@ -91,7 +110,7 @@ describe(MetaTagExtractorService.name, () => { }, ]; const url = 'https://de.wikipedia.org'; - ogsResponseMock = { ogImage }; + mockOgsResolve({ url, ogImage }); const result = await service.fetchMetaData(url); @@ -102,7 +121,8 @@ describe(MetaTagExtractorService.name, () => { describe('when url points to a file', () => { it('should return filename as title', async () => { const url = 'https://de.wikipedia.org/abc.jpg'; - ogsRejectMock = new Error('no open graph data included... probably not a webpage'); + + mockOgsReject(new Error('no open graph data included... probably not a webpage')); const result = await service.fetchMetaData(url); expect(result).toEqual(expect.objectContaining({ title: 'abc.jpg' })); @@ -112,7 +132,8 @@ describe(MetaTagExtractorService.name, () => { describe('when url is invalid', () => { it('should return url as it is', async () => { const url = 'not-a-real-domain'; - ogsRejectMock = new Error('no open graph data included... probably not a webpage'); + + mockOgsReject(new Error('no open graph data included... probably not a webpage')); const result = await service.fetchMetaData(url); expect(result).toEqual(expect.objectContaining({ url, title: '', description: '' })); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts index 93b1cc3a465..3db58412165 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts @@ -9,11 +9,13 @@ import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; export class MetaTagExtractorService { constructor(private readonly internalLinkMataTagService: MetaTagInternalUrlService) {} + // WIP: fetch => get async fetchMetaData(url: string): Promise { if (url.length === 0) { throw new Error(`MetaTagExtractorService requires a valid URL. Given URL: ${url}`); } + // WIP: make that nicer const metaData = (await this.internalLinkMataTagService.tryInternalLinkMetaTags(url)) ?? (await this.tryExtractMetaTags(url)) ?? diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts index c3bad97001c..685029de4e4 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts @@ -18,14 +18,15 @@ export class MetaTagInternalUrlService { } async tryInternalLinkMetaTags(url: string): Promise { - if (this.isInternalLink(url)) { + if (this.isInternalUrl(url)) { return this.composeMetaTags(url); } return Promise.resolve(undefined); } - isInternalLink(url: string) { - const domain = (Configuration.get('SC_DOMAIN') as string) ?? 'nothing-configured-for-internal-links.de'; + isInternalUrl(url: string) { + let domain = Configuration.get('SC_DOMAIN') as string; + domain = domain !== '' ? 'nothing-configured-for-internal-url.de' : domain; const isInternal = url.toLowerCase().includes(domain.toLowerCase()); return isInternal; } From 70b57c32bd128413296bd372c3e9bbd212b38606 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 15:12:47 +0100 Subject: [PATCH 03/12] chore: add tests for MetaTagExtractorService --- .../meta-tag-internal-url.service.spec.ts | 124 ++++++++++++++++++ .../service/meta-tag-internal-url.service.ts | 2 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts new file mode 100644 index 00000000000..24f7ee81171 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts @@ -0,0 +1,124 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { MetaData } from '../types'; +import { MetaTagExtractorService } from './meta-tag-extractor.service'; +import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; +import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler'; + +describe(MetaTagExtractorService.name, () => { + let module: TestingModule; + let taskUrlHandler: DeepMocked; + let lessonUrlHandler: DeepMocked; + let courseUrlHandler: DeepMocked; + let boardUrlHandler: DeepMocked; + let service: MetaTagInternalUrlService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + MetaTagInternalUrlService, + { + provide: TaskUrlHandler, + useValue: createMock(), + }, + { + provide: LessonUrlHandler, + useValue: createMock(), + }, + { + provide: CourseUrlHandler, + useValue: createMock(), + }, + { + provide: BoardUrlHandler, + useValue: createMock(), + }, + ], + }).compile(); + + taskUrlHandler = module.get(TaskUrlHandler); + lessonUrlHandler = module.get(LessonUrlHandler); + courseUrlHandler = module.get(CourseUrlHandler); + boardUrlHandler = module.get(BoardUrlHandler); + service = module.get(MetaTagInternalUrlService); + await setupEntities(); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => {}); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('isInternalUrl', () => { + const setup = () => { + Configuration.set('SC_DOMAIN', 'localhost'); + taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + boardUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + }; + + it('should return true for internal urls', () => { + setup(); + + const INTERNAL_URL = 'https://localhost/my-article'; + expect(service.isInternalUrl(INTERNAL_URL)).toBe(true); + }); + + it('should return false for external urls', () => { + setup(); + + const EXTERNAL_URL = 'https://de.wikipedia.org/example-article'; + expect(service.isInternalUrl(EXTERNAL_URL)).toBe(false); + }); + }); + + describe('tryInternalLinkMetaTags', () => { + const setup = () => { + Configuration.set('SC_DOMAIN', 'localhost'); + taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + boardUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + const url = 'https://localhost/fitting-url'; + const mockedMetaTags: MetaData = { + title: 'My Title', + url, + description: '', + type: 'course', + }; + + return { mockedMetaTags, url }; + }; + + describe('when url matches to a handler', () => { + it('should return the handlers meta tags', async () => { + const { mockedMetaTags, url } = setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(true); + courseUrlHandler.getMetaData.mockResolvedValueOnce(mockedMetaTags); + + const result = await service.tryInternalLinkMetaTags(url); + + expect(result).toEqual(mockedMetaTags); + }); + }); + + describe('when url matches to none of the handlers', () => { + it('should return default meta tags', async () => { + setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + + const internalUrl = 'https://localhost/playground/23hafe23234'; + const result = await service.tryInternalLinkMetaTags(internalUrl); + + expect(result).toEqual(expect.objectContaining({ type: 'unknown' })); + }); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts index 685029de4e4..5c0d5efca5c 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.ts @@ -26,7 +26,7 @@ export class MetaTagInternalUrlService { isInternalUrl(url: string) { let domain = Configuration.get('SC_DOMAIN') as string; - domain = domain !== '' ? 'nothing-configured-for-internal-url.de' : domain; + domain = domain === '' ? 'nothing-configured-for-internal-url.de' : domain; const isInternal = url.toLowerCase().includes(domain.toLowerCase()); return isInternal; } From dd12fc49e3f1b2b74875ba25bee64587c5d1606e Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 16:00:11 +0100 Subject: [PATCH 04/12] chore: fix test --- .../controller/api-test/meta-tag-extractor-get-data.api.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts index 5a6bdc062f8..8392f5f106a 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts @@ -63,7 +63,7 @@ describe(`get meta tags (api)`, () => { const response = await loggedInClient.post(undefined, { url: URL }); - expect(response?.body).toEqual(mockedResponse); + expect(response?.body).toEqual(expect.objectContaining(mockedResponse)); }); }); From 0fc1b5c64a5005e2850a7bf7e8941a53ca1847a8 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 16:45:45 +0100 Subject: [PATCH 05/12] chore: fix test --- .../meta-tag-internal-url.service.spec.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts index 24f7ee81171..04d2f8b0c77 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-internal-url.service.spec.ts @@ -7,6 +7,11 @@ import { MetaTagExtractorService } from './meta-tag-extractor.service'; import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; import { BoardUrlHandler, CourseUrlHandler, LessonUrlHandler, TaskUrlHandler } from './url-handler'; +const INTERNAL_DOMAIN = 'my-school-cloud.org'; +const INTERNAL_URL = `https://${INTERNAL_DOMAIN}/my-article`; +const UNKNOWN_INTERNAL_URL = `https://${INTERNAL_DOMAIN}/playground/23hafe23234`; +const EXTERNAL_URL = 'https://de.wikipedia.org/example-article'; + describe(MetaTagExtractorService.name, () => { let module: TestingModule; let taskUrlHandler: DeepMocked; @@ -58,7 +63,7 @@ describe(MetaTagExtractorService.name, () => { describe('isInternalUrl', () => { const setup = () => { - Configuration.set('SC_DOMAIN', 'localhost'); + Configuration.set('SC_DOMAIN', INTERNAL_DOMAIN); taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); @@ -68,42 +73,39 @@ describe(MetaTagExtractorService.name, () => { it('should return true for internal urls', () => { setup(); - const INTERNAL_URL = 'https://localhost/my-article'; expect(service.isInternalUrl(INTERNAL_URL)).toBe(true); }); it('should return false for external urls', () => { setup(); - const EXTERNAL_URL = 'https://de.wikipedia.org/example-article'; expect(service.isInternalUrl(EXTERNAL_URL)).toBe(false); }); }); describe('tryInternalLinkMetaTags', () => { const setup = () => { - Configuration.set('SC_DOMAIN', 'localhost'); + Configuration.set('SC_DOMAIN', INTERNAL_DOMAIN); taskUrlHandler.doesUrlMatch.mockReturnValueOnce(false); lessonUrlHandler.doesUrlMatch.mockReturnValueOnce(false); boardUrlHandler.doesUrlMatch.mockReturnValueOnce(false); - const url = 'https://localhost/fitting-url'; const mockedMetaTags: MetaData = { title: 'My Title', - url, + url: INTERNAL_URL, description: '', type: 'course', }; - return { mockedMetaTags, url }; + return { mockedMetaTags }; }; describe('when url matches to a handler', () => { it('should return the handlers meta tags', async () => { - const { mockedMetaTags, url } = setup(); + const { mockedMetaTags } = setup(); courseUrlHandler.doesUrlMatch.mockReturnValueOnce(true); courseUrlHandler.getMetaData.mockResolvedValueOnce(mockedMetaTags); - const result = await service.tryInternalLinkMetaTags(url); + const result = await service.tryInternalLinkMetaTags(INTERNAL_URL); expect(result).toEqual(mockedMetaTags); }); @@ -114,11 +116,21 @@ describe(MetaTagExtractorService.name, () => { setup(); courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); - const internalUrl = 'https://localhost/playground/23hafe23234'; - const result = await service.tryInternalLinkMetaTags(internalUrl); + const result = await service.tryInternalLinkMetaTags(UNKNOWN_INTERNAL_URL); expect(result).toEqual(expect.objectContaining({ type: 'unknown' })); }); }); + + describe('when url is external', () => { + it('should return undefined', async () => { + setup(); + courseUrlHandler.doesUrlMatch.mockReturnValueOnce(false); + + const result = await service.tryInternalLinkMetaTags(EXTERNAL_URL); + + expect(result).toBeUndefined(); + }); + }); }); }); From 85ee95863b2e9e92e87b4ae3e0e7bebab667ac74 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 17:46:47 +0100 Subject: [PATCH 06/12] chore: fix url handler tests --- .../url-handler/board-url-handler.spec.ts | 58 +++++++++++++++++++ .../url-handler/course-url-handler.spec.ts | 50 ++++++++++++++++ .../url-handler/lesson-url-handler.spec.ts | 50 ++++++++++++++++ .../url-handler/task-url-handler.spec.ts | 50 ++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts new file mode 100644 index 00000000000..d46ee716a5f --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -0,0 +1,58 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ColumnBoard } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { ColumnBoardService } from '@src/modules/board'; +import { CourseService } from '@src/modules/learnroom'; +import { BoardUrlHandler } from './board-url-handler'; + +describe(BoardUrlHandler.name, () => { + let module: TestingModule; + let columnBoardService: DeepMocked; + let boardUrlHandler: BoardUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardUrlHandler, + { + provide: ColumnBoardService, + useValue: createMock(), + }, + { + provide: CourseService, + useValue: createMock(), + }, + ], + }).compile(); + + columnBoardService = module.get(ColumnBoardService); + boardUrlHandler = module.get(BoardUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/rooms/${id}/board`; + + await boardUrlHandler.getMetaData(url); + + expect(columnBoardService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the board name', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/rooms/${id}/board`; + const boardName = 'My Board'; + columnBoardService.findById.mockResolvedValue({ + title: boardName, + context: { type: 'course', id: 'a-board-id' }, + } as ColumnBoard); + + const result = await boardUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: boardName, type: 'board' })); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts new file mode 100644 index 00000000000..863f448bd93 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts @@ -0,0 +1,50 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { CourseService } from '@src/modules/learnroom'; +import { CourseUrlHandler } from './course-url-handler'; + +describe(CourseUrlHandler.name, () => { + let module: TestingModule; + let courseService: DeepMocked; + let courseUrlHandler: CourseUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + CourseUrlHandler, + { + provide: CourseService, + useValue: createMock(), + }, + ], + }).compile(); + + courseService = module.get(CourseService); + courseUrlHandler = module.get(CourseUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/rooms/${id}`; + + await courseUrlHandler.getMetaData(url); + + expect(courseService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the course name', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/rooms/${id}`; + const courseName = 'My Course'; + courseService.findById.mockResolvedValue({ name: courseName } as Course); + + const result = await courseUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: courseName, type: 'course' })); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts new file mode 100644 index 00000000000..32f210b5736 --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts @@ -0,0 +1,50 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LessonEntity } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { LessonService } from '@src/modules/lesson/service'; +import { LessonUrlHandler } from './lesson-url-handler'; + +describe(LessonUrlHandler.name, () => { + let module: TestingModule; + let lessonService: DeepMocked; + let lessonUrlHandler: LessonUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LessonUrlHandler, + { + provide: LessonService, + useValue: createMock(), + }, + ], + }).compile(); + + lessonService = module.get(LessonService); + lessonUrlHandler = module.get(LessonUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + it('should call lessonService with the correct id', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/topics/${id}`; + + await lessonUrlHandler.getMetaData(url); + + expect(lessonService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the lessons name', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/topics/${id}`; + const lessonName = 'My lesson'; + lessonService.findById.mockResolvedValue({ name: lessonName } as LessonEntity); + + const result = await lessonUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: lessonName, type: 'lesson' })); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts new file mode 100644 index 00000000000..54992e0cb1c --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts @@ -0,0 +1,50 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Task } from '@shared/domain'; +import { setupEntities } from '@shared/testing'; +import { TaskService } from '@src/modules/task/service'; +import { TaskUrlHandler } from './task-url-handler'; + +describe(TaskUrlHandler.name, () => { + let module: TestingModule; + let taskService: DeepMocked; + let taskUrlHandler: TaskUrlHandler; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TaskUrlHandler, + { + provide: TaskService, + useValue: createMock(), + }, + ], + }).compile(); + + taskService = module.get(TaskService); + taskUrlHandler = module.get(TaskUrlHandler); + await setupEntities(); + }); + + describe('getMetaData', () => { + it('should call taskService with the correct id', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/homework/${id}`; + + await taskUrlHandler.getMetaData(url); + + expect(taskService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the tasks name', async () => { + const id = 'af322312feae'; + const url = `htttps://localhost/homework/${id}`; + const taskName = 'My Task'; + taskService.findById.mockResolvedValue({ name: taskName } as Task); + + const result = await taskUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: taskName, type: 'task' })); + }); + }); +}); From 4e5a1bde01db419bbe69c7fbd9bbae193dffbbe5 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 13 Nov 2023 18:24:14 +0100 Subject: [PATCH 07/12] chore: extend tests --- .../url-handler/abstract-url-handler.spec.ts | 43 +++++++++++++++++++ .../url-handler/board-url-handler.spec.ts | 4 +- .../url-handler/course-url-handler.spec.ts | 4 +- .../url-handler/lesson-url-handler.spec.ts | 4 +- .../url-handler/task-url-handler.spec.ts | 4 +- 5 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts new file mode 100644 index 00000000000..ced7b287d6c --- /dev/null +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts @@ -0,0 +1,43 @@ +import { AbstractUrlHandler } from './abstract-url-handler'; + +class DummyHandler extends AbstractUrlHandler { + patterns: RegExp[] = [/\/dummy\/([0-9a-z]+)$/i]; +} + +describe(AbstractUrlHandler.name, () => { + const setup = () => { + const id = 'af322312feae'; + const url = `https://localhost/dummy/${id}`; + const invalidUrl = `https://localhost/wrong/${id}`; + const handler = new DummyHandler(); + return { id, url, invalidUrl, handler }; + }; + + describe('doesUrlMatch', () => { + it('should be true for valid urls', () => { + const { url, handler } = setup(); + + const result = handler.doesUrlMatch(url); + + expect(result).toBe(true); + }); + + it('should be false for invalid urls', () => { + const { invalidUrl, handler } = setup(); + + const result = handler.doesUrlMatch(invalidUrl); + + expect(result).toBe(false); + }); + }); + + describe('getDefaultMetaData', () => { + it('should return meta data of type unknown', () => { + const { url, handler } = setup(); + + const result = handler.getDefaultMetaData(url); + + expect(result).toEqual(expect.objectContaining({ type: 'unknown', url })); + }); + }); +}); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts index d46ee716a5f..bf3d8f8a934 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -34,7 +34,7 @@ describe(BoardUrlHandler.name, () => { describe('getMetaData', () => { it('should call courseService with the correct id', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/rooms/${id}/board`; + const url = `https://localhost/rooms/${id}/board`; await boardUrlHandler.getMetaData(url); @@ -43,7 +43,7 @@ describe(BoardUrlHandler.name, () => { it('should take the title from the board name', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/rooms/${id}/board`; + const url = `https://localhost/rooms/${id}/board`; const boardName = 'My Board'; columnBoardService.findById.mockResolvedValue({ title: boardName, diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts index 863f448bd93..c9d89c3c430 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts @@ -29,7 +29,7 @@ describe(CourseUrlHandler.name, () => { describe('getMetaData', () => { it('should call courseService with the correct id', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/rooms/${id}`; + const url = `https://localhost/rooms/${id}`; await courseUrlHandler.getMetaData(url); @@ -38,7 +38,7 @@ describe(CourseUrlHandler.name, () => { it('should take the title from the course name', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/rooms/${id}`; + const url = `https://localhost/rooms/${id}`; const courseName = 'My Course'; courseService.findById.mockResolvedValue({ name: courseName } as Course); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts index 32f210b5736..93a06c8119c 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts @@ -29,7 +29,7 @@ describe(LessonUrlHandler.name, () => { describe('getMetaData', () => { it('should call lessonService with the correct id', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/topics/${id}`; + const url = `https://localhost/topics/${id}`; await lessonUrlHandler.getMetaData(url); @@ -38,7 +38,7 @@ describe(LessonUrlHandler.name, () => { it('should take the title from the lessons name', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/topics/${id}`; + const url = `https://localhost/topics/${id}`; const lessonName = 'My lesson'; lessonService.findById.mockResolvedValue({ name: lessonName } as LessonEntity); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts index 54992e0cb1c..d4d986a964d 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts @@ -29,7 +29,7 @@ describe(TaskUrlHandler.name, () => { describe('getMetaData', () => { it('should call taskService with the correct id', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/homework/${id}`; + const url = `https://localhost/homework/${id}`; await taskUrlHandler.getMetaData(url); @@ -38,7 +38,7 @@ describe(TaskUrlHandler.name, () => { it('should take the title from the tasks name', async () => { const id = 'af322312feae'; - const url = `htttps://localhost/homework/${id}`; + const url = `https://localhost/homework/${id}`; const taskName = 'My Task'; taskService.findById.mockResolvedValue({ name: taskName } as Task); From 494024491acd978e6c8873bfa4270e1402f8b000 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 16 Nov 2023 12:37:18 +0100 Subject: [PATCH 08/12] add test for extractid --- .../url-handler/abstract-url-handler.spec.ts | 16 ++++++++++++++++ .../service/url-handler/abstract-url-handler.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts index ced7b287d6c..b6900cfd492 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.spec.ts @@ -2,6 +2,10 @@ import { AbstractUrlHandler } from './abstract-url-handler'; class DummyHandler extends AbstractUrlHandler { patterns: RegExp[] = [/\/dummy\/([0-9a-z]+)$/i]; + + extractId(url: string): string | undefined { + return super.extractId(url); + } } describe(AbstractUrlHandler.name, () => { @@ -13,6 +17,18 @@ describe(AbstractUrlHandler.name, () => { return { id, url, invalidUrl, handler }; }; + describe('extractId', () => { + describe('when no id was extracted', () => { + it('should return undefined', () => { + const { invalidUrl, handler } = setup(); + + const result = handler.extractId(invalidUrl); + + expect(result).toBeUndefined(); + }); + }); + }); + describe('doesUrlMatch', () => { it('should be true for valid urls', () => { const { url, handler } = setup(); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts index ea6653ca326..fb618c3bf36 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/abstract-url-handler.ts @@ -10,7 +10,7 @@ export abstract class AbstractUrlHandler { .filter((result) => result !== null) .find((result) => (result?.length ?? 0) >= 2) as RegExpMatchArray; - if (results[1]) { + if (results && results[1]) { return results[1]; } return undefined; From 50c688894e63832e9a40ad4a950c2c0007b4afd6 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 16 Nov 2023 12:43:13 +0100 Subject: [PATCH 09/12] chore: extend handler tests --- .../url-handler/board-url-handler.spec.ts | 42 ++++++++++++------- .../url-handler/course-url-handler.spec.ts | 36 ++++++++++------ .../url-handler/lesson-url-handler.spec.ts | 36 ++++++++++------ .../url-handler/task-url-handler.spec.ts | 36 ++++++++++------ 4 files changed, 99 insertions(+), 51 deletions(-) diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts index bf3d8f8a934..eb8c1ed7766 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -32,27 +32,39 @@ describe(BoardUrlHandler.name, () => { }); describe('getMetaData', () => { - it('should call courseService with the correct id', async () => { - const id = 'af322312feae'; - const url = `https://localhost/rooms/${id}/board`; + describe('when url fits', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}/board`; - await boardUrlHandler.getMetaData(url); + await boardUrlHandler.getMetaData(url); - expect(columnBoardService.findById).toHaveBeenCalledWith(id); + expect(columnBoardService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the board name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}/board`; + const boardName = 'My Board'; + columnBoardService.findById.mockResolvedValue({ + title: boardName, + context: { type: 'course', id: 'a-board-id' }, + } as ColumnBoard); + + const result = await boardUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: boardName, type: 'board' })); + }); }); - it('should take the title from the board name', async () => { - const id = 'af322312feae'; - const url = `https://localhost/rooms/${id}/board`; - const boardName = 'My Board'; - columnBoardService.findById.mockResolvedValue({ - title: boardName, - context: { type: 'course', id: 'a-board-id' }, - } as ColumnBoard); + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; - const result = await boardUrlHandler.getMetaData(url); + const result = await boardUrlHandler.getMetaData(url); - expect(result).toEqual(expect.objectContaining({ title: boardName, type: 'board' })); + expect(result).toBeUndefined(); + }); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts index c9d89c3c430..631a8287d09 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts @@ -27,24 +27,36 @@ describe(CourseUrlHandler.name, () => { }); describe('getMetaData', () => { - it('should call courseService with the correct id', async () => { - const id = 'af322312feae'; - const url = `https://localhost/rooms/${id}`; + describe('when url fits', () => { + it('should call courseService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}`; - await courseUrlHandler.getMetaData(url); + await courseUrlHandler.getMetaData(url); - expect(courseService.findById).toHaveBeenCalledWith(id); + expect(courseService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the course name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/rooms/${id}`; + const courseName = 'My Course'; + courseService.findById.mockResolvedValue({ name: courseName } as Course); + + const result = await courseUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: courseName, type: 'course' })); + }); }); - it('should take the title from the course name', async () => { - const id = 'af322312feae'; - const url = `https://localhost/rooms/${id}`; - const courseName = 'My Course'; - courseService.findById.mockResolvedValue({ name: courseName } as Course); + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; - const result = await courseUrlHandler.getMetaData(url); + const result = await courseUrlHandler.getMetaData(url); - expect(result).toEqual(expect.objectContaining({ title: courseName, type: 'course' })); + expect(result).toBeUndefined(); + }); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts index 93a06c8119c..3e2968e058d 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts @@ -27,24 +27,36 @@ describe(LessonUrlHandler.name, () => { }); describe('getMetaData', () => { - it('should call lessonService with the correct id', async () => { - const id = 'af322312feae'; - const url = `https://localhost/topics/${id}`; + describe('when url fits', () => { + it('should call lessonService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/topics/${id}`; - await lessonUrlHandler.getMetaData(url); + await lessonUrlHandler.getMetaData(url); - expect(lessonService.findById).toHaveBeenCalledWith(id); + expect(lessonService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the lessons name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/topics/${id}`; + const lessonName = 'My lesson'; + lessonService.findById.mockResolvedValue({ name: lessonName } as LessonEntity); + + const result = await lessonUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: lessonName, type: 'lesson' })); + }); }); - it('should take the title from the lessons name', async () => { - const id = 'af322312feae'; - const url = `https://localhost/topics/${id}`; - const lessonName = 'My lesson'; - lessonService.findById.mockResolvedValue({ name: lessonName } as LessonEntity); + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; - const result = await lessonUrlHandler.getMetaData(url); + const result = await lessonUrlHandler.getMetaData(url); - expect(result).toEqual(expect.objectContaining({ title: lessonName, type: 'lesson' })); + expect(result).toBeUndefined(); + }); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts index d4d986a964d..5838b6a5a71 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts @@ -27,24 +27,36 @@ describe(TaskUrlHandler.name, () => { }); describe('getMetaData', () => { - it('should call taskService with the correct id', async () => { - const id = 'af322312feae'; - const url = `https://localhost/homework/${id}`; + describe('when url fits', () => { + it('should call taskService with the correct id', async () => { + const id = 'af322312feae'; + const url = `https://localhost/homework/${id}`; - await taskUrlHandler.getMetaData(url); + await taskUrlHandler.getMetaData(url); - expect(taskService.findById).toHaveBeenCalledWith(id); + expect(taskService.findById).toHaveBeenCalledWith(id); + }); + + it('should take the title from the tasks name', async () => { + const id = 'af322312feae'; + const url = `https://localhost/homework/${id}`; + const taskName = 'My Task'; + taskService.findById.mockResolvedValue({ name: taskName } as Task); + + const result = await taskUrlHandler.getMetaData(url); + + expect(result).toEqual(expect.objectContaining({ title: taskName, type: 'task' })); + }); }); - it('should take the title from the tasks name', async () => { - const id = 'af322312feae'; - const url = `https://localhost/homework/${id}`; - const taskName = 'My Task'; - taskService.findById.mockResolvedValue({ name: taskName } as Task); + describe('when url does not fit', () => { + it('should return undefined', async () => { + const url = `https://localhost/invalid/ef2345abe4e3b`; - const result = await taskUrlHandler.getMetaData(url); + const result = await taskUrlHandler.getMetaData(url); - expect(result).toEqual(expect.objectContaining({ title: taskName, type: 'task' })); + expect(result).toBeUndefined(); + }); }); }); }); From 2a19733c4f8dcfa106dee9c47730094483074748 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 Nov 2023 11:55:10 +0100 Subject: [PATCH 10/12] chore: cleanups --- .../meta-tag-extractor.service.spec.ts | 12 ++++++------ .../service/meta-tag-extractor.service.ts | 19 +++++++++++++------ .../url-handler/task-url-handler.spec.ts | 2 +- .../service/url-handler/task-url-handler.ts | 2 +- .../uc/meta-tag-extractor.uc.spec.ts | 2 +- .../uc/meta-tag-extractor.uc.ts | 2 +- apps/server/src/modules/task/index.ts | 3 +++ 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts index 8a76f96a56d..06fa3b09170 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.spec.ts @@ -68,7 +68,7 @@ describe(MetaTagExtractorService.name, () => { it('should thrown an error if url is an empty string', async () => { const url = ''; - await expect(service.fetchMetaData(url)).rejects.toThrow(); + await expect(service.getMetaData(url)).rejects.toThrow(); }); it('should return also the original url', async () => { @@ -76,7 +76,7 @@ describe(MetaTagExtractorService.name, () => { const url = 'https://de.wikipedia.org'; mockOgsResolve({ url, ogTitle }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ url })); }); @@ -86,7 +86,7 @@ describe(MetaTagExtractorService.name, () => { const url = 'https://de.wikipedia.org'; mockOgsResolve({ ogTitle }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ title: ogTitle })); }); @@ -112,7 +112,7 @@ describe(MetaTagExtractorService.name, () => { const url = 'https://de.wikipedia.org'; mockOgsResolve({ url, ogImage }); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ image: ogImage[1] })); }); @@ -124,7 +124,7 @@ describe(MetaTagExtractorService.name, () => { mockOgsReject(new Error('no open graph data included... probably not a webpage')); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ title: 'abc.jpg' })); }); }); @@ -135,7 +135,7 @@ describe(MetaTagExtractorService.name, () => { mockOgsReject(new Error('no open graph data included... probably not a webpage')); - const result = await service.fetchMetaData(url); + const result = await service.getMetaData(url); expect(result).toEqual(expect.objectContaining({ url, title: '', description: '' })); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts index 3db58412165..d64c3a42a58 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/meta-tag-extractor.service.ts @@ -9,19 +9,22 @@ import { MetaTagInternalUrlService } from './meta-tag-internal-url.service'; export class MetaTagExtractorService { constructor(private readonly internalLinkMataTagService: MetaTagInternalUrlService) {} - // WIP: fetch => get - async fetchMetaData(url: string): Promise { + async getMetaData(url: string): Promise { if (url.length === 0) { throw new Error(`MetaTagExtractorService requires a valid URL. Given URL: ${url}`); } - // WIP: make that nicer const metaData = - (await this.internalLinkMataTagService.tryInternalLinkMetaTags(url)) ?? + (await this.tryInternalLinkMetaTags(url)) ?? (await this.tryExtractMetaTags(url)) ?? - this.tryFilenameAsFallback(url); + this.tryFilenameAsFallback(url) ?? + this.getDefaultMetaData(url); - return metaData ?? { url, title: '', description: '', type: 'unknown' }; + return metaData; + } + + private async tryInternalLinkMetaTags(url: string): Promise { + return this.internalLinkMataTagService.tryInternalLinkMetaTags(url); } private async tryExtractMetaTags(url: string): Promise { @@ -59,6 +62,10 @@ export class MetaTagExtractorService { } } + private getDefaultMetaData(url: string): MetaData { + return { url, title: '', description: '', type: 'unknown' }; + } + private pickImage(images: ImageObject[], minWidth = 400): ImageObject | undefined { const sortedImages = [...images]; sortedImages.sort((a, b) => (a.width && b.width ? Number(a.width) - Number(b.width) : 0)); diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts index 5838b6a5a71..0423382f2a8 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { TaskService } from '@modules/task'; import { Test, TestingModule } from '@nestjs/testing'; import { Task } from '@shared/domain'; import { setupEntities } from '@shared/testing'; -import { TaskService } from '@src/modules/task/service'; import { TaskUrlHandler } from './task-url-handler'; describe(TaskUrlHandler.name, () => { diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts index d16eba9f73e..cb1cec86048 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/task-url-handler.ts @@ -1,5 +1,5 @@ +import { TaskService } from '@modules/task'; import { Injectable } from '@nestjs/common'; -import { TaskService } from '@src/modules/task/service'; import type { UrlHandler } from '../../interface/url-handler'; import { MetaData } from '../../types'; import { AbstractUrlHandler } from './abstract-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts index 118b7d82633..07bcd2938f6 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts @@ -68,7 +68,7 @@ describe(MetaTagExtractorUc.name, () => { const url = 'https://www.example.com/great-example'; await uc.fetchMetaData(user.id, url); - expect(metaTagExtractorService.fetchMetaData).toHaveBeenCalledWith(url); + expect(metaTagExtractorService.getMetaData).toHaveBeenCalledWith(url); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts index 45c1e1dadb2..1dfa6915569 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts @@ -18,7 +18,7 @@ export class MetaTagExtractorUc { throw new UnauthorizedException(); } - const result = await this.metaTagExtractorService.fetchMetaData(url); + const result = await this.metaTagExtractorService.getMetaData(url); return result; } } diff --git a/apps/server/src/modules/task/index.ts b/apps/server/src/modules/task/index.ts index 62ab8a69e94..8734b0eb623 100644 --- a/apps/server/src/modules/task/index.ts +++ b/apps/server/src/modules/task/index.ts @@ -1 +1,4 @@ +export * from './service/submission.service'; +export * from './service/task-copy.service'; +export * from './service/task.service'; export * from './task.module'; From 2e0ca99d6dc9d13f9c10fbf9b40c2c8464739a75 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 Nov 2023 16:13:34 +0100 Subject: [PATCH 11/12] chore: final fixes --- apps/server/src/modules/lesson/index.ts | 2 ++ .../api-test/meta-tag-extractor-get-data.api.spec.ts | 2 +- .../controller/meta-tag-extractor.controller.ts | 5 ++--- .../meta-tag-extractor-api.module.ts | 2 +- .../service/url-handler/board-url-handler.spec.ts | 4 ++-- .../service/url-handler/board-url-handler.ts | 4 ++-- .../service/url-handler/course-url-handler.spec.ts | 2 +- .../service/url-handler/course-url-handler.ts | 2 +- .../service/url-handler/lesson-url-handler.spec.ts | 2 +- .../service/url-handler/lesson-url-handler.ts | 2 +- .../uc/meta-tag-extractor.uc.spec.ts | 10 +++++----- .../meta-tag-extractor/uc/meta-tag-extractor.uc.ts | 4 ++-- 12 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/server/src/modules/lesson/index.ts b/apps/server/src/modules/lesson/index.ts index 61e512d84b7..65d4081416a 100644 --- a/apps/server/src/modules/lesson/index.ts +++ b/apps/server/src/modules/lesson/index.ts @@ -1,3 +1,5 @@ export * from './lesson.module'; +export * from './service/lesson.service'; export * from './types/lesson-copy-parent.params'; export * from './types/lesson-copy.params'; + diff --git a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts index 8392f5f106a..2a6c2a93d90 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/api-test/meta-tag-extractor-get-data.api.spec.ts @@ -24,7 +24,7 @@ describe(`get meta tags (api)`, () => { }) .overrideProvider(MetaTagExtractorService) .useValue({ - fetchMetaData: () => mockedResponse, + getMetaData: () => mockedResponse, }) .compile(); diff --git a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts index a141444f8ee..79f798c9e29 100644 --- a/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts +++ b/apps/server/src/modules/meta-tag-extractor/controller/meta-tag-extractor.controller.ts @@ -1,7 +1,6 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, InternalServerErrorException, Post, UnauthorizedException } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { MetaTagExtractorUc } from '../uc'; import { MetaTagExtractorResponse } from './dto'; import { GetMetaTagDataBody } from './post-link-url.body.params'; @@ -21,7 +20,7 @@ export class MetaTagExtractorController { @CurrentUser() currentUser: ICurrentUser, @Body() bodyParams: GetMetaTagDataBody ): Promise { - const result = await this.metaTagExtractorUc.fetchMetaData(currentUser.userId, bodyParams.url); + const result = await this.metaTagExtractorUc.getMetaData(currentUser.userId, bodyParams.url); const imageUrl = result.image?.url; const response = new MetaTagExtractorResponse({ ...result, imageUrl }); return response; diff --git a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts index d9095315e87..acc5eb31776 100644 --- a/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts +++ b/apps/server/src/modules/meta-tag-extractor/meta-tag-extractor-api.module.ts @@ -1,6 +1,6 @@ +import { AuthorizationModule } from '@modules/authorization'; import { forwardRef, Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; import { MetaTagExtractorController } from './controller'; import { MetaTagExtractorModule } from './meta-tag-extractor.module'; import { MetaTagExtractorUc } from './uc'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts index eb8c1ed7766..f7775b58f1f 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; import { Test, TestingModule } from '@nestjs/testing'; import { ColumnBoard } from '@shared/domain'; import { setupEntities } from '@shared/testing'; -import { ColumnBoardService } from '@src/modules/board'; -import { CourseService } from '@src/modules/learnroom'; import { BoardUrlHandler } from './board-url-handler'; describe(BoardUrlHandler.name, () => { diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts index 3052f65518b..013631244dd 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts @@ -1,7 +1,7 @@ +import { ColumnBoardService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; import { Injectable } from '@nestjs/common'; import { BoardExternalReferenceType } from '@shared/domain'; -import { ColumnBoardService } from '@src/modules/board'; -import { CourseService } from '@src/modules/learnroom'; import type { UrlHandler } from '../../interface/url-handler'; import { MetaData } from '../../types'; import { AbstractUrlHandler } from './abstract-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts index 631a8287d09..75a43876de4 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CourseService } from '@modules/learnroom'; import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain'; import { setupEntities } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom'; import { CourseUrlHandler } from './course-url-handler'; describe(CourseUrlHandler.name, () => { diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts index 08580b42bb9..def041886f1 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/course-url-handler.ts @@ -1,5 +1,5 @@ +import { CourseService } from '@modules/learnroom'; import { Injectable } from '@nestjs/common'; -import { CourseService } from '@src/modules/learnroom'; import type { UrlHandler } from '../../interface/url-handler'; import { MetaData } from '../../types'; import { AbstractUrlHandler } from './abstract-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts index 3e2968e058d..53b59d86ab7 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { LessonService } from '@modules/lesson'; import { Test, TestingModule } from '@nestjs/testing'; import { LessonEntity } from '@shared/domain'; import { setupEntities } from '@shared/testing'; -import { LessonService } from '@src/modules/lesson/service'; import { LessonUrlHandler } from './lesson-url-handler'; describe(LessonUrlHandler.name, () => { diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts index 6ef2689a1cd..c5264020a50 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/lesson-url-handler.ts @@ -1,5 +1,5 @@ +import { LessonService } from '@modules/lesson'; import { Injectable } from '@nestjs/common'; -import { LessonService } from '@src/modules/lesson/service'; import type { UrlHandler } from '../../interface/url-handler'; import { MetaData } from '../../types'; import { AbstractUrlHandler } from './abstract-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts index 07bcd2938f6..f5aa0c6cd72 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; import { MetaTagExtractorService } from '../service'; import { MetaTagExtractorUc } from './meta-tag-extractor.uc'; @@ -42,7 +42,7 @@ describe(MetaTagExtractorUc.name, () => { jest.resetAllMocks(); }); - describe('fetchMetaData', () => { + describe('getMetaData', () => { describe('when user exists', () => { const setup = () => { const user = userFactory.build(); @@ -57,7 +57,7 @@ describe(MetaTagExtractorUc.name, () => { authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); const url = 'https://www.example.com/great-example'; - await uc.fetchMetaData(user.id, url); + await uc.getMetaData(user.id, url); expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); }); @@ -66,7 +66,7 @@ describe(MetaTagExtractorUc.name, () => { const { user } = setup(); const url = 'https://www.example.com/great-example'; - await uc.fetchMetaData(user.id, url); + await uc.getMetaData(user.id, url); expect(metaTagExtractorService.getMetaData).toHaveBeenCalledWith(url); }); @@ -84,7 +84,7 @@ describe(MetaTagExtractorUc.name, () => { const { user } = setup(); const url = 'https://www.example.com/great-example'; - await expect(uc.fetchMetaData(user.id, url)).rejects.toThrow(UnauthorizedException); + await expect(uc.getMetaData(user.id, url)).rejects.toThrow(UnauthorizedException); }); }); }); diff --git a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts index 1dfa6915569..47ac53d88b0 100644 --- a/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts +++ b/apps/server/src/modules/meta-tag-extractor/uc/meta-tag-extractor.uc.ts @@ -1,6 +1,6 @@ +import { AuthorizationService } from '@modules/authorization'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { AuthorizationService } from '@src/modules/authorization'; import { MetaTagExtractorService } from '../service'; import { MetaData } from '../types'; @@ -11,7 +11,7 @@ export class MetaTagExtractorUc { private readonly metaTagExtractorService: MetaTagExtractorService ) {} - async fetchMetaData(userId: EntityId, url: string): Promise { + async getMetaData(userId: EntityId, url: string): Promise { try { await this.authorizationService.getUserWithPermissions(userId); } catch (error) { From 745cffb0de972bdc2427574cf09ed2c611a8f620 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 20 Nov 2023 11:45:29 +0100 Subject: [PATCH 12/12] chore: remove second empty line at end of file --- apps/server/src/modules/lesson/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/modules/lesson/index.ts b/apps/server/src/modules/lesson/index.ts index 65d4081416a..4ba8664de20 100644 --- a/apps/server/src/modules/lesson/index.ts +++ b/apps/server/src/modules/lesson/index.ts @@ -2,4 +2,3 @@ export * from './lesson.module'; export * from './service/lesson.service'; export * from './types/lesson-copy-parent.params'; export * from './types/lesson-copy.params'; -