diff --git a/.env.dist b/.env.dist
index 3c2c926b..970eeef5 100644
--- a/.env.dist
+++ b/.env.dist
@@ -13,4 +13,4 @@ MANAGER_HTTP_SERVER_HOST=
WORKER_ENABLED=1
WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS=
WORKER_RENDERER_AUTHORIZED_REQUEST_RESOURCES=
-WORKER_RENDERER_REDIRECTED_DOMAINS=
+WORKER_RENDERER_REDIRECTED_DOMAINS=external-nginx|nginx
diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml
index 144c75e0..ec518344 100644
--- a/docker-compose.dev.yaml
+++ b/docker-compose.dev.yaml
@@ -21,12 +21,16 @@ services:
image: nginx:1.20.0
volumes:
- ./fixtures/nginx/html:/usr/share/nginx/html:ro
+ - ./fixtures/nginx/templates:/etc/nginx/templates:ro
test:
image: knplabs/server-side-renderer:test-runner-dev
build:
context: .
target: dev
+ depends_on:
+ - manager
+ - nginx
command: yarn test
volumes:
- ./:/app
diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml
index 32814ab3..1c1a79c2 100644
--- a/docker-compose.test.yaml
+++ b/docker-compose.test.yaml
@@ -10,6 +10,7 @@ services:
- redis
environment:
- MANAGER_ENABLED=1
+ - WORKER_ENABLED=0
- QUEUE_REDIS_DSN=redis://redis:6379
worker:
@@ -20,8 +21,10 @@ services:
depends_on:
- redis
environment:
+ - MANAGER_ENABLED=0
- WORKER_ENABLED=1
- QUEUE_REDIS_DSN=redis://redis:6379
+ - WORKER_RENDERER_REDIRECTED_DOMAINS=external-nginx|nginx
redis:
image: redis:6.2.2-buster
@@ -30,12 +33,17 @@ services:
image: nginx:1.20.0
volumes:
- ./fixtures/nginx/html:/usr/share/nginx/html:ro
+ - ./fixtures/nginx/templates:/etc/nginx/templates:ro
test:
image: knplabs/server-side-renderer:test-runner-test
build:
context: .
target: dev
+ depends_on:
+ - manager
+ - worker
+ - nginx
command: yarn test
volumes:
- ./src:/app/src:ro
diff --git a/fixtures/nginx/html/redirection.html b/fixtures/nginx/html/redirection.html
new file mode 100644
index 00000000..cd795b56
--- /dev/null
+++ b/fixtures/nginx/html/redirection.html
@@ -0,0 +1,23 @@
+
+
+
+
+ A dynamic HTML page (redirections)
+
+
+ A dynamic HTML page (redirections)
+
+
+
diff --git a/fixtures/nginx/templates/default.conf.template b/fixtures/nginx/templates/default.conf.template
new file mode 100644
index 00000000..74bae2e5
--- /dev/null
+++ b/fixtures/nginx/templates/default.conf.template
@@ -0,0 +1,25 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
+
+ return 204;
+ }
+
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
diff --git a/src/configuration.js b/src/configuration.js
index 2bb7b48f..86769968 100644
--- a/src/configuration.js
+++ b/src/configuration.js
@@ -4,8 +4,10 @@ import {
__,
allPass,
anyPass,
+ both,
complement,
compose,
+ equals,
filter,
includes,
isEmpty,
@@ -13,22 +15,38 @@ import {
map,
path,
pipe,
+ reduce,
split,
trim,
unless,
} from 'ramda'
+// isDefined :: Mixed -> Boolean
+const isDefined = both(complement(isNil), complement(isEmpty))
+
// isLogConfigurationValid :: Configuration -> Boolean
const isLogConfigurationValid = compose(includes(__, validLogLevels), path(['log', 'level']))
// isQueueConfigurationValid :: Configuration -> Boolean
-const isQueueConfigurationValid = compose(complement(isEmpty), path(['queue', 'redis_dsn']))
+const isQueueConfigurationValid = compose(isDefined, path(['queue', 'redis_dsn']))
// isManagerConfigurationValid :: Configuration -> Boolean
const isManagerConfigurationValid = T
// isWorkerConfigurationValid :: Configuration -> Boolean
-const isWorkerConfigurationValid = T
+const isWorkerConfigurationValid = pipe(
+ path(['worker', 'renderer', 'domain_redirections']),
+ reduce(
+ (acc, { from, to }) => unless(
+ equals(false),
+ allPass([
+ () => isDefined(from),
+ () => isDefined(to),
+ ]),
+ )(acc),
+ true,
+ ),
+)
// validate :: Configuration -> Boolean
const validate = allPass([
@@ -38,40 +56,48 @@ const validate = allPass([
isWorkerConfigurationValid,
])
-// commaSeparatedStringToArray :: String -> String[]
-const commaSeparatedStringToArray = pipe(
- split(','),
+// stringToArray :: String -> String -> [String]
+const stringToArray = separator => pipe(
+ split(separator),
map(trim),
filter(complement(anyPass([isNil, isEmpty]))),
)
+// commaSeparatedStringToArray :: String -> [String]
+const commaSeparatedStringToArray = stringToArray(',')
+
+// pipeSeparatedStringToArray :: String -> [String]
+const pipeSeparatedStringToArray = stringToArray('|')
+
// generate :: _ -> Configuration
const generate = () => ({
log: {
- level: process.env.LOG_LEVEL || LEVEL_INFO,
+ level: process.env.LOG_LEVEL ?? LEVEL_INFO,
},
queue: {
redis_dsn: process.env.QUEUE_REDIS_DSN,
},
manager: {
- enabled: 1 === Number(process.env.MANAGER_ENABLED),
+ enabled: 1 === Number(process.env.MANAGER_ENABLED ?? 1),
http_server: {
- host: process.env.MANAGER_HTTP_SERVER_HOST || '0.0.0.0',
- port: Number(process.env.MANAGER_HTTP_SERVER_PORT) || 8080,
+ host: process.env.MANAGER_HTTP_SERVER_HOST ?? '0.0.0.0',
+ port: Number(process.env.MANAGER_HTTP_SERVER_PORT ?? 8080),
},
},
worker: {
- enabled: 1 === Number(process.env.WORKER_ENABLED),
+ enabled: 1 === Number(process.env.WORKER_ENABLED ?? 1),
renderer: {
authorized_request_domains: commaSeparatedStringToArray(
- process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS || '*',
+ process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS ?? '*',
),
authorized_request_resources: commaSeparatedStringToArray(
- process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_RESOURCES || '*',
- ),
- domain_redirections: commaSeparatedStringToArray(
- process.env.WORKER_RENDERER_REDIRECTED_DOMAINS || '',
+ process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_RESOURCES ?? '*',
),
+ domain_redirections: pipe(
+ commaSeparatedStringToArray,
+ map(pipeSeparatedStringToArray),
+ map(([from, to]) => ({ from, to })),
+ )(process.env.WORKER_RENDERER_REDIRECTED_DOMAINS ?? ''),
},
},
})
diff --git a/src/configuration.test.js b/src/configuration.test.js
index f3a08f53..96fb2efe 100644
--- a/src/configuration.test.js
+++ b/src/configuration.test.js
@@ -1,90 +1,175 @@
import createConfiguration from './configuration'
+const ORIGINAL_ENV = process.env
+
+beforeEach(() => {
+ jest.resetModules() // Most important - it clears the cache
+ process.env = { ...ORIGINAL_ENV } // Make a copy
+})
+
+afterAll(() => {
+ process.env = ORIGINAL_ENV // Restore old environment
+})
+
describe('configuration :: createConfiguration', () => {
- it(`creates a configuration with default values`, () => {
- process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
-
- expect(createConfiguration()).toStrictEqual({
- log: {
- level: 'INFO',
- },
- manager: {
- enabled: false,
- http_server: {
- host: '0.0.0.0',
- port: 8080,
- },
- },
- queue: {
- redis_dsn: 'redis://redis:6379',
- },
- worker: {
- enabled: false,
- renderer: {
- authorized_request_domains: [
- '*',
- ],
- authorized_request_resources: [
- '*',
- ],
- domain_redirections: [],
- },
- },
- })
+ it(`creates a configuration with default values`, () => {
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+
+ expect(createConfiguration()).toStrictEqual({
+ log: {
+ level: 'INFO',
+ },
+ manager: {
+ enabled: true,
+ http_server: {
+ host: '0.0.0.0',
+ port: 8080,
+ },
+ },
+ queue: {
+ redis_dsn: 'redis://redis:6379',
+ },
+ worker: {
+ enabled: true,
+ renderer: {
+ authorized_request_domains: [
+ '*',
+ ],
+ authorized_request_resources: [
+ '*',
+ ],
+ domain_redirections: [],
+ },
+ },
})
+ })
+
+ it(`creates a configuration with a manager disabled`, () => {
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+ process.env.MANAGER_ENABLED = 0
- it(`creates a configuration`, () => {
- process.env.LOG_LEVEL = 'ERROR'
- process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
- process.env.MANAGER_ENABLED = 1
- process.env.MANAGER_HTTP_SERVER_PORT = 8081
- process.env.MANAGER_HTTP_SERVER_HOST = '127.0.0.1'
- process.env.WORKER_ENABLED = 1
- process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS = 'localhost, nginx'
- process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_RESOURCES = 'document, script'
- process.env.WORKER_RENDERER_REDIRECTED_DOMAINS = 'example.com|nginx'
-
- expect(createConfiguration()).toStrictEqual({
- log: {
- level: 'ERROR',
- },
- manager: {
- enabled: true,
- http_server: {
- host: '127.0.0.1',
- port: 8081,
- },
- },
- queue: {
- redis_dsn: 'redis://redis:6379',
- },
- worker: {
- enabled: true,
- renderer: {
- authorized_request_domains: [
- 'localhost',
- 'nginx',
- ],
- authorized_request_resources: [
- 'document',
- 'script',
- ],
- domain_redirections: [
- 'example.com|nginx'
- ],
- },
- },
- })
+ expect(createConfiguration()).toStrictEqual({
+ log: {
+ level: 'INFO',
+ },
+ manager: {
+ enabled: false,
+ http_server: {
+ host: '0.0.0.0',
+ port: 8080,
+ },
+ },
+ queue: {
+ redis_dsn: 'redis://redis:6379',
+ },
+ worker: {
+ enabled: true,
+ renderer: {
+ authorized_request_domains: [
+ '*',
+ ],
+ authorized_request_resources: [
+ '*',
+ ],
+ domain_redirections: [],
+ },
+ },
})
+ })
- it(`throws an exception when the log configuration is invalid`, () => {
- process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
- process.env.LOG_LEVEL = 'invalid'
+ it(`creates a configuration with a worker disabled`, () => {
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+ process.env.WORKER_ENABLED = 0
- expect(() => createConfiguration()).toThrow('Invalid configuration.')
+ expect(createConfiguration()).toStrictEqual({
+ log: {
+ level: 'INFO',
+ },
+ manager: {
+ enabled: true,
+ http_server: {
+ host: '0.0.0.0',
+ port: 8080,
+ },
+ },
+ queue: {
+ redis_dsn: 'redis://redis:6379',
+ },
+ worker: {
+ enabled: false,
+ renderer: {
+ authorized_request_domains: [
+ '*',
+ ],
+ authorized_request_resources: [
+ '*',
+ ],
+ domain_redirections: [],
+ },
+ },
})
+ })
- it(`throws an exception when the queue configuration is invalid`, () => {
- expect(() => createConfiguration()).toThrow('Invalid configuration.')
+ it(`creates a configuration`, () => {
+ process.env.LOG_LEVEL = 'ERROR'
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+ process.env.MANAGER_ENABLED = 1
+ process.env.MANAGER_HTTP_SERVER_PORT = 8081
+ process.env.MANAGER_HTTP_SERVER_HOST = '127.0.0.1'
+ process.env.WORKER_ENABLED = 1
+ process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS = 'localhost, nginx'
+ process.env.WORKER_RENDERER_AUTHORIZED_REQUEST_RESOURCES = 'document, script'
+ process.env.WORKER_RENDERER_REDIRECTED_DOMAINS = 'example.com|nginx'
+
+ expect(createConfiguration()).toStrictEqual({
+ log: {
+ level: 'ERROR',
+ },
+ manager: {
+ enabled: true,
+ http_server: {
+ host: '127.0.0.1',
+ port: 8081,
+ },
+ },
+ queue: {
+ redis_dsn: 'redis://redis:6379',
+ },
+ worker: {
+ enabled: true,
+ renderer: {
+ authorized_request_domains: [
+ 'localhost',
+ 'nginx',
+ ],
+ authorized_request_resources: [
+ 'document',
+ 'script',
+ ],
+ domain_redirections: [{
+ from: 'example.com',
+ to: 'nginx',
+ }],
+ },
+ },
})
+ })
+
+ it(`throws an exception when the log configuration is invalid`, () => {
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+ process.env.LOG_LEVEL = 'invalid'
+
+ expect(() => createConfiguration()).toThrow('Invalid configuration.')
+ })
+
+ it(`throws an exception when the queue configuration is invalid`, () => {
+ expect(() => createConfiguration()).toThrow('Invalid configuration.')
+ })
+
+ it(`throws an exception when the worker configuration is invalid`, () => {
+ process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
+ process.env.WORKER_RENDERER_REDIRECTED_DOMAINS = 'example.com|'
+
+ expect(() => createConfiguration()).toThrow('Invalid configuration.')
+ })
})
diff --git a/src/e2e.test.js b/src/e2e.test.js
index 6d95328a..2f1471ac 100644
--- a/src/e2e.test.js
+++ b/src/e2e.test.js
@@ -58,3 +58,38 @@ describe('e2e :: dynamic', () => {
`))
})
})
+
+describe('e2e :: redirection', () => {
+ it(`asserts that the page is rendered`, async () => {
+ const res = await fetch(`http://manager:8080/render?url=http://nginx/redirection.html`)
+ const content = await res.text()
+
+ expect(res.status).toBe(200)
+ expect(trimStringForComparison(content)).toBe(trimStringForComparison(`
+
+
+
+
+ A dynamic HTML page (redirections)
+
+
+ A dynamic HTML page (redirections)
+
+ Some dynamic content.
+
+
+ `))
+ })
+})
diff --git a/src/logger.js b/src/logger.js
index 3e6dac11..52d1adf6 100644
--- a/src/logger.js
+++ b/src/logger.js
@@ -1,4 +1,4 @@
-import { F, always, bind, equals, findIndex, gt, gte, ifElse, partial, pickAll, pipe, when } from 'ramda'
+import { F, always, bind, equals, findIndex, gte, ifElse, partial, pickAll } from 'ramda'
/**
* @type Logger = {
diff --git a/src/logger.test.js b/src/logger.test.js
index 647b62c8..490fb57e 100644
--- a/src/logger.test.js
+++ b/src/logger.test.js
@@ -1,153 +1,153 @@
import { LEVEL_DEBUG, LEVEL_ERROR, LEVEL_INFO, LEVEL_WARN, default as createLogger } from './logger'
describe('logger :: createLogger', () => {
- it(`creates a logger with DEBUG level`, () => {
- const debugMock = jest.fn();
- const infoMock = jest.fn();
- const warnMock = jest.fn();
- const errorMock = jest.fn();
-
- const outputMock = {
- log: debugMock,
- info: infoMock,
- warn: warnMock,
- error: errorMock,
- }
-
- const logger = createLogger(LEVEL_DEBUG, outputMock)
-
- logger.debug('debug log')
- logger.info('info log')
- logger.warn('warn log')
- logger.error('error log')
-
- expect(debugMock.mock.calls.length).toBe(1)
- expect(debugMock.mock.calls[0][0]).toMatch(/ DEBUG:$/)
- expect(debugMock.mock.calls[0][1]).toBe('debug log')
- expect(infoMock.mock.calls.length).toBe(1)
- expect(infoMock.mock.calls[0][0]).toMatch(/ INFO:$/)
- expect(infoMock.mock.calls[0][1]).toBe('info log')
- expect(warnMock.mock.calls.length).toBe(1)
- expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
- expect(warnMock.mock.calls[0][1]).toBe('warn log')
- expect(errorMock.mock.calls.length).toBe(1)
- expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
- expect(errorMock.mock.calls[0][1]).toBe('error log')
- })
-
- it(`creates a logger with INFO level`, () => {
- const debugMock = jest.fn();
- const infoMock = jest.fn();
- const warnMock = jest.fn();
- const errorMock = jest.fn();
-
- const outputMock = {
- log: debugMock,
- info: infoMock,
- warn: warnMock,
- error: errorMock,
- }
-
- const logger = createLogger(LEVEL_INFO, outputMock)
-
- logger.debug('debug log')
- logger.info('info log')
- logger.warn('warn log')
- logger.error('error log')
-
- expect(debugMock.mock.calls.length).toBe(0)
- expect(infoMock.mock.calls.length).toBe(1)
- expect(infoMock.mock.calls[0][0]).toMatch(/ INFO:$/)
- expect(infoMock.mock.calls[0][1]).toBe('info log')
- expect(warnMock.mock.calls.length).toBe(1)
- expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
- expect(warnMock.mock.calls[0][1]).toBe('warn log')
- expect(errorMock.mock.calls.length).toBe(1)
- expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
- expect(errorMock.mock.calls[0][1]).toBe('error log')
- })
-
- it(`creates a logger with WARN level`, () => {
- const debugMock = jest.fn();
- const infoMock = jest.fn();
- const warnMock = jest.fn();
- const errorMock = jest.fn();
-
- const outputMock = {
- log: debugMock,
- info: infoMock,
- warn: warnMock,
- error: errorMock,
- }
-
- const logger = createLogger(LEVEL_WARN, outputMock)
-
- logger.debug('debug log')
- logger.info('info log')
- logger.warn('warn log')
- logger.error('error log')
-
- expect(debugMock.mock.calls.length).toBe(0)
- expect(infoMock.mock.calls.length).toBe(0)
- expect(warnMock.mock.calls.length).toBe(1)
- expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
- expect(warnMock.mock.calls[0][1]).toBe('warn log')
- expect(errorMock.mock.calls.length).toBe(1)
- expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
- expect(errorMock.mock.calls[0][1]).toBe('error log')
- })
-
- it(`creates a logger with ERROR level`, () => {
- const debugMock = jest.fn();
- const infoMock = jest.fn();
- const warnMock = jest.fn();
- const errorMock = jest.fn();
-
- const outputMock = {
- log: debugMock,
- info: infoMock,
- warn: warnMock,
- error: errorMock,
- }
-
- const logger = createLogger(LEVEL_ERROR, outputMock)
-
- logger.debug('debug log')
- logger.info('info log')
- logger.warn('warn log')
- logger.error('error log')
-
- expect(debugMock.mock.calls.length).toBe(0)
- expect(infoMock.mock.calls.length).toBe(0)
- expect(warnMock.mock.calls.length).toBe(0)
- expect(errorMock.mock.calls.length).toBe(1)
- expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
- expect(errorMock.mock.calls[0][1]).toBe('error log')
- })
-
- it(`creates a logger with an invalid level`, () => {
- const debugMock = jest.fn();
- const infoMock = jest.fn();
- const warnMock = jest.fn();
- const errorMock = jest.fn();
-
- const outputMock = {
- log: debugMock,
- info: infoMock,
- warn: warnMock,
- error: errorMock,
- }
-
- const logger = createLogger('invalid', outputMock)
-
- logger.debug('debug log')
- logger.info('info log')
- logger.warn('warn log')
- logger.error('error log')
-
- expect(debugMock.mock.calls.length).toBe(0)
- expect(infoMock.mock.calls.length).toBe(0)
- expect(warnMock.mock.calls.length).toBe(0)
- expect(errorMock.mock.calls.length).toBe(0)
- })
+ it(`creates a logger with DEBUG level`, () => {
+ const debugMock = jest.fn()
+ const infoMock = jest.fn()
+ const warnMock = jest.fn()
+ const errorMock = jest.fn()
+
+ const outputMock = {
+ log: debugMock,
+ info: infoMock,
+ warn: warnMock,
+ error: errorMock,
+ }
+
+ const logger = createLogger(LEVEL_DEBUG, outputMock)
+
+ logger.debug('debug log')
+ logger.info('info log')
+ logger.warn('warn log')
+ logger.error('error log')
+
+ expect(debugMock.mock.calls.length).toBe(1)
+ expect(debugMock.mock.calls[0][0]).toMatch(/ DEBUG:$/)
+ expect(debugMock.mock.calls[0][1]).toBe('debug log')
+ expect(infoMock.mock.calls.length).toBe(1)
+ expect(infoMock.mock.calls[0][0]).toMatch(/ INFO:$/)
+ expect(infoMock.mock.calls[0][1]).toBe('info log')
+ expect(warnMock.mock.calls.length).toBe(1)
+ expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
+ expect(warnMock.mock.calls[0][1]).toBe('warn log')
+ expect(errorMock.mock.calls.length).toBe(1)
+ expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
+ expect(errorMock.mock.calls[0][1]).toBe('error log')
+ })
+
+ it(`creates a logger with INFO level`, () => {
+ const debugMock = jest.fn()
+ const infoMock = jest.fn()
+ const warnMock = jest.fn()
+ const errorMock = jest.fn()
+
+ const outputMock = {
+ log: debugMock,
+ info: infoMock,
+ warn: warnMock,
+ error: errorMock,
+ }
+
+ const logger = createLogger(LEVEL_INFO, outputMock)
+
+ logger.debug('debug log')
+ logger.info('info log')
+ logger.warn('warn log')
+ logger.error('error log')
+
+ expect(debugMock.mock.calls.length).toBe(0)
+ expect(infoMock.mock.calls.length).toBe(1)
+ expect(infoMock.mock.calls[0][0]).toMatch(/ INFO:$/)
+ expect(infoMock.mock.calls[0][1]).toBe('info log')
+ expect(warnMock.mock.calls.length).toBe(1)
+ expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
+ expect(warnMock.mock.calls[0][1]).toBe('warn log')
+ expect(errorMock.mock.calls.length).toBe(1)
+ expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
+ expect(errorMock.mock.calls[0][1]).toBe('error log')
+ })
+
+ it(`creates a logger with WARN level`, () => {
+ const debugMock = jest.fn()
+ const infoMock = jest.fn()
+ const warnMock = jest.fn()
+ const errorMock = jest.fn()
+
+ const outputMock = {
+ log: debugMock,
+ info: infoMock,
+ warn: warnMock,
+ error: errorMock,
+ }
+
+ const logger = createLogger(LEVEL_WARN, outputMock)
+
+ logger.debug('debug log')
+ logger.info('info log')
+ logger.warn('warn log')
+ logger.error('error log')
+
+ expect(debugMock.mock.calls.length).toBe(0)
+ expect(infoMock.mock.calls.length).toBe(0)
+ expect(warnMock.mock.calls.length).toBe(1)
+ expect(warnMock.mock.calls[0][0]).toMatch(/ WARN:$/)
+ expect(warnMock.mock.calls[0][1]).toBe('warn log')
+ expect(errorMock.mock.calls.length).toBe(1)
+ expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
+ expect(errorMock.mock.calls[0][1]).toBe('error log')
+ })
+
+ it(`creates a logger with ERROR level`, () => {
+ const debugMock = jest.fn()
+ const infoMock = jest.fn()
+ const warnMock = jest.fn()
+ const errorMock = jest.fn()
+
+ const outputMock = {
+ log: debugMock,
+ info: infoMock,
+ warn: warnMock,
+ error: errorMock,
+ }
+
+ const logger = createLogger(LEVEL_ERROR, outputMock)
+
+ logger.debug('debug log')
+ logger.info('info log')
+ logger.warn('warn log')
+ logger.error('error log')
+
+ expect(debugMock.mock.calls.length).toBe(0)
+ expect(infoMock.mock.calls.length).toBe(0)
+ expect(warnMock.mock.calls.length).toBe(0)
+ expect(errorMock.mock.calls.length).toBe(1)
+ expect(errorMock.mock.calls[0][0]).toMatch(/ ERROR:$/)
+ expect(errorMock.mock.calls[0][1]).toBe('error log')
+ })
+
+ it(`creates a logger with an invalid level`, () => {
+ const debugMock = jest.fn()
+ const infoMock = jest.fn()
+ const warnMock = jest.fn()
+ const errorMock = jest.fn()
+
+ const outputMock = {
+ log: debugMock,
+ info: infoMock,
+ warn: warnMock,
+ error: errorMock,
+ }
+
+ const logger = createLogger('invalid', outputMock)
+
+ logger.debug('debug log')
+ logger.info('info log')
+ logger.warn('warn log')
+ logger.error('error log')
+
+ expect(debugMock.mock.calls.length).toBe(0)
+ expect(infoMock.mock.calls.length).toBe(0)
+ expect(warnMock.mock.calls.length).toBe(0)
+ expect(errorMock.mock.calls.length).toBe(0)
+ })
})
diff --git a/src/queue.test.js b/src/queue.test.js
index 7920ad77..ec2701ff 100644
--- a/src/queue.test.js
+++ b/src/queue.test.js
@@ -1,28 +1,28 @@
-import Bull from 'bull';
+import Bull from 'bull'
import createQueue from './queue'
-jest.mock('bull');
+jest.mock('bull')
beforeEach(() => {
- Bull.mockClear();
-});
+ Bull.mockClear()
+})
describe('queue :: createQueue', () => {
- it(`creates a queue with default options`, () => {
- const queueMock = { add: () => {}, process: () => {} }
- Bull.mockResolvedValue(queueMock);
+ it(`creates a queue with default options`, () => {
+ const queueMock = { add: () => {}, process: () => {} }
+ Bull.mockResolvedValue(queueMock)
- expect(createQueue('redis://redis:6379')).resolves.toBe(queueMock)
- expect(Bull).toHaveBeenCalledTimes(1)
- expect(Bull).toHaveBeenCalledWith('request-queue', 'redis://redis:6379', {})
- })
+ expect(createQueue('redis://redis:6379')).resolves.toBe(queueMock) // eslint-disable-line jest/valid-expect
+ expect(Bull).toHaveBeenCalledTimes(1)
+ expect(Bull).toHaveBeenCalledWith('request-queue', 'redis://redis:6379', {})
+ })
- it(`creates a queue with specified options`, () => {
- const queueMock = { add: () => {}, process: () => {} }
- Bull.mockResolvedValue(queueMock);
+ it(`creates a queue with specified options`, () => {
+ const queueMock = { add: () => {}, process: () => {} }
+ Bull.mockResolvedValue(queueMock)
- expect(createQueue('redis://redis:6379', { myOption: 'myOptionValue' })).resolves.toBe(queueMock)
- expect(Bull).toHaveBeenCalledTimes(1)
- expect(Bull).toHaveBeenCalledWith('request-queue', 'redis://redis:6379', { myOption: 'myOptionValue' })
- })
+ expect(createQueue('redis://redis:6379', { myOption: 'myOptionValue' })).resolves.toBe(queueMock) // eslint-disable-line jest/valid-expect
+ expect(Bull).toHaveBeenCalledTimes(1)
+ expect(Bull).toHaveBeenCalledWith('request-queue', 'redis://redis:6379', { myOption: 'myOptionValue' })
+ })
})
diff --git a/src/worker/renderers/chrome/browserRequestHandler.js b/src/worker/renderers/chrome/browserRequestHandler.js
new file mode 100644
index 00000000..38c53c74
--- /dev/null
+++ b/src/worker/renderers/chrome/browserRequestHandler.js
@@ -0,0 +1,62 @@
+import { T, anyPass, complement, cond, equals, find, ifElse, isNil, pipe, propEq, test } from 'ramda'
+
+// resolveRequestDomain :: Request -> String
+const resolveRequestDomain = req => req.url().match(/^(https?:\/\/)?(?[^/]+)/).groups.host
+
+// isMatchingDomain :: String -> String -> Boolean
+const isMatchingDomain = input => anyPass([
+ equals('*'),
+ value => test(new RegExp(`${value}$`, 'i'), input),
+])
+
+// isRequestDomainAuthorized :: [String] -> Request -> Boolean
+const isRequestDomainAuthorized = authorizedRequestDomains => pipe(
+ resolveRequestDomain,
+ domain => find(isMatchingDomain(domain), authorizedRequestDomains),
+ complement(isNil),
+)
+
+// isMatchingResourceType :: String -> String -> Boolean
+const isMatchingResourceType = input => anyPass([
+ equals('*'),
+ equals(input),
+])
+
+// isRequestResourceAuthorized :: [String] -> Request -> Boolean
+const isRequestResourceAuthorized = authorizedRequestResources => pipe(
+ req => req.resourceType(),
+ resourceType => find(isMatchingResourceType(resourceType), authorizedRequestResources),
+ complement(isNil),
+)
+
+// getDomainRedirection :: [Object] -> Request -> Object|Null
+const getDomainRedirection = domainRedirections => domain => find(propEq('from', domain), domainRedirections)
+
+// allowRequest :: Request -> _
+const allowRequest = domainRedirections => req => pipe(
+ resolveRequestDomain,
+ getDomainRedirection(domainRedirections),
+ ifElse(
+ isNil,
+ () => req.continue(),
+ ({ from, to }) => req.continue({
+ url: req.url().replace(from, to),
+ }),
+ ),
+)(req)
+
+// blockRequest :: (Logger, String) -> Request -> _
+const blockRequest = (logger, reason) => req => logger.debug(`Abort request ${req.url()} because of non authorized ${reason}.`) || req.abort()
+
+// browserRequestHandler :: (Configuration, Logger) => Puppeteer.Request => _
+export default (configuration, logger) => cond([
+ [
+ complement(isRequestDomainAuthorized(configuration.worker.renderer.authorized_request_domains)),
+ blockRequest(logger, 'domain'),
+ ],
+ [
+ complement(isRequestResourceAuthorized(configuration.worker.renderer.authorized_request_resources)),
+ blockRequest(logger, 'resource type'),
+ ],
+ [T, allowRequest(configuration.worker.renderer.domain_redirections)],
+])
diff --git a/src/worker/renderers/chrome/index.js b/src/worker/renderers/chrome/index.js
index a0d9861d..c8d6183c 100644
--- a/src/worker/renderers/chrome/index.js
+++ b/src/worker/renderers/chrome/index.js
@@ -1,59 +1,17 @@
-import { T, anyPass, complement, cond, equals, find, isNil, pipe, test } from 'ramda'
+import browserRequestHandler from './browserRequestHandler'
import { formatException } from './../../../logger'
import getBrowserProvider from './browserProvider'
-// resolveRequestDomain :: Request -> String
-const resolveRequestDomain = req => req.url().match(/^(https?:\/\/)?(?[^/]+)/).groups.host
-
-// isMatchingDomain :: String -> String -> Boolean
-const isMatchingDomain = input => anyPass([
- equals('*'),
- value => test(new RegExp(`${value}$`, 'i'), input),
-])
-
-// isRequestDomainAuthorized :: [String] -> Request -> Boolean
-const isRequestDomainAuthorized = authorizedRequestDomains => pipe(
- resolveRequestDomain,
- domain => find(isMatchingDomain(domain), authorizedRequestDomains),
- complement(isNil),
-)
-
-// isMatchingResourceType :: String -> String -> Boolean
-const isMatchingResourceType = input => anyPass([
- equals('*'),
- equals(input),
-])
-
-// isRequestResourceAuthorized :: [String] -> Request -> Boolean
-const isRequestResourceAuthorized = authorizedRequestResources => pipe(
- req => req.resourceType(),
- resourceType => find(isMatchingResourceType(resourceType), authorizedRequestResources),
- complement(isNil),
-)
-
-// allowRequest :: Request -> _
-const allowRequest = req => req.continue()
-
-// blockRequest :: (Logger, String) -> Request -> _
-const blockRequest = (logger, reason) => req => logger.debug(`Abort request ${req.url()} because of non authorized ${reason}.`) || req.abort()
-
// renderPageContent :: (Configuration, Logger, BrowserInstance, String) -> RenderedPage
const renderPageContent = async (configuration, logger, browserInstance, url) => {
const page = await browserInstance.newPage()
await page.setRequestInterception(true)
- page.on('request', cond([
- [
- complement(isRequestDomainAuthorized(configuration.worker.renderer.authorized_request_domains)),
- blockRequest(logger, 'domain'),
- ],
- [
- complement(isRequestResourceAuthorized(configuration.worker.renderer.authorized_request_resources)),
- blockRequest(logger, 'resource type'),
- ],
- // @todo add request redirection
- [T, allowRequest],
- ]))
+
+ page.on('request', browserRequestHandler(configuration, logger))
+ page.on('error', error => logger.error(formatException(error)))
+ page.on('pageerror', error => logger.error(formatException(error)))
+ page.on('requestfailed', req => logger.warn(`Browser request failed. ${req.url()}.`))
await page.goto(url, {
waitUntil: 'networkidle0',