Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the e2e tests wait for the app to start and close before running next test #2952

Merged
merged 5 commits into from
Oct 29, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Special thanks to: @rejas, @sdetweil
- Handle node_helper errors during startup (#2944)
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
- Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840)
- Tests not waiting for the application to start and stop before starting the next test

## [2.21.0] - 2022-10-01

Expand Down
10 changes: 6 additions & 4 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ function App() {
}
}

loadModules(modules, function () {
loadModules(modules, async function () {
httpServer = new Server(config);
const { app, io } = httpServer.open();
const { app, io } = await httpServer.open();
Log.log("Server started ...");

const nodePromises = [];
Expand Down Expand Up @@ -262,14 +262,16 @@ function App() {
* exists.
*
* Added to fix #1056
*
* @param {Function} callback Function to be called after the app has stopped
*/
this.stop = function () {
this.stop = function (callback) {
for (const nodeHelper of nodeHelpers) {
if (typeof nodeHelper.stop === "function") {
nodeHelper.stop();
}
}
httpServer.close();
httpServer.close().then(callback);
};

/**
Expand Down
202 changes: 109 additions & 93 deletions js/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,117 +26,133 @@ function Server(config) {
const serverSockets = new Set();
let server = null;

/**
* Opens the server for incoming connections
*
* @returns {Promise} A promise that is resolved when the server listens to connections
*/
this.open = function () {
if (config.useHttps) {
const options = {
key: fs.readFileSync(config.httpsPrivateKey),
cert: fs.readFileSync(config.httpsCertificate)
};
server = require("https").Server(options, app);
} else {
server = require("http").Server(app);
}
const io = require("socket.io")(server, {
cors: {
origin: /.*$/,
credentials: true
},
allowEIO3: true
});
return new Promise((resolve) => {
if (config.useHttps) {
const options = {
key: fs.readFileSync(config.httpsPrivateKey),
cert: fs.readFileSync(config.httpsCertificate)
};
server = require("https").Server(options, app);
} else {
server = require("http").Server(app);
}
const io = require("socket.io")(server, {
cors: {
origin: /.*$/,
credentials: true
},
allowEIO3: true
});

server.on("connection", (socket) => {
serverSockets.add(socket);
socket.on("close", () => {
serverSockets.delete(socket);
server.on("connection", (socket) => {
serverSockets.add(socket);
socket.on("close", () => {
serverSockets.delete(socket);
});
});
});

Log.log(`Starting server on port ${port} ... `);
server.listen(port, config.address || "localhost");
Log.log(`Starting server on port ${port} ... `);
server.listen(port, config.address || "localhost");

if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
}
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
}

app.use(function (req, res, next) {
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
if (err === undefined) {
res.header("Access-Control-Allow-Origin", "*");
return next();
}
Log.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
app.use(function (req, res, next) {
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
if (err === undefined) {
res.header("Access-Control-Allow-Origin", "*");
return next();
}
Log.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
});
});
});

app.use(helmet(config.httpHeaders));
app.use("/js", express.static(__dirname));

// TODO add tests directory only when running tests?
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
for (const directory of directories) {
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}

app.get("/cors", async function (req, res) {
// example: http://localhost:8080/cors?url=https://google.de

try {
const reg = "^/cors.+url=(.*)";
let url = "";

let match = new RegExp(reg, "g").exec(req.url);
if (!match) {
url = "invalid url: " + req.url;
Log.error(url);
res.send(url);
} else {
url = match[1];
Log.log("cors url: " + url);
const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version } });
const header = response.headers.get("Content-Type");
const data = await response.text();
if (header) res.set("Content-Type", header);
res.send(data);
}
} catch (error) {
Log.error(error);
res.send(error);
app.use(helmet(config.httpHeaders));
app.use("/js", express.static(__dirname));

// TODO add tests directory only when running tests?
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
for (const directory of directories) {
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}
});

app.get("/version", function (req, res) {
res.send(global.version);
});
app.get("/cors", async function (req, res) {
// example: http://localhost:8080/cors?url=https://google.de

try {
const reg = "^/cors.+url=(.*)";
let url = "";

let match = new RegExp(reg, "g").exec(req.url);
if (!match) {
url = "invalid url: " + req.url;
Log.error(url);
res.send(url);
} else {
url = match[1];
Log.log("cors url: " + url);
const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version } });
const header = response.headers.get("Content-Type");
const data = await response.text();
if (header) res.set("Content-Type", header);
res.send(data);
}
} catch (error) {
Log.error(error);
res.send(error);
}
});

app.get("/config", function (req, res) {
res.send(config);
});
app.get("/version", function (req, res) {
res.send(global.version);
});

app.get("/", function (req, res) {
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
html = html.replace("#VERSION#", global.version);
app.get("/config", function (req, res) {
res.send(config);
});

let configFile = "config/config.js";
if (typeof global.configuration_file !== "undefined") {
configFile = global.configuration_file;
}
html = html.replace("#CONFIG_FILE#", configFile);
app.get("/", function (req, res) {
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
html = html.replace("#VERSION#", global.version);

res.send(html);
});
let configFile = "config/config.js";
if (typeof global.configuration_file !== "undefined") {
configFile = global.configuration_file;
}
html = html.replace("#CONFIG_FILE#", configFile);

return {
app,
io
};
res.send(html);
});

server.on("listening", () => {
resolve({
app,
io
});
});
});
};

