From a2f758103925411be956e45cc642cebf44fafb1c Mon Sep 17 00:00:00 2001 From: Akosua <9094098+akosasante@users.noreply.github.com> Date: Sun, 24 Sep 2023 14:57:49 -0400 Subject: [PATCH 1/6] add comment about modulo operator --- .../optionalArrayMethodsReviewExtraAssignment.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js b/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js index 4bab4c0e97..9b269e25a9 100644 --- a/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js +++ b/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js @@ -40,6 +40,7 @@ const object = { // Every time we call `.behavior()`, the data (number) inside `object` is // incremented by 1, so we print "OOP demo 1", "OOP demo 2", etc. + object.behavior() object.behavior() object.behavior() @@ -61,11 +62,13 @@ object.behavior() // - The callback should return a boolean. If the return value is true, the // element becomes a member of the new array. If the return value is false, // the element is filtered (removed). - const integers = [1, 2, 3, 4, 5]; - // evenNumbers will be interger % 2 for each integer - const evenNumbers = integers.filter((integer) => { - return integer % 2 === 0 - }) + + const integers = [1, 2, 3, 4, 5]; +// evenNumbers will be interger % 2 for each integer +// '%' is the "modulo" operator. Here we are checking if `integer` divided by 2, leaves a remainder of 0, which is true for even numbers and false for odd numbers. + const evenNumbers = integers.filter((integer) => { + return integer % 2 === 0 + }) // - Array.prototype.map // - The callback recieves each item of the array. The return value is pushed From 59aa26fdb80f06f619fad4926961df9766b97f2d Mon Sep 17 00:00:00 2001 From: "John R. McGarvey" Date: Thu, 20 Jun 2024 21:00:43 -0400 Subject: [PATCH 2/6] add content files --- 01-node-tutorial/answers/content/first.txt | 1 + 01-node-tutorial/answers/content/second.txt | 1 + 01-node-tutorial/answers/content/subfolder/test.txt | 1 + 3 files changed, 3 insertions(+) create mode 100644 01-node-tutorial/answers/content/first.txt create mode 100644 01-node-tutorial/answers/content/second.txt create mode 100644 01-node-tutorial/answers/content/subfolder/test.txt diff --git a/01-node-tutorial/answers/content/first.txt b/01-node-tutorial/answers/content/first.txt new file mode 100644 index 0000000000..3c7d2fbdd6 --- /dev/null +++ b/01-node-tutorial/answers/content/first.txt @@ -0,0 +1 @@ +Hello this is first text file \ No newline at end of file diff --git a/01-node-tutorial/answers/content/second.txt b/01-node-tutorial/answers/content/second.txt new file mode 100644 index 0000000000..f108be0e0e --- /dev/null +++ b/01-node-tutorial/answers/content/second.txt @@ -0,0 +1 @@ +Hello this is second text file \ No newline at end of file diff --git a/01-node-tutorial/answers/content/subfolder/test.txt b/01-node-tutorial/answers/content/subfolder/test.txt new file mode 100644 index 0000000000..a78a2725b8 --- /dev/null +++ b/01-node-tutorial/answers/content/subfolder/test.txt @@ -0,0 +1 @@ +test txt \ No newline at end of file From a8b39adc9f6c187059d1913197ae376fb9a4e6a9 Mon Sep 17 00:00:00 2001 From: "John R. McGarvey" Date: Mon, 24 Jun 2024 00:12:39 -0400 Subject: [PATCH 3/6] many lesson revisions for chai 5 --- lessons/ctd-node-assignment-15.md | 436 ++++++++++++++++-------------- lessons/ctd-node-lesson-15.md | 25 +- 2 files changed, 245 insertions(+), 216 deletions(-) diff --git a/lessons/ctd-node-assignment-15.md b/lessons/ctd-node-assignment-15.md index 18da6b7f9a..d893678aa6 100644 --- a/lessons/ctd-node-assignment-15.md +++ b/lessons/ctd-node-assignment-15.md @@ -11,7 +11,7 @@ npm install --save-dev factory-bot npm install --save-dev @faker-js/faker npm install --save-dev puppeteer ``` -A suggestion: You probably should update the connect-mongodb-session package. There have been some serious security bugs in that package, now fixed. +A suggestion: You probably should update the connect-mongodb-session package. There have been some serious security bugs in that package, now fixed. When you have completed the above npm install operations, check your package.json. In the devDependencies stanza you should have entries for the packages above. Verify that your chai and chai-http entries are for some level of version 5 of those packages, as the instructions below are specific to version 5. ## Setting Up To Test @@ -22,7 +22,7 @@ Create a test directory in your repository. This is where you will put the actua ``` which will cause the tests to run. It also sets the NODE_ENV environment variable, which we'll use to load the test version of the database. Edit your app.js. You'll have a line that reads something like: ``` - await require("./db/connect")(process.env.MONGO_URI); + const url = process.env.MONGO_URI; ``` You should change it to look something like the following: ``` @@ -30,8 +30,8 @@ let mongoURL = process.env.MONGO_URI if (process.env.NODE_ENV == "test") { mongoURL = process.env.MONGO_URI_TEST } -await require("./db/connect")(mongoURL); ``` +and then change url to mongoURL in the section that starts ```const store = ```. The point of this is so that your testing doesn't interfere with your production database, and also so that your production or development data doesn't interfere with your testing. Also, you want to have a function that will bring the database to a known state, so that previous tests don't cause subsequent ones to give false results. Create a file util/seed_db.js. It should read as follows: ``` const Job = require("../models/Job") @@ -76,29 +76,61 @@ module.exports = { testUserPassword, factory, seed_db } ``` A couple of new ideas are introduced above. First, faker is being used to generate somewhat random but plausible data. Second, we are using factories to automate the creation of data, which is being written to the database. +## Chai 5 and Chai-Http 5 + +These packages are now ESM only! This was, in my humble opinion, a questionable move on the part of the developers, and they made quite a few other breaking changes. But we can accommodate these changes, without converting to ESM modules. (Some students are using ESM modules for these exercises. If you are doing this, discuss matters with your mentors if you have trouble.) + +For Chai 4 and Chai-http 4, we could do: +``` +const chai = require('chai') +const chaiHttp = require('chai-http') +chai.use(chaiHttp) +``` +This would give you access to chai.expect() (for evaluating results) and chai.request() (for sending http requests to the server and getting back the results). This is not going to work for V5 of these packages: You can't use request() to load an ESM only module. Also you can only call chai.use() once, for all of your test files and cases. So, we need the following utility module, util\get_chai.js: +``` +let chai_obj = null + +const get_chai = async () => { + if (!chai_obj) { + const {expect, use} = await import('chai') + const chaiHttp = await import('chai-http') + const chai = use(chaiHttp.default) + chai_obj = {expect: expect, request: chai.request} + } + return chai_obj +} + +module.exports = get_chai +``` +In this way, we avoid using request(), and we can ensure that chai.use() is only called once. But, get_chai() is asynchronous. When we use Mocha, we can't call an asynchronous function in the mainline of a test file, because mocha won't wait for the promise to resolve. Also, describe() functions can't be passed asynchronous functions. We can and should pass asynchronous functions to it() functions, for the individual tests. So, this is where we call get_chai(), inside each asynchronous function passed to it(). + +(Some students may be using EJS files. This is somewhat easier, in that you can use the import statement instead of the import() asynchronous function. However, you still need a utility module to ensure that use() is only called once.) + ## Unit Testing a Function -Create a file, utils/multiply.js. It should export a function, multiply, that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: +Create a file, utils/multiply.js. It should export a function, multiply(), that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: ``` const multiply = require('../util/multiply') -const expect = require('chai').expect +const get_chai = require('../util/get_chai') describe('testing multiply', () => { - it('should give 7*6 is 42', (done) => { + it('should give 7*6 is 42', async () => { + const {expect} = await get_chai() expect(multiply(7,6)).to.equal(42) - done() }) - it('should give 7*6 is 42', (done) => { + it('should give 7*6 is 97', async () => { + const {expect} = await get_chai() expect(multiply(7,6)).to.equal(97) - done() }) }) ``` +Here we get the value for expect() several times. By default, the test cases run in order, so one could store the value in a variable with module scope, and only get it once per test fil ... but one can run tests in parallel, in which case things would probably not work. + Then do: ```npm run test``` You will see that the first test passes, but the second one fails, as one would think. You can delete the second test. You might want to create tests for other numbers, to make sure the function doesn't always return 42. ## Function Testing for An API -Your current application doesn't have an API, so you can add one by adding the following to app.js: +Your current application doesn't have an API, so you can add one by adding the following, at an appropriate place (like before the not found handler) to app.js: ``` app.get("/multiply", (req,res)=> { const result = req.query.first * req.query.second @@ -115,7 +147,7 @@ You also have to change app.js to make your app available to the test. The bott const port = process.env.PORT || 3000; const start = () => { try { - require("./db/connect")(url); + require("./db/connect")(mongoURL); return app.listen(port, () => console.log(`Server is listening on port ${port}...`), ); @@ -128,43 +160,36 @@ const server = start(); module.exports = { app, server }; ``` -You can try it out if you like, by doing the following in your browser: +Here, to facilitate testing, we have made start() synchronous. You can try the multiply API out if you like, by starting the server and doing the following in your browser: ``` http://localhost:3000/multiply?first=5&second=27 ``` Then create a test, a file tests/test_multiply_api.js, as follows: ``` -const chai = require("chai"); -chai.use(require("chai-http")); const { app, server } = require("../app"); -const expect = chai.expect +const get_chai = require("../util/get_chai") describe("test multiply api", function () { - after(() => { - server.close(); - }); - it("should multiply two numbers", (done) => { - chai.request(app).get("/multiply") + it("should multiply two numbers", async () => { + const { expect, request } = await get_chai() + const req = request.execute(app).get("/multiply") .query({first: 7, second: 6}) .send() - .end((err,res)=> { - expect(err).to.equal(null) - expect(res).to.have.status(200) - expect(res).to.have.property("body") - expect(res.body).to.have.property("result") - expect(res.body.result).to.equal(42) - done() - }) + const res = await req + expect(res).to.have.status(200) + expect(res).to.have.property("body") + expect(res.body).to.have.property("result") + expect(res.body.result).to.equal(42) }) }) ``` -Note first of all that this file actually requires your app, which causes your app to run. Chai is going to send data to that running app. The chai-http package adds HTTP functions to Chai, so it now has the get() method (as well as post, patch, etc.), and these return a request object with methods query, send, and end. The end method has a callback that returns either an error or a result. One can then check the result status and body. Finally, each test must call done(). The server.close() ends the server. Do ```npm run test``` to try it out. +Note first of all that this file actually requires your app, which causes your app to run. You do not want your server running when you run the test, because the require() function for the app starts it. Chai is going to send data to that running app. The chai-http package adds HTTP functions to Chai, so it now has the get() method (as well as post, patch, etc.), and these return a request object with methods query and send. One can then check the result status and body. Do ```npm run test``` to try it out. ## Function Testing for Rendered HTML Of course, the application you are writing is not intended to provide an API. Instead it provides rendered HTML pages. You can test these as well. -There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai recieves a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error and call the callback, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately, with this middleware, which should be added before your routes: +There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai receives a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately with this middleware, which should be added to app.js before your routes: ``` app.use((req,res,next)=> { if (req.path == "/multiply") { @@ -177,27 +202,19 @@ app.use((req,res,next)=> { ``` Now create a simple UI test case, in tests/test_ui.js: ``` -const chai = require("chai"); -chai.use(require("chai-http")); const { app, server } = require("../app"); -const expect = chai.expect; +const get_chai = require("../util/get_chai") describe("test getting a page", function () { - after(() => { - server.close(); - }); - it("should get the index page", (done) => { - chai - .request(app) + it("should get the index page", async () => { + const { expect, request } = await get_chai() + const req = request.execute(app) .get("/") .send() - .end((err, res) => { - expect(err).to.equal(null); - expect(res).to.have.status(200); - expect(res).to.have.property("text"); - expect(res.text).to.include("Click this link"); - done(); - }); + const res = await req() + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Click this link"); }); }); @@ -206,49 +223,40 @@ In this case, you get a res.text, instead of a res.body. The text is the actual ## Testing Registration -Here is a test for registration: -```const chai = require("chai"); -chai.use(require("chai-http")); +Here is a test for registration. You should put it in a file tests/registration_logon.js. +``` const { app, server } = require("../app"); -const expect = chai.expect; - const { factory, seed_db } = require("../util/seed_db"); const faker = require("@faker-js/faker").fakerEN_US; +const get_chai = require("../util/get_chai") const User = require("../models/User"); describe("tests for registration and logon", function () { - after(() => { - server.close(); - }); - it("should get the registration page", (done) => { - chai - .request(app) + it("should get the registration page", async () => { + const {expect, request} = await get_chai() + const req = request.execute(app) .get("/session/register") .send() - .end((err, res) => { - expect(err).to.equal(null); - expect(res).to.have.status(200); - expect(res).to.have.property("text"); - expect(res.text).to.include("Enter your name"); - const textNoLineEnd = res.text.replaceAll("\n", ""); - const csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd); - expect(csrfToken).to.not.be.null; - this.csrfToken = csrfToken[1]; - expect(res).to.have.property("headers"); - expect(res.headers).to.have.property("set-cookie"); - const cookies = res.headers["set-cookie"]; - const csrfCookie = cookies.find((element) => - element.startsWith("csrfToken"), - ); - expect(csrfCookie).to.not.be.undefined; - const cookieValue = /csrfToken=(.*?);\s/.exec(csrfCookie); - this.csrfCookie = cookieValue[1]; - done(); - }); + const res = await req + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Enter your name"); + const textNoLineEnd = res.text.replaceAll("\n", ""); + const csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd); + expect(csrfToken).to.not.be.null; + this.csrfToken = csrfToken[1]; + expect(res).to.have.property("headers"); + expect(res.headers).to.have.property("set-cookie"); + const cookies = res.headers["set-cookie"]; + this.csrfCookie = cookies.find((element) => + element.startsWith("csrfToken"), + ); + expect(this.csrfCookie).to.not.be.undefined; }); it("should register the user", async () => { + const {expect, request} = await get_chai() this.password = faker.internet.password(); this.user = await factory.build("user", { password: this.password }); const dataToPost = { @@ -258,26 +266,19 @@ describe("tests for registration and logon", function () { password1: this.password, _csrf: this.csrfToken, }; - try { - const request = chai - .request(app) + const req = request.execute(app) .post("/session/register") - .set("Cookie", `csrfToken=${this.csrfCookie}`) + .set("Cookie", this.csrfCookie) .set("content-type", "application/x-www-form-urlencoded") .send(dataToPost); - res = await request; - console.log("got here"); - expect(res).to.have.status(200); - expect(res).to.have.property("text"); - expect(res.text).to.include("Jobs List"); - newUser = await User.findOne({ email: this.user.email }); - expect(newUser).to.not.be.null; - console.log(newUser); - } catch (err) { - console.log(err); - expect.fail("Register request failed"); - } - }); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Jobs List"); + newUser = await User.findOne({ email: this.user.email }); + expect(newUser).to.not.be.null; + }); + }); ``` Ok, there's a lot going on here. The test first gets the registration form. So far so good. Then, the task is to post values for the form so that the user is actually registered. But, to post a form, we have to get past the protection against cross site request forgery that you implemented in the last lesson. To do that, we need the CSRF token, which appears in the form itself, but we have to find it. We can do that using a regular expression. First we take the line ends out of the form, as they mess up regular expression parsing. Then we execute a regular expression to find the token itself. If you don't know regular expressions, they are good to learn, but otherwise just use the one herein provided. When we post the values for the form, we need to include the value for the csrf token. We store it in this.csrfToken, so that we can reuse the value. The other half of the CSRF protection is that we also need to send the cookie. Chai does not keep cookie values between tests. We have to preserve the ones we want, and include them on subsequent requests. Chai doesn't even store the cookies in a very friendly way. We have to parse them out of the response headers, so there is more logic to do that. For each of these steps, we do a Chai assertion (expect) so that we know all is working. @@ -292,18 +293,17 @@ describe("tests for registration and logon", function () { ``` and ``` - it("should get the registration page", (done) => { + it("should get the registration page", async () => { ``` -The difference is that arrow functions do not have their own "this"! They inherit the this of the context in which they were defined. So, when we save to the variable this.csrfToken, we do it in the context of the describe(). On that call to describe, we pass ```function ()```, and so the this is associated with that context. As a result this.csrfToken is available on our next it() call within that same describe, so long as that call to it() passes an arrow function. The best practice is to use arrow functions with it() and not to use them with describe(). +The difference is that arrow functions do not have their own "this"! They inherit the this of the context in which they were defined. So, when we save to the variable this.csrfToken, we do it in the context of the describe(). On that call to describe, we pass ```function ()```, and so the this is associated with that context. As a result this.csrfToken is available on our next it() call within that same describe, so long as that call to it() passes an arrow function. There are, of course, other ways to save the token, such is in a variable with module scope. ## Posting the Form Values Ok, so what do we post, and where do we post it? The post for register is /sessions/register. If we look at the register view, we see what is expected, from the names of the entry fields. These are name, email, password, and password1 (for password confirmation). To get values for these, we can use the user factory created in util/seed_db.js. But (a) we need to save the password, so that we can use it to test logon; (b) we need to save other values for the user, again for logon, and (c) we use factory.build, not factory.create, because we don't want the factory to store values in the database. That's what the actual register operation is supposed to do. -When we post, we have to set the cookie for CSRF protection. We also have to set the content-type, which would otherwise be JSON. We also have to include the csrfToken in the data that is posted, with the name _csrf. Now, we have a couple of asynchronous calls here. Therefore, on the call to the 'it' function, we do not include a callback with ```(done) =>```. Instead we pass an async function, so that we can use await on the factory call. Also, we need to be able to do assertion tests in an the same async function, because we need to check the database, which is an async call. The chai.request call returns a "thenable" which works like a promise, except we can further qualify it before resolving the promise.m So we set up the call, save the thenable in the request variable, and then resolve it to get the res object back, all within a try/catch. -We can then search the database to verify that the user object was actually created. +When we post, we have to set the cookie for CSRF protection. We also have to set the content-type, which would otherwise be JSON. We also have to include the csrfToken in the data that is posted, with the name _csrf. We post the resulting information, and then search the database to verify that the user object was actually created. -Just to restate, we have two kinds of it() statements; +There could be two kinds of it() statements; ``` it("should get the registration page", (done) => { ``` @@ -311,7 +311,7 @@ and ``` it("should register the user", async () => { ``` -In the first way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. +In the first way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. In our examples, we only use the second way, because we have to call get_chai(), which is asynchronous. If the user is actually created, our controller sends a redirect. By default, Chai traverses the redirect automatically, so that the res object coming back should have a status of 200. It should redirect to the index page, and on that page one should see "Click this link to logon". @@ -326,7 +326,7 @@ Be careful to include the status(400). If the status is 200, the request is exp ## Testing Logon -We saved this.user and this.password, so we should be able to log in. We'll skip actually loading the logon form -- you could add that test if you like -- and we'll do the post for logon. When you logon, you are redirected. By default, Chai then follows the redirection, but what it doesn't do is keep the cookies. When you do the .send for the test, the cookies are already gone. This is completely useless for logon. We need the session cookie for subsequent requests. It is pretty poor in another way. If you redirect, the session contains the flash information for user messages, but if the cookies are gone, so are the flash messages. So, a better policy is to disable redirects by doing .redirects(0) on the request. If a redirect occurs, the status is 302, and the req.headers.location is the target for the redirect. (Editorial aside: Chai really ought to save those cookies.) So, here is the logon test: +We saved this.user and this.password, so we should be able to log in. We'll skip actually loading the logon form -- you could add that test if you like -- and we'll do the post for logon. When you logon, you are redirected. By default, Chai then follows the redirection, but what it doesn't do is keep the cookies. When you do the .send for the test, the cookies are already gone. This is completely useless for logon. We need the session cookie for subsequent requests. It is pretty poor in another way. If you redirect, the session contains the flash information for user messages, but if the cookies are gone, so are the flash messages. So, a better policy is to disable redirects by doing .redirects(0) on the request. If a redirect occurs, the status is 302, and the req.headers.location is the target for the redirect. (Editorial aside: Chai really ought to save those cookies.) So, here is the logon test, which should be added to the previous describe() section: ``` it("should log the user on", async () => { const dataToPost = { @@ -334,61 +334,99 @@ We saved this.user and this.password, so we should be able to log in. We'll ski password: this.password, _csrf: this.csrfToken, }; - try { - const request = chai - .request(app) - .post("/session/logon") - .set("Cookie",this.csrfCookie) - .set("content-type", "application/x-www-form-urlencoded") - .redirects(0) - .send(dataToPost); - res = await request; - expect(res).to.have.status(302); - expect(res.headers.location).to.equal('/') - const cookies = res.headers["set-cookie"]; - this.sessionCookie = cookies.find((element) => + const {expect, request } = await get_chai() + const req = request.execute(app) + .post("/session/logon") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .redirects(0) + .send(dataToPost); + const res = await req + expect(res).to.have.status(302); + expect(res.headers.location).to.equal('/') + const cookies = res.headers["set-cookie"]; + this.sessionCookie = cookies.find((element) => element.startsWith("connect.sid"), ); expect(this.sessionCookie).to.not.be.undefined; - } catch (err) { - console.log(err); - expect.fail("Logon request failed"); - } }); - it("should get the index page", (done)=>{ - chai.request(app).get("/") - .set('Cookie',this.sessionCookie) - .send() - .end((err,res)=>{ - expect(err).to.equal(null) - expect(res).to.have.status(200) - expect(res).to.have.property("text") - expect(res.text).to.include(this.user.name) - done() - }) + + it("should get the index page", async ()=>{ + const {expect, request} = await get_chai() + const req = request.execute(app).get("/") + .set("Cookie", this.csrfCookie) + .set("Cookie", this.sessionCookie) + .send() + const res = await req + expect(res).to.have.status(200) + expect(res).to.have.property("text") + expect(res.text).to.include(this.user.name) }); ``` There are two parts to the test. The first does the logon. You get a redirect ... but it will redirect to the same place whether the logon succeeds or fails. And you will have a session cookie even before you log in. So how do you know whether the logon succeeded? The only way is to get the index page again. If the logon is successful, it will show the user's name, but if not, it will show the error message. To do this, we have to include the session cookie in the request, as we do above. **Now: Some code for you to write.** Create a test for logoff. Logoff won't work unless there has been a logon, and unless you send the _csrf value and set cookies for both the csrfToken and the sessionCookie. The latter code is: ``` -.set("Cookie", csrfToken + ";" + sessionCookie) +.set("Cookie", this.csrfToken + ";" + this.sessionCookie) ``` -The only way to know if it succeeded is to send a get request for the index page, passing the session cookie you already had. It will have the message about clicking the link to logon, and it will not have the user name, because that session has been invalidated on the server side. +You need to post data, as before, but the only field in the data is ```_csrf```. In this case, you let Chai follow the redirect, that is, do not do ```.redirects(0)```. You should get back a page that includes "link to logon". ## Testing Job CRUD Operations -Create a new file, tests/crud_operations.js. The flow for testing CRUD operations is as follows. +Create a new file, tests/crud_operations.js. You will need a couple extra require() statements, as follows: +``` +const Job = require("../models/Job") +const { seed_db, testUserPassword } = require("../util/seed_db"); +``` +The flow for testing CRUD operations is as follows. 1. Seed the database! You have a utility routine for that in util/seed_db.js -2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. -3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "
  • " appears on the page. Here's how you might do that part: +2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. Steps 1 and 2 are not tests, but you need an async before() call, inside your describe(), that does these things. Here is the before() that completes steps 1 and 2: + ``` before( async () => { + const { expect, request } = await get_chai() + this.test_user = await seed_db() + let req = request.execute(app) + .get("/session/logon") + .send() + let res = await req + const textNoLineEnd = res.text.replaceAll("\n", ""); + this.csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd)[1] + let cookies = res.headers["set-cookie"]; + this.csrfCookie = cookies.find((element) => + element.startsWith("csrfToken"), + ); + const dataToPost = { + email: this.test_user.email, + password: testUserPassword, + _csrf: this.csrfToken, + }; + req = request.execute(app) + .post("/session/logon") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .redirects(0) + .send(dataToPost); + res = await req + cookies = res.headers["set-cookie"]; + this.sessionCookie = cookies.find((element) => + element.startsWith("connect.sid"), + ); + expect(this.csrfToken).to.not.be.undefined + expect(this.sessionCookie).to.not.be.undefined + expect(this.csrfCookie).to.not.be.undefined + }) ``` - const pageParts = res.text.split("
  • ") +3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "" appears on the page. Here's how you might do that part: + ``` + const pageParts = res.text.split("") expect(pageParts).to.equal(21) ``` As you can see, scanning the page to see if the result is correct is kind of messy. -4. Add a job entry! This is a post for the job form. You will have to include _csrf in the post, and you will need to set the CSRF and session cookies. You could use the factory to create values for the job, via a factory.build('job'). It doesn't really matter if you disable redirect or not. The only test you need to do is a Job.findOne to find an entry with the same attributes as the one you built. But that is an asynchronous call, so you need to handle it just as was done for the register operation. +4. Add a job entry! This is a post for the job form. You will have to include _csrf in the post, and you will need to set the CSRF and session cookies. You could use the factory to create values for the job, via a factory.build('job'). The best way to test for success is to see that the database now has 21 entries, as follows: +``` + const jobs = await Job.find({createdBy: this.test_user._id}) + expect(jobs.length).to.equal(21) +``` ## Puppeteer @@ -399,68 +437,61 @@ const { server } = require("../app"); const { seed_db, testUserPassword } = require("../util/seed_db"); let testUser = null; - -const runTests = async () => { - let page = null; - let browser = null; - // Launch the browser and open a new blank page - describe("index page test", function () { - before(async function () { - this.timeout(10000); - browser = await puppeteer.launch(); - page = await browser.newPage(); - await page.goto("http://localhost:3000"); +let page = null; +let browser = null; +// Launch the browser and open a new blank page +describe("index page test", function () { + before(async function () { + this.timeout(10000); + browser = await puppeteer.launch(); + page = await browser.newPage(); + await page.goto("http://localhost:3000"); + }); + after(async function () { + this.timeout(5000); + await browser.close(); + }); + describe("got to site", function () { + it("should have completed a connection", async () => { }); - after(async function () { - this.timeout(5000); - await browser.close(); - server.close(); - return; + }); + describe("index page test", function () { + this.timeout(10000); + it("finds the index page logon link", async () => { + this.logonLink = await page.waitForSelector( + "a ::-p-text(Click this link to logon)", + ); }); - describe("got to site", function () { - it("should have completed a connection", function (done) { - done(); - }); + it("gets to the logon page", async () => { + await this.logonLink.click(); + await page.waitForNavigation(); + const email = await page.waitForSelector("input[name=email]"); }); - describe("index page test", function () { - this.timeout(10000); - it("finds the index page logon link", async () => { - this.logonLink = await page.waitForSelector( - "a ::-p-text(Click this link to logon)", - ); - }); - it("gets to the logon page", async () => { - await this.logonLink.click(); - await page.waitForNavigation(); - const email = await page.waitForSelector("input[name=email]"); - }); + }); + describe("logon page test", function () { + this.timeout(20000); + it("resolves all the fields", async () => { + this.email = await page.waitForSelector("input[name=email]"); + this.password = await page.waitForSelector("input[name=password]"); + this.submit = await page.waitForSelector("button ::-p-text(Logon)"); }); - describe("logon page test", function () { - console.log("at line 48", this.outerd, this.innerd, this.secondIt) - this.timeout(20000); - it("resolves all the fields", async () => { - this.email = await page.waitForSelector("input[name=email]"); - this.password = await page.waitForSelector("input[name=password]"); - this.submit = await page.waitForSelector("button ::-p-text(Logon)"); - }); - it("sends the logon", async () => { - testUser = await seed_db(); - await this.email.type(testUser.email); - await this.password.type(testUserPassword); - await this.submit.click(); - await page.waitForNavigation(); - await page.waitForSelector( - `p ::-p-text(${testUser.name} is logged on.)`, - ); - await page.waitForSelector("a ::-p-text(change the secret"); - await page.waitForSelector('a[href="/secretWord"]'); - const copyr = await page.waitForSelector("p ::-p-text(copyright)"); - const copyrText = await copyr.evaluate((el) => el.textContent); - console.log("copyright text: ", copyrText); - }); + it("sends the logon", async () => { + testUser = await seed_db(); + await this.email.type(testUser.email); + await this.password.type(testUserPassword); + await this.submit.click(); + await page.waitForNavigation(); + await page.waitForSelector( + `p ::-p-text(${testUser.name} is logged on.)`, + ); + await page.waitForSelector("a ::-p-text(change the secret"); + await page.waitForSelector('a[href="/secretWord"]'); + const copyr = await page.waitForSelector("p ::-p-text(copyright)"); + const copyrText = await copyr.evaluate((el) => el.textContent); + console.log("copyright text: ", copyrText); }); }); -}; +}); ``` In each of the describe() stanzas, as well as in before() and after(), we call this.timeout() to set a reasonable number of milliseconds after which the operation should be abandoned. The puppeteer.launch() call actually launches the browser, which by default is a version of Chrome. The browser.newPage() call opens up a browser page. The page.goto() call opens the root page of the application being tested. Then, we have the following calls: @@ -484,17 +515,16 @@ and then rerun the test, you'll see the test in progress. However, an aside for ## More Code to Write -The test you are to add is to verify that the job operations work correctly. Add to the test scenario as follows: - -1. Add the expect function from Chai to the test, as you will need it. -2. Add a new describe() stanza for this series of tests. -2. (test1) Make the test do a click on the link for the jobs list. -3. Verify that the job listings page comes up, and that there are 20 entries in that list. - A hint here: page.content() can be used to get the entire HTML page, and you can use the split() function on that to could the ```
  • ```entries. -4. (test2) Have the test click on the "Add A Job" button and to wait for the form to come up. Verify that it is the expected form. -5. (test3) Use factory.build('job') to create values for a new entry, have them typed into the form. Then have it click on the add button. -6. Wait for the jobs list to come back up, and verify that the message says that the job listing has been added. -7. Check the database to see that the latest jobs entry has the data entered. (This is where you use Chai expect.) +The test you are to add is to verify that the job operations work correctly. You will need to add the expect function from Chai to the test. You can do: +``` +const { expect } = await import('chai') +``` +but as this is an async call, you can only do this in the it() sections where you need it. +1. Add a new describe("puppeteer job operations" ...) stanza for this series of tests. +2. (test1) Make the test do a click on the link for the jobs list. Verify that the job listings page comes up, and that there are 20 entries in that list. + A hint here: page.content() can be used to get the entire HTML page, and you can use the split() function on that to find the ``````entries. +3. (test2) Have the test click on the "Add A Job" button and to wait for the form to come up. Verify that it is the expected form, and resolve the company and position fields and add button. +4. (test3) Type some values into the company and position fields. Then click on the add button. Wait for the jobs list to come back up, and verify that the message says that the job listing has been added. Check the database to see that the latest jobs entry has the data entered. You also use Job.find() as in the previous exercise.) ## Submit Your Code diff --git a/lessons/ctd-node-lesson-15.md b/lessons/ctd-node-lesson-15.md index fe0c5d7f20..d9f3e51e1b 100644 --- a/lessons/ctd-node-lesson-15.md +++ b/lessons/ctd-node-lesson-15.md @@ -18,23 +18,23 @@ Here is a sample test case that uses Mocha and Chai. It won't work for the pres describe("Jobs", function () { describe("GET /jobs", function () { // Test to get all jobs belonging to the logged on user - it("should get all jobs for the user", (done) => { - chai.request(app) + it("should get all jobs for the user", async () => { + // We need to get the values for request and expect here. + // More on that later. + const req = request.execute(app) .get('/api/v1/jobs') .send() - .end((err, res) => { - expect(res).to.have.status(200); - expect(res.body).to.be.a('object'); - expect(res.body.jobs.length).to.equal(3); // or whatever is in your test data - done(); - }); + const res = await req() + expect(res).to.have.status(200); + expect(res.body).to.be.a('object'); + expect(res.body.jobs.length).to.equal(3); // or whatever is in your test data }); ... ``` The Mocha keywords here are "describe" and "it". These organise the test suite into blocks and document the purpose of each test case. The test above is not really complete, in that one would want to verify that -the expected data is returned. And, as written, the test would fail, or at least would return an empty object, because of course, no user is logged in. So Mocha provides some additional keywords: before, beforeEach, after, and afterEach. These for things such as logon to be done before or after a given block, or before or after each test case in the block. +the expected data is returned. And, as written, the test would fail, or at least would return an empty object, because of course, no user is logged in. So Mocha provides some additional keywords: before, beforeEach, after, and afterEach. These are for things such as logon to be done before or after a given block, or before or after each test case in the block. -The Chai words here are, in this case, get, which returns a result or an error. Of course there are put, post, patch, and delete as well. The get function is implemented in chai-http. +The chai-http words here are, in this case, get, which returns a result or an error. Of course there are put, post, patch, and delete as well. The get function is implemented in chai-http. The Chai word we are using is expect, which checks that the result is correct. Some other things to think about: We have been using a single Mongo database for development. Were you building an actual production application, you would want separate databases for development, test, and production. Also, for testing, you would want a way to populate the database with sample data, so that it is in a known state at the start of the test. For this purpose, we'll use an npm package called factory-bot. @@ -59,8 +59,7 @@ page is retrieved: return; }); describe("got to site", function () { - it("should have completed a connection", function (done) { - done(); + it("should have completed a connection", async () => { }); }); describe("people form", function () { @@ -81,7 +80,7 @@ Then, we can start interacting with the form, as follows: await page.waitForNavigation(); const resultDataDiv = await page.waitForSelector("#result"); const resultData = await resultDataDiv.evaluate((element) => element.textContent); - resultData.should.include("A person record was added"); + expect(resultData).to.include("A person record was added"); ... ``` Here you see the code that actually types into entry fields, and then clicks on the submit button. You get the idea. From 02415ebc55343297da3270b3ed87cfb0ad12a32f Mon Sep 17 00:00:00 2001 From: "John R. McGarvey" Date: Mon, 24 Jun 2024 15:19:36 -0400 Subject: [PATCH 4/6] prettier and other cleanup --- lessons/ctd-node-assignment-15.md | 300 +++++++++++++++--------------- lessons/ctd-node-lesson-15.md | 1 - 2 files changed, 152 insertions(+), 149 deletions(-) diff --git a/lessons/ctd-node-assignment-15.md b/lessons/ctd-node-assignment-15.md index d893678aa6..591143c927 100644 --- a/lessons/ctd-node-assignment-15.md +++ b/lessons/ctd-node-assignment-15.md @@ -18,7 +18,7 @@ A suggestion: You probably should update the connect-mongodb-session package. Th Create a test directory in your repository. This is where you will put the actual test cases. Edit your .env file. Currently you have a line for MONGO_URI. Duplicate the line, and then change the copy to MONGO_URI_TEST. Add "-test" onto the end of the value. This gives you a separate test database. Edit your package.json. In the scripts stanza, the line for "test" should be changed to read: ``` - "test": "NODE_ENV=test mocha tests/*.js --exit", + "test": "NODE_ENV=test mocha tests/*.js --timeout 5000 --exit", ``` which will cause the tests to run. It also sets the NODE_ENV environment variable, which we'll use to load the test version of the database. Edit your app.js. You'll have a line that reads something like: ``` @@ -26,53 +26,53 @@ which will cause the tests to run. It also sets the NODE_ENV environment variab ``` You should change it to look something like the following: ``` -let mongoURL = process.env.MONGO_URI +let mongoURL = process.env.MONGO_URI; if (process.env.NODE_ENV == "test") { - mongoURL = process.env.MONGO_URI_TEST + mongoURL = process.env.MONGO_URI_TEST; } ``` and then change url to mongoURL in the section that starts ```const store = ```. The point of this is so that your testing doesn't interfere with your production database, and also so that your production or development data doesn't interfere with your testing. Also, you want to have a function that will bring the database to a known state, so that previous tests don't cause subsequent ones to give false results. Create a file util/seed_db.js. It should read as follows: ``` -const Job = require("../models/Job") -const User = require("../models/User") -const faker = require("@faker-js/faker").fakerEN_US -const FactoryBot = require('factory-bot'); -require('dotenv').config() - -const testUserPassword = faker.internet.password() -const factory = FactoryBot.factory -const factoryAdapter = new FactoryBot.MongooseAdapter() -factory.setAdapter(factoryAdapter) -factory.define('job',Job, { - company: () => faker.company.name(), - position: () => faker.person.jobTitle(), - status: () => ["interview","declined","pending"][Math.floor(3 * Math.random())], // random one of these -} -) -factory.define('user', User, { +const Job = require("../models/Job"); +const User = require("../models/User"); +const faker = require("@faker-js/faker").fakerEN_US; +const FactoryBot = require("factory-bot"); +require("dotenv").config(); + +const testUserPassword = faker.internet.password(); +const factory = FactoryBot.factory; +const factoryAdapter = new FactoryBot.MongooseAdapter(); +factory.setAdapter(factoryAdapter); +factory.define("job", Job, { + company: () => faker.company.name(), + position: () => faker.person.jobTitle(), + status: () => + ["interview", "declined", "pending"][Math.floor(3 * Math.random())], // random one of these +}); +factory.define("user", User, { name: () => faker.person.fullName(), email: () => faker.internet.email(), - password: () => faker.internet.password() -}) + password: () => faker.internet.password(), +}); const seed_db = async () => { - let testUser=null; + let testUser = null; try { - const mongoURL = process.env.MONGO_URI_TEST - await Job.deleteMany({}) // deletes all job records - await User.deleteMany({}) // and all the users - testUser = await factory.create('user', { password: testUserPassword }) - await factory.createMany('job', 20, {createdBy: testUser._id}) // put 30 job entries in the database. - } catch(e) { - console.log("database error") + const mongoURL = process.env.MONGO_URI_TEST; + await Job.deleteMany({}); // deletes all job records + await User.deleteMany({}); // and all the users + testUser = await factory.create("user", { password: testUserPassword }); + await factory.createMany("job", 20, { createdBy: testUser._id }); // put 30 job entries in the database. + } catch (e) { + console.log("database error"); console.log(e.message); - throw(e); + throw e; } return testUser; -} +}; -module.exports = { testUserPassword, factory, seed_db } +module.exports = { testUserPassword, factory, seed_db }; ``` A couple of new ideas are introduced above. First, faker is being used to generate somewhat random but plausible data. Second, we are using factories to automate the creation of data, which is being written to the database. @@ -88,19 +88,19 @@ chai.use(chaiHttp) ``` This would give you access to chai.expect() (for evaluating results) and chai.request() (for sending http requests to the server and getting back the results). This is not going to work for V5 of these packages: You can't use request() to load an ESM only module. Also you can only call chai.use() once, for all of your test files and cases. So, we need the following utility module, util\get_chai.js: ``` -let chai_obj = null +let chai_obj = null; const get_chai = async () => { if (!chai_obj) { - const {expect, use} = await import('chai') - const chaiHttp = await import('chai-http') - const chai = use(chaiHttp.default) - chai_obj = {expect: expect, request: chai.request} + const { expect, use } = await import("chai"); + const chaiHttp = await import("chai-http"); + const chai = use(chaiHttp.default); + chai_obj = { expect: expect, request: chai.request }; } - return chai_obj -} + return chai_obj; +}; -module.exports = get_chai +module.exports = get_chai; ``` In this way, we avoid using request(), and we can ensure that chai.use() is only called once. But, get_chai() is asynchronous. When we use Mocha, we can't call an asynchronous function in the mainline of a test file, because mocha won't wait for the promise to resolve. Also, describe() functions can't be passed asynchronous functions. We can and should pass asynchronous functions to it() functions, for the individual tests. So, this is where we call get_chai(), inside each asynchronous function passed to it(). @@ -110,21 +110,21 @@ In this way, we avoid using request(), and we can ensure that chai.use() is only Create a file, utils/multiply.js. It should export a function, multiply(), that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: ``` -const multiply = require('../util/multiply') -const get_chai = require('../util/get_chai') +const multiply = require("../util/multiply"); +const get_chai = require("../util/get_chai"); -describe('testing multiply', () => { - it('should give 7*6 is 42', async () => { - const {expect} = await get_chai() - expect(multiply(7,6)).to.equal(42) - }) +describe("testing multiply", () => { + it("should give 7*6 is 42", async () => { + const { expect } = await get_chai(); + expect(multiply(7, 6)).to.equal(42); + }); it('should give 7*6 is 97', async () => { - const {expect} = await get_chai() - expect(multiply(7,6)).to.equal(97) - }) -}) + const {expect} = await get_chai(); + expect(multiply(7,6)).to.equal(97); + }); +}); ``` -Here we get the value for expect() several times. By default, the test cases run in order, so one could store the value in a variable with module scope, and only get it once per test fil ... but one can run tests in parallel, in which case things would probably not work. +Here we get the value for expect() several times. By default, the test cases run in order, so one could store the value in a variable with module scope, and only get it once per test file ... but one can run tests in parallel, in which case things would probably not work. Then do: ```npm run test``` You will see that the first test passes, but the second one fails, as one would think. You can delete the second test. You might want to create tests for other numbers, to make sure the function doesn't always return 42. @@ -132,15 +132,15 @@ Then do: ```npm run test``` You will see that the first test passes, but the sec Your current application doesn't have an API, so you can add one by adding the following, at an appropriate place (like before the not found handler) to app.js: ``` -app.get("/multiply", (req,res)=> { - const result = req.query.first * req.query.second +app.get("/multiply", (req, res) => { + const result = req.query.first * req.query.second; if (result.isNaN) { - result = "NaN" + result = "NaN"; } else if (result == null) { - result = "null" + result = "null"; } - res.json({result: result}) -}) + res.json({ result: result }); +}); ``` You also have to change app.js to make your app available to the test. The bottom of the file should look like: ``` @@ -156,9 +156,9 @@ const start = () => { } }; -const server = start(); +start(); -module.exports = { app, server }; +module.exports = { app }; ``` Here, to facilitate testing, we have made start() synchronous. You can try the multiply API out if you like, by starting the server and doing the following in your browser: ``` @@ -166,22 +166,24 @@ http://localhost:3000/multiply?first=5&second=27 ``` Then create a test, a file tests/test_multiply_api.js, as follows: ``` -const { app, server } = require("../app"); -const get_chai = require("../util/get_chai") +const { app } = require("../app"); +const get_chai = require("../util/get_chai"); describe("test multiply api", function () { - it("should multiply two numbers", async () => { - const { expect, request } = await get_chai() - const req = request.execute(app).get("/multiply") - .query({first: 7, second: 6}) - .send() - const res = await req - expect(res).to.have.status(200) - expect(res).to.have.property("body") - expect(res.body).to.have.property("result") - expect(res.body.result).to.equal(42) - }) -}) + it("should multiply two numbers", async () => { + const { expect, request } = await get_chai(); + const req = request + .execute(app) + .get("/multiply") + .query({ first: 7, second: 6 }) + .send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("body"); + expect(res.body).to.have.property("result"); + expect(res.body.result).to.equal(42); + }); +}); ``` Note first of all that this file actually requires your app, which causes your app to run. You do not want your server running when you run the test, because the require() function for the app starts it. Chai is going to send data to that running app. The chai-http package adds HTTP functions to Chai, so it now has the get() method (as well as post, patch, etc.), and these return a request object with methods query and send. One can then check the result status and body. Do ```npm run test``` to try it out. @@ -191,33 +193,30 @@ Of course, the application you are writing is not intended to provide an API. I There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai receives a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately with this middleware, which should be added to app.js before your routes: ``` -app.use((req,res,next)=> { +app.use((req, res, next) => { if (req.path == "/multiply") { - res.set("Content-Type","application/json") + res.set("Content-Type", "application/json"); } else { - res.set("Content-Type","text/html") + res.set("Content-Type", "text/html"); } - next() -}) + next(); +}); ``` Now create a simple UI test case, in tests/test_ui.js: ``` -const { app, server } = require("../app"); -const get_chai = require("../util/get_chai") +const { app } = require("../app"); +const get_chai = require("../util/get_chai"); describe("test getting a page", function () { it("should get the index page", async () => { - const { expect, request } = await get_chai() - const req = request.execute(app) - .get("/") - .send() - const res = await req() + const { expect, request } = await get_chai(); + const req = request.execute(app).get("/").send(); + const res = await req; expect(res).to.have.status(200); expect(res).to.have.property("text"); expect(res.text).to.include("Click this link"); }); }); - ``` In this case, you get a res.text, instead of a res.body. The text is the actual HTML sent back in response to the request, as a string. Checking the string to see if the response was correct can be a little clumsy, as compared with checking the results of an API. Anyway, verify that your tests still pass, by doing ```npm run test```. If you used slightly different wording in your page, you'll have to change the test above. @@ -225,20 +224,21 @@ In this case, you get a res.text, instead of a res.body. The text is the actual Here is a test for registration. You should put it in a file tests/registration_logon.js. ``` -const { app, server } = require("../app"); +const { app } = require("../app"); const { factory, seed_db } = require("../util/seed_db"); const faker = require("@faker-js/faker").fakerEN_US; -const get_chai = require("../util/get_chai") +const get_chai = require("../util/get_chai"); const User = require("../models/User"); describe("tests for registration and logon", function () { + // after(() => { + // server.close(); + // }); it("should get the registration page", async () => { - const {expect, request} = await get_chai() - const req = request.execute(app) - .get("/session/register") - .send() - const res = await req + const { expect, request } = await get_chai(); + const req = request.execute(app).get("/session/register").send(); + const res = await req; expect(res).to.have.status(200); expect(res).to.have.property("text"); expect(res.text).to.include("Enter your name"); @@ -256,7 +256,7 @@ describe("tests for registration and logon", function () { }); it("should register the user", async () => { - const {expect, request} = await get_chai() + const { expect, request } = await get_chai(); this.password = faker.internet.password(); this.user = await factory.build("user", { password: this.password }); const dataToPost = { @@ -266,20 +266,21 @@ describe("tests for registration and logon", function () { password1: this.password, _csrf: this.csrfToken, }; - const req = request.execute(app) - .post("/session/register") - .set("Cookie", this.csrfCookie) - .set("content-type", "application/x-www-form-urlencoded") - .send(dataToPost); + const req = request + .execute(app) + .post("/session/register") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .send(dataToPost); const res = await req; expect(res).to.have.status(200); expect(res).to.have.property("text"); expect(res.text).to.include("Jobs List"); newUser = await User.findOne({ email: this.user.email }); expect(newUser).to.not.be.null; - }); - + }); }); + ``` Ok, there's a lot going on here. The test first gets the registration form. So far so good. Then, the task is to post values for the form so that the user is actually registered. But, to post a form, we have to get past the protection against cross site request forgery that you implemented in the last lesson. To do that, we need the CSRF token, which appears in the form itself, but we have to find it. We can do that using a regular expression. First we take the line ends out of the form, as they mess up regular expression parsing. Then we execute a regular expression to find the token itself. If you don't know regular expressions, they are good to learn, but otherwise just use the one herein provided. When we post the values for the form, we need to include the value for the csrf token. We store it in this.csrfToken, so that we can reuse the value. The other half of the CSRF protection is that we also need to send the cookie. Chai does not keep cookie values between tests. We have to preserve the ones we want, and include them on subsequent requests. Chai doesn't even store the cookies in a very friendly way. We have to parse them out of the response headers, so there is more logic to do that. For each of these steps, we do a Chai assertion (expect) so that we know all is working. @@ -311,7 +312,7 @@ and ``` it("should register the user", async () => { ``` -In the first way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. In our examples, we only use the second way, because we have to call get_chai(), which is asynchronous. +In the first (old style) way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. In our examples, we only use the second way, because we have to call get_chai(), which is asynchronous. If the user is actually created, our controller sends a redirect. By default, Chai traverses the redirect automatically, so that the res object coming back should have a status of 200. It should redirect to the index page, and on that page one should see "Click this link to logon". @@ -334,16 +335,17 @@ We saved this.user and this.password, so we should be able to log in. We'll ski password: this.password, _csrf: this.csrfToken, }; - const {expect, request } = await get_chai() - const req = request.execute(app) + const { expect, request } = await get_chai(); + const req = request + .execute(app) .post("/session/logon") .set("Cookie", this.csrfCookie) .set("content-type", "application/x-www-form-urlencoded") .redirects(0) .send(dataToPost); - const res = await req + const res = await req; expect(res).to.have.status(302); - expect(res.headers.location).to.equal('/') + expect(res.headers.location).to.equal("/"); const cookies = res.headers["set-cookie"]; this.sessionCookie = cookies.find((element) => element.startsWith("connect.sid"), @@ -351,16 +353,18 @@ We saved this.user and this.password, so we should be able to log in. We'll ski expect(this.sessionCookie).to.not.be.undefined; }); - it("should get the index page", async ()=>{ - const {expect, request} = await get_chai() - const req = request.execute(app).get("/") + it("should get the index page", async () => { + const { expect, request } = await get_chai(); + const req = request + .execute(app) + .get("/") .set("Cookie", this.csrfCookie) .set("Cookie", this.sessionCookie) - .send() - const res = await req - expect(res).to.have.status(200) - expect(res).to.have.property("text") - expect(res.text).to.include(this.user.name) + .send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include(this.user.name); }); ``` There are two parts to the test. The first does the logon. You get a redirect ... but it will redirect to the same place whether the logon succeeds or fails. And you will have a session cookie even before you log in. So how do you know whether the logon succeeded? The only way is to get the index page again. If the logon is successful, it will show the user's name, but if not, it will show the error message. To do this, we have to include the session cookie in the request, as we do above. @@ -373,7 +377,7 @@ You need to post data, as before, but the only field in the data is ```_csrf```. ## Testing Job CRUD Operations -Create a new file, tests/crud_operations.js. You will need a couple extra require() statements, as follows: +Create a new file, tests/crud_operations.js. You will need a couple of extra require() statements, as follows: ``` const Job = require("../models/Job") const { seed_db, testUserPassword } = require("../util/seed_db"); @@ -382,39 +386,39 @@ The flow for testing CRUD operations is as follows. 1. Seed the database! You have a utility routine for that in util/seed_db.js 2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. Steps 1 and 2 are not tests, but you need an async before() call, inside your describe(), that does these things. Here is the before() that completes steps 1 and 2: - ``` before( async () => { - const { expect, request } = await get_chai() - this.test_user = await seed_db() - let req = request.execute(app) - .get("/session/logon") - .send() - let res = await req + ``` + before(async () => { + const { expect, request } = await get_chai(); + this.test_user = await seed_db(); + let req = request.execute(app).get("/session/logon").send(); + let res = await req; const textNoLineEnd = res.text.replaceAll("\n", ""); - this.csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd)[1] + this.csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd)[1]; let cookies = res.headers["set-cookie"]; this.csrfCookie = cookies.find((element) => element.startsWith("csrfToken"), ); const dataToPost = { - email: this.test_user.email, - password: testUserPassword, - _csrf: this.csrfToken, - }; - req = request.execute(app) + email: this.test_user.email, + password: testUserPassword, + _csrf: this.csrfToken, + }; + req = request + .execute(app) .post("/session/logon") .set("Cookie", this.csrfCookie) .set("content-type", "application/x-www-form-urlencoded") .redirects(0) .send(dataToPost); - res = await req + res = await req; cookies = res.headers["set-cookie"]; this.sessionCookie = cookies.find((element) => element.startsWith("connect.sid"), ); - expect(this.csrfToken).to.not.be.undefined - expect(this.sessionCookie).to.not.be.undefined - expect(this.csrfCookie).to.not.be.undefined - }) + expect(this.csrfToken).to.not.be.undefined; + expect(this.sessionCookie).to.not.be.undefined; + expect(this.csrfCookie).to.not.be.undefined; + }); ``` 3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "" appears on the page. Here's how you might do that part: ``` @@ -423,26 +427,29 @@ The flow for testing CRUD operations is as follows. ``` As you can see, scanning the page to see if the result is correct is kind of messy. 4. Add a job entry! This is a post for the job form. You will have to include _csrf in the post, and you will need to set the CSRF and session cookies. You could use the factory to create values for the job, via a factory.build('job'). The best way to test for success is to see that the database now has 21 entries, as follows: -``` + ``` const jobs = await Job.find({createdBy: this.test_user._id}) expect(jobs.length).to.equal(21) -``` + ``` ## Puppeteer In actual practice, chai-http is mostly used for testing APIs. To test a user interface, whether it be server side rendered or full stack, one would use an actual browser testing engine such as puppeteer. Create a file, tests/puppeteer.js, with the following contents: ``` const puppeteer = require("puppeteer"); -const { server } = require("../app"); +require("../app"); const { seed_db, testUserPassword } = require("../util/seed_db"); +const Job = require("../models/Job"); let testUser = null; + let page = null; let browser = null; // Launch the browser and open a new blank page -describe("index page test", function () { +describe("jobs-ejs puppeteer test", function () { before(async function () { this.timeout(10000); + //await sleeper(5000) browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto("http://localhost:3000"); @@ -452,8 +459,7 @@ describe("index page test", function () { await browser.close(); }); describe("got to site", function () { - it("should have completed a connection", async () => { - }); + it("should have completed a connection", async function () {}); }); describe("index page test", function () { this.timeout(10000); @@ -465,14 +471,14 @@ describe("index page test", function () { it("gets to the logon page", async () => { await this.logonLink.click(); await page.waitForNavigation(); - const email = await page.waitForSelector("input[name=email]"); + const email = await page.waitForSelector('input[name="email"]'); }); }); describe("logon page test", function () { this.timeout(20000); it("resolves all the fields", async () => { - this.email = await page.waitForSelector("input[name=email]"); - this.password = await page.waitForSelector("input[name=password]"); + this.email = await page.waitForSelector('input[name="email"]'); + this.password = await page.waitForSelector('input[name="password"]'); this.submit = await page.waitForSelector("button ::-p-text(Logon)"); }); it("sends the logon", async () => { @@ -481,10 +487,8 @@ describe("index page test", function () { await this.password.type(testUserPassword); await this.submit.click(); await page.waitForNavigation(); - await page.waitForSelector( - `p ::-p-text(${testUser.name} is logged on.)`, - ); - await page.waitForSelector("a ::-p-text(change the secret"); + await page.waitForSelector(`p ::-p-text(${testUser.name} is logged on.)`); + await page.waitForSelector("a ::-p-text(change the secret)"); await page.waitForSelector('a[href="/secretWord"]'); const copyr = await page.waitForSelector("p ::-p-text(copyright)"); const copyrText = await copyr.evaluate((el) => el.textContent); diff --git a/lessons/ctd-node-lesson-15.md b/lessons/ctd-node-lesson-15.md index d9f3e51e1b..d8cbe5abab 100644 --- a/lessons/ctd-node-lesson-15.md +++ b/lessons/ctd-node-lesson-15.md @@ -55,7 +55,6 @@ page is retrieved: after(async function () { this.timeout(5000); await browser.close(); - server.close(); return; }); describe("got to site", function () { From 2a0faf7a145ab8c1c42a7773405e8638e50da3a3 Mon Sep 17 00:00:00 2001 From: "John R. McGarvey" Date: Mon, 1 Jul 2024 23:46:26 -0400 Subject: [PATCH 5/6] added syntax highlighting --- lessons/ctd-node-assignment-1.md | 6 ++--- lessons/ctd-node-assignment-11.md | 36 ++++++++++++++-------------- lessons/ctd-node-assignment-12.md | 14 +++++------ lessons/ctd-node-assignment-13.md | 30 ++++++++++++------------ lessons/ctd-node-assignment-14.md | 4 ++-- lessons/ctd-node-assignment-15.md | 39 ++++++++++++++++--------------- lessons/ctd-node-assignment-2.md | 14 +++++------ lessons/ctd-node-assignment-3.md | 6 ++--- lessons/ctd-node-assignment-4.md | 18 +++++++------- lessons/ctd-node-assignment-6.md | 2 +- lessons/ctd-node-assignment-8.md | 2 +- lessons/ctd-node-lesson-15.md | 6 ++--- lessons/ctd-node-lesson-7.md | 2 +- lessons/ctd-node-lesson-8.md | 4 ++-- 14 files changed, 92 insertions(+), 91 deletions(-) diff --git a/lessons/ctd-node-assignment-1.md b/lessons/ctd-node-assignment-1.md index fcbacd18ae..f75b13af08 100644 --- a/lessons/ctd-node-assignment-1.md +++ b/lessons/ctd-node-assignment-1.md @@ -23,7 +23,7 @@ Your homework should include the following programs: 2. `02-globals.js`: This program should use the `console.log` function to write some globals to the screen. Set an environment variable with the following command in your command line terminal: `export MY_VAR="Hi there!"` The program should then use `console.log` to print out the values of `__dirname` (a Node global variable) and `process.env.MY_VAR` (`process` is also a global, and contains the environment variables you set in your terminal.) You could print out other globals as well ([Node documentation](https://nodejs.org/api/globals.html#global-objects) on available globals). For each of these programs, you invoke them with `node` to make sure they work. 3. For the next part, you will write multiple programs. `04-names.js`, `05-utils.js`, `06-alternative-flavor.js`, and `07-mind-grenade.js` are modules that you load, using require statements, from the `03-modules.js` file, which is the main program. Remember that you must give the path name in your require statement, for example: -``` +```javascript const names = require("./04-names.js"); ``` @@ -38,7 +38,7 @@ const names = require("./04-names.js"); 1. `08-os-module.js`: This should load the built-in `os` Node module and display some interesting information from the resulting object. As for all modules, you load a reference to it with a require statement, in this case -``` +```javascript const os = require("os"); ``` @@ -57,7 +57,7 @@ C:\Users\JohnSmith\node-express-course\01-node-tutorial\answers 1. `10-fs-sync.js`: This should load `writeFileSync` and `readFileSync` functions from the `fs` module. Then you will use `writeFileSync` to write 3 lines to a file, `./temporary/fileA.txt`, using the `"append"` flag for each line after the first one. Then use `readFileSync` to read the file, and log the contents to the console. Be sure you create the file in the `temporary` directory. That will ensure that it isn’t pushed to Github when you submit your answers (because that file has been added to the `.gitignore` file for you already which tells git not to look at those files). 2. `11-fs-async.js`: This should load the `fs` module, and use the asynchronous function `writeFile` to write 3 lines to a file, `./temporary/fileB.txt`. Now, be careful here! This is our first use of **asynchronous functions** in this class, but we are going to use them a lot! First, you need to use the `"append"` flag for all but the first line. Second, each time you write a line to the file, you need to have a callback, because the `writeFile` operation is asynchronous. Third, for each line you write, you need to do the write for the line that follows it within the callback – otherwise the operations won’t happen in order. Put `console.log` statements at various points in your code to tell you when each step completes. Then run the code. Do the console log statements appear in the order you expect? Run the program several times and verify that the file is created correctly. Here is how you might start: -``` +```javascript const { writeFile } = require("fs"); console.log("at start"); writeFile("./temporary/output.txt", "This is line 1\n", (err, result) => { diff --git a/lessons/ctd-node-assignment-11.md b/lessons/ctd-node-assignment-11.md index 7109135fbd..cb16052697 100644 --- a/lessons/ctd-node-assignment-11.md +++ b/lessons/ctd-node-assignment-11.md @@ -102,7 +102,7 @@ This front end uses a single-page style. There are multiple views in the page, i Edit `app.js`. Comment out the following lines: -``` +```javascript app.get("/", (req, res) => { res.send('

    Jobs API

    Documentation'); }); @@ -110,7 +110,7 @@ app.get("/", (req, res) => { Add the following line below these commented out lines: -``` +```javascript app.use(express.static("public")); ``` @@ -134,7 +134,7 @@ To begin, add the following line to index.html, right above the close of the bod These modules call one another using the exports that each provides. For this to work, you must declare it as type `module`. Create `index.js` in the public directory. The `index.js` module should read as follows: -``` +```javascript let activeDiv = null; export const setDiv = (newDiv) => { if (newDiv != activeDiv) { @@ -203,7 +203,7 @@ You will need to create `loginRegister.js`, `register.js`, `login.js`, `jobs.js` The `loginRegister.js` module is as follows: -``` +```javascript import { inputEnabled, setDiv } from "./index.js"; import { showLogin } from "./login.js"; import { showRegister } from "./register.js"; @@ -237,7 +237,7 @@ A separate function handles display of the div. (React works in similar fashion, The `register.js` module is as follows: -``` +```javascript import { inputEnabled, setDiv, @@ -285,7 +285,7 @@ export const showRegister = () => { The `login.js` module is as follows: -``` +```javascript import { inputEnabled, setDiv, @@ -328,7 +328,7 @@ export const showLogin = () => { The `jobs.js` module is as follows: -``` +```javascript import { inputEnabled, setDiv, @@ -369,7 +369,7 @@ export const showJobs = async () => { The `addEdit.js` module is as follows: -``` +```javascript import { enableInput, inputEnabled, message, setDiv, token } from "./index.js"; import { showJobs } from "./jobs.js"; @@ -416,7 +416,7 @@ First, we’ll make register and logon work. For either register or logon, if th Adding these capabilities to `register.js` gives the following: -``` +```javascript import { inputEnabled, setDiv, @@ -514,7 +514,7 @@ Notice that we always clear out the input values before we switch to another pag The `login.js` module becomes: -``` +```javascript import { inputEnabled, setDiv, @@ -590,7 +590,7 @@ export const showLogin = () => { Make these changes and test the application again. You should find that you can register and logon. Logoff doesn’t work right at present, but this can be corrected in `jobs.js` with the following change: -``` +```javascript } else if (e.target === logoff) { setToken(null); @@ -606,7 +606,7 @@ Note that logoff involves no communication with the back end. The user is logged Next we need to make the changes so that we can create job entries. The `addEdit.js` module is changed as follows: -``` +```javascript addEditDiv.addEventListener("click", async (e) => { if (inputEnabled && e.target.nodeName === "BUTTON") { if (e.target === addingJob) { @@ -663,7 +663,7 @@ There is a somewhat tricky part to this. We want to have edit and delete buttons It looks like this in `jobs.js`: -``` +```javascript export const showJobs = async () => { try { enableInput(false); @@ -715,7 +715,7 @@ So, plug this code into `jobs.js` at the appropriate point, and then try the app However, the edit and delete buttons don’t actually work. This is because the click handler in `jobs.js` isn’t set to look for them yet. We can add a section to the click handler to remedy this. -``` +```javascript } else if (e.target.classList.contains("editButton")) { message.textContent = ""; showAddEdit(e.target.dataset.id); @@ -726,7 +726,7 @@ The `dataset.id` contains the id of the entry to be edited. That is then passed This function is in `addEdit.js`, and should be changed as follows: -``` +```javascript export const showAddEdit = async (jobId) => { if (!jobId) { company.value = ""; @@ -778,7 +778,7 @@ With this change, the `add/edit` div will be displayed with the appropriate valu So far, so good, but what happens when the user clicks on the update button? In this case, we need to do a PATCH instead of a POST, and we need to include the id of the entry to be updated in the URL. So we need the following additional changes to addEdit.js: -``` +```javascript if (e.target === addingJob) { enableInput(false); @@ -837,13 +837,13 @@ You’ll call the jobs delete API, and in the URL you will include the ID of the **Note:** There is an error in the implementation of the delete operation in the jobs controller. The instructor’s guidance is to use this line: -``` +```javascript res.status(StatusCodes.OK).send(); ``` This is incorrect, because an empty body is not valid JSON. Change it to: -``` +```javascript res.status(StatusCodes.OK).json({ msg: "The entry was deleted." }); ``` diff --git a/lessons/ctd-node-assignment-12.md b/lessons/ctd-node-assignment-12.md index 0e65d86497..39961e5c1e 100644 --- a/lessons/ctd-node-assignment-12.md +++ b/lessons/ctd-node-assignment-12.md @@ -80,7 +80,7 @@ You do not need a `public` directory. The pages are rendered by the `EJS` engine Next, create the boilerplate `app.js`. It should look as follows: -``` +```javascript const express = require("express"); require("express-async-errors"); @@ -227,7 +227,7 @@ SESSION_SECRET=123lkawjg091u82378429 The secret is some hard to guess string — and you **_never_** want to publicize it to Github! Then, add the following lines to `app.js`. These lines should be added _before_ any of the lines that govern routes, such as the `app.get` and `app.post` statements: -``` +```javascript require("dotenv").config(); // to load the .env file into the process.env object const session = require("express-session"); app.use( @@ -241,7 +241,7 @@ app.use( Change the logic so that the secret word is stored and retrieved in the session, as follows: -``` +```javascript // let secretWord = "syzygy"; <-- comment this out or remove this line app.get("/secretWord", (req, res) => { if (!req.session.secretWord) { @@ -267,7 +267,7 @@ This is the key used to retrieve session data. You can also see that the `HttpOn We want to store the session data in a durable way. To do this, we’ll use Mongo as a session store. Replace the one line that does the `app.use` for session with all of these lines: -``` +```javascript const MongoDBStore = require("connect-mongodb-session")(session); const url = process.env.MONGO_URI; @@ -312,7 +312,7 @@ app.use(require("connect-flash")()); We want to set some messages into flash. To do this, change the `POST` route for `/secretWord` to look like this: -``` +```javascript app.post("/secretWord", (req, res) => { if (req.body.secretWord.toUpperCase()[0] == "P") { req.flash("error", "That word won't work!"); @@ -344,7 +344,7 @@ Whoa! you may be saying. That doesn’t look like HTML! What will the browser do But, the problem is that the `info` and `errors` arrays need to get passed into the EJS file, when the render is called. This could be done as follows: -``` +```javascript res.render("secretWord", { secretWord, errors: flash("error"), @@ -354,7 +354,7 @@ res.render("secretWord", { But this is a little clumsy, because if we have a bunch of pages we render, every render statement would have to be modified. So, instead, we put the values in `res.locals`. That hash contains values that are always available to the EJS rendering engine. As follows: -``` +```javascript app.get("/secretWord", (req, res) => { if (!req.session.secretWord) { req.session.secretWord = "syzygy"; diff --git a/lessons/ctd-node-assignment-13.md b/lessons/ctd-node-assignment-13.md index e3f2da4c20..ac60cab4a6 100644 --- a/lessons/ctd-node-assignment-13.md +++ b/lessons/ctd-node-assignment-13.md @@ -119,7 +119,7 @@ These changes won’t suffice to do anything in the application, until routes ar Create a file `routes/sessionRoutes.js`, as follows: -``` +```javascript const express = require("express"); // const passport = require("passport"); const router = express.Router(); @@ -152,7 +152,7 @@ module.exports = router; Ignore the passport lines for the moment. This just sets up the routes. We need to create a corresponding file `controllers/sessionController.js`. Here we use the `User` model. However, the file you copied makes some references to the JWT library. You must edit `models/User.js` to remove those references in order for `User.js` to load. We aren’t using JWTs in this project. -``` +```javascript const User = require("../models/User"); const parseVErr = require("../util/parseValidationErr"); @@ -215,7 +215,7 @@ The `registerDo` handler will check if the two passwords the user entered match, If there is a Mongoose validation error when creating a user record, we need to parse the validation error object to return the issues to the user in a more helpful format, and we do that in the file util/parseValidationErrs.js: -``` +```javascript const parseValidationErrors = (e, req) => { const keys = Object.keys(e.errors); keys.forEach((key) => { @@ -228,7 +228,7 @@ module.exports = parseValidationErrors; We need some middleware to load `res.locals` with any variables we need, like the logged in user and flash properties. Create `middleware/storeLocals.js`: -``` +```javascript const storeLocals = (req, res, next) => { if (req.user) { res.locals.user = req.user; @@ -245,7 +245,7 @@ module.exports = storeLocals; Now, we need a couple of `app.use` statements. Add these lines right after the `connect-flash` line: -``` +```javascript app.use(require("./middleware/storeLocals")); app.get("/", (req, res) => { res.render("index"); @@ -257,7 +257,7 @@ The storeLocals middleware sets the values for errors, info, and user, but in re We are now using the database. So, we need to connect to it at startup. You need a file, `db/connect.js`. Check that it looks like the following: -``` +```javascript const mongoose = require("mongoose"); const connectDB = (url) => { @@ -269,7 +269,7 @@ module.exports = connectDB; Then add this line to `app.js`, just before the listen line: -``` +```javascript await require("./db/connect")(process.env.MONGO_URI); ``` @@ -279,7 +279,7 @@ Then try the application out, starting at the `"/"` URL. You can try each of the To use Passport, you have to tell it how to authenticate users, retrieving them from the database. Create a file `passport/passportInit.js`, as follows: -``` +```javascript const passport = require("passport"); const LocalStrategy = require("passport-local").Strategy; const User = require("../models/User"); @@ -349,7 +349,7 @@ Then when Passport is handling a protected route, it will use the `deserializeUs You can now add the following lines to `app.js`, right _after_ the `app.use` for session (Passport relies on session): -``` +```javascript const passport = require("passport"); const passportInit = require("./passport/passportInit"); @@ -364,7 +364,7 @@ Then we call `passport.initialize()` (which sets up Passport to work with Expres Finally, you can now uncomment the lines having to do with Passport in `routes/sessionRoutes.js`, so that the require statement for Passport is included, and so that the route for logon looks like -``` +```javascript router .route("/logon") .get(logonShow) @@ -381,7 +381,7 @@ This means that when someone sends a `POST` request to the `/sessions/logon` pat Since we’re letting Passport handle setting the `req.flash` properties now, we can remove the lines in `controllers/sessionController.js` that set the flash messages for the `loginShow` handler. So that should now just look like: -``` +```javascript const logonShow = (req, res) => { if (req.user) { return res.redirect("/"); @@ -398,7 +398,7 @@ To protect a route, you need some middleware, as follows. `middleware/auth.js`: -``` +```javascript const authMiddleware = (req, res, next) => { if (!req.user) { req.flash("error", "You can't access that page before logon."); @@ -417,7 +417,7 @@ We want to protect any route for the `"/secretWord"` path. The best practice is `routes/secretWord.js`: -``` +```javascript const express = require("express"); const router = express.Router(); @@ -448,14 +448,14 @@ We could further refactor this by moving the code for handling the routes (`(req Next let’s replace the `app.get` and `app.post` statements for the `"/secretWord"` routes in `app.js` with these lines: -``` +```javascript const secretWordRouter = require("./routes/secretWord"); app.use("/secretWord", secretWordRouter); ``` Then try out the secretWord page to make sure it still works. Turning on protection is simple. You add the authentication middleware to the route above as follows: -``` +```javascript const auth = require("./middleware/auth"); app.use("/secretWord", auth, secretWordRouter); ``` diff --git a/lessons/ctd-node-assignment-14.md b/lessons/ctd-node-assignment-14.md index 27f8c8bffe..8e21276f0a 100644 --- a/lessons/ctd-node-assignment-14.md +++ b/lessons/ctd-node-assignment-14.md @@ -56,7 +56,7 @@ This is really a form masquerading as a button. And, because it’s a form, you Ok, so how to build the table? The `GET` for `"/jobs"` comes in, and your router calls a function in your controller to pull all the job listings for that user from the database into a jobs array (which might be empty). Then the controller function makes the following call: -``` +```javascript res.render("jobs", { jobs }); ``` @@ -106,7 +106,7 @@ So that the actual id of the entry to delete is included in the URL on the `POST 1. Create `routes/jobs.js` and `controllers/jobs.js`. The router should have each of the routes previously described, and the controller should have functions to call when each route is invoked. Remember that `req.params` will have the id of the entry to be edited, updated, or deleted. You might want to start with simple `res.send()` operations to make sure each of the routes and controller functions are getting called as expected. 2. In `app.js`, `require` the jobs router, and add an `app.use` statement for it, at an appropriate place in the code. The `app.use` statement might look like: -``` +```javascript app.use("/jobs", auth, jobs); ``` diff --git a/lessons/ctd-node-assignment-15.md b/lessons/ctd-node-assignment-15.md index 18da6b7f9a..e211f95d8c 100644 --- a/lessons/ctd-node-assignment-15.md +++ b/lessons/ctd-node-assignment-15.md @@ -21,11 +21,11 @@ Create a test directory in your repository. This is where you will put the actua "test": "NODE_ENV=test mocha tests/*.js --exit", ``` which will cause the tests to run. It also sets the NODE_ENV environment variable, which we'll use to load the test version of the database. Edit your app.js. You'll have a line that reads something like: -``` +```javascript await require("./db/connect")(process.env.MONGO_URI); ``` You should change it to look something like the following: -``` +```javascript let mongoURL = process.env.MONGO_URI if (process.env.NODE_ENV == "test") { mongoURL = process.env.MONGO_URI_TEST @@ -33,7 +33,7 @@ if (process.env.NODE_ENV == "test") { await require("./db/connect")(mongoURL); ``` The point of this is so that your testing doesn't interfere with your production database, and also so that your production or development data doesn't interfere with your testing. Also, you want to have a function that will bring the database to a known state, so that previous tests don't cause subsequent ones to give false results. Create a file util/seed_db.js. It should read as follows: -``` +```javascript const Job = require("../models/Job") const User = require("../models/User") const faker = require("@faker-js/faker").fakerEN_US @@ -79,7 +79,7 @@ A couple of new ideas are introduced above. First, faker is being used to gener ## Unit Testing a Function Create a file, utils/multiply.js. It should export a function, multiply, that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: -``` +```javascript const multiply = require('../util/multiply') const expect = require('chai').expect @@ -99,7 +99,7 @@ Then do: ```npm run test``` You will see that the first test passes, but the sec ## Function Testing for An API Your current application doesn't have an API, so you can add one by adding the following to app.js: -``` +```javascript app.get("/multiply", (req,res)=> { const result = req.query.first * req.query.second if (result.isNaN) { @@ -111,7 +111,7 @@ app.get("/multiply", (req,res)=> { }) ``` You also have to change app.js to make your app available to the test. The bottom of the file should look like: -``` +```javascript const port = process.env.PORT || 3000; const start = () => { try { @@ -133,7 +133,7 @@ You can try it out if you like, by doing the following in your browser: http://localhost:3000/multiply?first=5&second=27 ``` Then create a test, a file tests/test_multiply_api.js, as follows: -``` +```javascript const chai = require("chai"); chai.use(require("chai-http")); const { app, server } = require("../app"); @@ -165,7 +165,7 @@ Note first of all that this file actually requires your app, which causes your a Of course, the application you are writing is not intended to provide an API. Instead it provides rendered HTML pages. You can test these as well. There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai recieves a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error and call the callback, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately, with this middleware, which should be added before your routes: -``` +```javascript app.use((req,res,next)=> { if (req.path == "/multiply") { res.set("Content-Type","application/json") @@ -176,7 +176,7 @@ app.use((req,res,next)=> { }) ``` Now create a simple UI test case, in tests/test_ui.js: -``` +```javascript const chai = require("chai"); chai.use(require("chai-http")); const { app, server } = require("../app"); @@ -207,7 +207,8 @@ In this case, you get a res.text, instead of a res.body. The text is the actual ## Testing Registration Here is a test for registration: -```const chai = require("chai"); +```javascript +const chai = require("chai"); chai.use(require("chai-http")); const { app, server } = require("../app"); const expect = chai.expect; @@ -287,11 +288,11 @@ If one of the expect() assertions fails, the rest of the code in that it() stanz ### A Reminder About Arrow Functions and Non-Arrow Functions You will notice that we declare anonymous functions two different ways: -``` +```javascript describe("tests for registration and logon", function () { ``` and -``` +```javascript it("should get the registration page", (done) => { ``` The difference is that arrow functions do not have their own "this"! They inherit the this of the context in which they were defined. So, when we save to the variable this.csrfToken, we do it in the context of the describe(). On that call to describe, we pass ```function ()```, and so the this is associated with that context. As a result this.csrfToken is available on our next it() call within that same describe, so long as that call to it() passes an arrow function. The best practice is to use arrow functions with it() and not to use them with describe(). @@ -304,11 +305,11 @@ When we post, we have to set the cookie for CSRF protection. We also have to se We can then search the database to verify that the user object was actually created. Just to restate, we have two kinds of it() statements; -``` +```javascript it("should get the registration page", (done) => { ``` and -``` +```javascript it("should register the user", async () => { ``` In the first way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. @@ -318,7 +319,7 @@ If the user is actually created, our controller sends a redirect. By default, C ### An Aside on Status Codes When the controller gets an error from a post, it can render the page again -``` +```javascript req.flash("error", "That email address is already registered."); return res.status(400).render("register", { errors: req.flash("error") }); ``` @@ -327,7 +328,7 @@ Be careful to include the status(400). If the status is 200, the request is exp ## Testing Logon We saved this.user and this.password, so we should be able to log in. We'll skip actually loading the logon form -- you could add that test if you like -- and we'll do the post for logon. When you logon, you are redirected. By default, Chai then follows the redirection, but what it doesn't do is keep the cookies. When you do the .send for the test, the cookies are already gone. This is completely useless for logon. We need the session cookie for subsequent requests. It is pretty poor in another way. If you redirect, the session contains the flash information for user messages, but if the cookies are gone, so are the flash messages. So, a better policy is to disable redirects by doing .redirects(0) on the request. If a redirect occurs, the status is 302, and the req.headers.location is the target for the redirect. (Editorial aside: Chai really ought to save those cookies.) So, here is the logon test: -``` +```javascript it("should log the user on", async () => { const dataToPost = { email: this.user.email, @@ -371,7 +372,7 @@ We saved this.user and this.password, so we should be able to log in. We'll ski There are two parts to the test. The first does the logon. You get a redirect ... but it will redirect to the same place whether the logon succeeds or fails. And you will have a session cookie even before you log in. So how do you know whether the logon succeeded? The only way is to get the index page again. If the logon is successful, it will show the user's name, but if not, it will show the error message. To do this, we have to include the session cookie in the request, as we do above. **Now: Some code for you to write.** Create a test for logoff. Logoff won't work unless there has been a logon, and unless you send the _csrf value and set cookies for both the csrfToken and the sessionCookie. The latter code is: -``` +```javascript .set("Cookie", csrfToken + ";" + sessionCookie) ``` The only way to know if it succeeded is to send a get request for the index page, passing the session cookie you already had. It will have the message about clicking the link to logon, and it will not have the user name, because that session has been invalidated on the server side. @@ -383,7 +384,7 @@ Create a new file, tests/crud_operations.js. The flow for testing CRUD operatio 1. Seed the database! You have a utility routine for that in util/seed_db.js 2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. 3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "
  • " appears on the page. Here's how you might do that part: - ``` + ```javascript const pageParts = res.text.split("
  • ") expect(pageParts).to.equal(21) ``` @@ -393,7 +394,7 @@ Create a new file, tests/crud_operations.js. The flow for testing CRUD operatio ## Puppeteer In actual practice, chai-http is mostly used for testing APIs. To test a user interface, whether it be server side rendered or full stack, one would use an actual browser testing engine such as puppeteer. Create a file, tests/puppeteer.js, with the following contents: -``` +```javascript const puppeteer = require("puppeteer"); const { server } = require("../app"); const { seed_db, testUserPassword } = require("../util/seed_db"); diff --git a/lessons/ctd-node-assignment-2.md b/lessons/ctd-node-assignment-2.md index a54835c856..10751ad4b0 100644 --- a/lessons/ctd-node-assignment-2.md +++ b/lessons/ctd-node-assignment-2.md @@ -38,7 +38,7 @@ The scripts give npm commands you can run as you develop. In this case, the comm Now for some code. The instructor showed several patterns for JavaScript asynchronous programming. An asynchronous Javascript function returns a [Promise](https://javascript.info/promise-basics), or in some cases a “thenable” which acts like a Promise. You need to resolve the Promise in order to get the actual return value. To resolve a Promise inside an `async function`, you use the keyword, `await`. This should be used inside a `try/catch` block so that you can handle any errors, as follows: -``` +```javascript const myFunc = async () => { ... return result @@ -59,7 +59,7 @@ is not async. In this case, you can’t use `await` – it will give a syntax error. So you can either use `.then` or you can wrap the function call as follows: -``` +```javascript const myFunc3 = () => { // not async, and in some contexts we better not make it async myFunc() .then((result) => { @@ -90,7 +90,7 @@ const myFunc4 = () => { // the other way to do it, via a wrapper: There’s one more trick with the `.then`. Suppose you need to make a string of calls to async functions. You can chain the `.then` statements as follows: -``` +```javascript const myFunc6 = () => { myFunc() // an async function, so it returns a promise .then((result) => { @@ -125,7 +125,7 @@ Create another async function called `reader` that reads the file with `await re Now we want to call the two functions in order, first the writer, and the reader. But, be careful! These are asynchronous functions, so if you just call them, you don’t know what order they’ll occur in. And you can’t use await in your mainline code. So, you write a third async function called `readWrite`. In that function, you call await reader and await writer. Finally, write a line at the bottom of the file that calls the `readWrite` function. Test your code. The temp.txt file that your code is creating should not be sent to Github, so you should add this filename as another line to your `.gitignore.` 2. Write another program called `writeWithPromisesThen.js` also in the `01-node-tutorial/answers` folder. Again you write to temp.txt. You start it the same way, but this time, you use the `.then` style of asynchronous programming. You don’t need to create any functions. Instead, you just use cascading .then statements in your mainline, like this: -``` +```javascript writeFile(...) // write line 1 .then(() => { return writeFile(...) // write line 2. @@ -142,14 +142,14 @@ Test your code by running `node writeWithPromisesThen.js`. You may want to sprinkle console.log statements in your code so that you understand the order of execution. 3. We want to understand event emitters. First, modify `prompter.js`, to add the following lines above the listen statement: -``` +```javascript server.on("request", (req) => { console.log("event received: ", req.method, req.url); }); ``` Then test this (`npm run dev`) and try with your browser to see the events the server is emitting. 4. Write a program named `customEmitter.js` in the `01-node-tutorial/answers` folder. In it, create one or several emitters. Then use the emitter `.on` function to handle the events you will emit, logging the parameters to the screen. Then use the emitter `.emit` function to emit several events, with one or several parameters, and make sure that the events are logged by your event handlers. This is your chance to be creative! You could have an event handler that emits a different event to be picked up by a different handler, for example. Here’s a couple tricks to try. You can trigger events with a timer, as follows: -``` +```javascript const EventEmitter = require("events"); const emitter = new EventEmitter(); setInterval(() => { @@ -158,7 +158,7 @@ setInterval(() => { emitter.on("timer", (msg) => console.log(msg)); ``` Or, you could make an async function that waits on an event: -``` +```javascript const EventEmitter = require("events"); const emitter = new EventEmitter(); const waitForEvent = () => { diff --git a/lessons/ctd-node-assignment-3.md b/lessons/ctd-node-assignment-3.md index b0599f87c2..8552bfee7d 100644 --- a/lessons/ctd-node-assignment-3.md +++ b/lessons/ctd-node-assignment-3.md @@ -17,7 +17,7 @@ You continue working in the node-express-course repository, but for this week, y 6. Try the URL http://localhost:3000/not-there. You should see that your `app.all` for page not found returns a 404 error. 7. For the next part, you will implement APIs that return JSON. Because you are using the browser to display the JSON, you may want to add a JSON formatter plugin into your browser ([here’s one for Chrome](https://chrome.google.com/webstore/detail/jsonvue/chklaanhfefbnpoihckbnefhakgolnmc), for example), so that it’s easier to view. Add an `app.get` statement to `app.js`. It should be _after_ the Express static middleware, but _before_ the “not found” handler. It should be for the URL `/api/v1/test`. It should return JSON using the following code: -``` +```javascript res.json({ message: "It worked!" }); ``` @@ -25,7 +25,7 @@ Try that URL from your browser, and verify that it works. 1. Next, we want to return some data. We haven’t learned how to access a database from Express yet, so the instructor has provided data to use. It is in `data.js`, so have a look at that file. Then add the following require statement to the top of the program: -``` +```javascript const { products } = require("./data"); ``` @@ -38,7 +38,7 @@ array. Test the url with your browser. 1. Next, you need to provide a way to retrieve a particular product by ID. This is done by having an `app.get` statement for the url `/api/v1/products/:productID`. The colon in this url means that `:productID` is a _parameter_. So, when your server receives the GET request for a URL like `/api/v1/products/7`, `req.params` will have the hash `{ productID: 7 }`. Try this out by creating the `app.get` statement and doing a `res.json(req.params)` to return the path parameter in the HTTP response itself. 2. Of course, the API should actually return, in JSON form, the product that has an ID of 7\. So you need to find that product in the array. For that, you use the `.find` [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Array/find) of the array: -``` +```javascript const idToFind = parseInt(req.params.productID); const product = products.find((p) => p.id === idToFind); ``` diff --git a/lessons/ctd-node-assignment-4.md b/lessons/ctd-node-assignment-4.md index 17bbd311bc..2994043cd4 100644 --- a/lessons/ctd-node-assignment-4.md +++ b/lessons/ctd-node-assignment-4.md @@ -16,7 +16,7 @@ At this point, you may get a merge conflict. This happens if there are changes t Make the following changes to `app.js` and related files. (Note that examples of code that perform these functions are available in the `final` directory.) First, create a _middleware function_ called `logger` in `app.js`. A middleware function is passed `req` and `res` as its first two parameters, just like an `app.get` call, but it is also passed a third parameter, `next`. The next() function must be called once middleware processing is completed, otherwise no response is sent back for the request. The middleware function you create should log the `method` and `url` properties from the `req` object, as well as the current time, before calling `next()`. Middleware functions are called in two ways. First, you can insert them into your route statements, as follows: -``` +```javascript app.get('/', logger, (req, res) => { ... }) @@ -25,7 +25,7 @@ app.get('/', logger, (req, res) => { This means the `logger` middleware function will run before any `GET` request to the `/` path. The second way to invoke middleware is via an `app.use()` statement: -``` +```javascript app.use(["/path1", "/path2"], logger); ``` @@ -33,20 +33,20 @@ In this case, the first argument is a path or an array of paths indicating the u Next, you need to implement some APIs for people. You have a require statement for `./data.js` that gets the value of products. Get the value for people, also from `./data.js` (add this in the same require statement). Then implement an `app.get` for `/api/v1/people`. Test it with your browser. You are returning JSON, so you call res.json(…) to send the data back. You now need to implement an `app.post` for `/api/v1/people`. This is to add an entry to the people array. Post operations are sent from the browser with a “request body”. You need to add middleware to parse this body into a Javascript object. The following statements do this parsing, returning the result as a hash in `req.body`. -``` +```javascript app.use(express.urlencoded({ extended: false })); app.use(express.json()); ``` The first of these parses url-encoded data, which is the format that is sent by an HTML form. This is not typically needed if you are only implementing a JSON API. The second statement parses a JSON body. You need these statements before your `app.post()` statement, so that the body is parsed before you do the rest of the processing. Now you implement the `app.post` statement for `/api/v1/people`. The statement should check `req.body` to see if there is a `req.body.name` property. If not, it should return JSON for an error, as follows: -``` +```javascript res.status(400).json({ success: false, message: "Please provide a name" }); ``` This sets the HTTP result code to [400](https://http.dev/400), which means there was an error on the client side, and also returns an error message. But suppose there is a value in req.body.name. You want to add this entry to the people array, as follows: -``` +```javascript people.push({ id: people.length + 1, name: req.body.name }); res.status(201).json({ success: true, name: req.body.name }); ``` @@ -57,20 +57,20 @@ Now is the time to test it out using Postman. Create a Postman GET request for ` The next step is refactoring. You do not want too much logic in the `app.js` file. Create directories called routes and controllers. Create a file called `routes/people.js`. It should start with the following statements: -``` +```javascript const express = require("express"); const router = express.Router(); ``` You then need to add a `router.get()` statement for the `"/"` path. This should do the same thing as your `app.get("/api/v1/people")` statement. Similarly, you need a `router.post()` statement for `"/"`. Finally, at the bottom, you need `module.exports = router`. You now need to add a require statement in the `app.js` file, to import the `peopleRouter` code. Then you need the following `app.use()` statement, also in app.js: -``` +```javascript app.use("/api/v1/people", peopleRouter); ``` Be careful that this `app.use` statement comes _after_ the parsing of the body, or stuff won’t work as expected. Then comment out your `app.get` and `app.post` statements for `/api/v1/people`. Test the result using Postman, fixing bugs as needed. The refactoring is not yet done. You need to create the file `controllers/people.js`. That should start with a require statement that gets the people array from `../data.js`. Then create functions `addPerson` and `getPeople`. These are each passed `req` and `res`. Copy the logic from your `router/people.js` file, for both the GET and the POST. Then export `{ addPerson, getPeople }`. Then require them in your `routes/people.js`, as follows: -``` +```javascript const { addPerson, getPeople } = require("../controllers/people.js"); ``` @@ -82,7 +82,7 @@ Then move the logic for the statement to `controllers/people.js`, and update the This optional assignment gives some idea of how authentication might work. You will use the `cookie-parser` npm package, so do an `npm install` for that package. Cookies are set, typically by the back end, the browser then stores them and attaches them to each subsequent request. This allows us to add some “state” to each HTTP request. That is, the browser and backend can ‘remember’ some information automatically across requests, like for example, which user is making these requests. Add to `app.js` a require statement for `cookie-parser`. Then, right after you parse the body of the request, add a statement to parse the cookies: -``` +```javascript app.use(cookieParser()); ``` diff --git a/lessons/ctd-node-assignment-6.md b/lessons/ctd-node-assignment-6.md index 93a2a7c9a6..43ea426681 100644 --- a/lessons/ctd-node-assignment-6.md +++ b/lessons/ctd-node-assignment-6.md @@ -6,7 +6,7 @@ Create a file in the starter directory called `QuizAnswers2.txt`. Put answers to 1. In this lesson, you created a middleware function called `asyncWrapper`. Why? 2. Suppose that you want to make sure that both a status code and an error message are sent back to the user when they request the URL for a task that does not exist. Assume that you’ve created a `CustomAPIError` class and an error handler that references that class. Complete the code: -``` +```javascript const getTask = asyncWrapper(async (req, res, next) => { const { id: taskID } = req.params; const task = await Task.findOne({ _id: taskID }); diff --git a/lessons/ctd-node-assignment-8.md b/lessons/ctd-node-assignment-8.md index 688447b56d..f0d55d9412 100644 --- a/lessons/ctd-node-assignment-8.md +++ b/lessons/ctd-node-assignment-8.md @@ -29,7 +29,7 @@ _(right-click and open image in a new tab to see it a little larger)_ Now open up the POST request for logon. Click on tests. Then enter the following code: -``` +```javascript const jsonData = pm.response.json(); pm.environment.set("token", jsonData.token); ``` diff --git a/lessons/ctd-node-lesson-15.md b/lessons/ctd-node-lesson-15.md index fe0c5d7f20..a7070d2b8f 100644 --- a/lessons/ctd-node-lesson-15.md +++ b/lessons/ctd-node-lesson-15.md @@ -14,7 +14,7 @@ For Node/Express, there are several standard testing tools. Mocha is a testing f Here is a sample test case that uses Mocha and Chai. It won't work for the present lesson, because it is for calling an API, not getting a page. -``` +```javascript describe("Jobs", function () { describe("GET /jobs", function () { // Test to get all jobs belonging to the logged on user @@ -42,7 +42,7 @@ Some other things to think about: We have been using a single Mongo database for With Puppeteer, you actually load the pages, find the HTML elements on the page, and interact with them, using a browser engine, which is typically Chrome. You can run Chrome in "headless" mode, but if you do not do this, you can actually watch your test typing values into a browser window. Puppeteer involves a lot of async/await. Here is the start of a Puppeteer test, where the connection to the browser is made and a page is retrieved: -``` +```javascript describe("Functional Tests with Puppeteer", function () { let browser = null; let page = null; @@ -73,7 +73,7 @@ page is retrieved: In the above code, this.timeout(5000) sets the timeout for that test, the amount of time by which the operation certainly should have succeeded. We can verify that the page came up with some entry fields, identified by their HTML IDs or other attributes. To find the button, we use some special syntax provided by Puppeteer, to find a button with "Add" in the text. Then, we can start interacting with the form, as follows: -``` +```javascript it("should create a person record given name and age", async () => { await this.nameField.type("Fred"); await this.ageField.type("10"); diff --git a/lessons/ctd-node-lesson-7.md b/lessons/ctd-node-lesson-7.md index 5e8769bfab..030718106a 100644 --- a/lessons/ctd-node-lesson-7.md +++ b/lessons/ctd-node-lesson-7.md @@ -4,7 +4,7 @@ In this lesson, you parse the query parameters passed with the REST request, app One part of this assignment is a little confusing. You will see code like this: -``` +```javascript let result = Product.find(queryObject); ... result = result.sort(sortList); diff --git a/lessons/ctd-node-lesson-8.md b/lessons/ctd-node-lesson-8.md index edc46e62cb..8fe55e2ed8 100644 --- a/lessons/ctd-node-lesson-8.md +++ b/lessons/ctd-node-lesson-8.md @@ -36,7 +36,7 @@ But some errors are not expected — that is, if your code is working right, the The `StatusError` class could look like this: -``` +```javascript class StatusError extends Error { constructor(message, resultCode) { super(message); @@ -47,7 +47,7 @@ class StatusError extends Error { Using this class, if your authentication middleware finds that the JWT token is missing or invalid, you can just throw the error as follows: -``` +```javascript throw new StatusError( "The request was not authenticated", StatusCodes.UNAUTHORIZED From 51ba39684a9b6777c9b98dfc5401bad99c9fa372 Mon Sep 17 00:00:00 2001 From: "John R. McGarvey" Date: Tue, 2 Jul 2024 00:01:11 -0400 Subject: [PATCH 6/6] add syntax highlighting --- lessons/ctd-node-assignment-15.md | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lessons/ctd-node-assignment-15.md b/lessons/ctd-node-assignment-15.md index 591143c927..7833548d2f 100644 --- a/lessons/ctd-node-assignment-15.md +++ b/lessons/ctd-node-assignment-15.md @@ -21,11 +21,11 @@ Create a test directory in your repository. This is where you will put the actua "test": "NODE_ENV=test mocha tests/*.js --timeout 5000 --exit", ``` which will cause the tests to run. It also sets the NODE_ENV environment variable, which we'll use to load the test version of the database. Edit your app.js. You'll have a line that reads something like: -``` +```javascript const url = process.env.MONGO_URI; ``` You should change it to look something like the following: -``` +```javascript let mongoURL = process.env.MONGO_URI; if (process.env.NODE_ENV == "test") { mongoURL = process.env.MONGO_URI_TEST; @@ -33,7 +33,7 @@ if (process.env.NODE_ENV == "test") { ``` and then change url to mongoURL in the section that starts ```const store = ```. The point of this is so that your testing doesn't interfere with your production database, and also so that your production or development data doesn't interfere with your testing. Also, you want to have a function that will bring the database to a known state, so that previous tests don't cause subsequent ones to give false results. Create a file util/seed_db.js. It should read as follows: -``` +```javascript const Job = require("../models/Job"); const User = require("../models/User"); const faker = require("@faker-js/faker").fakerEN_US; @@ -81,13 +81,13 @@ A couple of new ideas are introduced above. First, faker is being used to gener These packages are now ESM only! This was, in my humble opinion, a questionable move on the part of the developers, and they made quite a few other breaking changes. But we can accommodate these changes, without converting to ESM modules. (Some students are using ESM modules for these exercises. If you are doing this, discuss matters with your mentors if you have trouble.) For Chai 4 and Chai-http 4, we could do: -``` +```javascript const chai = require('chai') const chaiHttp = require('chai-http') chai.use(chaiHttp) ``` This would give you access to chai.expect() (for evaluating results) and chai.request() (for sending http requests to the server and getting back the results). This is not going to work for V5 of these packages: You can't use request() to load an ESM only module. Also you can only call chai.use() once, for all of your test files and cases. So, we need the following utility module, util\get_chai.js: -``` +```javascript let chai_obj = null; const get_chai = async () => { @@ -109,7 +109,7 @@ In this way, we avoid using request(), and we can ensure that chai.use() is only ## Unit Testing a Function Create a file, utils/multiply.js. It should export a function, multiply(), that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: -``` +```javascript const multiply = require("../util/multiply"); const get_chai = require("../util/get_chai"); @@ -131,7 +131,7 @@ Then do: ```npm run test``` You will see that the first test passes, but the sec ## Function Testing for An API Your current application doesn't have an API, so you can add one by adding the following, at an appropriate place (like before the not found handler) to app.js: -``` +```javascript app.get("/multiply", (req, res) => { const result = req.query.first * req.query.second; if (result.isNaN) { @@ -143,7 +143,7 @@ app.get("/multiply", (req, res) => { }); ``` You also have to change app.js to make your app available to the test. The bottom of the file should look like: -``` +```javascript const port = process.env.PORT || 3000; const start = () => { try { @@ -165,7 +165,7 @@ Here, to facilitate testing, we have made start() synchronous. You can try the m http://localhost:3000/multiply?first=5&second=27 ``` Then create a test, a file tests/test_multiply_api.js, as follows: -``` +```javascript const { app } = require("../app"); const get_chai = require("../util/get_chai"); @@ -192,7 +192,7 @@ Note first of all that this file actually requires your app, which causes your a Of course, the application you are writing is not intended to provide an API. Instead it provides rendered HTML pages. You can test these as well. There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai receives a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately with this middleware, which should be added to app.js before your routes: -``` +```javascript app.use((req, res, next) => { if (req.path == "/multiply") { res.set("Content-Type", "application/json"); @@ -203,7 +203,7 @@ app.use((req, res, next) => { }); ``` Now create a simple UI test case, in tests/test_ui.js: -``` +```javascript const { app } = require("../app"); const get_chai = require("../util/get_chai"); @@ -223,7 +223,7 @@ In this case, you get a res.text, instead of a res.body. The text is the actual ## Testing Registration Here is a test for registration. You should put it in a file tests/registration_logon.js. -``` +```javascript const { app } = require("../app"); const { factory, seed_db } = require("../util/seed_db"); const faker = require("@faker-js/faker").fakerEN_US; @@ -289,11 +289,11 @@ If one of the expect() assertions fails, the rest of the code in that it() stanz ### A Reminder About Arrow Functions and Non-Arrow Functions You will notice that we declare anonymous functions two different ways: -``` +```javascript describe("tests for registration and logon", function () { ``` and -``` +```javascript it("should get the registration page", async () => { ``` The difference is that arrow functions do not have their own "this"! They inherit the this of the context in which they were defined. So, when we save to the variable this.csrfToken, we do it in the context of the describe(). On that call to describe, we pass ```function ()```, and so the this is associated with that context. As a result this.csrfToken is available on our next it() call within that same describe, so long as that call to it() passes an arrow function. There are, of course, other ways to save the token, such is in a variable with module scope. @@ -305,11 +305,11 @@ Ok, so what do we post, and where do we post it? The post for register is /sessi When we post, we have to set the cookie for CSRF protection. We also have to set the content-type, which would otherwise be JSON. We also have to include the csrfToken in the data that is posted, with the name _csrf. We post the resulting information, and then search the database to verify that the user object was actually created. There could be two kinds of it() statements; -``` +```javascript it("should get the registration page", (done) => { ``` and -``` +```javascript it("should register the user", async () => { ``` In the first (old style) way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. In our examples, we only use the second way, because we have to call get_chai(), which is asynchronous. @@ -319,7 +319,7 @@ If the user is actually created, our controller sends a redirect. By default, C ### An Aside on Status Codes When the controller gets an error from a post, it can render the page again -``` +```javascript req.flash("error", "That email address is already registered."); return res.status(400).render("register", { errors: req.flash("error") }); ``` @@ -328,7 +328,7 @@ Be careful to include the status(400). If the status is 200, the request is exp ## Testing Logon We saved this.user and this.password, so we should be able to log in. We'll skip actually loading the logon form -- you could add that test if you like -- and we'll do the post for logon. When you logon, you are redirected. By default, Chai then follows the redirection, but what it doesn't do is keep the cookies. When you do the .send for the test, the cookies are already gone. This is completely useless for logon. We need the session cookie for subsequent requests. It is pretty poor in another way. If you redirect, the session contains the flash information for user messages, but if the cookies are gone, so are the flash messages. So, a better policy is to disable redirects by doing .redirects(0) on the request. If a redirect occurs, the status is 302, and the req.headers.location is the target for the redirect. (Editorial aside: Chai really ought to save those cookies.) So, here is the logon test, which should be added to the previous describe() section: -``` +```javascript it("should log the user on", async () => { const dataToPost = { email: this.user.email, @@ -370,7 +370,7 @@ We saved this.user and this.password, so we should be able to log in. We'll ski There are two parts to the test. The first does the logon. You get a redirect ... but it will redirect to the same place whether the logon succeeds or fails. And you will have a session cookie even before you log in. So how do you know whether the logon succeeded? The only way is to get the index page again. If the logon is successful, it will show the user's name, but if not, it will show the error message. To do this, we have to include the session cookie in the request, as we do above. **Now: Some code for you to write.** Create a test for logoff. Logoff won't work unless there has been a logon, and unless you send the _csrf value and set cookies for both the csrfToken and the sessionCookie. The latter code is: -``` +```javascript .set("Cookie", this.csrfToken + ";" + this.sessionCookie) ``` You need to post data, as before, but the only field in the data is ```_csrf```. In this case, you let Chai follow the redirect, that is, do not do ```.redirects(0)```. You should get back a page that includes "link to logon". @@ -378,7 +378,7 @@ You need to post data, as before, but the only field in the data is ```_csrf```. ## Testing Job CRUD Operations Create a new file, tests/crud_operations.js. You will need a couple of extra require() statements, as follows: -``` +```javascript const Job = require("../models/Job") const { seed_db, testUserPassword } = require("../util/seed_db"); ``` @@ -386,7 +386,7 @@ The flow for testing CRUD operations is as follows. 1. Seed the database! You have a utility routine for that in util/seed_db.js 2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. Steps 1 and 2 are not tests, but you need an async before() call, inside your describe(), that does these things. Here is the before() that completes steps 1 and 2: - ``` + ```javascript before(async () => { const { expect, request } = await get_chai(); this.test_user = await seed_db(); @@ -421,13 +421,13 @@ The flow for testing CRUD operations is as follows. }); ``` 3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "" appears on the page. Here's how you might do that part: - ``` + ```javascript const pageParts = res.text.split("") expect(pageParts).to.equal(21) ``` As you can see, scanning the page to see if the result is correct is kind of messy. 4. Add a job entry! This is a post for the job form. You will have to include _csrf in the post, and you will need to set the CSRF and session cookies. You could use the factory to create values for the job, via a factory.build('job'). The best way to test for success is to see that the database now has 21 entries, as follows: - ``` + ```javascript const jobs = await Job.find({createdBy: this.test_user._id}) expect(jobs.length).to.equal(21) ``` @@ -435,7 +435,7 @@ The flow for testing CRUD operations is as follows. ## Puppeteer In actual practice, chai-http is mostly used for testing APIs. To test a user interface, whether it be server side rendered or full stack, one would use an actual browser testing engine such as puppeteer. Create a file, tests/puppeteer.js, with the following contents: -``` +```javascript const puppeteer = require("puppeteer"); require("../app"); const { seed_db, testUserPassword } = require("../util/seed_db");