Open Source Development 4 - Testing and Issues
References for this Part
Brasseur, V. M. Forge Your Future with Open Source 1st ed.,
Pragmatic Bookshelf, 2018
(Brasseur, V. M., 2018, chapter 3-5)
https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-an-issue
https://dev.to/github/how-to-create-the-perfect-readme-for-your-open-source-project-1k69
Model Solutions Previous Lesson
This model solution is available in total from
https://codeberg.org/arosano/restapi_done.git
Example 1. routes/users.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
const express = require('express');
const router = express.Router();
const con = require('../controllers/controllers');
/* GET user register ie send form */
router.get('/register', function(req, res, next) {
res.render('register', {
title: 'Please Register',
subtitle: 'Follow the embedded cues'
});
});
router.post('/register', con.handleRegistration, function(req, res, next) {
res.status(201).json({message: 'Registration succesfull'});
});
router.get('/login', function(req, res, next) {
res.render('login', {
title: 'Please Login'
});
});
router.post('/login', con.handleLogin, function(req, res, next) {
res.status(200).json("Bearer " + res.locals.token);
});
router.patch('/toggleAdmin/:email', con.isAuth, con.isAdmin, con.toggleAdmin, function(req, res, next) {
res.status(201).json('Update successful');
});
module.exports = router;
|
Example 2. routes/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
const express = require('express');
const router = express.Router();
const conw = require('../controllers/controllersworld');
const con = require('../controllers/controllers');
const TITLE = 'Rest API Pattern Project';
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', {
title: TITLE,
subtitle: 'Front Page'
});
});
/* API endpoints */
/* GET continents */
router.get('/continents', con.isAuth, conw.getContinents, function (req, res) {
// variables from middleware
res.json({continents: res.locals.continents});
});
/* GET countries */
router.get('/countries', con.isAuth, conw.getCountries, function (req, res) {
// variables from middleware
res.json({countries: res.locals.countries});
});
/* GET cities */
router.get('/cities', con.isAuth, conw.getCities, function (req, res) {
// variables from middleware
res.json({cities: res.locals.cities});
});
/* GET cities from a country */
router.get('/cities/:ctry', con.isAuth, conw.getCitiesCtry, function (req, res) {
// variables from middleware
res.json({cities: res.locals.cities});
});
/* post city */
router.post('/city', con.isAuth, con.isAdmin, conw.postCity, function (req, res) {
res.status(201).json({message: "City in db"});
});
/* GET languages */
router.get('/languages', con.isAuth, conw.getLanguages, function (req, res) {
// variables from middleware
res.json({languages: res.locals.languages});
});
module.exports = router;
|
Example 3. controllers/controllers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
/* controllers.js */
require ('dotenv').config();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const model = require('../models/dbhandlers');
const SU = 0; // admin
module.exports = {
handleLogin: async function(req, res, next) {
let errmsg = 'Error in credentials\n';
let errmsgt = 'Error in token signing\n';
try {
await model.getUser(req, res, next);
let rc = await bcrypt.compare(req.body.password, res.locals.user.password);
if (!rc)
throw new Error(errmsg);
const payload = { email: res.locals.user.email, profile: res.locals.user.profile };
const lifetime = { expiresIn: '1h' };
let token = await jwt.sign(payload, process.env.SECRET, lifetime);
if (!token)
throw new Error(errmsgt);
res.locals.token = token;
next();
} catch (err) {
return res.status(500).json({message: err.message});
}
},
handleRegistration: async function(req, res, next) {
try {
let hash = await bcrypt.hash(req.body.password, parseInt(process.env.ROUNDS));
res.locals.hash = hash;
await model.insertUser(req, res, next);
next();
} catch (err) {
console.log(err);
return res.status(500).json({message: err.message});
}
},
isAuth: async function(req, res, next) {
try {
let errmsg = 'You must be logged in';
let token = req.headers.authorization && req.headers.authorization.split(' ')[1];
if (!token)
throw new Error(errmsg);
errmsg = 'Failed to authenticate token';
let rc = await jwt.verify(token, process.env.SECRET);
console.log(rc);
if (!rc)
throw new Error(errmsg);
res.locals.authorized = true;
res.locals.profile = rc.profile;
next();
} catch(err) {
res.status(500).json({message: err.message});
}
},
isAdmin: function(req, res, next) {
try {
if (res.locals.authorized && res.locals.profile == SU)
next();
else
throw new Error('You must be a logged in admin');
} catch(err) {
res.status(500).json({message: err.message});
}
},
toggleAdmin: async function(req, res, next) {
try {
req.body.email = req.params.email;
await model.getUser(req, res, next);
if (!res.locals.user)
throw new Error('User not found');
if (res.locals.user.profile == SU) {
let count = await model.countAdmins();
if (count <= 1)
throw new Error('Cannot remove last admin');
await model.toggleAdmin(req, res, next);
}
next();
} catch(err) {
res.status(500).json({message: err.message});
}
}
}
|
Example 4. controllers/controllersworld.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
const models = require('../models/dbhandlers');
module.exports = {
getContinents: async function (req, res, next) {
let rows = await models.getAllContinents(req, res, next);
res.locals.continents = rows;
next();
},
getCountries: async function (req, res, next) {
let rows = await models.getAllCountries(req, res, next);
res.locals.countries = rows;
next();
},
getCities: async function (req, res, next) {
let rows = await models.getAllCities(req, res, next);
res.locals.cities = rows;
next();
},
getCitiesCtry: async function (req, res, next) {
let rows = await models.getCitiesCtry(req, res, next);
res.locals.cities = rows;
next();
},
postCity: async function (req, res, next) {
try {
await models.insertCity(req, res, next);
next();
} catch(err) {
console.log(err);
return res.status(500).json({message: err.message});
}
},
getLanguages: async function (req, res, next) {
let rows = await models.getAllLanguages(req, res, next);
res.locals.languages = rows;
next();
}
}
|
Example 5. models/dbhandlers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
const path = require('path');
const sqlite3 = require("better-sqlite3");
const SU = 0;
// Start db connection
const connect = async function () {
try {
const db = await new sqlite3(path.resolve('db/sampleAPI.db'), {fileMustExist: true});
return db;
} catch (err) {
console.error(err);
}
};
module.exports = {
countAdmins: async function() {
try {
let db = await connect();
let sql = 'select count(*) as c from user where profile = ?';
let query = db.prepare(sql);
let row = await query.all(SU);
return row[0].c;
} catch (err) {
return 0;
}
},
getAllCities: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from city';
let query = db.prepare(sql);
let rows = await query.all();
return rows;
} catch (err) {
res.status(400).json(err.message);
}
},
getAllContinents: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from continent';
let query = db.prepare(sql);
let rows = await query.all();
return rows;
} catch (err) {
res.status(400).json(err.message);
}
},
getAllCountries: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from country';
let query = db.prepare(sql);
let rows = await query.all();
return rows;
} catch (err) {
res.status(400).json(err.message);
}
},
getAllLanguages: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from countrylanguage';
let query = db.prepare(sql);
let rows = await query.all();
return rows;
} catch (err) {
res.status(400).json(err.message);
}
},
getCitiesCtry: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from city where countrycode = ?';
let query = db.prepare(sql);
let rows = await query.all(req.params.ctry);
return rows;
} catch (err) {
res.status(400).json(err.message);
}
},
getUser: async function (req, res, next) {
try {
let db = await connect();
let sql = 'select * from user where email = ?';
let query = db.prepare(sql);
let row = await query.get(req.body.email);
res.locals.user = row;
} catch (err) {
res.status(400).json(err.message);
}
},
insertUser: async function (req, res, next) {
try {
let db = await connect();
let sql = 'insert into user (email, password, bio) values(?, ?, ?)';
let query = db.prepare(sql);
let row = await query.run(req.body.email, res.locals.hash, req.body.bio);
res.locals.user = row
} catch (err) {
res.status(400).json(err.message);
}
},
insertCity: async function (req, res, next) {
try {
let db = await connect();
let sql = 'insert into city values(?, ?, ?, ?)';
let query = db.prepare(sql);
let row = await query.run(req.body.name, req.body.countrycode, req.body.district, req.body.population);
} catch (err) {
res.status(400).json(err.message);
}
},
toggleAdmin: async function(req, res, next) {
try {
let db = await connect();
await this.getUser(req, res, next);
console.log(res.locals.user);
let pro = res.locals.user.profile;
if (pro == SU)
pro = 99;
else
pro = SU;
let sql = 'update user set profile = ? where email = ?';
let query = db.prepare(sql);
await query.run(pro, res.locals.user.email);
} catch (err) {
res.status(400).json(err.message);
}
}
}
|
Example 6. Testing by curlit.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#!/usr/bin/env sh
TOKEN=`curl -s -d email=$1 -d password=$APIPWD http://localhost:3000/users/login`
TOKENC=$(echo "$TOKEN" | sed 's/"//g')
curl -H "Authorization: ${TOKENC}" \
-d "name=$2" \
-d "population=$3" \
-d "district=$4" \
-d "countrycode=$5" \
-s http://localhost:3000/city
echo ""
curl -s http://localhost:3000/continents
echo ""
curl -H "Authorization: ${TOKENC}" \
-s http://localhost:3000/cities/DNK
echo ""
curl -X PATCH \
-H "Authorization: ${TOKENC}" \
-s http://localhost:3000/users/toggleAdmin/admin@nml.nml
|
Example 7. curlit2.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/usr/bin/env sh
TOKEN=`curl -s -d email=$1 -d password=$APIPWDQ http://localhost:3000/users/login`
TOKENC=$(echo "$TOKEN" | sed 's/"//g')
curl -H "Authorization: ${TOKENC}" \
-d "name=$2" \
-d "population=$3" \
-d "district=$4" \
-d "countrycode=$5" \
-s http://localhost:3000/city
echo ""
curl -s http://localhost:3000/continents
echo ""
curl -H "Authorization: ${TOKENC}" \
-s http://localhost:3000/cities/DNK
echo ""
|
Issues
Let me quote your textbook from chapter 3:
Issue Tracking
One of the key characteristics of free and open source software projects is
that they are just that: projects. As projects, some form of project management
is usually required to make sure all development proceeds smoothly. One of
the most important of these is the issue tracker.
Issue tracking, bug tracking, ticketing system… Different terms but all the
same concept: an issue tracker is where a project tracks individual issues in
the project. Yeah, I know, with functionality like that, how did they ever come
up with the name “issue tracker?” It’s a mystery. Jokes aside, issue trackers
are vital for making sure the project knows what is going on, when, and
by whom.
The features of issue trackers vary by tracker provider, and many projects
don’t even use all of the features available. Some projects use the tracker
solely for logging bugs in the software. Others use it for bug tracking, feature
requests, support questions, design discussions, team conversations and
debates… It all depends on the needs and workflow of the project.
The only wrong way to use a project’s issue tracker is “anything different from
how the project uses it.” Don’t inject your own preferences or workflow into
a project’s issue tracker. Sometimes a project documents its issue workflow.
If it does, follow it. If it doesn’t, have a look at completed (“closed”) issues to
see which workflow was used for them. As always: ask the community if you
have any questions or even just to verify your assumptions. It’s better to ask
now than to do the wrong thing and make a lot more work for you and for
the community.
And later, in chapter 5, as part of your reviewing a project
and planning what your contribution should be:
Review the Issue Tracker
If you didn’t already do so in the prior chapter, invest some time to review
the issue or bug tracker for your chosen project (see Find a Project for more
information about the issue tracker). It’s an amazing resource for learning
what a project has done in the past, what it’s currently trying to accomplish,
what it’s looking to do in the future, and just as importantly as all those:
what it’s decided it doesn’t need to do at all.
Regardless of whether the project tags its issues as suitable for a new contrib-
utor, reviewing the open issues in its issue tracker can lead you to a number
of potential contributions. As you skim the issues, look for those that are
interesting in some way. Are bugs reported that have bitten you in the past?
Maybe there are issues that were opened but have no activity yet, or issues
marked as needing work but are not yet assigned to nor claimed by anyone.
Picking up tasks that no one else has had the time to do can be a great way
to make your mark in a community.
Clone and Branch, re chapter 5
Here is my adaptation of a figure from
(Brasseur, V. M., 2018, chapter 5, p 59)

