From 58de4abe0438510f3d1725e3f7ed2be30094e12d Mon Sep 17 00:00:00 2001 From: Anat Dagan Date: Thu, 23 Jul 2020 02:18:06 +0300 Subject: [PATCH] node.js testing HW --- package.json | 5 +- src/app/scraper.js | 32 +++++- src/app/scraper.test.js | 150 +++++++++++++++++++++++++++ src/middlewares/isAuthorized.test.js | 53 ++++++++++ 4 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/app/scraper.test.js create mode 100644 src/middlewares/isAuthorized.test.js diff --git a/package.json b/package.json index 266127f..0e3e8f7 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "version": "1.0.0", "description": "", "main": "server.js", - "type": "module", "keywords": [], "author": "", "license": "ISC", "dependencies": { - "cors": "^2.8.5", "express": "^4.17.1", "google-play-scraper": "^7.1.3", "jsonwebtoken": "^8.5.1" + }, + "devDependencies": { + "jest": "^26.1.0" } } diff --git a/src/app/scraper.js b/src/app/scraper.js index f36cf5f..01fb292 100644 --- a/src/app/scraper.js +++ b/src/app/scraper.js @@ -2,13 +2,41 @@ const gplay = require('google-play-scraper'); async function findAppById(req, res) { const {appId} = req.params; - const app = await gplay.app({appId}); - res.send(app); + try { + const app = await gplay.app({appId}); + res.send(app); + } catch (err) { + if (err.status === 404) { + res.status(404) + res.send('App not found') + return + } + res.send(err) + return + } + } async function searchApps(req, res) { const {term} = req.params; const {count} = req.query; + + if (isNaN(count)) { + res.status(400) + res.send(new Error('count must be a number')) + return + } + if (count < 1) { + res.status(400) + res.send(new Error('count must be a positive number')) + return + } + if (term.length > 100) { + res.status(400) + res.send(new Error('term can not be longer than 100 characters')) + return + } + const apps = await gplay.search({term, num: count}); res.send(apps); } diff --git a/src/app/scraper.test.js b/src/app/scraper.test.js new file mode 100644 index 0000000..9601cc2 --- /dev/null +++ b/src/app/scraper.test.js @@ -0,0 +1,150 @@ +jest.mock('google-play-scraper'); + +const scraper = require("./scraper") +class HTTPCustomError extends Error { + constructor(status = 400, ...params) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(...params) + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HTTPCustomError) + } + + this.name = 'HTTPCustomError' + this.status = status + } + } + + +describe("scraper", ()=> { + const mockRequest = (headers, query, params) => ({ + headers: headers, + query: query, + params: params + }); + const mockResponse = (fSend, fStatus) => ({ + send:fSend, + status: fStatus + }); + describe("/app/:appId", ()=> { + let gplay = require('google-play-scraper') + const gplay_res = { + title: 'Google Translate', + description: 'Translate between 103 languages by typing\r\n...' , + descriptionHTML: 'Translate between 103 languages by typing
...', + summary: 'The world is closer than ever with over 100 languages', + installs: '500,000,000+', + minInstalls: 500000000, + maxInstalls: 898626813, + score: 4.482483, + scoreText: '4.5', + ratings: 6811669, + reviews: 1614618, + histogram: { '1': 370042, '2': 145558, '3': 375720, '4': 856865, '5': 5063481 }, + price: 0, + free: true, + currency: 'USD', + priceText: 'Free', + offersIAP: false, + IAPRange: undefined, + size: 'Varies with device', + androidVersion: 'VARY', + androidVersionText: 'Varies with device', + developer: 'Google LLC', + developerId: '5700313618786177705', + developerEmail: 'translate-android-support@google.com', + developerWebsite: 'http://support.google.com/translate', + developerAddress: '1600 Amphitheatre Parkway, Mountain View 94043', + privacyPolicy: 'http://www.google.com/policies/privacy/', + developerInternalID: '5700313618786177705', + genre: 'Tools', + genreId: 'TOOLS', + familyGenre: undefined, + familyGenreId: undefined, + icon: 'https://lh3.googleusercontent.com/ZrNeuKthBirZN7rrXPN1JmUbaG8ICy3kZSHt-WgSnREsJzo2txzCzjIoChlevMIQEA', + headerImage: 'https://lh3.googleusercontent.com/e4Sfy0cOmqpike76V6N6n-tDVbtbmt6MxbnbkKBZ_7hPHZRfsCeZhMBZK8eFDoDa1Vf-', + screenshots: [ + 'https://lh3.googleusercontent.com/dar060xShkqnJjWC2j_EazWBpLo28X4IUWCYXZgS2iXes7W99LkpnrvIak6vz88xFQ', + 'https://lh3.googleusercontent.com/VnzidUTSWK_yhpNK0uqTSfpVgow5CsZOnBdN3hIpTxODdlZg1VH1K4fEiCrdUQEZCV0', + ], + video: undefined, + videoImage: undefined, + contentRating: 'Everyone', + contentRatingDescription: undefined, + adSupported: false, + released: undefined, + updated: 1576868577000, + version: 'Varies with device', + recentChanges: 'Improved offline translations with upgraded language downloads', + comments: [], + editorsChoice: true, + appId: 'com.google.android.apps.translate', + url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.translate&hl=en&gl=us' + } + jest.mock('google-play-scraper'); + gplay.app.mockReturnValue(gplay_res) + + test("if the app exists return the app",async ()=>{ + const spy = jest.fn() + res = await scraper.findAppById(mockRequest({},{},{appId:"com.google.android.apps.translate"}), mockResponse(spy)) + expect(spy).toHaveBeenCalledWith(gplay_res); + }) + test("if the app does not exist return 'app not found' with status code 404",async ()=>{ + gplay.app.mockRejectedValue(new HTTPCustomError(404,'App not found')) + const spysend = jest.fn() + const spystatus = jest.fn() + res = await scraper.findAppById(mockRequest({},{},{appId:"com.google.android.apps.translate2"}), mockResponse(spysend,spystatus)) + expect(spysend).toHaveBeenCalledWith('App not found'); + expect(spystatus).toHaveBeenCalledWith(404); + + }) + }) + describe("/app/search/:term", ()=> { + let gplay = require('google-play-scraper') + test("call gplay.search with the correct params",async ()=>{ + const iron_apps =[{title: 'Iron Browser - by SRWare', appId: 'org.iron.srware', url: 'https://play.google.com/store/apps/details?id=org.iron.srware', icon: 'https://lh3.googleusercontent.com/FnAqc4oG_n6B…8GwTUC9KhDSwUrrarsi7gu5LVJPY9LV5T8fdCN46WjlWE', developer: 'SRWare'} + ,{title: 'Iron Marines: rts offline game', appId: 'com.ironhidegames.android.ironmarines', url: 'https://play.google.com/store/apps/details?id=com.ironhidegames.android.ironmarines', icon: 'https://lh3.googleusercontent.com/APnGswTAqXSk…-EDcyovQMPTlcNbxFNZnibn6qZ4WzXGAar-4MRFRNrVJo', developer: 'Ironhide Game Studio'} + ,{title: 'Iron Rope Hero: Vice Town', appId: 'rope.ironman.vice.town', url: 'https://play.google.com/store/apps/details?id=rope.iro'}] + gplay.search.mockReturnValue(iron_apps) + const spy = jest.fn() + res = await scraper.searchApps(mockRequest({},{count:2}, {term:"iron"}),mockResponse(spy)) + expect(gplay.search).toHaveBeenCalledWith({term: "iron", num:2}); + }) + test("if count is NaN, send the response 'count must be a number' with status 400 ",async ()=>{ + const iron_apps =[{title: 'Iron Browser - by SRWare', appId: 'org.iron.srware', url: 'https://play.google.com/store/apps/details?id=org.iron.srware', icon: 'https://lh3.googleusercontent.com/FnAqc4oG_n6B…8GwTUC9KhDSwUrrarsi7gu5LVJPY9LV5T8fdCN46WjlWE', developer: 'SRWare'} + ,{title: 'Iron Marines: rts offline game', appId: 'com.ironhidegames.android.ironmarines', url: 'https://play.google.com/store/apps/details?id=com.ironhidegames.android.ironmarines', icon: 'https://lh3.googleusercontent.com/APnGswTAqXSk…-EDcyovQMPTlcNbxFNZnibn6qZ4WzXGAar-4MRFRNrVJo', developer: 'Ironhide Game Studio'} + ,{title: 'Iron Rope Hero: Vice Town', appId: 'rope.ironman.vice.town', url: 'https://play.google.com/store/apps/details?id=rope.iro'}] + gplay.search.mockReturnValue(iron_apps) + const spysend = jest.fn() + const spystatus = jest.fn() + res = await scraper.searchApps(mockRequest({},{count:'two'}, {term:"iron"}),mockResponse(spysend, spystatus)) + expect(spysend).toHaveBeenCalledWith(new Error('count must be a number')); + expect(spystatus).toHaveBeenCalledWith(400) + }) + test("if count <1, send the response 'count must be a positive number' with status 400",async ()=>{ + const iron_apps =[{title: 'Iron Browser - by SRWare', appId: 'org.iron.srware', url: 'https://play.google.com/store/apps/details?id=org.iron.srware', icon: 'https://lh3.googleusercontent.com/FnAqc4oG_n6B…8GwTUC9KhDSwUrrarsi7gu5LVJPY9LV5T8fdCN46WjlWE', developer: 'SRWare'} + ,{title: 'Iron Marines: rts offline game', appId: 'com.ironhidegames.android.ironmarines', url: 'https://play.google.com/store/apps/details?id=com.ironhidegames.android.ironmarines', icon: 'https://lh3.googleusercontent.com/APnGswTAqXSk…-EDcyovQMPTlcNbxFNZnibn6qZ4WzXGAar-4MRFRNrVJo', developer: 'Ironhide Game Studio'} + ,{title: 'Iron Rope Hero: Vice Town', appId: 'rope.ironman.vice.town', url: 'https://play.google.com/store/apps/details?id=rope.iro'}] + gplay.search.mockReturnValue(iron_apps) + const spysend = jest.fn() + const spystatus = jest.fn() + res = await scraper.searchApps(mockRequest({},{count:-3}, {term:"iron"}),mockResponse(spysend, spystatus)) + expect(spysend).toHaveBeenCalledWith(new Error('count must be a positive number')); + expect(spystatus).toHaveBeenCalledWith(400) + }) + test("if term is longer than 100 characters send the response 'term can not be longer than 100 characters' with status 400",async ()=>{ + const iron_apps =[{title: 'Iron Browser - by SRWare', appId: 'org.iron.srware', url: 'https://play.google.com/store/apps/details?id=org.iron.srware', icon: 'https://lh3.googleusercontent.com/FnAqc4oG_n6B…8GwTUC9KhDSwUrrarsi7gu5LVJPY9LV5T8fdCN46WjlWE', developer: 'SRWare'} + ,{title: 'Iron Marines: rts offline game', appId: 'com.ironhidegames.android.ironmarines', url: 'https://play.google.com/store/apps/details?id=com.ironhidegames.android.ironmarines', icon: 'https://lh3.googleusercontent.com/APnGswTAqXSk…-EDcyovQMPTlcNbxFNZnibn6qZ4WzXGAar-4MRFRNrVJo', developer: 'Ironhide Game Studio'} + ,{title: 'Iron Rope Hero: Vice Town', appId: 'rope.ironman.vice.town', url: 'https://play.google.com/store/apps/details?id=rope.iro'}] + gplay.search.mockReturnValue(iron_apps) + const spysend = jest.fn() + const spystatus = jest.fn() + res = await scraper.searchApps(mockRequest({},{count:1}, {term: new Array(102).join("a"), count:1}), mockResponse(spysend, spystatus)) + expect(spysend).toHaveBeenCalledWith(new Error('term can not be longer than 100 characters')); + expect(spystatus).toHaveBeenCalledWith(400) + + }) + + }) +}) \ No newline at end of file diff --git a/src/middlewares/isAuthorized.test.js b/src/middlewares/isAuthorized.test.js new file mode 100644 index 0000000..99915a6 --- /dev/null +++ b/src/middlewares/isAuthorized.test.js @@ -0,0 +1,53 @@ +describe("isAuthorized middleware", ()=> { + const isAuthorized = require("./isAuthorized") + const jwt = require("jsonwebtoken") + + jest.mock("jsonwebtoken") + const mockRequest = (headers, query, params) => ({ + headers: headers, + query: query, + params: params + }); + const mockResponse = (fSend, fStatus) => ({ + send:fSend, + status: fStatus + }); + + describe("isAuthorized", ()=> { + test("if the headers are correct, next is called",async ()=>{ + const spynext = jest.fn(); + jwt.verify.mockReturnValue({ + iat:1595399171, + password:'1234', + userName:'anat'}) + await isAuthorized.isAuthorized(mockRequest({authorization:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImFuYXQiLCJwYXNzd29yZCI6IjEyMzQiLCJpYXQiOjE1OTUzOTkxNzF9.vm2MnUzjQ4hykm8Np4V1-QOD1mIfMX4ylYqa9_WP27k"}, {}, {}), mockResponse(), spynext) + expect(spynext).toHaveBeenCalled() + }) + test("if there is no authorization header, response status is 401 and the string 'Unauthorized user' is sent to the response",async ()=>{ + const spynext = jest.fn(); + const spystatus = jest.fn() + const spysend = jest.fn() + await isAuthorized.isAuthorized(mockRequest({}, {}, {}), mockResponse(spysend, spystatus), spynext) + expect(spystatus).toHaveBeenCalledWith(401) + expect(spysend).toHaveBeenCalledWith('Unauthorized user') + }) + + test("if the user has wrong authorization header, response status is 401 and the string 'Unauthorized user' is sent to the response", async ()=>{ + const spynext = jest.fn(); + const spystatus = jest.fn() + const spysend = jest.fn() + await isAuthorized.isAuthorized(mockRequest({authrization: "anat"}, {}, {}), mockResponse(spysend, spystatus), spynext) + expect(spystatus).toHaveBeenCalledWith(401) + expect(spysend).toHaveBeenCalledWith('Unauthorized user') + }) + test("if jwt.verify throws an error, response status is 401 and the error is sent to the response",async ()=>{ + const spynext = jest.fn(); + const spystatus = jest.fn() + const spysend = jest.fn() + jwt.verify.mockRejectedValue(new Error("now you broke it!")) + await isAuthorized.isAuthorized(mockRequest({authorization:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImFuYXQiLCJwYXNzd29yZCI6IjEyMzQiLCJpYXQiOjE1OTUzOTkxNzF9.vm2MnUzjQ4hykm8Np4V1-QOD1mIfMX4ylYqa9_WP27k"}, {}, {}), mockResponse(spysend, spystatus), spynext) + expect(spystatus).toHaveBeenCalledWith(401) + expect(spysend).toHaveBeenCalledWith(new Error("now you broke it!")) + }) + }) +}) \ No newline at end of file