├── .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 |Select a book to see it's details and comments
48 |...and '+ (data.length - 15)+' more!
'); 14 | } 15 | $(''+data+'
Refresh the page
'); 43 | } 44 | }); 45 | }); 46 | 47 | $('#bookDetail').on('click','button.addComment',function() { 48 | let newComment = $('#commentToAdd').val(); 49 | $.ajax({ 50 | url: '/api/books/'+this.id, 51 | type: 'post', 52 | dataType: 'json', 53 | data: $('#newCommentForm').serialize(), 54 | success: function(data) { 55 | comments.unshift(newComment); //adds new comment to top of list 56 | $('#detailComments').html(comments.join('')); 57 | } 58 | }); 59 | }); 60 | 61 | $('#newBook').click(function() { 62 | $.ajax({ 63 | url: '/api/books', 64 | type: 'post', 65 | dataType: 'json', 66 | data: $('#newBookForm').serialize(), 67 | success: function(data) { 68 | //update list 69 | } 70 | }); 71 | }); 72 | 73 | $('#deleteAllBooks').click(function() { 74 | $.ajax({ 75 | url: '/api/books', 76 | type: 'delete', 77 | dataType: 'json', 78 | data: $('#newBookForm').serialize(), 79 | success: function(data) { 80 | //update list 81 | } 82 | }); 83 | }); 84 | 85 | }); -------------------------------------------------------------------------------- /tests/2_functional-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * FILL IN EACH FUNCTIONAL TEST BELOW COMPLETELY 5 | * -----[Keep the tests in the same order!]----- 6 | * 7 | */ 8 | 9 | const chaiHttp = require('chai-http'); 10 | const chai = require('chai'); 11 | const assert = chai.assert; 12 | const server = require('../server'); 13 | 14 | chai.use(chaiHttp); 15 | 16 | suite('Functional Tests', function() { 17 | 18 | /* 19 | * ----[EXAMPLE TEST]---- 20 | * Each test should completely test the response of the API end-point including response status code! 21 | */ 22 | test('#example Test GET /api/books', function(done){ 23 | chai.request(server) 24 | .get('/api/books') 25 | .end(function(err, res){ 26 | assert.equal(res.status, 200); 27 | assert.isArray(res.body, 'response should be an array'); 28 | assert.property(res.body[0], 'commentcount', 'Books in array should contain commentcount'); 29 | assert.property(res.body[0], 'title', 'Books in array should contain title'); 30 | assert.property(res.body[0], '_id', 'Books in array should contain _id'); 31 | done(); 32 | }); 33 | }); 34 | /* 35 | * ----[END of EXAMPLE TEST]---- 36 | */ 37 | 38 | suite('Routing tests', function() { 39 | 40 | 41 | suite('POST /api/books with title => create book object/expect book object', function() { 42 | 43 | test('Test POST /api/books with title', function(done) { 44 | //done(); 45 | }); 46 | 47 | test('Test POST /api/books with no title given', function(done) { 48 | //done(); 49 | }); 50 | 51 | }); 52 | 53 | 54 | suite('GET /api/books => array of books', function(){ 55 | 56 | test('Test GET /api/books', function(done){ 57 | //done(); 58 | }); 59 | 60 | }); 61 | 62 | 63 | suite('GET /api/books/[id] => book object with [id]', function(){ 64 | 65 | test('Test GET /api/books/[id] with id not in db', function(done){ 66 | //done(); 67 | }); 68 | 69 | test('Test GET /api/books/[id] with valid id in db', function(done){ 70 | //done(); 71 | }); 72 | 73 | }); 74 | 75 | 76 | suite('POST /api/books/[id] => add comment/expect book object with id', function(){ 77 | 78 | test('Test POST /api/books/[id] with comment', function(done){ 79 | //done(); 80 | }); 81 | 82 | test('Test POST /api/books/[id] without comment field', function(done){ 83 | //done(); 84 | }); 85 | 86 | test('Test POST /api/books/[id] with comment, id not in db', function(done){ 87 | //done(); 88 | }); 89 | 90 | }); 91 | 92 | suite('DELETE /api/books/[id] => delete book object id', function() { 93 | 94 | test('Test DELETE /api/books/[id] with valid id in db', function(done){ 95 | //done(); 96 | }); 97 | 98 | test('Test DELETE /api/books/[id] with id not in db', function(done){ 99 | //done(); 100 | }); 101 | 102 | }); 103 | 104 | }); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /assertion-analyser.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 | function objParser(str, init) { 29 | // finds objects, arrays, strings, and function arguments 30 | // between parens, because they may contain ',' 31 | let openSym = ['[', '{', '"', "'", '(']; 32 | let closeSym = [']', '}', '"', "'", ')']; 33 | let type; 34 | let i; 35 | for(i = (init || 0); i < str.length; i++ ) { 36 | type = openSym.indexOf(str[i]); 37 | if( type !== -1) break; 38 | } 39 | if (type === -1) return null; 40 | let open = openSym[type]; 41 | let close = closeSym[type]; 42 | let count = 1; 43 | let k; 44 | for(k = i+1; k < str.length; k++) { 45 | if(open === '"' || open === "'") { 46 | if(str[k] === close) count--; 47 | if(str[k] === '\\') k++; 48 | } else { 49 | if(str[k] === open) count++; 50 | if(str[k] === close) count--; 51 | } 52 | if(count === 0) break; 53 | } 54 | if(count !== 0) return null; 55 | let obj = str.slice(i, k+1); 56 | return { 57 | start : i, 58 | end: k, 59 | obj: obj 60 | }; 61 | } 62 | 63 | function replacer(str) { 64 | // replace objects with a symbol ( __#n) 65 | let obj; 66 | let cnt = 0; 67 | let data = []; 68 | while(obj = objParser(str)) { 69 | data[cnt] = obj.obj; 70 | str = str.substring(0, obj.start) + '__#' + cnt++ + str.substring(obj.end+1) 71 | } 72 | return { 73 | str : str, 74 | dictionary : data 75 | } 76 | } 77 | 78 | function splitter(str) { 79 | // split on commas, then restore the objects 80 | let strObj = replacer(str); 81 | let args = strObj.str.split(','); 82 | args = args.map(function(a){ 83 | let m = a.match(/__#(\d+)/); 84 | while (m) { 85 | a = a.replace(/__#(\d+)/, strObj.dictionary[m[1]]); 86 | m = a.match(/__#(\d+)/); 87 | } 88 | return a.trim(); 89 | }) 90 | return args; 91 | } 92 | 93 | function assertionAnalyser(body) { 94 | 95 | // already filtered in the test runner 96 | // // remove comments 97 | // body = body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 98 | // // get test function body 99 | // body = body.match(/\{\s*([\s\S]*)\}\s*$/)[1]; 100 | 101 | if(!body) return "invalid assertion"; 102 | // replace assertions bodies, so that they cannot 103 | // contain the word 'assertion' 104 | 105 | let cleanedBody = body.match(/(?:browser\s*\.\s*)?assert\s*\.\s*\w*\([\s\S]*\)/) 106 | if(cleanedBody && Array.isArray(cleanedBody)) { 107 | body = cleanedBody[0]; 108 | } else { 109 | // No assertions present 110 | return []; 111 | } 112 | let s = replacer(body); 113 | // split on 'assertion' 114 | let splittedAssertions = s.str.split('assert'); 115 | let assertions = splittedAssertions.slice(1); 116 | // match the METHODS 117 | 118 | let assertionBodies = []; 119 | let methods = assertions.map(function(a, i){ 120 | let m = a.match(/^\s*\.\s*(\w+)__#(\d+)/); 121 | assertionBodies.push(parseInt(m[2])); 122 | let pre = splittedAssertions[i].match(/browser\s*\.\s*/) ? 'browser.' : ''; 123 | return pre + m[1]; 124 | }); 125 | if(methods.some(function(m){ return !m })) return "invalid assertion"; 126 | // remove parens from the assertions bodies 127 | let bodies = assertionBodies.map(function(b){ 128 | return s.dictionary[b].slice(1,-1).trim(); 129 | }); 130 | assertions = methods.map(function(m, i) { 131 | return { 132 | method: m, 133 | args: splitter(bodies[i]) //replace objects, split on ',' ,then restore objects 134 | } 135 | }) 136 | return assertions; 137 | } 138 | 139 | module.exports = assertionAnalyser; 140 | --------------------------------------------------------------------------------