Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-5434 - meta data endpoint #4488

Merged
merged 23 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
70763f7
refactoring meta data stuff to an module and independent endpoint
hoeppner-dataport Oct 20, 2023
92c382c
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server
hoeppner-dataport Oct 20, 2023
476f7d5
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Oct 20, 2023
c68a85b
Merge branch 'main' into BC-5434-meta-data-endpoint
hoeppner-dataport Oct 20, 2023
a03c678
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Oct 20, 2023
2eaacc5
Merge branch 'BC-5434-meta-data-endpoint' of github.com:hpi-schul-clo…
hoeppner-dataport Oct 20, 2023
b12ee6c
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Oct 20, 2023
06a0e7d
chore: remove commented out line
hoeppner-dataport Oct 23, 2023
344fab3
chore: implement fallback of linked server does not respond
hoeppner-dataport Oct 23, 2023
dce9e83
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Oct 23, 2023
e8093ff
feature: implement fallback to filename if no open graph data
hoeppner-dataport Oct 24, 2023
7f6396a
chore: extend update visitor tests
hoeppner-dataport Oct 25, 2023
5a73546
chore: coverage of meta tag extractor service
hoeppner-dataport Oct 25, 2023
2590b94
chore: add tests
hoeppner-dataport Nov 1, 2023
389e127
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Nov 1, 2023
e8fb31b
add check if user is authorized
hoeppner-dataport Nov 3, 2023
b86f891
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Nov 3, 2023
13e134b
chore: add test for meta-tag-extractor.uc
hoeppner-dataport Nov 3, 2023
9302de6
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Nov 3, 2023
c5b7187
chore: remove unneeded logger
hoeppner-dataport Nov 3, 2023
066bdab
WIP: api-test-implementation
hoeppner-dataport Nov 3, 2023
05741fd
add API-Test for new endpoint
hoeppner-dataport Nov 3, 2023
ad43c02
Merge branch 'main' of github.com:hpi-schul-cloud/schulcloud-server i…
hoeppner-dataport Nov 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/server/src/modules/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
ColumnBoardService,
ColumnService,
ContentElementService,
OpenGraphProxyService,
SubmissionItemService,
} from './service';
import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './service/board-do-copy-service';
Expand All @@ -38,7 +37,6 @@ import { ColumnBoardCopyService } from './service/column-board-copy.service';
BoardDoCopyService,
ColumnBoardCopyService,
SchoolSpecificFileCopyServiceFactory,
OpenGraphProxyService,
],
exports: [
BoardDoAuthorizableService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, ValidateNested } from 'class-validator';
import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator';

export abstract class ElementContentBody {
@IsEnum(ContentElementType)
Expand Down Expand Up @@ -32,10 +32,26 @@ export class FileElementContentBody extends ElementContentBody {
@ApiProperty()
content!: FileContentBody;
}

export class LinkContentBody {
@IsString()
@IsUrl()
@ApiProperty({})
url!: string;

@IsString()
@IsOptional()
@ApiProperty({})
title?: string;

@IsString()
@IsOptional()
@ApiProperty({})
description?: string;

@IsString()
@IsOptional()
@ApiProperty({})
imageUrl?: string;
}

export class LinkElementContentBody extends ElementContentBody {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import {
submissionContainerElementFactory,
submissionItemFactory,
} from '@shared/testing';
import { ExternalToolContentBody, FileContentBody, RichTextContentBody } from '../controller/dto';
import { ExternalToolContentBody, FileContentBody, LinkContentBody, RichTextContentBody } from '../controller/dto';
import { ContentElementUpdateVisitor } from './content-element-update.visitor';
import { OpenGraphProxyService } from './open-graph-proxy.service';

describe(ContentElementUpdateVisitor.name, () => {
describe('when visiting an unsupported component', () => {
Expand All @@ -25,8 +24,7 @@ describe(ContentElementUpdateVisitor.name, () => {
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const submissionItem = submissionItemFactory.build();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { board, column, card, submissionItem, updater };
};
Expand Down Expand Up @@ -66,8 +64,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { fileElement, updater };
};
Expand All @@ -79,31 +76,12 @@ describe(ContentElementUpdateVisitor.name, () => {
});
});

describe('when visiting a link element using the wrong content', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);

return { linkElement, updater };
};

it('should throw an error', async () => {
const { linkElement, updater } = setup();

await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow();
});
});

describe('when visiting a rich text element using the wrong content', () => {
const setup = () => {
const richTextElement = richTextElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { richTextElement, updater };
};
Expand All @@ -121,8 +99,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { submissionContainerElement, updater };
};
Expand All @@ -134,14 +111,76 @@ describe(ContentElementUpdateVisitor.name, () => {
});
});

describe('when visiting a link element', () => {
describe('when content is valid', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new LinkContentBody();
content.url = 'https://super-example.com/';
content.title = 'SuperExample - the best examples in the web';
content.imageUrl = '/preview/image.jpg';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, content, updater };
};

it('should update the content', async () => {
const { linkElement, content, updater } = setup();

await updater.visitLinkElementAsync(linkElement);

expect(linkElement.url).toEqual(content.url);
expect(linkElement.title).toEqual(content.title);
expect(linkElement.imageUrl).toEqual(content.imageUrl);
});
});

describe('when content is not a link element', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, updater };
};

it('should throw an error', async () => {
const { linkElement, updater } = setup();

await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow();
});
});

