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

chore(tests): [RO-26598] Improve test coverage #83

Merged
merged 7 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 6 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
quote_type = single
trim_trailing_whitespace = true

[node_modules/**.js]
codepaint = false
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"prettier.enable": false,
}
12 changes: 6 additions & 6 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ module.exports = {
coveragePathIgnorePatterns: ['/node_modules/'],
coverageThreshold: {
global: {
branches: 62,
functions: 84,
lines: 89,
statements: 89
}
branches: 88,
functions: 98,
lines: 98,
statements: 98,
},
},
maxWorkers: 1,
moduleFileExtensions: ['js','json'],
moduleFileExtensions: ['js', 'json'],
rootDir: '.',
testEnvironment: 'node',
testMatch: ['**/*.spec.js'],
Expand Down
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"devDependencies": {
"@types/jest": "29.5.12",
"colors": "1.4.0",
"ejs": "3.1.9",
Copy link
Contributor Author

@otrreeves otrreeves Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary for testing the express server's render method (called via renderAsync from PromiseMiddleware), which wasn't being tested at all previously. I also couldn't find usage within the OpenTable codebase, but this is a public repo so it could be used somewhere in the world.

The alternative would be to instruct the test coverage to ignore the code, but the code did contain errors...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be better if you replace it with a mock and add it as an injection override. This is one of the more annoying dependencies in the system. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"eslint": "8.42.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.7.0",
Expand Down
27 changes: 10 additions & 17 deletions src/middleware/ErrorMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
const _some = require('lodash.some');
const _assignIn = require('lodash.assignin');

module.exports = function (
SpurErrors,
Logger,
HtmlErrorRender,
BaseMiddleware
) {
module.exports = function (SpurErrors, Logger, HtmlErrorRender, BaseMiddleware) {
class ErrorMiddleware extends BaseMiddleware {

configure(app) {
super.configure(app);

Expand All @@ -34,29 +28,28 @@ module.exports = function (
res.format({
text: () => this.sendTextResponse(err, req, res),
html: () => this.sendHtmlResponse(err, req, res),
json: () => this.sendJsonResponse(err, req, res)
json: () => this.sendJsonResponse(err, req, res),
});

next();
}

logErrorStack(err) {
const statusCode = err.statusCode || 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning fallback statusCode of 0 is not necessary, it's only use to check it the status code exists in EXCLUDE_STATUSCODE_FROM_LOGS, which doesn't contains null or undefined.

According to the unit test coverage, the || 0 "branch" is never executed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird. I thought this was added as a patch. I don't recall why but if you add this I can't guarantee you may not introduce a big issue.

Remember this is dependency injected, so there are callers outside of this code base that could trigger this, so I would refrain from refactoring code checks that are not needed. The intent of this project is to remove dependecies, not refactoring

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow, when using the middleware as expected, this.EXCLUDE_STATUSCODE_FROM_LOGS will always be [404], and calling some on this array with an undefined value is the same as calling it with 0, i.e. it returns false.
image

When calling the method directly, e.g. ErrorMiddleware.logErrorState({statusCode: 404}), this.EXCLUDE_STATUSCODE_FROM_LOGS is undefined and will always return false, which obviously is not expected.

I will revert the change, but it defies logic

const { statusCode } = err;
const checkStatus = (status) => status === statusCode;

if (!_some(this.EXCLUDE_STATUSCODE_FROM_LOGS, checkStatus)) {
Logger.error(err, '\n', err.stack, '\n', (err.data || ''));
Logger.error(err, '\n', err.stack, '\n', err.data);
}
}

appendRequestData(err, req) {
if (err.data == null) {
err.data = {};
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was a patch to something. The fact that it wasn't defaulted to an empty object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated logic does the same thing:
image


err.data = _assignIn(err.data, {
url: req.url
});
err.data = _assignIn(
{ ...err.data },
{
url: req.url,
},
);
}

sendTextResponse(err, req, res) {
Expand Down
13 changes: 4 additions & 9 deletions src/middleware/PromiseMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
module.exports = function (
Promise,
BaseMiddleware
) {

module.exports = function (Promise, BaseMiddleware) {
class PromiseMiddleware extends BaseMiddleware {

configure(app) {
super.configure(app);

Expand All @@ -14,8 +9,8 @@ module.exports = function (
.catch(this.req.next);
};

this.app.response.renderAsync = function (view, properties = {}) {
return Promise.props(properties)
this.app.response.renderAsync = function (view, properties) {
Comment on lines -17 to -18
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the "default" value of {} to a "fallback" value because the default value only gets used when the argument is undefined or not provided, but if null is provided, it will fail.

return Promise.props(properties ?? {})
.then((props) => this.render(view, props))
.catch(this.req.next);
};
Expand All @@ -42,7 +37,7 @@ module.exports = function (
},
json: () => {
this.json(results);
}
},
});
})
.catch(this.req.next);
Expand Down
18 changes: 6 additions & 12 deletions src/webserver/BaseWebServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,15 @@ module.exports = function (
ErrorMiddleware,
config,
ControllerRegistration,
WinstonRequestLoggingMiddleware
WinstonRequestLoggingMiddleware,
) {
class BaseWebServer {

constructor() {
this.app = express();
}

getPort() {
let port = config.Port;

if (this.server) {
port = this.server.address().port || port;
}

return port;
Comment on lines -19 to -25
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified logic because port property is always defined for this.server.address()

return this.server?.address()?.port ?? config.Port;
}

registerDefaultMiddleware() {
Expand Down Expand Up @@ -88,19 +81,20 @@ module.exports = function (
}

getCloseAsync() {
if (this.server && this.server.closeAsync) {
if (this.server?.closeAsync) {
return this.server.closeAsync();
}

return Promise.resolve();
}

startedMessage() {
const port = this.getPort();
if (this.cluster) {
return `Worker ${this.cluster.worker.id} started on port ${this.getPort()}`;
return `Worker ${this.cluster.worker.id} started on port ${port}`;
}

return `Express app started on port ${this.getPort()}`;
return `Express app started on port ${port}`;
}

logSectionHeader(message) {
Expand Down
18 changes: 14 additions & 4 deletions test/fixtures/controllers/PromiseMiddlewareTestController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
module.exports = function (BaseController, Promise) {

class PromiseMiddlewareTestController extends BaseController {

configure(app) {
super.configure(app);

app.set('view engine', 'ejs');
app.set('views', `${__dirname}/views`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed. In the test you mock and inject ejs, where ever it's failing should be depending on PromiseMiddlewareTestController

You will need code like this in near the existing injector:

   const ejsMock = {
  render: jest.fn()
};

    injector()
      .addDependency('ejs', ejsMock, true)
      .inject((TasksPrinter) => {
        this.TasksPrinter = TasksPrinter;
      });

Copy link
Contributor Author

@otrreeves otrreeves Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app.set('view engine', ...); is not needed if the view name includes the extension (which I've done to remove it), but setting the views directory is still needed even when mocking the view engine, otherwise express throws Internal Server Error

From my experience trying to implement your injection suggestion, express doesn't seem pick up an injected view engine, even if it's a known engine like ejs, you have to register the engine via app.engine(...)


app.get('/promise-middleware-test--jsonasync', this.getJsonAsync);
app.get('/promise-middleware-test--renderasync', this.getRenderAsync);
app.get('/promise-middleware-test--sendAsync', this.getSendAsync);
Expand All @@ -16,7 +18,15 @@ module.exports = function (BaseController, Promise) {
}

getRenderAsync(req, res) {
res.renderAsync('renderView', Promise.resolve({ microapp: 'renderAsync success' }));
const { headers } = req;
const view = headers['x-view'];
const viewPropsHeader = headers['x-view-props'];
const viewProps = viewPropsHeader !== 'undefined' ? JSON.parse(viewPropsHeader) : undefined;
if (viewProps) {
res.renderAsync(view, Promise.resolve(viewProps));
} else {
res.renderAsync(view);
}
}

getSendAsync(req, res) {
Expand All @@ -28,7 +38,7 @@ module.exports = function (BaseController, Promise) {
}

getFormatAsync(req, res) {
res.formatAsync(Promise.resolve('formatAsync success'));
res.formatAsync('innerHTML', Promise.resolve('formatAsync success'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is needed.

Copy link
Contributor Author

@otrreeves otrreeves Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

express().response.formatAsync expects 2 arguments, this fixes a bug.

}
}

Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/controllers/views/home.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Testing</title>
</head>
<body>
No render view-props provided
</body>
</html>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mock ejs in the injector, you don't need any of this.

Copy link
Contributor Author

@otrreeves otrreeves Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out mocking a view engine still requires view-files to exist, otherwise express throws Internal Server Error

9 changes: 9 additions & 0 deletions test/fixtures/controllers/views/microapp.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Testing</title>
</head>
<body>
<%= microapp %>
</body>
</html>
18 changes: 17 additions & 1 deletion test/unit/middleware/NoCacheMiddleware.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
describe('NoCacheMiddleware', function () {

beforeEach(() => {
return injector().inject((NoCacheMiddleware) => {
this.NoCacheMiddleware = NoCacheMiddleware;
Expand All @@ -9,4 +8,21 @@ describe('NoCacheMiddleware', function () {
it('should exist', () => {
expect(this.NoCacheMiddleware).toBeDefined();
});

it('should set no-cache headers', () => {
const response = {
headers: { Expires: 5184000 },
header: (name, value) => {
response.headers[name] = value;
},
};

this.NoCacheMiddleware({}, response, () => {});

expect(response.headers).toStrictEqual({
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: 0,
});
});
});
Loading
Loading