It gives a practical, high level guide to how the workflow
of an active FOSS’er plays out.
Exercises
Based on the brief talk today, and chapters 3 through 5
of Vicky’s book, do either OSD.4.0 or OSD.4.1, not both
and OSD.4.2. I encourage you to work in groups of 2 or
3 students. You will learn more.
We expect you to work with these exercises for hands on
experience through the next two weeks. That is we don’t
expect submissions next week, but at the end of the
following week.
Exercise OSD.4.0
Given my repo at https://codeberg.org/arosano/exercises_osd0.git
- Clone it
- Test it thoroughly, not as in unit testing but as
a general test to see how it works, and find things
you would like to be different, and/or better
- Based on the previous, submit at least one issue
on the project’s page
- Improve the README.md and submit a pull request
with your improved README
Exercise OSD.4.1
Given my repo at https://codeberg.org/arosano/exercises_osd1.git
- Clone it
- Test it thoroughly, not as in unit testing but as
a general test to see how it works, and find things
you would like to be different, and/or better
- Based on the previous, submit at least one issue
on the project’s page
- Improve the README.md and submit a pull request
with your improved README. Choose either a JS, or
a Python angle, not both
Exercise OSD.4.2
Given my repo at https://codeberg.org/arosano/restapi_done.git,
the one you have worked with for today.
- Clone it
- Compare it to your solution to todays submission
- Test it thoroughly, not as in unit testing but as
a general test to see how it works, and find things
you would like to be different, and/or better
- Based on the previous, submit at least one issue
on the project’s page. We shall be happy to see
this based on what you did yourself
- Write a README.md and add a LICENSE
- submit a pull request with all your suggested
changes