├── .gitignore ├── sample.env ├── .gitpod.yml ├── README.md ├── public ├── style.css └── client.js ├── package.json ├── routes ├── api.js └── fcctesting.js ├── server.js ├── views └── index.html ├── test-runner.js ├── tests └── 2_functional-tests.js └── assertion-analyser.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | PORT= 2 | # NODE_ENV=test 3 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-node-lts 2 | 3 | ports: 4 | - port: 3000 5 | onOpen: open-preview 6 | visibility: public 7 | 8 | tasks: 9 | - init: npm install 10 | command: npm run start 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Library 2 | 3 | This is the boilerplate for the Personal Library project. Instructions for building your project can be found at https://www.freecodecamp.org/learn/quality-assurance/quality-assurance-projects/personal-library 4 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | .border { 2 | border-style: solid; 3 | border-width: 1px; 4 | margin: 10px; 5 | } 6 | 7 | #sampleui { 8 | max-width: 450px; 9 | margin-left: 5%; 10 | height: 100%; 11 | } 12 | 13 | #sampleposting { 14 | max-width: 450px; 15 | text-align: center; 16 | margin-left: 5%; 17 | } 18 | 19 | #userstories { 20 | margin-left: 5%; 21 | } 22 | 23 | form { 24 | padding: 5px; 25 | } 26 | 27 | label { 28 | display: block; 29 | font-weight: bold; 30 | } 31 | 32 | input { 33 | margin-bottom: 5px; 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fcc-library", 3 | "version": "1.0.0", 4 | "description": "boilerplate", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "^1.15.2", 11 | "chai": "^4.2.0", 12 | "chai-http": "^4.3.0", 13 | "cors": "^2.8.1", 14 | "dotenv": "^8.2.0", 15 | "express": "^4.14.0", 16 | "mocha": "^3.2.0", 17 | "zombie": "^5.0.5" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/freeCodeCamp/boilerplate-project-library" 22 | }, 23 | "keywords": [ 24 | "node", 25 | "hyperdev", 26 | "express" 27 | ], 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * Complete the API routing below 5 | * 6 | * 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function (app) { 12 | 13 | app.route('/api/books') 14 | .get(function (req, res){ 15 | //response will be array of book objects 16 | //json res format: [{"_id": bookid, "title": book_title, "commentcount": num_of_comments },...] 17 | }) 18 | 19 | .post(function (req, res){ 20 | let title = req.body.title; 21 | //response will contain new book object including atleast _id and title 22 | }) 23 | 24 | .delete(function(req, res){ 25 | //if successful response will be 'complete delete successful' 26 | }); 27 | 28 | 29 | 30 | app.route('/api/books/:id') 31 | .get(function (req, res){ 32 | let bookid = req.params.id; 33 | //json res format: {"_id": bookid, "title": book_title, "comments": [comment,comment,...]} 34 | }) 35 | 36 | .post(function(req, res){ 37 | let bookid = req.params.id; 38 | let comment = req.body.comment; 39 | //json res format same as .get 40 | }) 41 | 42 | .delete(function(req, res){ 43 | let bookid = req.params.id; 44 | //if successful response will be 'delete successful' 45 | }); 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const cors = require('cors'); 6 | require('dotenv').config(); 7 | 8 | const apiRoutes = require('./routes/api.js'); 9 | const fccTestingRoutes = require('./routes/fcctesting.js'); 10 | const runner = require('./test-runner'); 11 | 12 | const app = express(); 13 | 14 | app.use('/public', express.static(process.cwd() + '/public')); 15 | 16 | app.use(cors({origin: '*'})); //USED FOR FCC TESTING PURPOSES ONLY! 17 | 18 | app.use(bodyParser.json()); 19 | app.use(bodyParser.urlencoded({ extended: true })); 20 | 21 | //Index page (static HTML) 22 | app.route('/') 23 | .get(function (req, res) { 24 | res.sendFile(process.cwd() + '/views/index.html'); 25 | }); 26 | 27 | //For FCC testing purposes 28 | fccTestingRoutes(app); 29 | 30 | //Routing for API 31 | apiRoutes(app); 32 | 33 | //404 Not Found Middleware 34 | app.use(function(req, res, next) { 35 | res.status(404) 36 | .type('text') 37 | .send('Not Found'); 38 | }); 39 | 40 | //Start our server and tests! 41 | const listener = app.listen(process.env.PORT || 3000, function () { 42 | console.log('Your app is listening on port ' + listener.address().port); 43 | if(process.env.NODE_ENV==='test') { 44 | console.log('Running Tests...'); 45 | setTimeout(function () { 46 | try { 47 | runner.run(); 48 | } catch(e) { 49 | console.log('Tests are not valid:'); 50 | console.error(e); 51 | } 52 | }, 1500); 53 | } 54 | }); 55 | 56 | module.exports = app; //for unit/functional testing 57 | -------------------------------------------------------------------------------- /routes/fcctesting.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | 'use strict'; 29 | 30 | const cors = require('cors'); 31 | const fs = require('fs'); 32 | const runner = require('../test-runner'); 33 | 34 | module.exports = function (app) { 35 | 36 | app.route('/_api/server.js') 37 | .get(function(req, res, next) { 38 | console.log('requested'); 39 | fs.readFile(__dirname + '/server.js', function(err, data) { 40 | if(err) return next(err); 41 | res.send(data.toString()); 42 | }); 43 | }); 44 | app.route('/_api/routes/api.js') 45 | .get(function(req, res, next) { 46 | console.log('requested'); 47 | fs.readFile(__dirname + '/routes/api.js', function(err, data) { 48 | if(err) return next(err); 49 | res.type('txt').send(data.toString()); 50 | }); 51 | }); 52 | 53 | app.get('/_api/get-tests', cors(), function(req, res, next){ 54 | console.log('requested'); 55 | if(process.env.NODE_ENV === 'test') return next(); 56 | res.json({status: 'unavailable'}); 57 | }, 58 | function(req, res, next){ 59 | if(!runner.report) return next(); 60 | res.json(testFilter(runner.report, req.query.type, req.query.n)); 61 | }, 62 | function(req, res){ 63 | runner.on('done', function(report){ 64 | process.nextTick(() => res.json(testFilter(runner.report, req.query.type, req.query.n))); 65 | }); 66 | }); 67 | app.get('/_api/app-info', function(req, res) { 68 | let hs = Object.keys(res._headers) 69 | .filter(h => !h.match(/^access-control-\w+/)); 70 | let hObj = {}; 71 | hs.forEach(h => {hObj[h] = res._headers[h]}); 72 | delete res._headers['strict-transport-security']; 73 | res.json({headers: hObj}); 74 | }); 75 | 76 | }; 77 | 78 | function testFilter(tests, type, n) { 79 | let out; 80 | switch (type) { 81 | case 'unit' : 82 | out = tests.filter(t => t.context.match('Unit Tests')); 83 | break; 84 | case 'functional': 85 | out = tests.filter(t => t.context.match('Functional Tests') && !t.title.match('#example')); 86 | break; 87 | default: 88 | out = tests; 89 | } 90 | if(n !== undefined) { 91 | return out[n] || out; 92 | } 93 | return out; 94 | } -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Personal Library 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |

