diff --git a/e2e/codecept.conf.js b/e2e/codecept.conf.js
index 8df4195b..2f4075ee 100644
--- a/e2e/codecept.conf.js
+++ b/e2e/codecept.conf.js
@@ -42,6 +42,7 @@ exports.config = {
newsPage: path.join(__dirname, 'spec', 'pages', 'news.ts'),
planetsPage: path.join(__dirname, 'spec', 'pages', 'planets.ts'),
hooksPage: path.join(__dirname, 'spec', 'pages', 'hooks.ts'),
+ common: path.join(__dirname, 'spec', 'pages', 'common.ts'),
},
tests: './spec/**/*.spec.e2e.ts',
name: 'ilc',
diff --git a/e2e/spec/500.spec.e2e.ts b/e2e/spec/500.spec.e2e.ts
new file mode 100644
index 00000000..d8b1f072
--- /dev/null
+++ b/e2e/spec/500.spec.e2e.ts
@@ -0,0 +1,39 @@
+Feature('500 error handling');
+
+//region 500 page
+Scenario('Renders 500 page for domain "localhost:8233" (default)', (I, common: common) => {
+ I.amOnPage(common.url.urlInternalServerError);
+ I.seeInSource(common.textError500);
+ I.seeInSource(common.textErrorId);
+ I.dontSeeInSource(common.textError500ForLocalhostAsIPv4);
+ I.seeInCurrentUrl(common.url.urlInternalServerError);
+});
+Scenario('should open 500 error page when an error happens for domain "localhost:8233" (default)', async (I, newsPage: newsPage, common: common) => {
+ I.amOnPage(newsPage.url.main);
+ I.waitInUrl(newsPage.url.main, 10);
+ I.waitForElement(newsPage.generateError, 10);
+ I.click(newsPage.generateError);
+ I.seeInSource(common.textError500);
+ I.seeInSource(common.textErrorId);
+ I.dontSeeInSource(common.textError500ForLocalhostAsIPv4);
+ I.seeInCurrentUrl(newsPage.url.main);
+});
+
+Scenario('Renders 500 page for domain "127.0.0.1:8233"', (I, common: common) => {
+ I.amOnPage(common.url.localhostAsIPv4 + common.url.urlInternalServerError);
+ I.seeInSource(common.textError500ForLocalhostAsIPv4);
+ I.seeInSource(common.textErrorId);
+ I.dontSeeInSource(common.textError500);
+ I.seeInCurrentUrl(common.url.localhostAsIPv4 + common.url.urlInternalServerError);
+});
+Scenario('should open 500 error page when an error happens for domain "127.0.0.1:8233"', async (I, newsPage: newsPage, common: common) => {
+ I.amOnPage(common.url.localhostAsIPv4 + newsPage.url.main);
+ I.waitInUrl(newsPage.url.main, 10);
+ I.waitForElement(newsPage.generateError, 10);
+ I.click(newsPage.generateError);
+ I.seeInSource(common.textError500ForLocalhostAsIPv4);
+ I.seeInSource(common.textErrorId);
+ I.dontSeeInSource(common.textError500);
+ I.seeInCurrentUrl(common.url.localhostAsIPv4 + newsPage.url.main);
+});
+//endregion 500 page
diff --git a/e2e/spec/news.spec.e2e.ts b/e2e/spec/news.spec.e2e.ts
index c6a4a2b4..5c3a4727 100644
--- a/e2e/spec/news.spec.e2e.ts
+++ b/e2e/spec/news.spec.e2e.ts
@@ -36,12 +36,3 @@ Scenario('should open an article page from a direct link', async (I, newsPage: n
I.seeInCurrentUrl(lastNewsSourceLinkHref);
I.closeOtherTabs();
});
-
-Scenario('should open 500 error page when an error happens', async (I, newsPage: newsPage) => {
- I.amOnPage(newsPage.url.main);
- I.waitInUrl(newsPage.url.main, 10);
- I.waitForElement(newsPage.generateError, 10);
- I.click(newsPage.generateError);
- I.waitForElement(newsPage.errorId);
- I.seeInCurrentUrl(newsPage.url.main);
-});
diff --git a/e2e/spec/pages/common.ts b/e2e/spec/pages/common.ts
new file mode 100644
index 00000000..42990496
--- /dev/null
+++ b/e2e/spec/pages/common.ts
@@ -0,0 +1,8 @@
+export const url = {
+ urlInternalServerError: '/_ilc/500',
+ localhostAsIPv4: 'http://127.0.0.1:8233',
+};
+
+export const textError500 = '
ERROR 500
';
+export const textError500ForLocalhostAsIPv4 = '500 Internal Server Error on 127.0.0.1
';
+export const textErrorId = 'Error ID:';
diff --git a/e2e/spec/pages/news.ts b/e2e/spec/pages/news.ts
index 7c161028..6f252bd9 100644
--- a/e2e/spec/pages/news.ts
+++ b/e2e/spec/pages/news.ts
@@ -14,7 +14,6 @@ export const newsView = 'body > div#body > div.single-spa-container.news-app > d
export const newsSources = `${newsView} > div.sources > div.container > ol > li.source`;
export const bannerHeadline = `${newsView} > div.banner > h1`;
export const generateError = `${newsView} > div.banner > a`;
-export const errorId = `body > div#error > p:nth-child(3)`;
export const lastNewsSource = `${newsSources}:last-child`;
export const lastNewsSourceLink = `${lastNewsSource} > p.action > a`;
export const newsSourceArticles = `${newsView} > div.container > div.articles > ol > li.article`;
diff --git a/e2e/typings/steps.d.ts b/e2e/typings/steps.d.ts
index 5abe9cf2..78e42249 100644
--- a/e2e/typings/steps.d.ts
+++ b/e2e/typings/steps.d.ts
@@ -3,11 +3,12 @@ type peoplePage = typeof import('../spec/pages/people');
type newsPage = typeof import('../spec/pages/news');
type planetsPage = typeof import('../spec/pages/planets');
type hooksPage = typeof import('../spec/pages/hooks');
+type common = typeof import('../spec/pages/common');
type MockRequestHelper = import('@codeceptjs/mock-request');
declare namespace CodeceptJS {
- interface SupportObject { I: CodeceptJS.I, peoplePage: peoplePage, newsPage: newsPage, planetsPage: planetsPage, hooksPage: hooksPage }
- interface CallbackOrder { [0]: CodeceptJS.I; [1]: peoplePage; [2]: newsPage; [3]: planetsPage; [4]: hooksPage }
+ interface SupportObject { I: CodeceptJS.I, peoplePage: peoplePage, newsPage: newsPage, planetsPage: planetsPage, hooksPage: hooksPage, common: common }
+ interface CallbackOrder { [0]: CodeceptJS.I; [1]: peoplePage; [2]: newsPage; [3]: planetsPage; [4]: hooksPage; [5]: common }
interface Methods extends CodeceptJS.Puppeteer, MockRequestHelper {}
interface I extends WithTranslation {}
namespace Translation {
diff --git a/ilc/common/wrapWithCache.js b/ilc/common/wrapWithCache.js
index 06f0c44a..ed1213e7 100644
--- a/ilc/common/wrapWithCache.js
+++ b/ilc/common/wrapWithCache.js
@@ -6,13 +6,14 @@ errors.WrapWithCacheError = extendError('WrapWithCacheError');
const wrapWithCache = (localStorage, logger, createHash = hashFn) => (fn, cacheParams = {}) => {
const {
cacheForSeconds = 60,
+ name = '', // "hash" of returned value is based only on arguments, so with the help "name" we can add prefix to hash
} = cacheParams;
const cacheResolutionPromise = {};
return (...args) => {
const now = Math.floor(Date.now() / 1000);
- const hash = args.length > 0 ? createHash(JSON.stringify(args)) : '__null__';
+ const hash = `${name ? name + '__' : ''}${args.length > 0 ? createHash(JSON.stringify(args)) : '__null__'}`;
if (localStorage.getItem(hash) === null || JSON.parse(localStorage.getItem(hash)).cachedAt < now - cacheForSeconds) {
if (cacheResolutionPromise[hash] !== undefined) {
diff --git a/ilc/server/app.js b/ilc/server/app.js
index 800caf14..e7318f49 100644
--- a/ilc/server/app.js
+++ b/ilc/server/app.js
@@ -43,7 +43,8 @@ module.exports = (registryService, pluginManager) => {
app.register(require('./ping'));
app.get('/_ilc/api/v1/registry/template/:templateName', async (req, res) => {
- const data = await registryService.getTemplate(req.params.templateName);
+ const currentDomain = req.hostname;
+ const data = await registryService.getTemplate(req.params.templateName, currentDomain);
res.status(200).send(data.data.content);
});
diff --git a/ilc/server/errorHandler/ErrorHandler.js b/ilc/server/errorHandler/ErrorHandler.js
index faf8c0cb..461526e4 100644
--- a/ilc/server/errorHandler/ErrorHandler.js
+++ b/ilc/server/errorHandler/ErrorHandler.js
@@ -51,7 +51,8 @@ module.exports = class ErrorHandler {
errorId
});
- let data = await this.#registryService.getTemplate('500');
+ const currentDomain = req.hostname;
+ let data = await this.#registryService.getTemplate('500', currentDomain);
data = data.data.content.replace('%ERRORID%', `Error ID: ${errorId}`);
nres.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
diff --git a/ilc/server/errorHandler/ErrorHandler.spec.js b/ilc/server/errorHandler/ErrorHandler.spec.js
index 5ec0b3e7..1a1c0410 100644
--- a/ilc/server/errorHandler/ErrorHandler.spec.js
+++ b/ilc/server/errorHandler/ErrorHandler.spec.js
@@ -29,6 +29,7 @@ describe('ErrorHandler', () => {
});
it('should show 500 error page with an error id', async () => {
+ nock(config.get('registry').address).get('/api/v1/router_domains').reply(200, []);
nock(config.get('registry').address).get(`/api/v1/template/500/rendered`).reply(200, {
content:
'' +
@@ -59,6 +60,7 @@ describe('ErrorHandler', () => {
});
it('should send an error message when showing 500 error page throws an error', async () => {
+ nock(config.get('registry').address).get('/api/v1/router_domains').reply(200, []);
const replyingError = new Error('Something awful happened.');
nock(config.get('registry').address).get(`/api/v1/template/500/rendered`).replyWithError(replyingError.message);
diff --git a/ilc/server/registry/Registry.js b/ilc/server/registry/Registry.js
index c6c1a03c..9e878e37 100644
--- a/ilc/server/registry/Registry.js
+++ b/ilc/server/registry/Registry.js
@@ -13,6 +13,7 @@ module.exports = class Registry {
#cacheHeated = {
config: false,
template: false,
+ routerDomains: false,
};
/**
@@ -30,6 +31,7 @@ module.exports = class Registry {
const getConfigMemo = wrapFetchWithCache(this.#getConfig, {
cacheForSeconds: 5,
+ name: 'registry_getConfig',
});
this.getConfig = async (options) => {
@@ -38,13 +40,28 @@ module.exports = class Registry {
return res;
};
- this.getTemplate = wrapFetchWithCache(this.#getTemplate, {
+ this.getRouterDomains = wrapFetchWithCache(this.#getRouterDomains, {
cacheForSeconds: 30,
+ name: 'registry_routerDomains',
});
+
+ const getTemplateMemo = wrapFetchWithCache(this.#getTemplate, {
+ cacheForSeconds: 30,
+ });
+
+ this.getTemplate = async (templateName, forDomain) => {
+ if (templateName === '500' && forDomain) {
+ const routerDomains = await this.getRouterDomains();
+ const redefined500 = routerDomains.data.find(item => item.domainName === forDomain)?.template500;
+ templateName = redefined500 || templateName;
+ }
+
+ return await getTemplateMemo(templateName);
+ };
}
async preheat() {
- if (this.#cacheHeated.template && this.#cacheHeated.config) {
+ if (this.#cacheHeated.template && this.#cacheHeated.config && this.#cacheHeated.routerDomains) {
return;
}
@@ -53,6 +70,7 @@ module.exports = class Registry {
await Promise.all([
this.getConfig(),
this.getTemplate('500'),
+ this.getRouterDomains(),
]);
this.#logger.info('Registry preheated successfully!');
@@ -102,6 +120,27 @@ module.exports = class Registry {
return res.data;
};
+ #getRouterDomains = async () => {
+ this.#logger.debug('Calling get routerDomains registry endpoint...');
+
+ const url = urljoin(this.#address, 'api/v1/router_domains');
+ let res;
+ try {
+ res = await axios.get(url, { responseType: 'json' });
+ } catch (e) {
+ throw new errors.RegistryError({
+ message: `Error while requesting routerDomains from registry`,
+ cause: e,
+ data: {
+ requestedUrl: url
+ }
+ });
+ }
+
+ this.#cacheHeated.routerDomains = true;
+ return res.data;
+ };
+
#filterConfig = (config, filter) => {
if (!filter || !Object.keys(filter).length) {
return config;
diff --git a/registry/client/src/routerDomains/Edit.js b/registry/client/src/routerDomains/Edit.js
index cd0124b7..196ae939 100644
--- a/registry/client/src/routerDomains/Edit.js
+++ b/registry/client/src/routerDomains/Edit.js
@@ -6,6 +6,8 @@ import {
TextInput,
required,
TextField,
+ ReferenceInput,
+ SelectInput,
} from 'react-admin'; // eslint-disable-line import/no-unresolved
const Title = ({ record }) => {
@@ -19,7 +21,12 @@ const InputForm = ({ mode = 'edit', ...props }) => {
?
: null}
-
+
+
+
+
);
};
diff --git a/registry/client/src/routerDomains/List.js b/registry/client/src/routerDomains/List.js
index 377ee4f0..9a7e7652 100644
--- a/registry/client/src/routerDomains/List.js
+++ b/registry/client/src/routerDomains/List.js
@@ -51,6 +51,7 @@ const PostList = props => {
+
diff --git a/registry/server/app.ts b/registry/server/app.ts
index fa70b3ae..cd069b03 100644
--- a/registry/server/app.ts
+++ b/registry/server/app.ts
@@ -40,7 +40,7 @@ export default (withAuth: boolean = true) => {
app.use('/api/v1/auth_entities', authMw, routes.authEntities);
app.use('/api/v1/versioning', authMw, routes.versioning);
app.use('/api/v1/settings', routes.settings(authMw));
- app.use('/api/v1/router_domains', authMw, routes.routerDomains);
+ app.use('/api/v1/router_domains', routes.routerDomains(authMw));
app.use(errorHandler);
diff --git a/registry/server/migrations/20210405164831_router_domains.ts b/registry/server/migrations/20210405164831_router_domains.ts
index 45d73f12..6817ae88 100644
--- a/registry/server/migrations/20210405164831_router_domains.ts
+++ b/registry/server/migrations/20210405164831_router_domains.ts
@@ -5,6 +5,7 @@ export async function up(knex: Knex): Promise {
return knex.schema.createTable('router_domains', table => {
table.increments('id');
table.string('domainName', 255).notNullable();
+ table.string('template500', 50).nullable().references('templates.name')
});
}
diff --git a/registry/server/routerDomains/interfaces/index.ts b/registry/server/routerDomains/interfaces/index.ts
index 5a1866e3..0e6c28f3 100644
--- a/registry/server/routerDomains/interfaces/index.ts
+++ b/registry/server/routerDomains/interfaces/index.ts
@@ -1,18 +1,24 @@
import Joi from 'joi';
+import { templateNameSchema } from '../../templates/interfaces';
export default interface RouterDomains {
id: number,
domainName: string,
+ template500?: string,
};
export const routerDomainIdSchema = Joi.string().trim().required();
-const routerDomainNameSchema = Joi.string().trim().min(1);
+const commonRouterDomainsSchema = {
+ domainName: Joi.string().trim().min(1),
+ template500: templateNameSchema.allow(null),
+};
export const partialRouterDomainsSchema = Joi.object({
- domainName: routerDomainNameSchema,
+ ...commonRouterDomainsSchema,
});
export const routerDomainsSchema = Joi.object({
- domainName: routerDomainNameSchema.required(),
+ ...commonRouterDomainsSchema,
+ domainName: commonRouterDomainsSchema.domainName.required(),
});
diff --git a/registry/server/routerDomains/routes/index.ts b/registry/server/routerDomains/routes/index.ts
index 75c23721..e0a355d7 100644
--- a/registry/server/routerDomains/routes/index.ts
+++ b/registry/server/routerDomains/routes/index.ts
@@ -1,4 +1,4 @@
-import express from 'express';
+import express, { RequestHandler } from 'express';
import getRouterDomains from './getRouterDomains';
import getAllRouterDomains from './getAllRouterDomains';
@@ -6,12 +6,14 @@ import updateRouterDomains from './updateRouterDomains';
import createRouterDomains from './createRouterDomains';
import deleteRouterDomains from './deleteRouterDomains';
-const routerDomainsRouter = express.Router();
+export default (authMw: RequestHandler) => {
+ const routerDomainsRouter = express.Router();
-routerDomainsRouter.get('/', ...getAllRouterDomains);
-routerDomainsRouter.post('/', ...createRouterDomains);
-routerDomainsRouter.get('/:id', ...getRouterDomains);
-routerDomainsRouter.put('/:id', ...updateRouterDomains);
-routerDomainsRouter.delete('/:id', ...deleteRouterDomains);
+ routerDomainsRouter.get('/', ...getAllRouterDomains);
+ routerDomainsRouter.post('/', authMw, ...createRouterDomains);
+ routerDomainsRouter.get('/:id', authMw, ...getRouterDomains);
+ routerDomainsRouter.put('/:id', authMw, ...updateRouterDomains);
+ routerDomainsRouter.delete('/:id', authMw, ...deleteRouterDomains);
-export default routerDomainsRouter;
+ return routerDomainsRouter;
+};
diff --git a/registry/server/seeds/00_cleanup.ts b/registry/server/seeds/00_cleanup.ts
index b17668de..3d60957b 100644
--- a/registry/server/seeds/00_cleanup.ts
+++ b/registry/server/seeds/00_cleanup.ts
@@ -8,8 +8,9 @@ export async function seed(knex: Knex): Promise {
await knex('route_slots').transacting(trx).truncate();
await knex('routes').transacting(trx).truncate();
await knex('apps').transacting(trx).truncate();
- await knex('templates').transacting(trx).truncate();
await knex('shared_props').transacting(trx).truncate();
+ await knex('router_domains').transacting(trx).truncate();
+ await knex('templates').transacting(trx).truncate();
} finally {
isMySQL(knex) && await knex.schema.raw('SET FOREIGN_KEY_CHECKS = 1;').transacting(trx);
}
diff --git a/registry/server/seeds/02_templates.ts b/registry/server/seeds/02_templates.ts
index ff66d94e..b36b84d2 100644
--- a/registry/server/seeds/02_templates.ts
+++ b/registry/server/seeds/02_templates.ts
@@ -17,5 +17,11 @@ export async function seed(knex: Knex): Promise {
encoding: 'utf8',
})
},
+ {
+ name: '500ForLocalhostAsIPv4',
+ content: fs.readFileSync(path.join(__dirname, './data/templates/500ForLocalhostAsIPv4.html'), {
+ encoding: 'utf8',
+ })
+ },
]);
}
diff --git a/registry/server/seeds/06_routerDomains.ts b/registry/server/seeds/06_routerDomains.ts
new file mode 100644
index 00000000..68504dce
--- /dev/null
+++ b/registry/server/seeds/06_routerDomains.ts
@@ -0,0 +1,11 @@
+import * as Knex from 'knex';
+
+export async function seed(knex: Knex): Promise {
+ return knex("router_domains").insert([
+ {
+ id: 1,
+ domainName: '127.0.0.1:8233',
+ template500: '500ForLocalhostAsIPv4',
+ },
+ ]);
+}
diff --git a/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html b/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html
new file mode 100644
index 00000000..9f1152d1
--- /dev/null
+++ b/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html
@@ -0,0 +1,396 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
500 Internal Server Error on 127.0.0.1
+
%ERRORID%
+
Things are a little unstable here
+
I suggest come back later
+
+
+
+
\ No newline at end of file
diff --git a/registry/tests/routerDomains.spec.ts b/registry/tests/routerDomains.spec.ts
index 3c332fc4..582c1a77 100644
--- a/registry/tests/routerDomains.spec.ts
+++ b/registry/tests/routerDomains.spec.ts
@@ -29,6 +29,17 @@ describe(`Tests ${example.url}`, () => {
.expect(422, '"domainName" must be a string');
});
+ it('should not create record with non-existed template500', async () => {
+ const response = await request.post(example.url)
+ .send({
+ ...example.correct,
+ template500: 'nonExistedTemplate',
+ })
+ .expect(500);
+
+ expect(response.text).to.include('Internal server error occurred.');
+ });
+
it('should successfully create record', async () => {
let routerDomainsId;
@@ -56,6 +67,46 @@ describe(`Tests ${example.url}`, () => {
}
});
+ it('should successfully create record with template for 500', async () => {
+ const templateName = 'testTemplate500';
+ let routerDomainsId;
+
+ const exampleWithTemplate500 = {
+ ...example.correct,
+ template500: templateName,
+ };
+
+ try {
+ await request.post('/api/v1/template/')
+ .send({
+ name: templateName,
+ content: 'ncTestTemplateContent'
+ });
+
+ const responseCreation = await request.post(example.url)
+ .send(exampleWithTemplate500)
+ .expect(200)
+
+ routerDomainsId = responseCreation.body.id;
+
+ expect(responseCreation.body).deep.equal({
+ id: routerDomainsId,
+ ...exampleWithTemplate500,
+ });
+
+ const responseFetching = await request.get(example.url + routerDomainsId)
+ .expect(200);
+
+ expect(responseFetching.body).deep.equal({
+ id: routerDomainsId,
+ ...exampleWithTemplate500,
+ });
+ } finally {
+ routerDomainsId && await request.delete(example.url + routerDomainsId);
+ await request.delete('/api/v1/template/' + templateName);
+ }
+ });
+
describe('Authentication / Authorization', () => {
it('should deny access w/o authentication', async () => {
await requestWithAuth.post(example.url)
@@ -159,9 +210,6 @@ describe(`Tests ${example.url}`, () => {
describe('Authentication / Authorization', () => {
it('should deny access w/o authentication', async () => {
- await requestWithAuth.get(example.url)
- .expect(401);
-
await requestWithAuth.get(example.url + 123)
.expect(401);
});
diff --git a/registry/tests/templates.spec.ts b/registry/tests/templates.spec.ts
index 5ff56ae9..d58e81dc 100644
--- a/registry/tests/templates.spec.ts
+++ b/registry/tests/templates.spec.ts
@@ -292,6 +292,38 @@ describe(`Tests ${example.url}`, () => {
expect(response.body).deep.equal({});
});
+ it('should successfully delete record if doesn\'t have any reference from foreign (routerDomains -> template500) to current primary key', async () => {
+ let routerDomainsId;
+
+ try {
+ await request.post(example.url).send(example.correct).expect(200);
+
+ const responseRouterDomains = await request.post('/api/v1/router_domains/')
+ .send({
+ domainName: 'domainNameCorrect',
+ template500: example.correct.name,
+ })
+ .expect(200)
+
+ routerDomainsId = responseRouterDomains.body.id;
+
+ const response = await request.delete(example.url + example.correct.name)
+ .expect(500);
+ expect(response.text).to.include('Internal server error occurred.');
+
+ await request.delete('/api/v1/router_domains/' + routerDomainsId);
+
+ await request.delete(example.url + example.correct.name)
+ .expect(204, '');
+
+ await request.get(example.url + example.correct.name)
+ .expect(404, 'Not found');
+ } finally {
+ routerDomainsId && await request.delete('/api/v1/router_domains/' + routerDomainsId);
+ await request.delete(example.url + example.correct.name)
+ }
+ });
+
it('should successfully delete record', async () => {
await request.post(example.url).send(example.correct).expect(200);