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',