Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"aot": true,
"assets": [
"src/assets",
"src/static-files",
"src/robots.txt"
],
"styles": [
Expand Down
107 changes: 107 additions & 0 deletions src/app/shared/html-content.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { firstValueFrom } from 'rxjs';

import { HtmlContentService } from './html-content.service';
import { LocaleService } from '../core/locale/locale.service';
import { APP_CONFIG } from '../../config/app-config.interface';

class LocaleServiceStub {
languageCode = 'en';

getCurrentLanguageCode(): string {
return this.languageCode;
}
}

describe('HtmlContentService', () => {
let service: HtmlContentService;
let httpMock: HttpTestingController;
let localeService: LocaleServiceStub;

function setup(nameSpace: string): void {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
HtmlContentService,
{ provide: LocaleService, useClass: LocaleServiceStub },
{
provide: APP_CONFIG,
useValue: {
ui: { nameSpace },
},
},
],
});

service = TestBed.inject(HtmlContentService);
httpMock = TestBed.inject(HttpTestingController);
localeService = TestBed.inject(LocaleService) as any;
}

afterEach(() => {
if (httpMock) {
httpMock.verify();
}
});

it('should request root namespaced URL for default locale', async () => {
setup('/');
localeService.languageCode = 'en';

const promise = service.getHmtlContentByPathAndLocale('license-ud-1.0');

const request = httpMock.expectOne('/static-files/license-ud-1.0.html');
expect(request.request.method).toBe('GET');
request.flush('Universal Dependencies 1.0 License Set');

const content = await promise;
expect(content).toBe('Universal Dependencies 1.0 License Set');
});

it('should request locale-specific namespaced URL for non-default locale', async () => {
setup('/repository');
localeService.languageCode = 'cs';

const promise = service.getHmtlContentByPathAndLocale('license-ud-1.0');

const request = httpMock.expectOne('/repository/static-files/cs/license-ud-1.0.html');
expect(request.request.method).toBe('GET');
request.flush('Localized content');

const content = await promise;
expect(content).toBe('Localized content');
});

it('should fallback from locale-specific to default namespaced URL when localized content is empty', fakeAsync(() => {
setup('/repository/');
localeService.languageCode = 'cs';

let content: string | undefined;
service.getHmtlContentByPathAndLocale('license-ud-1.0').then((result) => {
content = result;
});

const localizedRequest = httpMock.expectOne('/repository/static-files/cs/license-ud-1.0.html');
localizedRequest.flush('');
tick();

const fallbackRequest = httpMock.expectOne('/repository/static-files/license-ud-1.0');
fallbackRequest.flush('Fallback content');
tick();

expect(content).toBe('Fallback content');
}));

it('should return empty string from getHtmlContent when request fails', async () => {
setup('/repository');

const contentPromise = firstValueFrom(service.getHtmlContent('static-files/missing-page.html'));

const request = httpMock.expectOne('/repository/static-files/missing-page.html');
request.flush('Not Found', { status: 404, statusText: 'Not Found' });

const content = await contentPromise;
expect(content).toBe('');
});
});
59 changes: 54 additions & 5 deletions src/app/shared/html-content.service.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,76 @@
import { Injectable } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { firstValueFrom, of as observableOf } from 'rxjs';
import { HTML_SUFFIX, STATIC_FILES_PROJECT_PATH } from '../static-page/static-page-routing-paths';
import { isEmpty, isNotEmpty } from './empty.util';
import { LocaleService } from '../core/locale/locale.service';
import { APP_CONFIG, AppConfig } from '../../config/app-config.interface';
import { REQUEST } from '@nguniversal/express-engine/tokens';

/**
* Service for loading static `.html` files stored in the `/static-files` folder.
*/
@Injectable()
export class HtmlContentService {
constructor(private http: HttpClient,
private localeService: LocaleService,) {}
private localeService: LocaleService,
@Inject(APP_CONFIG) protected appConfig?: AppConfig,
@Inject(PLATFORM_ID) private platformId?: object,
@Optional() @Inject(REQUEST) private request?: any,
) {}

private getNamespacePrefix(): string {
const nameSpace = this.appConfig?.ui?.nameSpace ?? '/';
if (nameSpace === '/') {
return '';
}
return nameSpace.endsWith('/') ? nameSpace.slice(0, -1) : nameSpace;
}

private composeNamespacedUrl(url: string): string {
if (/^https?:\/\//i.test(url)) {
return url;
}

const normalizedPath = url.startsWith('/') ? url : `/${url}`;
const namespacePrefix = this.getNamespacePrefix();

if (namespacePrefix && normalizedPath.startsWith(`${namespacePrefix}/`)) {
return normalizedPath;
}

return `${namespacePrefix}${normalizedPath}`;
}

private buildRuntimeUrl(path: string): string {
if (!isPlatformServer(this.platformId) || !this.request) {
return path;
}

const protocol = this.request.protocol;
const host = this.request.get?.('host');
if (!protocol || !host) {
return path;
}

return `${protocol}://${host}${path}`;
}

getHtmlContent(url: string) {
const namespacedUrl = this.composeNamespacedUrl(url);
const runtimeUrl = this.buildRuntimeUrl(namespacedUrl);
return this.http.get(runtimeUrl, { responseType: 'text' }).pipe(
catchError(() => observableOf('')));
}

/**
* Load `.html` file content or return empty string if an error.
* @param url file location
*/
fetchHtmlContent(url: string) {
// catchError -> return empty value.
return this.http.get(url, { responseType: 'text' }).pipe(
catchError(() => observableOf('')));
return this.getHtmlContent(url);
}

/**
Expand Down
14 changes: 12 additions & 2 deletions src/app/static-page/static-page.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { environment } from '../../environments/environment';
import { ClarinSafeHtmlPipe } from '../shared/utils/clarin-safehtml.pipe';

describe('StaticPageComponent', () => {
async function setupTest(html: string, restBase?: string) {
async function setupTest(html: string, restBase?: string, route: string = '/static/test-file.html') {
const htmlContentService = jasmine.createSpyObj('htmlContentService', {
fetchHtmlContent: of(html),
getHmtlContentByPathAndLocale: Promise.resolve(html)
});

const router = new RouterMock();
router.setRoute(route);

const appConfig = {
...environment,
ui: {
Expand All @@ -36,7 +39,7 @@ describe('StaticPageComponent', () => {
],
providers: [
{ provide: HtmlContentService, useValue: htmlContentService },
{ provide: Router, useValue: new RouterMock() },
{ provide: Router, useValue: router },
{ provide: APP_CONFIG, useValue: appConfig }
]
}).compileComponents();
Expand All @@ -58,6 +61,13 @@ describe('StaticPageComponent', () => {
expect(component.htmlContent.value).toBe('<div id="idShouldNotBeRemoved">TEST MESSAGE</div>');
});

it('should call HtmlContentService with the route html file name', async () => {
const { component, htmlContentService } = await setupTest('<div>TEST MESSAGE</div>', undefined, '/static/license-ud-1.0.html');
await component.ngOnInit();

expect(htmlContentService.getHmtlContentByPathAndLocale).toHaveBeenCalledWith('license-ud-1.0.html');
});

it('should rewrite OAI link with rest.baseUrl', async () => {
const oaiHtml = '<a href="/server/oai/request?verb=ListSets">OAI</a>';
const { fixture, component } = await setupTest(oaiHtml, 'https://api.example.org/server');
Expand Down