describe('when imageUrl for preview image is not a relative url', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new LinkContentBody();
content.url = 'https://super-example.com/';
content.title = 'SuperExample - the best examples in the web';
content.imageUrl = 'https://www.external.de/fake-preview-image.jpg';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, content, updater };
};

it('should ignore the image url', async () => {
const { linkElement, updater } = setup();

await updater.visitLinkElementAsync(linkElement);

expect(linkElement.imageUrl).toBe('');
});
});
});

describe('when visiting a external tool element', () => {
describe('when visiting a external tool element with valid content', () => {
const setup = () => {
const externalToolElement = externalToolElementFactory.build({ contextExternalToolId: undefined });
const content = new ExternalToolContentBody();
content.contextExternalToolId = new ObjectId().toHexString();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater, content };
};
Expand All @@ -161,8 +200,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater };
};
Expand All @@ -178,8 +216,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const setup = () => {
const externalToolElement = externalToolElementFactory.build();
const content = new ExternalToolContentBody();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater };
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import {
RichTextContentBody,
SubmissionContainerContentBody,
} from '../controller/dto';
import { OpenGraphProxyService } from './open-graph-proxy.service';

@Injectable()
export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync {
private readonly content: AnyElementContentBody;

constructor(content: AnyElementContentBody, private readonly openGraphProxyService: OpenGraphProxyService) {
constructor(content: AnyElementContentBody) {
this.content = content;
}

Expand All @@ -55,13 +54,19 @@ export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync {

async visitLinkElementAsync(linkElement: LinkElement): Promise<void> {
if (this.content instanceof LinkContentBody) {
const urlWithProtocol = /:\/\//.test(this.content.url) ? this.content.url : `https://${this.content.url}`;
linkElement.url = new URL(urlWithProtocol).toString();
const openGraphData = await this.openGraphProxyService.fetchOpenGraphData(linkElement.url);
linkElement.title = openGraphData.title;
linkElement.description = openGraphData.description;
if (openGraphData.image) {
linkElement.imageUrl = openGraphData.image.url;
linkElement.url = new URL(this.content.url).toString();
linkElement.title = this.content.title ?? '';
linkElement.description = this.content.description ?? '';
if (this.content.imageUrl) {
const isRelativeUrl = (url: string) => {
const fallbackHostname = 'https://www.fallback-url-if-url-is-relative.org';
const imageUrlObject = new URL(url, fallbackHostname);
return imageUrlObject.origin === fallbackHostname;
};

if (isRelativeUrl(this.content.imageUrl)) {
linkElement.imageUrl = this.content.imageUrl;
}
}
return Promise.resolve();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ import {
import { BoardDoRepo } from '../repo';
import { BoardDoService } from './board-do.service';
import { ContentElementService } from './content-element.service';
import { OpenGraphProxyService } from './open-graph-proxy.service';

describe(ContentElementService.name, () => {
let module: TestingModule;
let service: ContentElementService;
let boardDoRepo: DeepMocked<BoardDoRepo>;
let boardDoService: DeepMocked<BoardDoService>;
let contentElementFactory: DeepMocked<ContentElementFactory>;
let openGraphProxyService: DeepMocked<OpenGraphProxyService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -52,18 +50,13 @@ describe(ContentElementService.name, () => {
provide: ContentElementFactory,
useValue: createMock<ContentElementFactory>(),
},
{
provide: OpenGraphProxyService,
useValue: createMock<OpenGraphProxyService>(),
},
],
}).compile();

service = module.get(ContentElementService);
boardDoRepo = module.get(BoardDoRepo);
boardDoService = module.get(BoardDoService);
contentElementFactory = module.get(ContentElementFactory);
openGraphProxyService = module.get(OpenGraphProxyService);

await setupEntities();
});
Expand Down Expand Up @@ -302,8 +295,6 @@ describe(ContentElementService.name, () => {
image: { url: 'https://my-open-graph-proxy.scvs.de/image/adefcb12ed3a' },
};

openGraphProxyService.fetchOpenGraphData.mockResolvedValueOnce(imageResponse);

return { linkElement, content, card, imageResponse };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import { AnyElementContentBody } from '../controller/dto';
import { BoardDoRepo } from '../repo';
import { BoardDoService } from './board-do.service';
import { ContentElementUpdateVisitor } from './content-element-update.visitor';
import { OpenGraphProxyService } from './open-graph-proxy.service';

@Injectable()
export class ContentElementService {
constructor(
private readonly boardDoRepo: BoardDoRepo,
private readonly boardDoService: BoardDoService,
private readonly contentElementFactory: ContentElementFactory,
private readonly openGraphProxyService: OpenGraphProxyService
private readonly contentElementFactory: ContentElementFactory
) {}

async findById(elementId: EntityId): Promise<AnyContentElementDo> {
Expand Down Expand Up @@ -58,7 +56,7 @@ export class ContentElementService {
}

async update(element: AnyContentElementDo, content: AnyElementContentBody): Promise<AnyContentElementDo> {
const updater = new ContentElementUpdateVisitor(content, this.openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);
await element.acceptAsync(updater);

const parent = await this.boardDoRepo.findParentOfId(element.id);
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/modules/board/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ export * from './card.service';
export * from './column-board.service';
export * from './column.service';
export * from './content-element.service';
export * from './open-graph-proxy.service';
export * from './submission-item.service';
Loading
Loading