Personal Library

18 |
19 |
20 |
21 |

Test API responses

22 |
23 |

Test post to /api/books

24 | 25 |
26 | 27 |
28 |
29 |

Test post to /api/books/{bookid}

30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 |

Sample Front-End

40 |
41 | 42 | 43 | 44 |
45 |
46 |
47 |

Select a book to see it's details and comments

48 |
    49 |
    50 | 51 |
    52 |
    53 | 54 | 57 | 58 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /test-runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | const analyser = require('./assertion-analyser'); 29 | const EventEmitter = require('events').EventEmitter; 30 | 31 | const Mocha = require('mocha'), 32 | fs = require('fs'), 33 | path = require('path'); 34 | 35 | const mocha = new Mocha(); 36 | const testDir = './tests' 37 | 38 | 39 | // Add each .js file to the mocha instance 40 | fs.readdirSync(testDir).filter(function(file){ 41 | // Only keep the .js files 42 | return file.substr(-3) === '.js'; 43 | 44 | }).forEach(function(file){ 45 | mocha.addFile( 46 | path.join(testDir, file) 47 | ); 48 | }); 49 | 50 | let emitter = new EventEmitter(); 51 | emitter.run = function() { 52 | 53 | let tests = []; 54 | let context = ""; 55 | let separator = ' -> '; 56 | // Run the tests. 57 | try { 58 | let runner = mocha.ui('tdd').run() 59 | .on('test end', function(test) { 60 | // remove comments 61 | let body = test.body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 62 | // collapse spaces 63 | body = body.replace(/\s+/g,' '); 64 | let obj = { 65 | title: test.title, 66 | context: context.slice(0, -separator.length), 67 | state: test.state, 68 | // body: body, 69 | assertions: analyser(body) 70 | }; 71 | tests.push(obj); 72 | }) 73 | .on('end', function() { 74 | emitter.report = tests; 75 | emitter.emit('done', tests) 76 | }) 77 | .on('suite', function(s) { 78 | context += (s.title + separator); 79 | 80 | }) 81 | .on('suite end', function(s) { 82 | context = context.slice(0, -(s.title.length + separator.length)) 83 | }) 84 | } catch(e) { 85 | throw(e); 86 | } 87 | }; 88 | 89 | module.exports = emitter; 90 | 91 | /* 92 | * Mocha.runner Events: 93 | * can be used to build a better custom report 94 | * 95 | * - `start` execution started 96 | * - `end` execution complete 97 | * - `suite` (suite) test suite execution started 98 | * - `suite end` (suite) all tests (and sub-suites) have finished 99 | * - `test` (test) test execution started 100 | * - `test end` (test) test completed 101 | * - `hook` (hook) hook execution started 102 | * - `hook end` (hook) hook complete 103 | * - `pass` (test) test passed 104 | * - `fail` (test, err) test failed 105 | * - `pending` (test) test pending 106 | */ -------------------------------------------------------------------------------- /public/client.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | let items = []; 3 | let itemsRaw = []; 4 | 5 | $.getJSON('/api/books', function(data) { 6 | //let items = []; 7 | itemsRaw = data; 8 | $.each(data, function(i, val) { 9 | items.push('
  1. ' + val.title + ' - ' + val.commentcount + ' comments
  2. '); 10 | return ( i !== 14 ); 11 | }); 12 | if (items.length >= 15) { 13 | items.push('

    ...and '+ (data.length - 15)+' more!

    '); 14 | } 15 | $('