diff --git a/src/entry-points/sensors-api.js b/src/entry-points/sensors-api.js index e442f4a..32b122c 100644 --- a/src/entry-points/sensors-api.js +++ b/src/entry-points/sensors-api.js @@ -69,7 +69,12 @@ function defineAllRoutes(expressApp) { router.get('/sensor-events/:id', async (req, res, next) => { const sensorsService = new SensorsService(); const sensorToReturn = await sensorsService.getSensorById(req.params.id); - res.json(sensorToReturn); + + if (sensorToReturn === null) { + res.status(404).json(sensorToReturn); + } else { + res.json(sensorToReturn); + } }); router.delete('/sensor-events/:id', async (req, res, next) => { diff --git a/test/mission-basic-response-bay.test.js b/test/mission-basic-response-bay.test.js index 6b5ed64..4ab4b26 100644 --- a/test/mission-basic-response-bay.test.js +++ b/test/mission-basic-response-bay.test.js @@ -12,6 +12,7 @@ const { } = require('../src/entry-points/sensors-api'); const { getShortUnique, getSensorEvent } = require('./test-helper'); const sinon = require('sinon'); +const MessageQueueClient = require('../src/libraries/message-queue/mq-client'); let expressApp; @@ -51,7 +52,7 @@ describe('Sensors test', () => { color: 'Green', weight: 80, status: 'active', - category: 'Kids-Room', + category: undefined, // 💡 TIP: Consider explicitly specify that category is undefined by assigning 'undefined' }; @@ -59,23 +60,31 @@ describe('Sensors test', () => { // 💡 TIP: use any http client lib like Axios OR supertest // 💡 TIP: This is how it is done with Supertest -> await request(expressApp).post("/sensor-events").send(eventToAdd); + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); // Assert // 💡 TIP: Check that the received response is indeed as stated in the test name // 💡 TIP: Use this syntax for example: expect(receivedResponse.status).toBe(...); + expect(receivedResponse.status).toBe(400); }); // ✅ TASK: Test that when a new valid event is posted to /sensor-events route, we get back a valid response // 💡 TIP: Consider checking both the HTTP status and the body test('When inserting a valid event, should get successful response', async () => { // Arrange + const eventToAdd = getSensorEvent(); + // Act // 💡 TIP: use any http client lib like Axios OR supertest // 💡 TIP: This is how it is done with Supertest -> await request(expressApp).post("/sensor-events").send(eventToAdd); + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); + // Assert // 💡 TIP: You may check the body and the status all together with the following syntax: - // expect(receivedResponse).toMatchObject({status: 200, body: {...}}); + expect(receivedResponse).toMatchObject({status: 200, body: { + ...eventToAdd + }}); }); // ✅ TASK: Test that when a new valid event is posted to /sensor-events route, it's indeed retrievable from the DB @@ -93,8 +102,14 @@ describe('Sensors test', () => { // 💡 TIP: Use the library sinon to alter the behaviour of existing function and make it throw error // https://sinonjs.org/releases/latest/stubs/ // 💡 TIP: Here is the syntax: sinon.stub(someClass.prototype, 'methodName').rejects(new Error("Error explanation")); + const eventToAdd = getSensorEvent(); + sinon.stub(MessageQueueClient.prototype, 'publish').rejects(new Error("MessageQueueClient error")); + // Act + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); + // Assert + expect(receivedResponse.status).toBe(500); }); // ✅ Ensure that the webserver is closed when all the tests are completed diff --git a/test/mission-database-bay.test.js b/test/mission-database-bay.test.js index e68eef2..04175e2 100644 --- a/test/mission-database-bay.test.js +++ b/test/mission-database-bay.test.js @@ -37,7 +37,7 @@ describe('Sensors test', () => { const eventToAdd = { category: 'Home equipment', temperature: 20, - reason: `Thermostat-failed`, // This must be unique + reason: `Thermostat-failed-${getShortUnique()}`, // This must be unique color: 'Green', weight: 80, status: 'active', @@ -46,9 +46,14 @@ describe('Sensors test', () => { // Act // 💡 TIP: use any http client lib like Axios OR supertest // 💡 TIP: This is how it is done with Supertest -> await request(expressApp).post("/sensor-events").send(eventToAdd); + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); // Assert // 💡 TIP: Check not only the HTTP status bot also the body + expect(receivedResponse).toMatchObject({status: 200, body: { + ...eventToAdd, + id: expect.any(Number) + }}); }); // ✅ TASK: Run the test above twice, it fails, ah? Let's fix! @@ -63,16 +68,33 @@ describe('Sensors test', () => { // ✅ TASK: Let's test that the system indeed enforces the 'reason' field uniqueness by writing this test below 👇 // 💡 TIP: This test probably demands two POST calls, you can use the same JSON payload twice - // test('When a record exist with a specific reason and trying to add a second one, then it fails with status 409'); + test('When a record exist with a specific reason and trying to add a second one, then it fails with status 409', async () => { + const eventToAdd = getSensorEvent(); + + await request(expressApp).post("/sensor-events").send(eventToAdd); + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); + + expect(receivedResponse.status).toBe(409); + }); // ✅ TASK: Let's write the test below 👇 that checks that querying by ID works. For now, temporarily please query for the event that // was added using the first test above 👆. // 💡 TIP: This is not the recommended technique (reusing records from previous tests), we do this to understand // The consequences - test('When querying for event by id, Then the right event is being returned', () => { + test('When querying for event by id, Then the right event is being returned', async () => { // 💡 TIP: At first, query for the event that was added in the first test (In the first test above, store // the ID of the added event globally). In this test, query for that ID // 💡 TIP: This is the GET sensor URL: await request(expressApp).get(`/sensor-events/${id}`, + const eventToAdd = getSensorEvent(); + + const { body: { id } } = await request(expressApp).post("/sensor-events").send(eventToAdd); + + const receivedResponse = await request(expressApp).get(`/sensor-events/${id}`); + + expect(receivedResponse).toMatchObject({status: 200, body: { + ...eventToAdd, + id + }}); }); // ✅ TASK: Run the last test 👆 alone (without running other tests). Does it pass now? @@ -90,31 +112,90 @@ describe('Sensors test', () => { // ✅ TASK: Test that when a new event is posted to /sensor-events route, the temperature is not specified -> the event is NOT saved to the DB! // 💡 TIP: Testing the response is not enough, the adequate state (e.g. DB) should also satisfy the expectation // 💡 TIP: In the assert phase, query to get the event that was (not) added - Ensure the response is empty + test('When adding an event with temperature unspecified, Then the event is NOT saved to the DB', async () => { + const eventToAdd = getSensorEvent({ temperature: undefined }); + + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); + expect(receivedResponse).toMatchObject({status: 400, body: {}}); + }); // ✅ TASK: Test that when an event is deleted, then its indeed not existing anymore + test('When an event deleted, Then the event is removed from the DB', async () => { + const eventToAdd = getSensorEvent(); + + const { body: {id} } = await request(expressApp).post("/sensor-events").send(eventToAdd); + await request(expressApp).delete(`/sensor-events/${id}`); + const receivedResponse = await request(expressApp).get(`/sensor-events/${id}`); + + expect(receivedResponse).toMatchObject({status: 404, body: {}}); + }); // ✅ TASK: Write the following test below 👇 to check that the app is able to return all records // 💡 TIP: Checking the number of records in the response might be fragile as there other processes and tests // that add data. Consider sampling for some records to get partial confidence that it works - test('When adding multiple events, then all of them appear in the result', () => {}); + test('When adding multiple events, then all of them appear in the result', async () => { + const eventToAdd1 = getSensorEvent(); + const eventToAdd2 = getSensorEvent(); + const eventToAdd3 = getSensorEvent(); + + const { body: { id: id1 }} = await request(expressApp).post("/sensor-events").send(eventToAdd1); + const { body: { id: id2 }} = await request(expressApp).post("/sensor-events").send(eventToAdd2); + const { body: { id: id3 }} = await request(expressApp).post("/sensor-events").send(eventToAdd3); + + const receivedResponse1 = await request(expressApp).get(`/sensor-events/${id1}`); + const receivedResponse2 = await request(expressApp).get(`/sensor-events/${id2}`); + const receivedResponse3 = await request(expressApp).get(`/sensor-events/${id3}`); + + expect(receivedResponse1).toMatchObject({status: 200, body: { id: id1 }}); + expect(receivedResponse2).toMatchObject({status: 200, body: { id: id2 }}); + expect(receivedResponse3).toMatchObject({status: 200, body: { id: id3 }}); + }); // ✅ TASK: Spread your tests across multiple files, let the test runner invoke tests in multiple processes - Ensure all pass // 💡 TIP: You might face port collision where two APIs instances try to open the same port // 💡 TIP: Use the flag 'jest --maxWorkers='. Assign zero for max value of some specific number greater than 1 // ✅🚀 TASK: Test the following - test('When querying for a non-existing event, then get http status 404', () => {}); + test('When querying for a non-existing event, then get http status 404', async () => { + const nonExistentId = "-1"; + + const receivedResponse = await request(expressApp).get(`/sensor-events/${nonExistentId}`); + + expect(receivedResponse).toMatchObject({status: 404, body: {}}); + }); // 💡 TIP: How could you be sure that an item does not exist? 🤔 // ✅🚀 TASK: Let's ensure that two new events can be added at the same time - This ensure there are no concurrency and unique-key issues // Check that when adding two events at the same time, both are saved successfully // 💡 TIP: To check something was indeed saved, it's not enough to rely on the response - Ensure that it is retrievable // 💡 TIP: Promise.all function might be helpful to parallelize the requests + test('When adding two events in parallel, then both are saved to the DB', async () => { + const eventToAdd1 = getSensorEvent(); + const eventToAdd2 = getSensorEvent(); + + const [receivedResponse1, receivedResponse2] = await Promise.all([request(expressApp).post("/sensor-events").send(eventToAdd1), + request(expressApp).post("/sensor-events").send(eventToAdd2)]); + + expect(receivedResponse1).toMatchObject({status: 200, body: { ...eventToAdd1 }}); + expect(receivedResponse2).toMatchObject({status: 200, body: { ...eventToAdd2 }}); + }); // ✅🚀 When adding a valid event, we get back some fields with dynamic values: createdAt, updatedAt, id // Check that these fields are not null and have the right schema // 💡 TIP: Jest has a dedicated matcher for unknown values, read about: // https://jestjs.io/docs/en/expect#expectanyconstructor + test('When adding an event, then the event is saved in DB and createdAt, updatedAt, id are non-null', async () => { + const eventToAdd = getSensorEvent(); + + const receivedResponse = await request(expressApp).post("/sensor-events").send(eventToAdd); + + expect(receivedResponse).toMatchObject({status: 200, body: { + ...eventToAdd, + id: expect.any(Number), + createdAt: expect.any(String), + updatedAt: expect.any(String) + }}); + }); // ✅🚀 TASK: Although we don't clean-up the DB during the tests, it's useful to clean-up in the end. Let's delete the data tables after all the tests // 💡 TIP: Choose the right hook thoughtfully and remember that two test files might get executed at the same time @@ -123,12 +204,58 @@ describe('Sensors test', () => { // ✅🚀 TASK: Test that querying for /sensor-events route (i.e. get all) and sorting by the field 'temperature', the results are indeed sorted // 💡 TIP: The following route allows sorting by specific field: /sensor-events/:category/:sortBy // 💡 TIP: Each test should be independent and might run alone without others, don't count on data (events) from other tests + test('When query for events by sorting, then the events should be sorted', async () => { + const sortingCategory = `test-sorting-${getShortUnique()}`; + const eventToAdd1 = getSensorEvent({ category: sortingCategory, temperature: 10 }) + const eventToAdd2 = getSensorEvent({ category: sortingCategory, temperature: 20 }) + const eventToAdd3 = getSensorEvent({ category: sortingCategory, temperature: 30 }) + + await request(expressApp).post("/sensor-events").send(eventToAdd1); + await request(expressApp).post("/sensor-events").send(eventToAdd2); + await request(expressApp).post("/sensor-events").send(eventToAdd3); + + const receivedResponse = await request(expressApp).get(`/sensor-events/${sortingCategory}/temperature`); + + const allEventResponses = await request(expressApp).get("/sensor-events/"); + const sortedEventResults = allEventResponses.body.filter(event => event.category === sortingCategory).sort((a, b) => a.temperature - b.temperature); + expect(receivedResponse.body).toEqual(sortedEventResults); + }); // ✅🚀 TASK: Test that querying for /sensor-events route (i.e. get all) and sorting by the field 'temperature', the results are indeed sorted // 💡 TIP: The following route allows sorting by specific field: /sensor-events/:category/:sortBy // 💡 TIP: Each test should be independent and might run alone without others, don't count on data (events) from other tests + test('When query for events by sorting AGAIN, then the events should be sorted', async () => { + const sortingCategory = `test-sorting-${getShortUnique()}`; + const eventToAdd1 = getSensorEvent({ category: sortingCategory, temperature: 10 }) + const eventToAdd2 = getSensorEvent({ category: sortingCategory, temperature: 20 }) + const eventToAdd3 = getSensorEvent({ category: sortingCategory, temperature: 30 }) + + await request(expressApp).post("/sensor-events").send(eventToAdd1); + await request(expressApp).post("/sensor-events").send(eventToAdd2); + await request(expressApp).post("/sensor-events").send(eventToAdd3); + + const receivedResponse = await request(expressApp).get(`/sensor-events/${sortingCategory}/temperature`); + + const allEventResponses = await request(expressApp).get("/sensor-events/"); + const sortedEventResults = allEventResponses.body.filter(event => event.category === sortingCategory).sort((a, b) => a.temperature - b.temperature); + + expect(receivedResponse.body).toEqual(sortedEventResults); + }); // ✅🚀 TASK: Test when a sensor event is deleted, the code is not mistakenly deleting data that was not // supposed to be deleted // 💡 TIP: You may need to add more than one event to achieve this + test('When an event is deleted, then only the event is deleted from the DB', async () => { + const eventToAdd1 = getSensorEvent(); + const eventToAdd2 = getSensorEvent(); + + const { body: {id: id1} } = await request(expressApp).post("/sensor-events").send(eventToAdd1); + const { body: {id: id2} } = await request(expressApp).post("/sensor-events").send(eventToAdd2); + await request(expressApp).delete(`/sensor-events/${id1}`); + const deletedEventResponse = await request(expressApp).get(`/sensor-events/${id1}`); + const notdeletedEventResponse = await request(expressApp).get(`/sensor-events/${id2}`); + + expect(deletedEventResponse).toMatchObject({status: 404, body: {}}); + expect(notdeletedEventResponse).toMatchObject({status: 200, body: { id: id2 }}); + }); });