/
31 | );
32 | });
33 | });
34 |
35 | test('should respond with a 200 with invalid query parameters', () => {
36 | return request(app)
37 | .get('/?tags=california123&tagmode=all')
38 | .expect('Content-Type', /html/)
39 | .expect(200)
40 | .then(response => {
41 | expect(response.text).toMatch(/
/);
42 | });
43 | });
44 |
45 | test('should respond with a 500 error due to bad jsonp data', () => {
46 | return request(app)
47 | .get('/?tags=error&tagmode=all')
48 | .expect('Content-Type', /json/)
49 | .expect(500)
50 | .then(response => {
51 | expect(response.body).toEqual({ error: 'Internal server error' });
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/app/__tests__/form_validator.test.js:
--------------------------------------------------------------------------------
1 | const formValidator = require('../../app/form_validator');
2 |
3 | describe('isValidCommaDelimitedList(value)', () => {
4 | test('should return true for valid list of search terms', () => {
5 | const tags = 'california, sunset, red';
6 | expect(formValidator.isValidCommaDelimitedList(tags)).toBe(true);
7 | });
8 |
9 | test('should return true for valid single search term', () => {
10 | const tags = 'dogs';
11 | expect(formValidator.isValidCommaDelimitedList(tags)).toBe(true);
12 | });
13 |
14 | test('should return false for search term containing numbers', () => {
15 | const tags = 'dogs123';
16 | expect(formValidator.isValidCommaDelimitedList(tags)).toBe(false);
17 | });
18 |
19 | test('should return false for search term containing special characters', () => {
20 | const tags = 'dogs%$';
21 | expect(formValidator.isValidCommaDelimitedList(tags)).toBe(false);
22 | });
23 | });
24 |
25 | describe('isValidTagmode(value)', () => {
26 | test('should return true for valid tagmode "any"', () => {
27 | const tagmode = 'any';
28 | expect(formValidator.isValidTagmode(tagmode)).toBe(true);
29 | });
30 |
31 | test('should return true for valid tagmode "all"', () => {
32 | const tagmode = 'all';
33 | expect(formValidator.isValidTagmode(tagmode)).toBe(true);
34 | });
35 |
36 | test('should return false for invalid tagmode', () => {
37 | const tagmode = 'many';
38 | expect(formValidator.isValidTagmode(tagmode)).toBe(false);
39 | });
40 | });
41 |
42 | describe('hasValidFlickrAPIParams(tags, tagmode)', () => {
43 | test('should return true for valid params', () => {
44 | const tags = 'dogs, poodles';
45 | const tagmode = 'all';
46 | expect(formValidator.hasValidFlickrAPIParams(tags, tagmode)).toBe(true);
47 | });
48 |
49 | test('should return false for invalid tags', () => {
50 | const tags = 'dogs, poodles123';
51 | const tagmode = 'all';
52 | expect(formValidator.hasValidFlickrAPIParams(tags, tagmode)).toBe(false);
53 | });
54 |
55 | test('should return false for invalid tagmode', () => {
56 | const tags = 'dogs, poodles';
57 | const tagmode = 'all123';
58 | expect(formValidator.hasValidFlickrAPIParams(tags, tagmode)).toBe(false);
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/app/__tests__/jsonp_helper.test.js:
--------------------------------------------------------------------------------
1 | const jsonpHelper = require('../../app/jsonp_helper');
2 |
3 | describe('parseJSONP(jsonpData)', () => {
4 | test('should parse valid jsonp data', () => {
5 | const jsonpData = 'jsonFlickrFeed({"title": "tagged california"});';
6 | const jsObject = jsonpHelper.parseJSONP(jsonpData);
7 | expect(jsObject).toMatchObject({
8 | title: 'tagged california'
9 | });
10 | });
11 |
12 | test('should parse jsonp data with escaped single quotes', () => {
13 | const jsonpData =
14 | 'jsonFlickrFeed({"title": "tagged california\'s coast"});';
15 | const jsObject = jsonpHelper.parseJSONP(jsonpData);
16 | expect(jsObject).toMatchObject({
17 | title: "tagged california's coast"
18 | });
19 | });
20 |
21 | test('should throw error when parsing invalid jsonp data', () => {
22 | // invalid json because of missing double quotes around title value
23 | const jsonpData = 'jsonFlickrFeed({"title": tagged california});';
24 |
25 | expect(function() {
26 | // call the add(item) method without passing in an item
27 | jsonpHelper.parseJSONP(jsonpData);
28 | }).toThrowError(/Failed to convert jsonp to json/);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/app/__tests__/photo_model.test.js:
--------------------------------------------------------------------------------
1 | let photoModel;
2 |
3 | describe('getFlickrPhotos(tags, tagmode, callback)', () => {
4 | beforeEach(() => {
5 | jest.resetModules();
6 | });
7 |
8 | test('should return photos', () => {
9 | // mock the flickr public feed api endpoint
10 | jest.doMock('got', () => {
11 | return jest.fn(() => {
12 | const jsonpData = `jsonFlickrFeed({
13 | "items": [
14 | {
15 | "title": "Boating",
16 | "media": {
17 | "m": "http://farm4.staticflickr.com/3727/12608622365_9e9b8b377d_m.jpg"
18 | }
19 | },
20 | {
21 | "title": "Signs",
22 | "media": {
23 | "m": "http://farm8.staticflickr.com/7446/12608714423_efaf73400c_m.jpg"
24 | }
25 | }
26 | ]
27 | })`;
28 | return Promise.resolve({
29 | body: jsonpData
30 | });
31 | });
32 | });
33 |
34 | photoModel = require('../../app/photo_model');
35 |
36 | const expectedSubset = [
37 | {
38 | title: 'Boating',
39 | media: {
40 | t: expect.stringMatching(/t.jpg/),
41 | m: expect.stringMatching(/m.jpg/),
42 | b: expect.stringMatching(/b.jpg/)
43 | }
44 | }
45 | ];
46 |
47 | return photoModel.getFlickrPhotos('california', 'all').then(photos => {
48 | expect(photos).toEqual(expect.arrayContaining(expectedSubset));
49 | });
50 | });
51 |
52 | test('should error when api returns 500 http status code', () => {
53 | // mock the flickr public feed api endpoint and return a 500 error
54 | jest.doMock('got', () => {
55 | return jest.fn(() => {
56 | return Promise.reject('Response code 500 (Internal Server Error)');
57 | });
58 | });
59 |
60 | photoModel = require('../../app/photo_model');
61 |
62 | return photoModel.getFlickrPhotos('california', 'all').catch(error => {
63 | expect(error.toString()).toMatch(/Response code 500/);
64 | });
65 | });
66 |
67 | test('should error with invalid jsonp data', () => {
68 | // mock the flickr public feed api endpoint with invalid jsonp data that's missing parentheses
69 | jest.doMock('got', () => {
70 | return jest.fn(() => {
71 | const jsonpData = `jsonFlickrFeed{
72 | "items": [
73 | {
74 | "title": "Boating",
75 | "media": {
76 | "m": "http://farm4.staticflickr.com/3727/12608622365_9e9b8b377d_m.jpg"
77 | }
78 | }
79 | ]
80 | }`;
81 | return Promise.resolve({
82 | body: jsonpData
83 | });
84 | });
85 | });
86 |
87 | photoModel = require('../../app/photo_model');
88 |
89 | return photoModel.getFlickrPhotos('california', 'all').catch(error => {
90 | expect(error.toString()).toMatch(/Failed to convert jsonp to json/);
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/app/form_validator.js:
--------------------------------------------------------------------------------
1 | function isValidCommaDelimitedList(value) {
2 | // allow letters, commas, and spaces
3 | const commaDelimitedListRegEx = /^[A-Za-z,\s]+$/;
4 | return commaDelimitedListRegEx.test(value);
5 | }
6 |
7 | function isValidTagmode(value) {
8 | return value === 'all' || value === 'any';
9 | }
10 |
11 | function hasValidFlickrAPIParams(tags, tagmode) {
12 | return isValidCommaDelimitedList(tags) && isValidTagmode(tagmode);
13 | }
14 |
15 | module.exports = {
16 | isValidCommaDelimitedList,
17 | isValidTagmode,
18 | hasValidFlickrAPIParams
19 | };
20 |
--------------------------------------------------------------------------------
/app/jsonp_helper.js:
--------------------------------------------------------------------------------
1 | function parseJSONP(jsonpData) {
2 | try {
3 | const startPos = jsonpData.indexOf('({');
4 | const endPos = jsonpData.lastIndexOf('})');
5 | let jsonString = jsonpData.substring(startPos + 1, endPos + 1);
6 |
7 | // remove escaped single quotes since they are not valid json
8 | jsonString = jsonString.replace(/\\'/g, "'");
9 |
10 | return JSON.parse(jsonString);
11 | } catch (e) {
12 | const error = new Error(`Failed to convert jsonp to json. ${e.message}`);
13 | throw error;
14 | }
15 | }
16 |
17 | module.exports = {
18 | parseJSONP
19 | };
20 |
--------------------------------------------------------------------------------
/app/photo_model.js:
--------------------------------------------------------------------------------
1 | const got = require('got');
2 | const querystring = require('querystring');
3 | const jsonpHelper = require('./jsonp_helper');
4 |
5 | function getFlickrPhotos(tags, tagmode) {
6 | const qs = querystring.stringify({ tags, tagmode, format: 'json' });
7 |
8 | const options = {
9 | protocol: 'https:',
10 | hostname: 'api.flickr.com',
11 | path: `/services/feeds/photos_public.gne?${qs}`,
12 | timeout: 10000
13 | };
14 |
15 | return got(options).then(response => {
16 | const photoFeed = jsonpHelper.parseJSONP(response.body);
17 |
18 | photoFeed.items.forEach(photo => {
19 | photo.media.t = photo.media.m.split('m.jpg')[0] + 't.jpg';
20 | photo.media.b = photo.media.m.split('m.jpg')[0] + 'b.jpg';
21 | });
22 |
23 | return photoFeed.items;
24 | });
25 | }
26 |
27 | module.exports = {
28 | getFlickrPhotos
29 | };
30 |
--------------------------------------------------------------------------------
/app/public/css/style.css:
--------------------------------------------------------------------------------
1 | .search-results ul { padding: 0; }
2 | .search-results li { display: inline; }
3 |
4 | .thumbnail {
5 | display: inline-block;
6 | margin: 0 10px 20px 0;
7 | }
8 |
--------------------------------------------------------------------------------
/app/public/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregjopa/express-app-testing-demo/c68df7b899b23f74d8dd8a0862fd4e512ec0f6f5/app/public/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/app/public/fonts/glyphicons-halflings-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/public/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregjopa/express-app-testing-demo/c68df7b899b23f74d8dd8a0862fd4e512ec0f6f5/app/public/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/app/public/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregjopa/express-app-testing-demo/c68df7b899b23f74d8dd8a0862fd4e512ec0f6f5/app/public/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/app/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregjopa/express-app-testing-demo/c68df7b899b23f74d8dd8a0862fd4e512ec0f6f5/app/public/images/favicon.ico
--------------------------------------------------------------------------------
/app/route.js:
--------------------------------------------------------------------------------
1 | const formValidator = require('./form_validator');
2 | const photoModel = require('./photo_model');
3 |
4 | function route(app) {
5 | app.get('/', (req, res) => {
6 | const tags = req.query.tags;
7 | const tagmode = req.query.tagmode;
8 |
9 | const ejsLocalVariables = {
10 | tagsParameter: tags || '',
11 | tagmodeParameter: tagmode || '',
12 | photos: [],
13 | searchResults: false,
14 | invalidParameters: false
15 | };
16 |
17 | // if no input params are passed in then render the view with out querying the api
18 | if (!tags && !tagmode) {
19 | return res.render('index', ejsLocalVariables);
20 | }
21 |
22 | // validate query parameters
23 | if (!formValidator.hasValidFlickrAPIParams(tags, tagmode)) {
24 | ejsLocalVariables.invalidParameters = true;
25 | return res.render('index', ejsLocalVariables);
26 | }
27 |
28 | // get photos from flickr public feed api
29 | return photoModel
30 | .getFlickrPhotos(tags, tagmode)
31 | .then(photos => {
32 | ejsLocalVariables.photos = photos;
33 | ejsLocalVariables.searchResults = true;
34 | return res.render('index', ejsLocalVariables);
35 | })
36 | .catch(error => {
37 | return res.status(500).send({ error });
38 | });
39 | });
40 | }
41 |
42 | module.exports = route;
43 |
--------------------------------------------------------------------------------
/app/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const favicon = require('serve-favicon');
3 | const path = require('path');
4 |
5 | const app = express();
6 |
7 | // public assets
8 | app.use(express.static(path.join(__dirname, 'public')));
9 | app.use(favicon(path.join(__dirname, 'public/images', 'favicon.ico')));
10 | app.use('/coverage', express.static(path.join(__dirname, '..', 'coverage')));
11 |
12 | // ejs for view templates
13 | app.engine('.html', require('ejs').__express);
14 | app.set('views', path.join(__dirname, 'views'));
15 | app.set('view engine', 'html');
16 |
17 | // load route
18 | require('./route')(app);
19 |
20 | // server
21 | const port = process.env.PORT || 3000;
22 | app.server = app.listen(port);
23 | console.log(`listening on port ${port}`);
24 |
25 | module.exports = app;
26 |
--------------------------------------------------------------------------------
/app/views/_footer.html:
--------------------------------------------------------------------------------
1 |
14 |