diff --git a/routes/v1-launches.js b/routes/v1-launches.js index f850d8a9..b4c57d9c 100644 --- a/routes/v1-launches.js +++ b/routes/v1-launches.js @@ -2,6 +2,7 @@ const express = require("express") const v1 = express.Router() +const error = {error: "No results found"} // Get most recent launch v1.get("/latest", (req, res, next) => { @@ -10,7 +11,11 @@ v1.get("/latest", (req, res, next) => { if (err) { return next(err) } - res.json(doc) + if (doc.length == 0) { + res.status(404) + return res.json(error) + } + res.json(doc[0]) }) }) diff --git a/test/utilities/custom-asymmetric-matchers.js b/test/utilities/custom-asymmetric-matchers.js new file mode 100644 index 00000000..27b121fe --- /dev/null +++ b/test/utilities/custom-asymmetric-matchers.js @@ -0,0 +1,103 @@ +/** + * We make ourselves compatible with AsymmetricMatcher class used by Jest + * @see https://github.com/facebook/jest/blob/master/packages/expect/src/asymmetric_matchers.js + * + * Perhaps we can simplify that a bit and write only Jasmine-compatible matchers. + * @see https://jasmine.github.io/2.4/introduction.html#section-Custom_asymmetric_equality_tester + */ +class CustomAsymmetricMatcher { + constructor() { + // $$typeof is used internally by Jest and just to be sure let's use jest Symbol + this.$$typeof = Symbol.for("jest.asymmetricMatcher") + } +} + +/** + * Expect metric and imperial volume numbers in object. + */ +class SpacexVolume extends CustomAsymmetricMatcher { + asymmetricMatch(any) { + expect(any).toEqual(expect.anything()) + expect(any).toHaveProperty("cubic_meters", expect.any(Number)) + expect(any).toHaveProperty("cubic_feet", expect.any(Number)) + expect(any.cubic_meters).toBeGreaterThanOrEqual(0) + expect(any.cubic_feet).toBeGreaterThanOrEqual(0) + return true + } + + toString() { + return "SpacexVolume" + } + + getExpectedType() { + return "object" + } + + toAsymmetricMatcher() { + return "SpacexVolume" + } +} + +/** + * Expect metric and imperial length (or dimension) numbers in object. + */ +class SpacexLength extends CustomAsymmetricMatcher { + asymmetricMatch(any) { + expect(any).toEqual(expect.anything()) + expect(any).toHaveProperty("meters", expect.any(Number)) + expect(any).toHaveProperty("feet", expect.any(Number)) + expect(any.meters).toBeGreaterThanOrEqual(0) + expect(any.feet).toBeGreaterThanOrEqual(0) + return true + } + + toString() { + return "SpacexLength" + } + + getExpectedType() { + return "object" + } + + toAsymmetricMatcher() { + return "SpacexLength" + } +} + +/** + * Expect metric and imperial mass numbers in object. + */ +class SpacexMass extends CustomAsymmetricMatcher { + asymmetricMatch(any) { + expect(any).toEqual(expect.anything()) + expect(any).toHaveProperty("kg", expect.any(Number)) + expect(any).toHaveProperty("lb", expect.any(Number)) + expect(any.kg).toBeGreaterThanOrEqual(0) + expect(any.lb).toBeGreaterThanOrEqual(0) + return true + } + + toString() { + return "SpacexMass" + } + + getExpectedType() { + return "object" + } + + toAsymmetricMatcher() { + return "SpacexMass" + } +} + +module.exports = { + volume: () => { + return new SpacexVolume() + }, + length: () => { + return new SpacexLength() + }, + mass: () => { + return new SpacexMass() + } +} diff --git a/test/v1-all.test.js b/test/v1-all.test.js index 78b8828f..be86a012 100644 --- a/test/v1-all.test.js +++ b/test/v1-all.test.js @@ -1,6 +1,7 @@ const app = require("../app") const request = require("supertest") +const customMatchers = require("./utilities/custom-asymmetric-matchers") beforeAll((done) => { app.on("ready", () => { @@ -15,7 +16,9 @@ beforeAll((done) => { test("It should return 404 endpoint error", () => { return request(app).get("/v2").then(response => { expect(response.statusCode).toBe(404) - expect(response.text).toContain("No results found") + expect(response.body).toMatchObject({ + error: "No results found" + }) }) }) @@ -51,7 +54,12 @@ test("It should return 500 error", () => { test("It should return home info", () => { return request(app).get("/v1").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("SpaceX-API") + expect(response.body).toHaveProperty("description") + expect(response.body).toHaveProperty("organization", "r/SpaceX") + expect(response.body).toHaveProperty("organization_link", "https://github.com/r-spacex") + expect(response.body).toHaveProperty("project_link", "https://github.com/r-spacex/SpaceX-API") + expect(response.body).toHaveProperty("project_name", "SpaceX-API") + expect(response.body).toHaveProperty("version") }) }) @@ -62,7 +70,22 @@ test("It should return home info", () => { test("It should return company info", () => { return request(app).get("/v1/info").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Elon Musk") + expect(response.body).toHaveProperty("name", "SpaceX") + expect(response.body).toHaveProperty("founder", "Elon Musk") + expect(response.body).toHaveProperty("founded", 2002) + expect(response.body).toHaveProperty("employees") + expect(response.body).toHaveProperty("vehicles") + expect(response.body).toHaveProperty("launch_sites") + expect(response.body).toHaveProperty("test_sites") + expect(response.body).toHaveProperty("ceo") + expect(response.body).toHaveProperty("cto") + expect(response.body).toHaveProperty("coo") + expect(response.body).toHaveProperty("cto_propulsion") + expect(response.body).toHaveProperty("valuation") + expect(response.body).toHaveProperty("headquarters.address", "Rocket Road") + expect(response.body).toHaveProperty("headquarters.city", "Hawthorne") + expect(response.body).toHaveProperty("headquarters.state", "California") + expect(response.body).toHaveProperty("summary") }) }) @@ -73,38 +96,217 @@ test("It should return company info", () => { test("It should return all vehicle info", () => { return request(app).get("/v1/vehicles").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Falcon 1") - expect(response.text).toContain("Falcon 9") - expect(response.text).toContain("Falcon Heavy") - expect(response.text).toContain("Dragon") + expect(response.body).toHaveLength(4) + expect(response.body[0]).toHaveProperty("name", "Falcon 1") + expect(response.body[1]).toHaveProperty("name", "Falcon 9") + expect(response.body[2]).toHaveProperty("name", "Falcon Heavy") + expect(response.body[3]).toHaveProperty("name", "Dragon 1") + + response.body.forEach(item => { + expect(item).toHaveProperty("id", expect.any(String)) + expect(item).toHaveProperty("name", expect.any(String)) + expect(item).toHaveProperty("type", expect.stringMatching(/^(?:rocket|capsule)$/)) + expect(item).toHaveProperty("active", expect.any(Boolean)) + if (item.type === "rocket") { + expect(item).toHaveProperty("stages", expect.any(Number)) + // expect(item).toHaveProperty("boosters", expect.any(Number)) // missing from falcon1 + expect(item).toHaveProperty("cost_per_launch", expect.any(Number)) + // expect(item).toHaveProperty("orbit_duration_yr", expect.any(Number)) // missing from falcon1 + // expect(item).toHaveProperty("success_rate_pct", expect.any(Number)) // missing from FH + expect(item).toHaveProperty("first_flight", expect.stringMatching(/^(?:[0-9]{4}-[0-9]{2}-[0-9]{2}|TBD)$/)) + // expect(item).toHaveProperty("launchpad", expect.any(String)) // missing from falcon9 + expect(item).toHaveProperty("country", expect.any(String)) + expect(item).toHaveProperty("company", expect.any(String)) + expect(item).toHaveProperty("height", customMatchers.length()) + // expect(item).toHaveProperty("diameter", customMatchers.length()) // missing from FH + // expect(item).toHaveProperty("total_width",customMatchers.length()) // missing from falcon1 + expect(item).toHaveProperty("mass", customMatchers.mass()) + expect(item).toHaveProperty("payload_weights", expect.any(Array)) // TODO test it deeper + expect(item).toHaveProperty("first_stage", expect.any(Object)) // TODO test it deeper + expect(item).toHaveProperty("second_stage", expect.any(Object)) // TODO test it deeper + // expect(item).toHaveProperty("landing_legs", expect.any(Number)) // missing from falcon1 + expect(item).toHaveProperty("description", expect.any(String)) + } + else if (item.type === "capsule") { + expect(item).toHaveProperty("sidewall_angle_deg", expect.any(Number)) + expect(item).toHaveProperty("orbit_duration_yr", expect.any(Number)) + expect(item).toHaveProperty("variations", expect.any(Object)) // TODO test it deeper + expect(item).toHaveProperty("heat_shield.dev_partner", expect.any(String)) + expect(item).toHaveProperty("heat_shield.material", expect.any(String)) + expect(item).toHaveProperty("heat_shield.size_meters", expect.any(Number)) + expect(item).toHaveProperty("heat_shield.temp_degrees", expect.any(Number)) + expect(item).toHaveProperty("thrusters.amount", expect.any(Number)) + expect(item).toHaveProperty("thrusters.fuel_1", expect.any(String)) + expect(item).toHaveProperty("thrusters.fuel_2", expect.any(String)) + expect(item).toHaveProperty("thrusters.pods", expect.any(Number)) + expect(item).toHaveProperty("thrusters.thrust.kN", expect.any(Number)) + expect(item).toHaveProperty("thrusters.thrust.lbf", expect.any(Number)) + expect(item).toHaveProperty("thrusters.type", expect.any(String)) + expect(item).toHaveProperty("launch_payload_mass", customMatchers.mass()) + expect(item).toHaveProperty("launch_payload_vol", customMatchers.volume()) + expect(item).toHaveProperty("return_payload_mass", customMatchers.mass()) + expect(item).toHaveProperty("return_payload_vol", customMatchers.volume()) + expect(item).toHaveProperty("pressurized_capsule.payload_volume", customMatchers.volume()) + expect(item).toHaveProperty("trunk.cargo.solar_array", expect.any(Number)) + expect(item).toHaveProperty("trunk.cargo.unpressurized_cargo", expect.any(Boolean)) + expect(item).toHaveProperty("trunk.trunk_volume", customMatchers.volume()) + expect(item).toHaveProperty("height_w_trunk", customMatchers.length()) + expect(item).toHaveProperty("diameter", customMatchers.length()) + } + }) }) }) test("It should return Falcon 1 info", () => { return request(app).get("/v1/vehicles/falcon1").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Falcon 1") + expect(response.body).toHaveProperty("name", "Falcon 1") + expect(response.body).toHaveProperty("stages", 2) + expect(response.body).toHaveProperty("cost_per_launch") + expect(response.body).toHaveProperty("success_rate_pct") + expect(response.body).toHaveProperty("first_flight", "2006-03-24") + expect(response.body).toHaveProperty("country") + expect(response.body).toHaveProperty("company", "SpaceX") + expect(response.body).toHaveProperty("description") + expect(Object.keys(response.body)).toEqual([ + "id", + "name", + "type", + "active", + "stages", + // "boosters", + "cost_per_launch", + "success_rate_pct", + "first_flight", + "launchpad", + "country", + "company", + "height", + "diameter", + // "total_width", + "mass", + "payload_weights", + "first_stage", + "second_stage", + // "engines", + // "landing_legs", + "description" + ]) }) }) test("It should return Falcon 9 info", () => { return request(app).get("/v1/vehicles/falcon9").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Falcon 9") + expect(response.body).toHaveProperty("name", "Falcon 9") + expect(response.body).toHaveProperty("stages", 2) + expect(response.body).toHaveProperty("cost_per_launch") + expect(response.body).toHaveProperty("success_rate_pct") + expect(response.body).toHaveProperty("first_flight", "2010-06-04") + expect(response.body).toHaveProperty("country") + expect(response.body).toHaveProperty("company", "SpaceX") + expect(response.body).toHaveProperty("description") + expect(Object.keys(response.body)).toEqual([ + "id", + "name", + "type", + "active", + "stages", + // "boosters", + "cost_per_launch", + "success_rate_pct", + "first_flight", + // "launchpad", + "country", + "company", + "height", + "diameter", + // "total_width", + "mass", + "payload_weights", + "first_stage", + "second_stage", + "engines", + "landing_legs", + "description" + ]) }) }) test("It should return Falcon Heavy info", () => { return request(app).get("/v1/vehicles/falconheavy").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Falcon Heavy") + expect(response.body).toHaveProperty("name", "Falcon Heavy") + expect(response.body).toHaveProperty("stages", 2) + expect(response.body).toHaveProperty("cost_per_launch") + // expect(response.body).toHaveProperty("success_rate_pct") + // expect(response.body).toHaveProperty("first_flight") + expect(response.body).toHaveProperty("country") + expect(response.body).toHaveProperty("company", "SpaceX") + expect(response.body).toHaveProperty("description") + expect(Object.keys(response.body)).toEqual([ + "id", + "name", + "type", + "active", + "stages", + "boosters", + "cost_per_launch", + // "success_rate_pct", + "first_flight", + // "launchpad", + "country", + "company", + "height", + // "diameter", + "total_width", + "mass", + "payload_weights", + "first_stage", + "second_stage", + "engines", + "landing_legs", + "description" + ]) }) }) test("It should return Dragon info", () => { return request(app).get("/v1/vehicles/dragon").then(response => { expect(response.statusCode).toBe(200) - expect(response.text).toContain("Dragon") + expect(response.body).toHaveProperty("name", "Dragon 1") + expect(Object.keys(response.body)).toEqual([ + "id", + "name", + "type", + "active", + // "stages", + // "cost_per_launch", + // "success_rate_pct", + // "first_flight", + // "launchpad", + // "country", + // "company", + // "height", + "sidewall_angle_deg", // capsule specific field + "orbit_duration_yr", // capsule specific field + "variations", // capsule specific field + "heat_shield", // capsule specific field + "thrusters", // capsule specific field + "launch_payload_mass", // capsule specific field + "launch_payload_vol", // capsule specific field + "return_payload_mass", // capsule specific field + "return_payload_vol", // capsule specific field + "pressurized_capsule", // capsule specific field + "trunk", // capsule specific field + "height_w_trunk", // capsule specific field + "diameter", + // "mass", + // "payload_weights", + // "first_stage", + // "second_stage", + // "description" + ]) }) }) @@ -115,7 +317,14 @@ test("It should return Dragon info", () => { test("It should return all launchpads", () => { return request(app).get("/v1/launchpads").then(response => { expect(response.statusCode).toBe(200) - expect(response.text.length).toBe(3735) + expect(response.body).toHaveLength(8) + response.body.forEach(item => { + expect(item).toHaveProperty("id") + expect(item).toHaveProperty("full_name") + expect(item).toHaveProperty("status") + expect(item).toHaveProperty("vehicles_launched") + expect(item).toHaveProperty("details") + }) }) }) @@ -140,14 +349,51 @@ test("It should return no launchpads found info", () => { test("It should return all past launches", () => { return request(app).get("/v1/launches").then(response => { expect(response.statusCode).toBe(200) - expect(response.text.length).toBeGreaterThan(70000) + expect(response.body.length).toBeGreaterThanOrEqual(50) + response.body.forEach(item => { + expect(item).toHaveProperty("flight_number", expect.anything()) + expect(item).toHaveProperty("launch_year", expect.stringMatching(/^[0-9]{4}$/)) + // expect(item).toHaveProperty("launch_date_unix") + expect(item).toHaveProperty("launch_date_utc", expect.anything()) + expect(item).toHaveProperty("launch_date_local", expect.anything()) + expect(item).toHaveProperty("rocket.rocket_id") + expect(item).toHaveProperty("rocket.rocket_name") + expect(item).toHaveProperty("rocket.rocket_type") + expect(item).toHaveProperty("telemetry.flight_club") + expect(item).toHaveProperty("core_serial") + expect(item).toHaveProperty("cap_serial") + expect(item).toHaveProperty("reuse.core") + expect(item).toHaveProperty("reuse.side_core1") + expect(item).toHaveProperty("reuse.side_core2") + expect(item).toHaveProperty("reuse.fairings") + expect(item).toHaveProperty("reuse.capsule") + expect(item).toHaveProperty("launch_site.site_id") + expect(item).toHaveProperty("launch_site.site_name") + expect(item).toHaveProperty("payloads") + expect(item.payloads.length).toBeGreaterThan(0) + item.payloads.forEach(payload => { + expect(payload).toHaveProperty("payload_id") + expect(payload).toHaveProperty("customers") + expect(payload).toHaveProperty("payload_type") + expect(payload).toHaveProperty("payload_mass_kg") + expect(payload).toHaveProperty("payload_mass_lbs") + expect(payload).toHaveProperty("orbit") + }) + expect(item).toHaveProperty("launch_success") + expect(item).toHaveProperty("reused") + expect(item).toHaveProperty("land_success") + expect(item).toHaveProperty("landing_type") + expect(item).toHaveProperty("landing_vehicle") + expect(item).toHaveProperty("links") + expect(item).toHaveProperty("details") + }) }) }) test("It should return the latest launch", () => { return request(app).get("/v1/launches/latest").then(response => { expect(response.statusCode).toBe(200) - expect(response.text.length).toBeGreaterThan(0) + expect(response.body).toHaveProperty("flight_number") }) })