Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ jobs:

strategy:
matrix:
node-version: [20.x]
# MuhammaraJS does not have pre-built binaries for Node 25, skip this version for now, though docker uses it
node-version: [20.x, 24.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.claude

dist
dist/
Expand Down
106 changes: 77 additions & 29 deletions __tests__/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,58 @@ const path = require('path');
const fs = require('fs');
const http = require('http');
const https = require('https');
const testDataPDF = require('./samples/smoke/base_https.pdf.json');
const testDataPNG = require('./samples/smoke/base_https.png.json');
const { getTmpFilePath, assertImage } = require('./utils.js');
const { getTmpFilePath } = require('./utils.js');

const testPageHTML = fs.readFileSync(path.join(__dirname, 'samples/smoke/base.html'), 'utf-8');
const commonTestData = {
// Navigate to this URL to fix web security issues
clientURL : 'http://localhost:{port}/resources/build/grid.css',
orientation : 'portrait',
// This is calculated canvas size for the HTML being rendered
format : '1120*2389',
fileName : 'base_https',
sendAsBinary : true
}
const testDataPDF = {
...commonTestData,
html : [{ html : testPageHTML }],
fileFormat : 'pdf'
}
const testDataPNG = {
...commonTestData,
html : [{ html : testPageHTML }],
fileFormat : 'png'
}

// https://github.com/request/request/issues/418#issuecomment-274105600
// Allow self-signed certificates
https.globalAgent.options.rejectUnauthorized = false;

/**
* @param {String} json
* @param {'http'|'https'} protocol
* @param {'pdf'|'png'} fileFormat
* @param {String} host
* @param {Number} port
* @param {Number} timeout
* @returns {Promise<Buffer>}
*/
async function getFile(json, protocol, fileFormat, host, port, timeout) {
json = json.replace(/{port}/g, String(port));

// Default timeout: 30 seconds for CI environments
const requestTimeout = timeout != null ? timeout : 30000;

return new Promise((resolve, reject) => {
let settled = false;

const settle = (fn, value) => {
if (!settled) {
settled = true;
fn(value);
}
};

const request = (protocol === 'http' ? http : https).request({
hostname : host,
port : port,
Expand All @@ -20,7 +62,7 @@ async function getFile(json, protocol, fileFormat, host, port, timeout) {
'Content-Type' : 'application/json',
'Content-Length' : Buffer.byteLength(json)
},
timeout : timeout != null ? timeout : undefined
timeout : requestTimeout
}, response => {
const chunks = [];
response.on('data', function(data) {
Expand All @@ -30,60 +72,66 @@ async function getFile(json, protocol, fileFormat, host, port, timeout) {
const result = Buffer.concat(chunks);

if (response.statusCode === 200) {
// fs.writeFileSync(path.join(__dirname, `test.${fileFormat}`), result);
resolve(result);
settle(resolve, result);
}
else if (/application\/json/.test(response.headers['content-type'])) {
reject(new Error(result.toString()));
settle(reject, new Error(result.toString()));
}
else {
reject('Request ended unexpectedly');
settle(reject, new Error('Request ended unexpectedly'));
}
});
});

request.on('timeout', () => {
request.destroy();
settle(reject, new Error('timeout'));
});

reject(new Error('timeout'));
// Handle errors to prevent unhandled 'error' events after timeout/destroy
request.on('error', (error) => {
settle(reject, error);
});

request.write(json);
request.end();
});
}

async function assertExportedFile({ protocol, host, port, fileFormat }) {
async function assertExportedFile({ protocol, host, port, fileFormat, timeout }) {
const json = JSON.stringify(fileFormat === 'pdf' ? testDataPDF : testDataPNG);

const exportedFile = await getFile(json, protocol, fileFormat, host, port);

let result = false;

if (fileFormat === 'png') {
result = await assertImage(path.join(process.cwd(), '__tests__', 'samples', 'smoke', 'base_https.png'), exportedFile);
}
else {
let baseSize = fs.statSync(path.join(process.cwd(), '__tests__', 'samples', 'smoke', `base_https.pdf`)).size;
const exportedFile = await getFile(json, protocol, fileFormat, host, port, timeout);

const gotSize = Math.abs(baseSize - exportedFile.length);
const expectedSize = baseSize * 0.05;
let baseSize = fs.statSync(path.join(process.cwd(), '__tests__', 'samples', 'smoke', `base_https.${fileFormat}`)).size;

if (gotSize > expectedSize) {
const tmpFilePath = getTmpFilePath(fileFormat);
const gotSize = Math.abs(baseSize - exportedFile.length);
const expectedSize = baseSize * 0.05;

fs.writeFileSync(tmpFilePath, exportedFile);
if (gotSize > expectedSize) {
const tmpFilePath = getTmpFilePath(fileFormat);

fail(`${fileFormat} length differs very much from expected.\nCheck exported file here: ${tmpFilePath}`);
}
fs.writeFileSync(tmpFilePath, exportedFile);

expect(gotSize).toBeLessThanOrEqual(expectedSize);
fail(`${fileFormat} length differs very much from expected.\nCheck exported file here: ${tmpFilePath}`);
}

return result;
expect(gotSize).toBeLessThanOrEqual(expectedSize);
}

async function waitForWithTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms.`));
}, timeout);
})
]);
}

module.exports = {
getFile,
assertExportedFile
assertExportedFile,
waitForWithTimeout
};
82 changes: 82 additions & 0 deletions __tests__/e2e/http.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* E2E tests for HTTP connectivity.
* These tests verify that the HTTP server correctly receives requests and returns responses.
* Most export logic testing is done in queue tests which are faster.
*/
const fs = require('fs');
const path = require('path');
const { startServer, stopServer, getLoggerConfig, getPort, certExists } = require('../utils.js');
const { assertExportedFile } = require('../assertions.js');

jest.setTimeout(60 * 1000);

let server;

afterEach(() => {
if (server) {
return stopServer(server).then(() => server = null);
}
});

describe('E2E HTTP Export', () => {
test('Should accept POST request and return PDF', async () => {
const port = getPort();

server = await startServer({
protocol : 'http',
port,
workers : 1,
logger : getLoggerConfig('e2e_http_pdf')
});

await assertExportedFile({
fileFormat : 'pdf',
host : 'localhost',
protocol : 'http',
port : server.httpPort
});
});

test('Should accept POST request and return PNG', async () => {
const port = getPort();

server = await startServer({
protocol : 'http',
port,
workers : 1,
logger : getLoggerConfig('e2e_http_png')
});

await assertExportedFile({
fileFormat : 'png',
host : 'localhost',
protocol : 'http',
port : server.httpPort
});
});
});

describe('E2E HTTPS Export', () => {
if (certExists) {
test('Should accept POST request over HTTPS', async () => {
const port = getPort();

server = await startServer({
protocol : 'https',
port,
workers : 1,
logger : getLoggerConfig('e2e_https_pdf')
});

await assertExportedFile({
fileFormat : 'pdf',
host : 'localhost',
protocol : 'https',
port : server.httpsPort
});
});
}
else {
test('Cert is not found, skipping HTTPS tests', () => {});
}
});
Loading
Loading