Add Issue Tracker Certification project
This commit is contained in:
parent
c121d6a025
commit
8459b661d5
16 changed files with 5985 additions and 0 deletions
2
7-quality-assurance/2-issue-tracker/.gitattributes
vendored
Normal file
2
7-quality-assurance/2-issue-tracker/.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Tell Linguist to exclude HTML files to help Replit language detection.
|
||||||
|
*.html linguist-vendored
|
3
7-quality-assurance/2-issue-tracker/.gitignore
vendored
Normal file
3
7-quality-assurance/2-issue-tracker/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/node_modules/
|
||||||
|
.env
|
||||||
|
*.vscode
|
1
7-quality-assurance/2-issue-tracker/.prettierignore
Normal file
1
7-quality-assurance/2-issue-tracker/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.replit
|
10
7-quality-assurance/2-issue-tracker/.replit
Normal file
10
7-quality-assurance/2-issue-tracker/.replit
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
run = "npm start"
|
||||||
|
entrypoint = "server.js"
|
||||||
|
|
||||||
|
[packager]
|
||||||
|
language = "nodejs"
|
||||||
|
|
||||||
|
[packager.features]
|
||||||
|
packageSearch = true
|
||||||
|
guessImports = true
|
||||||
|
enabledForHosting = false
|
3
7-quality-assurance/2-issue-tracker/README.md
Normal file
3
7-quality-assurance/2-issue-tracker/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Issue Tracker
|
||||||
|
|
||||||
|
This is the boilerplate for the Issue Tracker project. Instructions for building your project can be found at https://www.freecodecamp.org/learn/quality-assurance/quality-assurance-projects/issue-tracker
|
140
7-quality-assurance/2-issue-tracker/assertion-analyser.js
Normal file
140
7-quality-assurance/2-issue-tracker/assertion-analyser.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* DO NOT EDIT THIS FILE
|
||||||
|
* For FCC testing purposes!
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function objParser(str, init) {
|
||||||
|
// finds objects, arrays, strings, and function arguments
|
||||||
|
// between parens, because they may contain ','
|
||||||
|
let openSym = ['[', '{', '"', "'", '('];
|
||||||
|
let closeSym = [']', '}', '"', "'", ')'];
|
||||||
|
let type;
|
||||||
|
let i;
|
||||||
|
for(i = (init || 0); i < str.length; i++ ) {
|
||||||
|
type = openSym.indexOf(str[i]);
|
||||||
|
if( type !== -1) break;
|
||||||
|
}
|
||||||
|
if (type === -1) return null;
|
||||||
|
let open = openSym[type];
|
||||||
|
let close = closeSym[type];
|
||||||
|
let count = 1;
|
||||||
|
let k;
|
||||||
|
for(k = i+1; k < str.length; k++) {
|
||||||
|
if(open === '"' || open === "'") {
|
||||||
|
if(str[k] === close) count--;
|
||||||
|
if(str[k] === '\\') k++;
|
||||||
|
} else {
|
||||||
|
if(str[k] === open) count++;
|
||||||
|
if(str[k] === close) count--;
|
||||||
|
}
|
||||||
|
if(count === 0) break;
|
||||||
|
}
|
||||||
|
if(count !== 0) return null;
|
||||||
|
let obj = str.slice(i, k+1);
|
||||||
|
return {
|
||||||
|
start : i,
|
||||||
|
end: k,
|
||||||
|
obj: obj
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacer(str) {
|
||||||
|
// replace objects with a symbol ( __#n)
|
||||||
|
let cnt = 0;
|
||||||
|
let data = [];
|
||||||
|
let obj = objParser(str);
|
||||||
|
while(obj) {
|
||||||
|
data[cnt] = obj.obj;
|
||||||
|
str = str.substring(0, obj.start) + '__#' + cnt++ + str.substring(obj.end+1);
|
||||||
|
obj = objParser(str);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
str : str,
|
||||||
|
dictionary : data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitter(str) {
|
||||||
|
// split on commas, then restore the objects
|
||||||
|
let strObj = replacer(str);
|
||||||
|
let args = strObj.str.split(',');
|
||||||
|
args = args.map(function(a){
|
||||||
|
let m = a.match(/__#(\d+)/);
|
||||||
|
while (m) {
|
||||||
|
a = a.replace(/__#(\d+)/, strObj.dictionary[m[1]]);
|
||||||
|
m = a.match(/__#(\d+)/);
|
||||||
|
}
|
||||||
|
return a.trim();
|
||||||
|
})
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertionAnalyser(body) {
|
||||||
|
|
||||||
|
// already filtered in the test runner
|
||||||
|
// // remove comments
|
||||||
|
// body = body.replace(/\/\/.*\n|\/\*.*\*\//g, '');
|
||||||
|
// // get test function body
|
||||||
|
// body = body.match(/\{\s*([\s\S]*)\}\s*$/)[1];
|
||||||
|
|
||||||
|
if(!body) return "invalid assertion";
|
||||||
|
// replace assertions bodies, so that they cannot
|
||||||
|
// contain the word 'assertion'
|
||||||
|
|
||||||
|
let cleanedBody = body.match(/(?:browser\s*\.\s*)?assert\s*\.\s*\w*\([\s\S]*\)/)
|
||||||
|
if(cleanedBody && Array.isArray(cleanedBody)) {
|
||||||
|
body = cleanedBody[0];
|
||||||
|
} else {
|
||||||
|
// No assertions present
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let s = replacer(body);
|
||||||
|
// split on 'assertion'
|
||||||
|
let splittedAssertions = s.str.split('assert');
|
||||||
|
let assertions = splittedAssertions.slice(1);
|
||||||
|
// match the METHODS
|
||||||
|
|
||||||
|
let assertionBodies = [];
|
||||||
|
let methods = assertions.map(function(a, i){
|
||||||
|
let m = a.match(/^\s*\.\s*(\w+)__#(\d+)/);
|
||||||
|
assertionBodies.push(parseInt(m[2]));
|
||||||
|
let pre = splittedAssertions[i].match(/browser\s*\.\s*/) ? 'browser.' : '';
|
||||||
|
return pre + m[1];
|
||||||
|
});
|
||||||
|
if(methods.some(function(m){ return !m })) return "invalid assertion";
|
||||||
|
// remove parens from the assertions bodies
|
||||||
|
let bodies = assertionBodies.map(function(b){
|
||||||
|
return s.dictionary[b].slice(1,-1).trim();
|
||||||
|
});
|
||||||
|
assertions = methods.map(function(m, i) {
|
||||||
|
return {
|
||||||
|
method: m,
|
||||||
|
args: splitter(bodies[i]) //replace objects, split on ',' ,then restore objects
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return assertions;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = assertionAnalyser;
|
4965
7-quality-assurance/2-issue-tracker/package-lock.json
generated
Normal file
4965
7-quality-assurance/2-issue-tracker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
7-quality-assurance/2-issue-tracker/package.json
Normal file
30
7-quality-assurance/2-issue-tracker/package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "fcc-issue-tracker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Issue Tracker Boilerplate",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "mocha --timeout 5000 --recursive --exit --ui tdd tests/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"chai-http": "^4.3.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"mocha": "^8.1.3",
|
||||||
|
"mongoose": "^6.7.1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/freeCodeCamp/boilerplate-project-issuetracker"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"node",
|
||||||
|
"hyperdev",
|
||||||
|
"express"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
23
7-quality-assurance/2-issue-tracker/public/style.css
Normal file
23
7-quality-assurance/2-issue-tracker/public/style.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
* {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue {
|
||||||
|
margin: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
width: 60%;
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
font-size: 10.5px;
|
||||||
|
}
|
80
7-quality-assurance/2-issue-tracker/routes/api.js
Normal file
80
7-quality-assurance/2-issue-tracker/routes/api.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (app, Issues) {
|
||||||
|
|
||||||
|
app.route('/api/issues/:project')
|
||||||
|
|
||||||
|
.get(function (req, res){
|
||||||
|
const project = req.params.project;
|
||||||
|
const filter = {project: project}
|
||||||
|
for (let q in req.query){
|
||||||
|
let value = req.query[q]
|
||||||
|
if (q === "open"){value = value.toLowerCase() === 'true'}
|
||||||
|
filter[q] = value
|
||||||
|
}
|
||||||
|
Issues.find(filter, (err, result) => {
|
||||||
|
if (err) {return res.json(err)}
|
||||||
|
res.json(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
.post(function (req, res){
|
||||||
|
const project = req.params.project;
|
||||||
|
if (!(req.body.issue_title && req.body.issue_text && req.body.created_by)) {return res.json({error: 'required field(s) missing'})}
|
||||||
|
const doc = {
|
||||||
|
project: project,
|
||||||
|
issue_title: req.body.issue_title,
|
||||||
|
issue_text: req.body.issue_text,
|
||||||
|
created_by: req.body.created_by,
|
||||||
|
assigned_to: req.body.assigned_to,
|
||||||
|
status_text: req.body.status_text
|
||||||
|
}
|
||||||
|
Issues.create(doc, (err, result) => {
|
||||||
|
if (err) {return res.json(err)}
|
||||||
|
const newEntry = {
|
||||||
|
_id: result._id,
|
||||||
|
issue_title: result.issue_title,
|
||||||
|
issue_text: result.issue_text,
|
||||||
|
created_by: result.created_by,
|
||||||
|
assigned_to: result.assigned_to,
|
||||||
|
status_text: result.status_text,
|
||||||
|
created_on: result.created_on,
|
||||||
|
updated_on: result.updated_on,
|
||||||
|
open: result.open,
|
||||||
|
}
|
||||||
|
res.json(newEntry)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
.put(function (req, res){
|
||||||
|
const _id = req.body._id
|
||||||
|
const doc = {}
|
||||||
|
for (let i in req.body){
|
||||||
|
if (i !== '_id') {
|
||||||
|
if (req.body[i] !== ""){doc[i] = req.body[i]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_id) {return res.json({error: 'missing _id'})}
|
||||||
|
if (Object.values(doc).every(el => el === undefined)){return res.json({error: 'no update field(s) sent', _id: _id})}
|
||||||
|
doc.updated_on = new Date().toISOString()
|
||||||
|
Issues.findByIdAndUpdate(_id, {"$set": doc}, (err, result) => {
|
||||||
|
if (err){return res.json({error: 'could not update', _id: _id})}
|
||||||
|
if (!result){return res.json({error: 'could not update', _id: _id})}
|
||||||
|
res.json({ result: 'successfully updated', '_id': _id })
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
.delete(function (req, res){
|
||||||
|
const _id = req.body._id
|
||||||
|
if (!_id) {return res.json({error: 'missing _id'})}
|
||||||
|
if (req.body.length > 1){return res.json({error: 'could not delete', _id: _id})}
|
||||||
|
Issues.findByIdAndDelete(_id, (err, result) => {
|
||||||
|
if (err){return res.json({error: 'could not delete', _id: _id})}
|
||||||
|
if (!result){return res.json({error: 'could not delete', _id: _id})}
|
||||||
|
res.json({result: 'successfully deleted', _id: _id})
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
85
7-quality-assurance/2-issue-tracker/routes/fcctesting.js
Normal file
85
7-quality-assurance/2-issue-tracker/routes/fcctesting.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* DO NOT EDIT THIS FILE
|
||||||
|
* For FCC testing purposes!
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const cors = require('cors');
|
||||||
|
const fs = require('fs');
|
||||||
|
const runner = require('../test-runner');
|
||||||
|
|
||||||
|
module.exports = function (app) {
|
||||||
|
|
||||||
|
app.route('/_api/server.js')
|
||||||
|
.get(function(req, res, next) {
|
||||||
|
console.log('requested');
|
||||||
|
fs.readFile(__dirname + '/server.js', function(err, data) {
|
||||||
|
if(err) return next(err);
|
||||||
|
res.send(data.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.route('/_api/routes/api.js')
|
||||||
|
.get(function(req, res, next) {
|
||||||
|
console.log('requested');
|
||||||
|
fs.readFile(__dirname + '/routes/api.js', function(err, data) {
|
||||||
|
if(err) return next(err);
|
||||||
|
res.type('txt').send(data.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/_api/get-tests', cors(), function(req, res, next){
|
||||||
|
console.log('requested');
|
||||||
|
if(process.env.NODE_ENV === 'test') return next();
|
||||||
|
res.json({status: 'unavailable'});
|
||||||
|
},
|
||||||
|
function(req, res, next){
|
||||||
|
if(!runner.report) return next();
|
||||||
|
res.json(testFilter(runner.report, req.query.type, req.query.n));
|
||||||
|
},
|
||||||
|
function(req, res){
|
||||||
|
runner.on('done', function(report){
|
||||||
|
process.nextTick(() => res.json(testFilter(runner.report, req.query.type, req.query.n)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function testFilter(tests, type, n) {
|
||||||
|
let out;
|
||||||
|
switch (type) {
|
||||||
|
case 'unit' :
|
||||||
|
out = tests.filter(t => t.context.match('Unit Tests'));
|
||||||
|
break;
|
||||||
|
case 'functional':
|
||||||
|
out = tests.filter(t => t.context.match('Functional Tests') && !t.title.match('#example'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
out = tests;
|
||||||
|
}
|
||||||
|
if(n !== undefined) {
|
||||||
|
return out[n] || out;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
114
7-quality-assurance/2-issue-tracker/server.js
Normal file
114
7-quality-assurance/2-issue-tracker/server.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const expect = require('chai').expect;
|
||||||
|
const cors = require('cors');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const apiRoutes = require('./routes/api.js');
|
||||||
|
const fccTestingRoutes = require('./routes/fcctesting.js');
|
||||||
|
const runner = require('./test-runner');
|
||||||
|
|
||||||
|
db().catch(err => console.log(err));
|
||||||
|
|
||||||
|
async function db() {
|
||||||
|
await mongoose.connect(process.env.MONGO_URI);
|
||||||
|
console.log('connected to DB')
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueSchema = new mongoose.Schema({
|
||||||
|
project: {
|
||||||
|
type: String,
|
||||||
|
default: 'apitest'
|
||||||
|
},
|
||||||
|
issue_title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
issue_text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
created_on: {
|
||||||
|
type: Date,
|
||||||
|
default: new Date()
|
||||||
|
},
|
||||||
|
updated_on: {
|
||||||
|
type: Date,
|
||||||
|
default: new Date()
|
||||||
|
},
|
||||||
|
created_by: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
assigned_to: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
status_text: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const Issues = mongoose.model('Issues', issueSchema);
|
||||||
|
Issues.create({_id: '63657911922d375e25ad1b85', project: 'apitest', issue_title: 'test', issue_text: 'test', created_by: 'test'}, (err, result) => {
|
||||||
|
err ? console.log(err) : console.log(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
let app = express();
|
||||||
|
|
||||||
|
app.use('/public', express.static(process.cwd() + '/public'));
|
||||||
|
|
||||||
|
app.use(cors({origin: '*'})); //For FCC testing purposes only
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
//Sample front-end
|
||||||
|
app.route('/:project/')
|
||||||
|
.get(function (req, res) {
|
||||||
|
res.sendFile(process.cwd() + '/views/issue.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
//Index page (static HTML)
|
||||||
|
app.route('/')
|
||||||
|
.get(function (req, res) {
|
||||||
|
res.sendFile(process.cwd() + '/views/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
//For FCC testing purposes
|
||||||
|
fccTestingRoutes(app);
|
||||||
|
|
||||||
|
//Routing for API
|
||||||
|
apiRoutes(app, Issues);
|
||||||
|
|
||||||
|
//404 Not Found Middleware
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
res.status(404)
|
||||||
|
.type('text')
|
||||||
|
.send('Not Found');
|
||||||
|
});
|
||||||
|
|
||||||
|
//Start our server and tests!
|
||||||
|
const listener = app.listen(process.env.PORT || 3000, function () {
|
||||||
|
console.log('Your app is listening on port ' + listener.address().port);
|
||||||
|
if(process.env.NODE_ENV==='test') {
|
||||||
|
console.log('Running Tests...');
|
||||||
|
setTimeout(function () {
|
||||||
|
try {
|
||||||
|
runner.run();
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Tests are not valid:');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app; //for testing
|
106
7-quality-assurance/2-issue-tracker/test-runner.js
Normal file
106
7-quality-assurance/2-issue-tracker/test-runner.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* DO NOT EDIT THIS FILE
|
||||||
|
* For FCC testing purposes!
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const analyser = require('./assertion-analyser');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
const Mocha = require('mocha'),
|
||||||
|
fs = require('fs'),
|
||||||
|
path = require('path');
|
||||||
|
|
||||||
|
const mocha = new Mocha();
|
||||||
|
let testDir = './tests'
|
||||||
|
|
||||||
|
|
||||||
|
// Add each .js file to the mocha instance
|
||||||
|
fs.readdirSync(testDir).filter(function(file){
|
||||||
|
// Only keep the .js files
|
||||||
|
return file.substr(-3) === '.js';
|
||||||
|
|
||||||
|
}).forEach(function(file){
|
||||||
|
mocha.addFile(
|
||||||
|
path.join(testDir, file)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let emitter = new EventEmitter();
|
||||||
|
emitter.run = function() {
|
||||||
|
|
||||||
|
let tests = [];
|
||||||
|
let context = "";
|
||||||
|
let separator = ' -> ';
|
||||||
|
// Run the tests.
|
||||||
|
try {
|
||||||
|
let runner = mocha.ui('tdd').run()
|
||||||
|
.on('test end', function(test) {
|
||||||
|
// remove comments
|
||||||
|
let body = test.body.replace(/\/\/.*\n|\/\*.*\*\//g, '');
|
||||||
|
// collapse spaces
|
||||||
|
body = body.replace(/\s+/g,' ');
|
||||||
|
let obj = {
|
||||||
|
title: test.title,
|
||||||
|
context: context.slice(0, -separator.length),
|
||||||
|
state: test.state,
|
||||||
|
// body: body,
|
||||||
|
assertions: analyser(body)
|
||||||
|
};
|
||||||
|
tests.push(obj);
|
||||||
|
})
|
||||||
|
.on('end', function() {
|
||||||
|
emitter.report = tests;
|
||||||
|
emitter.emit('done', tests)
|
||||||
|
})
|
||||||
|
.on('suite', function(s) {
|
||||||
|
context += (s.title + separator);
|
||||||
|
|
||||||
|
})
|
||||||
|
.on('suite end', function(s) {
|
||||||
|
context = context.slice(0, -(s.title.length + separator.length))
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
throw(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = emitter;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mocha.runner Events:
|
||||||
|
* can be used to build a better custom report
|
||||||
|
*
|
||||||
|
* - `start` execution started
|
||||||
|
* - `end` execution complete
|
||||||
|
* - `suite` (suite) test suite execution started
|
||||||
|
* - `suite end` (suite) all tests (and sub-suites) have finished
|
||||||
|
* - `test` (test) test execution started
|
||||||
|
* - `test end` (test) test completed
|
||||||
|
* - `hook` (hook) hook execution started
|
||||||
|
* - `hook end` (hook) hook complete
|
||||||
|
* - `pass` (test) test passed
|
||||||
|
* - `fail` (test, err) test failed
|
||||||
|
* - `pending` (test) test pending
|
||||||
|
*/
|
202
7-quality-assurance/2-issue-tracker/tests/2_functional-tests.js
Normal file
202
7-quality-assurance/2-issue-tracker/tests/2_functional-tests.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
const chaiHttp = require('chai-http');
|
||||||
|
const chai = require('chai');
|
||||||
|
const assert = chai.assert;
|
||||||
|
const server = require('../server');
|
||||||
|
|
||||||
|
chai.use(chaiHttp);
|
||||||
|
|
||||||
|
suite('Functional Tests', () => {
|
||||||
|
suite('POST requests', () => {
|
||||||
|
test('Create an issue with every field: POST request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.post('/api/issues/apitest')
|
||||||
|
.send({
|
||||||
|
"issue_title": "Test issue",
|
||||||
|
"issue_text": "This is a test",
|
||||||
|
"created_by": "sammy",
|
||||||
|
"assigned_to": "sammy",
|
||||||
|
"status_text": "none"
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
assert.equal(res.body.issue_title, 'Test issue')
|
||||||
|
assert.equal(res.body.issue_text, 'This is a test')
|
||||||
|
assert.equal(res.body.created_by, 'sammy')
|
||||||
|
assert.equal(res.body.assigned_to, 'sammy')
|
||||||
|
assert.equal(res.body.status_text, 'none')
|
||||||
|
assert.equal(res.body.open, true)
|
||||||
|
assert.isOk(res.body._id)
|
||||||
|
assert.isOk(res.body.created_on)
|
||||||
|
assert.isOk(res.body.updated_on)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create an issue with only required fields: POST request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.post('/api/issues/apitest')
|
||||||
|
.send({
|
||||||
|
"issue_title": "Test issue",
|
||||||
|
"issue_text": "This is a test",
|
||||||
|
"created_by": "sammy"
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
assert.equal(res.body.issue_title, 'Test issue')
|
||||||
|
assert.equal(res.body.issue_text, 'This is a test')
|
||||||
|
assert.equal(res.body.created_by, 'sammy')
|
||||||
|
assert.equal(res.body.assigned_to, '')
|
||||||
|
assert.equal(res.body.status_text, '')
|
||||||
|
assert.exists(res.body._id)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create an issue with missing required fields: POST request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.post('/api/issues/apitest')
|
||||||
|
.send({
|
||||||
|
"issue_title": "Test issue",
|
||||||
|
"issue_text": "This is a test",
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'required field(s) missing')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
suite('GET requests', () => {
|
||||||
|
test('View issues on a project: GET request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.get('/api/issues/apitest')
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
assert.exists(res.body[0])
|
||||||
|
assert.exists(res.body[0].issue_title)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('View issues on a project with one filter: GET request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.get('/api/issues/apitest')
|
||||||
|
.query({open: false})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body[0].open, false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('View issues on a project with multiple filters: GET request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.get('/api/issues/apitest')
|
||||||
|
.query({open: false, assigned_to: 'nobody'})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body[0].open, false)
|
||||||
|
assert.equal(res.body[0].assigned_to, 'nobody')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
suite('PUT requests', () => {
|
||||||
|
test('Update one field on an issue: PUT request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.put('/api/issues/apitest')
|
||||||
|
.send({_id: '63657aca0afa26c228f5e0c1', open: false})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.result, 'successfully updated')
|
||||||
|
assert.equal(res.body._id, '63657aca0afa26c228f5e0c1')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update multiple fields on an issue: PUT request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.put('/api/issues/apitest')
|
||||||
|
.send({_id: '63657ac80afa26c228f5e0bf', open: false, assigned_to: 'not sammy'})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.result, 'successfully updated')
|
||||||
|
assert.equal(res.body._id, '63657ac80afa26c228f5e0bf')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update an issue with missing _id: PUT request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.put('/api/issues/apitest')
|
||||||
|
.send({open: false})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'missing _id')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update an issue with no fields to update: PUT request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.put('/api/issues/apitest')
|
||||||
|
.send({_id: '63657911922d375e25ad1b85'})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'no update field(s) sent')
|
||||||
|
assert.equal(res.body._id, '63657911922d375e25ad1b85')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update an issue with an invalid _id: PUT request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.put('/api/issues/apitest')
|
||||||
|
.send({_id: 'invalid', open: false})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'could not update')
|
||||||
|
assert.equal(res.body._id, 'invalid')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
suite('DELETE requests', () => {
|
||||||
|
test('Delete an issue: DELETE request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.delete('/api/issues/apitest')
|
||||||
|
.send({_id: '63657911922d375e25ad1b85'})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.result, 'successfully deleted')
|
||||||
|
assert.equal(res.body._id, '63657911922d375e25ad1b85')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Delete an issue with an invalid _id: DELETE request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.delete('/api/issues/apitest')
|
||||||
|
.send({_id: 'INVALID STILL'})
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'could not delete')
|
||||||
|
assert.equal(res.body._id, 'INVALID STILL')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Delete an issue with missing _id: DELETE request to /api/issues/{project}', (done) => {
|
||||||
|
chai
|
||||||
|
.request(server)
|
||||||
|
.delete('/api/issues/apitest')
|
||||||
|
.end((err, res) => {
|
||||||
|
assert.equal(res.body.error, 'missing _id')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
115
7-quality-assurance/2-issue-tracker/views/index.html
vendored
Normal file
115
7-quality-assurance/2-issue-tracker/views/index.html
vendored
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Issue Tracker</title>
|
||||||
|
<link rel="icon" type="image/png" href="https://cdn.freecodecamp.org/universal/favicons/favicon-16x16.png" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="/public/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
Issue Tracker
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<section style="margin: 0 50px;">
|
||||||
|
<h3>Example <code>GET</code> usage</h3>
|
||||||
|
<code>/api/issues/{project}</code><br>
|
||||||
|
<code>/api/issues/{project}?open=true&assigned_to=Joe</code><br>
|
||||||
|
<h3>Example return</h3>
|
||||||
|
<pre>
|
||||||
|
<code>[
|
||||||
|
{
|
||||||
|
"_id": "5871dda29faedc3491ff93bb",
|
||||||
|
"issue_title": "Fix error in posting data",
|
||||||
|
"issue_text": "When we post data it has an error.",
|
||||||
|
"created_on": "2017-01-08T06:35:14.240Z",
|
||||||
|
"updated_on": "2017-01-08T06:35:14.240Z",
|
||||||
|
"created_by": "Joe",
|
||||||
|
"assigned_to": "Joe",
|
||||||
|
"open": true,
|
||||||
|
"status_text": "In QA"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
<hr style='margin: 0 50px'>
|
||||||
|
<section id='testui' style='margin-left: 50px;'>
|
||||||
|
<h3>Examples</h3>
|
||||||
|
Go to <a href='/api/issues/apitest/'><code>/api/issues/apitest/</code></a> project
|
||||||
|
<h2 style="text-align: left">API Tests</h2>
|
||||||
|
<h3>Submit issue on <i>apitest</i></h3>
|
||||||
|
<form id="testForm" class="border">
|
||||||
|
<input type="text" name="issue_title" placeholder="*Title" style="width: 100px" required=''><br>
|
||||||
|
<textarea type="text" name="issue_text" placeholder="*Text" style="width: 100px" required=''></textarea><br>
|
||||||
|
<input type="text" name="created_by" placeholder="*Created by" style="width: 100px" required=''><br>
|
||||||
|
<input type="text" name="assigned_to" placeholder="(opt)Assigned to" style="width: 100px"><br>
|
||||||
|
<input type="text" name="status_text" placeholder="(opt)Status text" style="width: 100px"><br>
|
||||||
|
<button type="submit">Submit Issue</button>
|
||||||
|
</form><br>
|
||||||
|
<h3>Update issue on <i>apitest</i> (Change any or all to update issue on the _id supplied)</h3>
|
||||||
|
<form id="testForm2" class="border">
|
||||||
|
<input type="text" name="_id" placeholder="*_id" style="width: 100px" required=''><br>
|
||||||
|
<input type="text" name="issue_title" placeholder="(opt)Title" style="width: 100px"><br>
|
||||||
|
<textarea type="text" name="issue_text" placeholder="(opt)Text" style="width: 100px"></textarea><br>
|
||||||
|
<input type="text" name="created_by" placeholder="(opt)Created by" style="width: 100px"><br>
|
||||||
|
<input type="text" name="assigned_to" placeholder="(opt)Assigned to" style="width: 100px"><br>
|
||||||
|
<input type="text" name="status_text" placeholder="(opt)Status text" style="width: 100px"><br>
|
||||||
|
<label><input type="checkbox" name="open" value="false"> Check to close issue</label><br>
|
||||||
|
<button type="submit">Submit Issue</button>
|
||||||
|
</form><br>
|
||||||
|
<h3>Delete issue on <i>apitest</i></h3>
|
||||||
|
<form id="testForm3" class="border">
|
||||||
|
<input type="text" name="_id" placeholder="_id" style="width: 100px" required=''><br>
|
||||||
|
<button type="submit">Delete Issue</button>
|
||||||
|
</form>
|
||||||
|
<code id='jsonResult'></code>
|
||||||
|
</section>
|
||||||
|
<hr style='margin: 50px; margin-top: 200px'>
|
||||||
|
<!-- Your web-app is https, so your scripts need to be too -->
|
||||||
|
<script src="https://code.jquery.com/jquery-2.2.1.min.js"
|
||||||
|
integrity="sha256-gvQgAFzTH6trSrAWoH1iPo9Xc96QxSZ3feW6kem+O00="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('#testForm').submit(function(e) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/issues/apitest',
|
||||||
|
type: 'post',
|
||||||
|
data: $('#testForm').serialize(),
|
||||||
|
success: function(data) {
|
||||||
|
$('#jsonResult').text(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
$('#testForm2').submit(function(e) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/issues/apitest',
|
||||||
|
type: 'put',
|
||||||
|
data: $('#testForm2').serialize(),
|
||||||
|
success: function(data) {
|
||||||
|
$('#jsonResult').text(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
$('#testForm3').submit(function(e) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/issues/apitest',
|
||||||
|
type: 'delete',
|
||||||
|
data: $('#testForm3').serialize(),
|
||||||
|
success: function(data) {
|
||||||
|
$('#jsonResult').text(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
106
7-quality-assurance/2-issue-tracker/views/issue.html
vendored
Normal file
106
7-quality-assurance/2-issue-tracker/views/issue.html
vendored
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Issue Tracker - Project Page</title>
|
||||||
|
<link rel="icon" type="image/png" href="https://cdn.freecodecamp.org/universal/favicons/favicon-16x16.png" />
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="/public/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1 id='projectTitle'></h1>
|
||||||
|
</header>
|
||||||
|
<center>
|
||||||
|
<div id='submitNewIssue'>
|
||||||
|
<br>
|
||||||
|
<h3>Submit a new issue:</h3>
|
||||||
|
<form id="newIssue" method="post" action="/api/">
|
||||||
|
<input type="text" name="issue_title" placeholder="*Title" style="width: 320px; margin-bottom: 3px;" required=''><br>
|
||||||
|
<textarea type="text" name="issue_text" placeholder="*Text" style="width: 320px; height: 100px;" required=''></textarea><br>
|
||||||
|
<input type="text" name="created_by" placeholder="*Created by" style="width: 100px" required=''>
|
||||||
|
<input type="text" name="assigned_to" placeholder="(opt)Assigned to" style="width: 100px">
|
||||||
|
<input type="text" name="status_text" placeholder="(opt)Status text" style="width: 100px"><br>
|
||||||
|
<button type="submit">Submit Issue</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='issueDisplay'></div>
|
||||||
|
|
||||||
|
<hr style='margin: 50px; margin-top: 200px'>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-2.2.1.min.js"
|
||||||
|
integrity="sha256-gvQgAFzTH6trSrAWoH1iPo9Xc96QxSZ3feW6kem+O00="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
let currentProject = window.location.pathname.replace(/\//g, "");;
|
||||||
|
let url = "/api/issues/"+currentProject;
|
||||||
|
$('#projectTitle').text('All issues for: '+currentProject)
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: url,
|
||||||
|
success: function(data)
|
||||||
|
{
|
||||||
|
let issues= [];
|
||||||
|
data.forEach(function(ele) {
|
||||||
|
console.log(ele);
|
||||||
|
let openstatus;
|
||||||
|
(ele.open) ? openstatus = 'open' : openstatus = 'closed';
|
||||||
|
let single = [
|
||||||
|
'<div class="issue '+openstatus+'">',
|
||||||
|
'<p class="id">id: '+ele._id+'</p>',
|
||||||
|
'<h3>'+ele.issue_title+' - ('+openstatus+')</h3>',
|
||||||
|
'<br>',
|
||||||
|
'<p>'+ele.issue_text+'</p>',
|
||||||
|
'<p>'+ele.status_text+'</p>',
|
||||||
|
'<br>',
|
||||||
|
'<p class="id"><b>Created by:</b> '+ele.created_by+' <b>Assigned to:</b> '+ele.assigned_to,
|
||||||
|
'<p class="id"><b>Created on:</b> '+ele.created_on+' <b>Last updated:</b> '+ele.updated_on,
|
||||||
|
'<br><a href="#" class="closeIssue" id="'+ele._id+'">close?</a> <a href="#" class="deleteIssue" id="'+ele._id+'">delete?</a>',
|
||||||
|
'</div>'
|
||||||
|
|
||||||
|
];
|
||||||
|
issues.push(single.join(''));
|
||||||
|
});
|
||||||
|
$('#issueDisplay').html(issues.join(''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#newIssue').submit(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).attr('action', "/api/issues/" + currentProject);
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: $(this).serialize(),
|
||||||
|
success: function(data) { window.location.reload(true); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#issueDisplay').on('click','.closeIssue', function(e) {
|
||||||
|
let url = "/api/issues/"+currentProject;
|
||||||
|
$.ajax({
|
||||||
|
type: "PUT",
|
||||||
|
url: url,
|
||||||
|
data: {_id: $(this).attr('id'), open: false},
|
||||||
|
success: function(data) { alert(data); window.location.reload(true); }
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
$('#issueDisplay').on('click','.deleteIssue', function(e) {
|
||||||
|
let url = "/api/issues/"+currentProject;
|
||||||
|
$.ajax({
|
||||||
|
type: "DELETE",
|
||||||
|
url: url,
|
||||||
|
data: {_id: $(this).attr('id')},
|
||||||
|
success: function(data) { alert(data); window.location.reload(true); }
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in a new issue