/**
* Closes the server and destroys all lingering connections to it.
*
* @returns {Promise} A promise that resolves when server has successfully shut down
*/
this.close = function () {
for (const socket of serverSockets.values()) {
socket.destroy();
}
server.close();
return new Promise((resolve) => {
for (const socket of serverSockets.values()) {
socket.destroy();
}
server.close(resolve);
});
};
}

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/env_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const helpers = require("./helpers/global-setup");

describe("App environment", () => {
beforeAll(async () => {
helpers.startApplication("tests/configs/default.js");
await helpers.startApplication("tests/configs/default.js");
await helpers.getDocument();
});
afterAll(async () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/fonts_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ describe("All font files from roboto.css should be downloadable", () => {
match = regex.exec(fileContent);
}

beforeAll(() => {
helpers.startApplication("tests/configs/without_modules.js");
beforeAll(async () => {
await helpers.startApplication("tests/configs/without_modules.js");
});
afterAll(async () => {
await helpers.stopApplication();
Expand Down
28 changes: 15 additions & 13 deletions tests/e2e/helpers/global-setup.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const jsdom = require("jsdom");
const corefetch = require("fetch");

exports.startApplication = (configFilename, exec) => {
exports.startApplication = async (configFilename, exec) => {
jest.resetModules();
this.stopApplication();
if (global.app) {
await this.stopApplication();
}
// Set config sample for use in test
if (configFilename === "") {
process.env.MM_CONFIG_FILE = "config/config.js";
Expand All @@ -12,14 +13,20 @@ exports.startApplication = (configFilename, exec) => {
}
if (exec) exec;
global.app = require("app.js");
global.app.start();

return new Promise((resolve) => {
global.app.start(resolve);
});
};

exports.stopApplication = async () => {
if (global.app) {
global.app.stop();
return new Promise((resolve) => {
global.app.stop(resolve);
delete global.app;
});
}
await new Promise((resolve) => setTimeout(resolve, 100));
return Promise.resolve();
};

exports.getDocument = () => {
Expand Down Expand Up @@ -75,13 +82,8 @@ exports.waitForAllElements = (selector) => {
});
};

exports.fetch = (url) => {
return new Promise((resolve) => {
corefetch(url).then((res) => {
resolve(res);
});
});
};
// When native fetch is used keep-alive is set which causes issues with tests that should not share the connection, fall back to use the older one for now...
exports.fetch = require("node-fetch");

exports.testMatch = async (element, regex) => {
const elem = await this.waitForElement(element);
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/helpers/weather-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ exports.startApp = async (configFile, additionalMockData) => {
let content = fs.readFileSync(path.resolve(__dirname + "../../../../" + configFile)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(__dirname + "../../../../config/config.js"), content);
helpers.startApplication("");
await helpers.startApplication("");
await helpers.getDocument();
};
8 changes: 4 additions & 4 deletions tests/e2e/ipWhitelist_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const helpers = require("./helpers/global-setup");

describe("ipWhitelist directive configuration", () => {
describe("Set ipWhitelist without access", () => {
beforeAll(() => {
helpers.startApplication("tests/configs/noIpWhiteList.js");
beforeAll(async () => {
await helpers.startApplication("tests/configs/noIpWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
Expand All @@ -16,8 +16,8 @@ describe("ipWhitelist directive configuration", () => {
});

describe("Set ipWhitelist []", () => {
beforeAll(() => {
helpers.startApplication("tests/configs/empty_ipWhiteList.js");
beforeAll(async () => {
await helpers.startApplication("tests/configs/empty_ipWhiteList.js");
});
afterAll(async () => {
await helpers.stopApplication();
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/modules/alert_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const helpers = require("../helpers/global-setup");

describe("Alert module", () => {
beforeAll(async () => {
helpers.startApplication("tests/configs/modules/alert/default.js");
await helpers.startApplication("tests/configs/modules/alert/default.js");
await helpers.getDocument();
});
afterAll(async () => {
Expand Down
Loading