NML Says

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)

Schematic Clone and Branch

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