Node.js Unit Tests - Use Mocha and Chai to create a Roman Number Library Pt.4

In Pt.3 we implemented the conversion from Hindu-Arabic numbers to Roman numbers.
In this final chapter, we will complete our library by adding the conversion from Roman numbers to Hindu-Arabic numbers.

Convert the first five Roman numbers

We will set up the test cases the way we did for the other conversions: we will check both the method toInt() and the method toString().
We will test the first five Roman numbers: ‘I’. ‘II’. ‘III’. ‘IV’ and ‘V’:

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
it('The Roman number "I" should be equal to the arabic number 1', () => {
let romannum = RomanNumber('I');
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The Roman number "II" should be equal to the arabic number 2', () => {
let romannum = RomanNumber('II');
romannum.toString().should.equal('II');
romannum.toInt().should.equal(2);
});
it('The Roman number "III" should be equal to the arabic number 3', () => {
let romannum = RomanNumber('III');
romannum.toString().should.equal('III');
romannum.toInt().should.equal(3);
});
it('The Roman number "IV" should be equal to the arabic number 4', () => {
let romannum = RomanNumber('IV');
romannum.toString().should.equal('IV');
romannum.toInt().should.equal(4);
});
it('The Roman number "V" should be equal to the arabic number 5', () => {
let romannum = RomanNumber('V');
romannum.toString().should.equal('V');
romannum.toInt().should.equal(5);
});

To pass these very first conversion tests, we just need to update the library constructor in this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
if(RomanNumber.isValidInt(val)) {
this.intVal = parseInt(val);
this.strVal = RomanNumber.intToRoman(this.intVal);
}
else if(RomanNumber.checkOnlyRomanSymbols(val)) {
this.strVal = val.toUpperCase();
let patterns = new Map();
patterns.set('I', 1);
patterns.set('II', 2);
patterns.set('III', 3);
patterns.set('IV', 4);
patterns.set('V', 5);
this.intVal = patterns.get(this.strVal);
}
else {
throw new Error('invalid value');
}
...

In the code block relative to checkOnlyRomanSymbols we used a Map structure, where we matched Roman numbers (as keys) to Hindu-Arabic numbers (as values).

All the tests passed correctly as shown below:
Test first five Roman numbers: passed

Convert Roman numbers composed of simple patterns

The next tests we are going to perform, are about ‘simple’ Roman numbers: for simple, we mean those numbers represented by the following patterns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Patterns:
*
* | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
* Ones: | I | II | III | IV | V | VI | VII | VIII | IX |
*
* | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
* Tens: | X | XX | XXX | XL | L | LX | LXX | LXXX | XC |
*
* | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 |
* Hundreds: | C | CC | CCC | CD | D | DC | DCC | DCCC | CM |
*
* | 1000 | 2000 | 3000 | - | - | - | - | - | - |
* Thousands: | M | MM | MMM | - | - | - | - | - | - |
*/

In particular we will test: ‘XXX’, ‘XL’, ‘XC’, ‘CC’, ‘CD’, ‘DCCC’ and ‘MMM’.

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
it('The Roman number "XXX" should be equal to the arabic number 30', () => {
let romannum = RomanNumber('XXX');
romannum.toString().should.equal('XXX');
romannum.toInt().should.equal(30);
});
it('The Roman number "XL" should be equal to the arabic number 40', () => {
let romannum = RomanNumber('XL');
romannum.toString().should.equal('XL');
romannum.toInt().should.equal(40);
});
it('The Roman number "XC" should be equal to the arabic number 90', () => {
let romannum = RomanNumber('XC');
romannum.toString().should.equal('XC');
romannum.toInt().should.equal(90);
});
it('The Roman number "CC" should be equal to the arabic number 200', () => {
let romannum = RomanNumber('CC');
romannum.toString().should.equal('CC');
romannum.toInt().should.equal(200);
});
it('The Roman number "CD" should be equal to the arabic number 400', () => {
let romannum = RomanNumber('CD');
romannum.toString().should.equal('CD');
romannum.toInt().should.equal(400);
});
it('The Roman number "DCCC" should be equal to the arabic number 800', () => {
let romannum = RomanNumber('DCCC');
romannum.toString().should.equal('DCCC');
romannum.toInt().should.equal(800);
});
it('The Roman number "MMM" should be equal to the arabic number 3000', () => {
let romannum = RomanNumber('MMM');
romannum.toString().should.equal('MMM');
romannum.toInt().should.equal(3000);
});

In order to pass the previous tests, we first need to update our library constructor to its final value:

1
2
3
4
5
6
7
8
9
10
11
if(RomanNumber.isValidInt(val)) {
this.intVal = parseInt(val);
this.strVal = RomanNumber.intToRoman(this.intVal);
}
else if(RomanNumber.checkOnlyRomanSymbols(val)) {
this.strVal = val.toUpperCase();
this.intVal = RomanNumber.romanToInt(this.strVal);
}
else {
throw new Error('invalid value');
}

Then we must add the final static method, named romanToInt:

1
2
3
4
5
6
7
8
9
10
11
12
13
romanNumber.romanToInt = function romanToInt(val) {
let patternsArray = [
["I", 1], ["II", 2], ["III", 3], ["IV", 4], ["V", 5], ["VI", 6], ["VII", 7], ["VIII", 8], ["IX", 9], // Ones
["X", 10], ["XX", 20], ["XXX", 30], ["XL", 40], ["L", 50], ["LX", 60], ["LXX", 70], ["LXXX", 80], ["XC", 90], // Tens
["C", 100], ["CC", 200], ["CCC", 300], ["CD", 400], ["D", 500], ["DC", 600], ["DCC", 700], ["DCCC", 800], ["CM", 900], // Hundreds
["M", 1000], ["MM", 2000], ["MMM", 3000] // Thousands
];
let patterns = new Map(patternsArray);
let finalInt = patterns.get(val);
return finalInt;
};

In this method we implemented the patterns seen previously, by using a Map structure initialized with an array of mapping values.

Check the results by yourself, you won’t be disappointed!

Convert any valid Roman number

We’re now ready to mix the patterns we’ve just set up in order to convert any valid Roman number.

This is the test-case list: “CDXXIX”, “MCDLXXXII”, “MCMLXXX”, “MMMCMXCIX”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
it('The Roman number "CDXXIX" should be equal to the arabic number 429', () => {
let romannum = RomanNumber('CDXXIX');
romannum.toString().should.equal('CDXXIX');
romannum.toInt().should.equal(429);
});
it('The Roman number "MCDLXXXII" should be equal to the arabic number 1482', () => {
let romannum = RomanNumber('MCDLXXXII');
romannum.toString().should.equal('MCDLXXXII');
romannum.toInt().should.equal(1482);
});
it('The Roman number "MCMLXXX" should be equal to the arabic number 1980', () => {
let romannum = RomanNumber('MCMLXXX');
romannum.toString().should.equal('MCMLXXX');
romannum.toInt().should.equal(1980);
});
it('The Roman number "MMMCMXCIX" should be equal to the arabic number 3999', () => {
let romannum = RomanNumber('MMMCMXCIX');
romannum.toString().should.equal('MMMCMXCIX');
romannum.toInt().should.equal(3999);
});

Here is how we will have to update the romanToInt method:

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
romanNumber.romanToInt = function romanToInt(val) {
let patternsArray = [
["I", 1], ["II", 2], ["III", 3], ["IV", 4], ["V", 5], ["VI", 6], ["VII", 7], ["VIII", 8], ["IX", 9], // Ones
["X", 10], ["XX", 20], ["XXX", 30], ["XL", 40], ["L", 50], ["LX", 60], ["LXX", 70], ["LXXX", 80], ["XC", 90], // Tens
["C", 100], ["CC", 200], ["CCC", 300], ["CD", 400], ["D", 500], ["DC", 600], ["DCC", 700], ["DCCC", 800], ["CM", 900], // Hundreds
["M", 1000], ["MM", 2000], ["MMM", 3000] // Thousands
];
let patterns = new Map(patternsArray);
let i = 0;
let finalInt = 0;
// this loop is used to read the entire Roman string
while(i < val.length) {
let tmpPattern = '';
// this loop is used to build the next pattern
while(i < val.length) {
if(tmpPattern.length == 0) {
tmpPattern += val[i++];
}
else {
let tmpPatternVal = patterns.get(tmpPattern + val[i]);
if(typeof(tmpPatternVal) !== 'undefined') {
tmpPattern += val[i++];
}
else {
break;
}
}
}
if(tmpPattern.length > 0) {
let tmpNum = patterns.get(tmpPattern);
finalInt += tmpNum;
}
}
return finalInt;
};

The first while loop represents a way to read all the string’s symbols, and the second one will be used to look for a single pattern. Any time a pattern is found, we’ll add the relative integer value to the finalInt.

Let’s finally check the tests:
$ npm test

…and here we go!
Test any valid Roman number: passed

Make our library more resilient

Now that our Roman numbers library is able to correctly convert any valid Roman number, we just want to make it better by adding the last checks for invalid cases:

  • No more that three equal consecutive symbols can be found;
  • A pattern relative to a certain set (e.g. hundreds) cannot be found after a higher or equal set (e.g. thousands or hundreds): for example the invalid Roman number “MIM” contains the pattern “M”, belonging to thousands, after “I”, belonging to ones.

Here are the very last test cases to be added inside Check exceptions:

1
2
3
4
5
6
7
it('The constructor invoked with "IIII" should return an exception: invalid value', () => {
expect(() => RomanNumber('IIII')).to.throw(Error, /^invalid value$/);
});
it('The constructor invoked with "MMMMCMXCIX" should return an exception: invalid value', () => {
expect(() => RomanNumber('MMMMCMXCIX')).to.throw(Error, /^invalid value$/);
});

and

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
it('The constructor invoked with "MMMMDMXCIX" should return an exception: invalid value', () => {
expect(() => RomanNumber('MMMMDMXCIX')).to.throw(Error, /^invalid value$/);
});
it('The constructor invoked with "MMMDMXCIX" should return an exception: invalid value', () => {
expect(() => RomanNumber('MMMDMXCIX')).to.throw(Error, /^invalid value$/);
});
it('The constructor invoked with "MIM" should return an exception: invalid value', () => {
expect(() => RomanNumber('MIM')).to.throw(Error, /^invalid value$/);
});
it('The constructor invoked with "MDCVV" should return an exception: invalid value', () => {
expect(() => RomanNumber('MDCVV')).to.throw(Error, /^invalid value$/);
});

Even though all these new test cases should throw an invalid value exception, at the current state they still don’t:
Test particular invalid Roman numbers: failed

Update the library method romanToInt to its final value and repeat the tests:

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
/**
* Static method romanToInt
* @param {Roman String} val: must be a string composed of Roman symbols only
*/
romanNumber.romanToInt = function romanToInt(val) {
let patternsArray = [
["I", 1], ["II", 2], ["III", 3], ["IV", 4], ["V", 5], ["VI", 6], ["VII", 7], ["VIII", 8], ["IX", 9], // Ones
["X", 10], ["XX", 20], ["XXX", 30], ["XL", 40], ["L", 50], ["LX", 60], ["LXX", 70], ["LXXX", 80], ["XC", 90], // Tens
["C", 100], ["CC", 200], ["CCC", 300], ["CD", 400], ["D", 500], ["DC", 600], ["DCC", 700], ["DCCC", 800], ["CM", 900], // Hundreds
["M", 1000], ["MM", 2000], ["MMM", 3000] // Thousands
];
let patterns = new Map(patternsArray);
let i = 0;
let finalInt = 0;
let numConsecutives;
let lastPatternFound = '';
let lastPatternNumFound = 0;
// this loop is used to read the entire Roman string
while(i < val.length) {
let tmpPattern = '';
// this loop is used to build the next pattern and to check for more than 3 consecutive symbols
while(i < val.length) {
// check for more than 3 consecutive symbols ////////////
if((i > 0) && (val[i] == val[i-1])) {
if(numConsecutives == 3) {
throw new Error('invalid value');
}
else {
numConsecutives++;
}
}
else {
numConsecutives = 1;
}
/////////////////////////////////////////////////////////
if(tmpPattern.length == 0) {
tmpPattern += val[i++];
}
else {
let tmpPatternVal = patterns.get(tmpPattern + val[i]);
if(typeof(tmpPatternVal) !== 'undefined') {
tmpPattern += val[i++];
}
else {
break;
}
}
}
if(tmpPattern.length > 0) {
let tmpNum = patterns.get(tmpPattern);
if(typeof(tmpNum) !== 'undefined') {
if(lastPatternFound.length > 0) {
// new pattern must belong to a lower category than the previous one
// if a previous value found was 400, the following value must be from 1 to 99
if(tmpNum.toString().length >= lastPatternNumFound.toString().length) {
throw new Error('invalid value');
}
}
lastPatternFound = tmpPattern;
lastPatternNumFound = tmpNum;
finalInt += tmpNum;
}
else {
throw new Error('invalid value');
}
}
}
return finalInt;
};

All tests passed:
Test particular invalid Roman numbers: failed

Thank you very much :)

Thank you so much for your attention and your interest towards these blog posts :)

Check out the Roman Library Repository to have a look at the final code.

Pay a visit to my web page: MyPortfolio Danilo Carrabino

Node.js Unit Tests - Use Mocha and Chai to create a Roman Number Library Pt.3

In Pt.2 we mainly focused on exception handling.

In this third part we will start converting Hindu-Arabic numbers to Roman numbers.

Convert numbers from 1 to 5

Let’s start by creating a brand new describe section in our test file:

1
2
describe('Check values', () => {
});

We will check the first 5 numbers: 1, 2, 3, 4 and 5.

For any number we want to check both the value returned by toInt (Hindu-Arabic number) and the one returned by toString (Roman Number).
For number 1, we will test also its string form ‘1’.
Let’s insert our six test cases inside our new describe section:

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
describe('Check values', () => {
it('The arabic number 1 should be equal to the Roman number "I"', () => {
let romannum = RomanNumber(1);
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The arabic number "1" should be equal to the Roman number "I"', () => {
let romannum = RomanNumber('1');
romannum.toString().should.equal('I');
romannum.toInt().should.equal(1);
});
it('The arabic number 2 should be equal to the Roman number "II"', () => {
let romannum = RomanNumber(2);
romannum.toString().should.equal('II');
romannum.toInt().should.equal(2);
});
it('The arabic number 3 should be equal to the Roman number "III"', () => {
let romannum = RomanNumber(3);
romannum.toString().should.equal('III');
romannum.toInt().should.equal(3);
});
it('The arabic number 4 should be equal to the Roman number "IV"', () => {
let romannum = RomanNumber(4);
romannum.toString().should.equal('IV');
romannum.toInt().should.equal(4);
});
it('The arabic number 5 should be equal to the Roman number "V"', () => {
let romannum = RomanNumber(5);
romannum.toString().should.equal('V');
romannum.toInt().should.equal(5);
});
});

We need a big refactor in our code. We need to add two static methods to check:

  • if a Hindu-Arabic number is valid => isValidInt
  • if a string contains only Roman symbols => checkOnlyRomanSymbols
    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
    romanNumber.isValidInt = function isValidInt(val) {
    if( (Number.isInteger(val)) ||
    ((typeof(val) === "string" || val instanceof String) && Number.isInteger(parseInt(val))) ) {
    let intVal = parseInt(val);
    if((intVal < 1) || (intVal > 3999)) {
    throw new Error('invalid range');
    }
    return true;
    }
    else {
    return false;
    }
    };
    romanNumber.checkOnlyRomanSymbols = function checkOnlyRomanSymbols(val) {
    if((typeof(val) !== "string") && !(val instanceof String)) {
    return false;
    }
    let romanSymbols = ['M', 'D', 'C', 'L', 'X', 'V', 'I'];
    for(let i = 0; i < val.length; i++) {
    if(romanSymbols.indexOf(val[i].toUpperCase()) < 0) {
    return false;
    }
    }
    return true;
    };

Then we will complete or public methods toInt and toString

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* toInt
*/
romanNumber.prototype.toInt = function toInt() {
return parseInt(this.intVal);
};
/**
* toString
*/
romanNumber.prototype.toString = function toString() {
return this.strVal;
};

In the end we will rewrite our library constructor to make use of our new methods:

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
const romanNumber = function RomanNumber(val) {
if(!new.target) {
return new RomanNumber(val);
}
if((typeof(val) === 'undefined') || (val === null) || (val === '')) {
throw new Error('value required');
}
if(RomanNumber.isValidInt(val)) {
this.intVal = parseInt(val);
if(this.intVal == 1) {
this.strVal = 'I';
}
else if(this.intVal == 2) {
this.strVal = 'II';
}
else if(this.intVal == 3) {
this.strVal = 'III';
}
else if(this.intVal == 4) {
this.strVal = 'IV';
}
else if(this.intVal == 5) {
this.strVal = 'V';
}
}
else if(RomanNumber.checkOnlyRomanSymbols(val)) {
}
else {
throw new Error('invalid value');
}
};

As you can see, inside the block of code executed when the value passed is a valid int, we added also the controls to convert the numbers from 1 to 5.

If we check now these new tests, we will have a correct result:
Test 1 to 5 values conversion: passed

Two-digit numbers

Let’s see what happens if we try to test the number 49.

1
2
3
4
5
it('The arabic number 49 should be equal to the Roman number "XLIX"', () => {
let romannum = RomanNumber(49);
romannum.toString().should.equal('XLIX');
romannum.toInt().should.equal(49);
});

As expected we have an error because we never handled any two-digit value:
Test 49 value conversion: failed

Let’s set up a separate static method to convert Hindu-Arabic numbers to Roman Numbers: intToRoman

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
romanNumber.intToRoman = function intToRoman(val) {
let intVal = parseInt(val);
let finalStr = '';
let ones = intVal % 10;
intVal = parseInt(intVal / 10);
let tens = intVal % 10;
switch(ones) {
case 1:
finalStr = 'I';
break;
case 2:
finalStr = 'II';
break;
case 3:
finalStr = 'III';
break;
case 4:
finalStr = 'IV';
break;
case 5:
finalStr = 'V';
break;
case 6:
finalStr = 'VI';
break;
case 7:
finalStr = 'VII';
break;
case 8:
finalStr = 'VIII';
break;
case 9:
finalStr = 'IX';
break;
default:
break;
}
switch(tens) {
case 1:
finalStr = 'X' + finalStr;
break;
case 2:
finalStr = 'XX' + finalStr;
break;
case 3:
finalStr = 'XXX' + finalStr;
break;
case 4:
finalStr = 'XL' + finalStr;
break;
case 5:
finalStr = 'L' + finalStr;
break;
case 6:
finalStr = 'LX' + finalStr;
break;
case 7:
finalStr = 'LXX' + finalStr;
break;
case 8:
finalStr = 'LXXX' + finalStr;
break;
case 9:
finalStr = 'XC' + finalStr;
break;
default:
break;
}
return finalStr;
};

This function computes ones and tens values from the input, and then it handles them separately to get the appropriate Roman symbols (See how the case 0 is never handled because we have no Roman symbol representing it).

Now let’s just replace the isValidInt code block in the library constructor

1
2
3
4
5
6
...
if(RomanNumber.isValidInt(val)) {
this.intVal = parseInt(val);
this.strVal = RomanNumber.intToRoman(this.intVal);
}
...

…and all the test cases will be passed!

Convert any valid Hindu-Arabic number

We’re now ready to go and convert any Hindu-Arabic number.

Add these test conversions for numbers 1968, “1473”, 2999, 3000 and 3999.

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
it('The arabic number 1968 should be equal to the Roman number "MCMLXVIII"', () => {
let romannum = RomanNumber(1968);
romannum.toString().should.equal('MCMLXVIII');
romannum.toInt().should.equal(1968);
});
it('The arabic number "1473" should be equal to the Roman number "MCDLXXIII"', () => {
let romannum = RomanNumber("1473");
romannum.toString().should.equal('MCDLXXIII');
romannum.toInt().should.equal(1473);
});
it('The arabic number 2999 should be equal to the Roman number "MMCMXCIX"', () => {
let romannum = RomanNumber(2999);
romannum.toString().should.equal('MMCMXCIX');
romannum.toInt().should.equal(2999);
});
it('The arabic number 3000 should be equal to the Roman number "MMM"', () => {
let romannum = RomanNumber(3000);
romannum.toString().should.equal('MMM');
romannum.toInt().should.equal(3000);
});
it('The arabic number 3999 should be equal to the Roman number "MMMCMXCIX"', () => {
let romannum = RomanNumber(3999);
romannum.toString().should.equal('MMMCMXCIX');
romannum.toInt().should.equal(3999);
});

We need to enhance the static method intToRoman to handle the hundreds and thousands too:

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
romanNumber.intToRoman = function intToRoman(val) {
let intVal = parseInt(val);
let finalStr = '';
let ones = intVal % 10;
intVal = parseInt(intVal / 10);
let tens = intVal % 10;
intVal = parseInt(intVal / 10);
let hundreds = intVal % 10;
intVal = parseInt(intVal / 10);
let thousands = intVal % 10;
switch(ones) {
case 1:
finalStr = 'I';
break;
case 2:
finalStr = 'II';
break;
case 3:
finalStr = 'III';
break;
case 4:
finalStr = 'IV';
break;
case 5:
finalStr = 'V';
break;
case 6:
finalStr = 'VI';
break;
case 7:
finalStr = 'VII';
break;
case 8:
finalStr = 'VIII';
break;
case 9:
finalStr = 'IX';
break;
default:
break;
}
switch(tens) {
case 1:
finalStr = 'X' + finalStr;
break;
case 2:
finalStr = 'XX' + finalStr;
break;
case 3:
finalStr = 'XXX' + finalStr;
break;
case 4:
finalStr = 'XL' + finalStr;
break;
case 5:
finalStr = 'L' + finalStr;
break;
case 6:
finalStr = 'LX' + finalStr;
break;
case 7:
finalStr = 'LXX' + finalStr;
break;
case 8:
finalStr = 'LXXX' + finalStr;
break;
case 9:
finalStr = 'XC' + finalStr;
break;
default:
break;
}
switch(hundreds) {
case 1:
finalStr = 'C' + finalStr;
break;
case 2:
finalStr = 'CC' + finalStr;
break;
case 3:
finalStr = 'CCC' + finalStr;
break;
case 4:
finalStr = 'CD' + finalStr;
break;
case 5:
finalStr = 'D' + finalStr;
break;
case 6:
finalStr = 'DC' + finalStr;
break;
case 7:
finalStr = 'DCC' + finalStr;
break;
case 8:
finalStr = 'DCCC' + finalStr;
break;
case 9:
finalStr = 'CM' + finalStr;
break;
default:
break;
}
switch(thousands) {
case 1:
finalStr = 'M' + finalStr;
break;
case 2:
finalStr = 'MM' + finalStr;
break;
case 3:
finalStr = 'MMM' + finalStr;
break;
default:
break;
}
return finalStr;
};

Now let’s check what we accomplished so far, by typing:
$ npm test

Everything works :)
Test any Hindu-Arabic number conversion: passed

Refactor intToRoman

Even though our intToRoman method performs correctly, it really needs refactoring (for style’s sake).

The idea is to group the Roman symbols, based on the ones used for ones, tens, hundreds and thousands.
In fact, the pattern used is always the same: it’s just the symbols that change.

Here is the final version of our conversion method intToRoman:

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
/**
* static method intToRoman
* @param {Integer} val: must be an integer between 1 and 3999 (even in the form '1' to '3999')
*
* The patterns for ones, tens, hundreds and thousands are the same:
* only sumbols change:
*
* Patterns:
*
* | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
* Ones: | I | II | III | IV | V | VI | VII | VIII | IX |
*
* | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
* Tens: | X | XX | XXX | XL | L | LX | LXX | LXXX | XC |
*
* | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 |
* Hundreds: | C | CC | CCC | CD | D | DC | DCC | DCCC | CM |
*
* | 1000 | 2000 | 3000 | - | - | - | - | - | - |
* Thousands: | M | MM | MMM | - | - | - | - | - | - |
*/
romanNumber.intToRoman = function intToRoman(val) {
let intVal = parseInt(val);
let onesSym = ['I', 'V', 'X'];
let tensSym = ['X', 'L', 'C'];
let hundredsSym = ['C', 'D', 'M'];
let thousandsSym = ['M', '-', '-'];
let finalStr = '';
// Retrieve units, tens, hundreds and thousands from val
for(let i = 0; i < 4; i++) {
let tmpSym;
let tmpVal;
if(i == 0) { // ones
tmpSym = onesSym;
}
else if(i == 1) { // tens
tmpSym = tensSym;
}
else if(i == 2) { // hundreds
tmpSym = hundredsSym;
}
else { // thousands
tmpSym = thousandsSym;
}
tmpVal = intVal % 10;
intVal = parseInt(intVal / 10);
switch(tmpVal) {
case 1:
finalStr = tmpSym[0] + finalStr;
break;
case 2:
finalStr = tmpSym[0] + tmpSym[0] + finalStr;
break;
case 3:
finalStr = tmpSym[0] + tmpSym[0] + tmpSym[0] + finalStr;
break;
case 4:
finalStr = tmpSym[0] + tmpSym[1] + finalStr;
break;
case 5:
finalStr = tmpSym[1] + finalStr;
break;
case 6:
finalStr = tmpSym[1] + tmpSym[0] + finalStr;
break;
case 7:
finalStr = tmpSym[1] + tmpSym[0] + tmpSym[0] + finalStr;
break;
case 8:
finalStr = tmpSym[1] + tmpSym[0] + tmpSym[0] + tmpSym[0] + finalStr;
break;
case 9:
finalStr = tmpSym[0] + tmpSym[2] + finalStr;
break;
default:
break;
}
}
return finalStr;
};

If you just relaunch the tests, you will see that any of them is correctly passed.

Even more to come (we’re going to make it)

In Pt.4 we will complete our library by adding conversion facility from Roman numbers to Hindu-Arabic numbers.

Check out the Roman Library Repository if you cannot wait for the last chapter of this blog

Node.js Unit Tests - Use Mocha and Chai to create a Roman Number Library Pt.2

In Pt.1 we introduced the concepts of Test-Driven Development, Unit Test, Roman number, and we listed the requirements to build our Roman Number library.

The second requirement: invoke library as a function (without new)

Let’s create the test for this requirement by adding this code to our testRomanNumber.js:

1
2
3
4
5
6
...
it('The constructor should be callable without new', () => {
let romanNumber = RomanNumber(1);
assert.isObject(romanNumber);
});
...

If we run this new test by typing:
$ npm test

we’ll see this error:
Test Invoke without new: error

In order to pass this test we may use the property new.target added in ECMAScript 2015 (ES6)

The new.target property lets you detect whether a function or constructor was called using the new operator. In constructors and functions instantiated with the new operator, new.target returns a reference to the constructor or function. In normal function calls, new.target is undefined.

Let’s add this piece of code inside RomanNumber constructor

1
2
3
if(!new.target) {
return new RomanNumber(val);
}

Then we run the test
$ npm test

And, as if by magic, the error disappears:
Test Invoke without new: passed

The third requirement: add toInt and toString() methods

Add two test cases for this requirement, inside the describe “Check exceptions”

1
2
3
4
5
6
7
8
9
it('The object should contain method toInt', () => {
let romanNumber = RomanNumber(1);
assert.isFunction(romanNumber.toInt);
});
it('The object should contain method toString', () => {
let romanNumber = RomanNumber(1);
assert.isFunction(romanNumber.toString);
});

These test cases are bases on the isFunction method of Chai assert interface

If we run the test with:
$ npm test

we’ll see that only the toInt() method does not exist:
Test presence of methods toInt and toString: error

This is because in Javascript every object has a toString() method

So, in order to pass the test, we just have to add this code to RomanNumber.js:

1
2
3
4
5
6
/**
* toInt
*/
romanNumber.prototype.toInt = function toInt() {
return 1;
};

Test the correct result by yourself, by typing:
$ npm test

Requirement: value passed not null or empty

We have to create three test cases to check null value, empty value, and no value.

1
2
3
4
5
6
7
8
9
10
11
it('The constructor invoked with null should return an exception: value required', () => {
expect(() => RomanNumber(null)).to.throw(Error, /^value required$/);
});
it('The constructor invoked with empty string should return an exception: value required', () => {
expect(() => RomanNumber('')).to.throw(Error, /^value required$/);
});
it('The constructor invoked with no value should return an exception: value required', () => {
expect(() => RomanNumber()).to.throw(Error, /^value required$/);
});

These test cases use Chai expect interface and the method throw.

In this way we can check that, given a particular value, the library constructor throws an Error with a particular message.

We already know that no exception will be thrown, so let’s step forward and add the code to our library constructor:

1
2
3
if((typeof(val) === 'undefined') || (val === null) || (val === '')) {
throw new Error('value required');
}

Let’s run our test
$ npm test

and… All tests passed! Very good job indeed so far!

Test for null or empty values: passed

Requirement: invalid range value

The test cases for this requirement, will be integer values not in [1,3999]

1
2
3
4
5
6
7
it('The constructor invoked with 0 should return an exception: invalid range', () => {
expect(() => RomanNumber(0)).to.throw(Error, /^invalid range$/);
});
it('The constructor invoked with 10000 should return an exception: invalid range', () => {
expect(() => RomanNumber(10000)).to.throw(Error, /^invalid range$/);
});

Even these test cases use Chai expect interface and the method throw.

As for the previous requirement we’ll skip the test part here and we’ll go to add this piece of code to our library constructor:

1
2
3
if((val < 1) || (val > 3999)) {
throw new Error('invalid range');
}

As expected: all tests passed.
Test for not in range values: passed

Invalid values

The next two tests will be about checking two types of invalid values:

  1. A String containing non Roman symbols: ‘error’
  2. A String containing Roman symbols and Hindu-Arabic symbols: ‘CD1X’

For the both of these cases, our roman number library will have to throw an Error exception with message: ‘Invalid value’

Here are the test cases to add to testRomanNumber.js

1
2
3
4
5
6
7
it('The constructor invoked with "error" should return an exception: invalid value', () => {
expect(() => RomanNumber('error')).to.throw(Error, /^invalid value$/);
});
it('The constructor invoked with "CD1X" should return an exception: invalid value', () => {
expect(() => RomanNumber('CD1X')).to.throw(Error, /^invalid value$/);
});

In order to pass these tests, we have to check that any string passed in input must contain only Roman symbols.

To add this little improvement a minor refactor must be performed on our constructor. Replace:

1
2
3
if((val < 1) || (val > 3999)) {
throw new Error('invalid range');
}

with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(Number.isInteger(val)) {
if((val < 1) || (val > 3999)) {
throw new Error('invalid range');
}
}
else if((typeof(val) === "string") || (val instanceof String)) {
let romanSymbols = ['M', 'D', 'C', 'L', 'X', 'V', 'I'];
for(let i = 0; i < val.length; i++) {
if(romanSymbols.indexOf(val[i].toUpperCase()) < 0) {
throw new Error('invalid value');
}
}
}
else {
throw new Error('invalid value');
}

If the input is a string, we make sure that any of its characters belong to our romanSymbols array.

Test the correct result by yourself, by typing:
$ npm test

Allow Hindu-Arabic numbers as String

Because we want to allow anyone using our library to pass an integer as a string (i.e. ‘1473’ => 1473), we’ll add this quick test:

1
2
3
it('The constructor invoked with "1473" should not return an exception', () => {
expect(() => RomanNumber('1473')).to.not.throw();
});

Let’s have a look at what happens if we run the test now:
Test for Integer string: error

The exception is thrown because so far we assumed that any string was supposed to be in Roman symbols only.

For this reason we have must handle also this case, when a string contains Hindu-Arabic symbols (integer).

We will transform the constructor in this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if( (Number.isInteger(val)) ||
((typeof(val) === "string" || val instanceof String) && Number.isInteger(parseInt(val))) ) {
let intVal = parseInt(val);
if((intVal < 1) || (intVal > 3999)) {
throw new Error('invalid range');
}
}
else if((typeof(val) === "string") || (val instanceof String)) {
let romanSymbols = ['M', 'D', 'C', 'L', 'X', 'V', 'I'];
for(let i = 0; i < val.length; i++) {
if(romanSymbols.indexOf(val[i].toUpperCase()) < 0) {
throw new Error('invalid value');
}
}
}
else {
throw new Error('invalid value');
}

In this way we can treat an integer string like an integer.

Let’s run the test:
$ npm test

All tests passed
Test for Integer string: error

Even more to come

In Pt.3 we will start converting numbers (from Hindu-Arabic to Roman numbers).

Check out the Roman Library Repository if you cannot wait for the last chapter of this blog

Node.js Unit Tests - Use Mocha and Chai to create a Roman Number Library Pt.1

Test-Driven Development is a software development process that relies on the repetition of a very short development cycle: Requirements are turned into very specific test cases, then the software is improved to pass the new tests.

  1. Add a test
  2. Run all tests and see if the new test fails
  3. Write the code
  4. Run tests
  5. Refactor code
  6. Repeat

Thereby the cornerstone of this process is represented by the Unit Test.

Mocha and Chai

  • In Node.js a wide used test framework is Mocha: it makes synchronous and asynchronous testing simple and straightforward, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.
  • The assertion library we will use is Chai: it has several interfaces that allow the developer to choose the most comfortable (should, expect, assert).

Roman Numbers

A Roman number represents an integer (Hindu-Arabic numerals) using a small set of symbols:

Roman Numeral Hindu-Arabic Equivalent
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

There are a few rules for writing numbers with Roman numerals.

  • Repeating a numeral up to three times represents addition of the number. For example, III represents 1 + 1 + 1 = 3. Only I, X, C, and M can be repeated; V, L, and D cannot be, and there is no need to do so.
  • Writing numerals that decrease from left to right represents addition of the numbers. For example, LX represents 50 + 10 = 60 and XVI represents 10 + 5 + 1 = 16.
  • To write a number that otherwise would take repeating of a numeral four or more times, there is a subtraction rule. Writing a smaller numeral to the left of a larger numeral represents subtraction. For example, IV represents 5 - 1 = 4 and IX represents 10 - 1 = 9. To avoid ambiguity, the only pairs of numerals that use this subtraction rule are:
Roman Numeral Hindu-Arabic Equivalent
IV 4 = 5 - 1
IX 9 = 10 - 1
XL 40 = 50 - 10
XC 90 = 100 - 10
CD 400 = 500 - 100
CM 900 = 1000 - 100

Following these rules, every Hindu-Arabic number between 1 and 3999 (MMMCMXCIX) can be represented as a Roman numeral.

Requirements

The Roman Number Library to build will have to take a value in input (a Roman Number or an Hindu-Arabic number), and will have to return an object containing two methods:

  • toInt()
  • toString()
1
2
3
4
5
6
7
8
// Example
let romanNumber1 = new RomanNumber('MMMCMXCIX'
let romanNumber2 = new RomanNumber(1
console.log(romanNumber1.toInt()); // => 3999
console.log(romanNumber1.toString()); // => 'MMMCMXCIX'
console.log(romanNumber2.toInt()); // => 1
console.log(romanNumber2.toString()); // => 'I'
  • If the value passed is null or empty, it should throw a ‘value required’ exception error (e.g. ‘throw new
    Error(‘value required’);’ ).
  • If the value passed is outside of 1 to 3999, it should throw an ‘invalid range’ exception error.
  • If the value passed is invalid, it should throw an ‘invalid value’ exception error.
  • If the library is called as a function (i.e. without the new prefix), it must still pass back a new object.

Kick-off

Let’s kick our application off!

Create the folder RomanNumber:
$ mkdir RomanNumber
$ cd RomanNumber

Initialize the application:
$ npm init

Add Mocha framework:
$ npm install --save-dev mocha

Add Chai:
$ npm install --save-dev chai

Create the empty file RomanNumber.js

Create the folder test:
$ mkdir test
$ cd test

Create and launch the first test

Under the test folder, create the file testRomanNumber.js with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Require the dev-dependencies
let chai = require('chai');
let should = chai.should();
let expect = chai.expect;
let assert = chai.assert;
const RomanNumber = require('../RomanNumber');
describe('RomanNumber', () => {
describe('Check exceptions', () => {
it('The constructor should return an object', () => {
let romanNumber = new RomanNumber(1);
assert.isObject(romanNumber);
});
});
});

In this piece of code we include Chai with its three interfaces (should, expect, assert).

Then we include RomanNumber.js (which is still empty).

describe“ is used merely for grouping test cases, which you can nest as deep.

it“ is a test case: here the very first test case is about invoking the library’s constructor let romanNumber = new RomanNumber(1) and check that it returns an object assert.isObject(romanNumber).

To launch the test, just type:
$ npm test

First test: error

We have an error stating that RomanNumber is not a constructor: this was the expected error hence our library code is still empty.

Init Library

In order to pass the test, let’s add this simple piece of code to out Roman Number library (RomanNumber.js):

1
2
3
4
const romanNumber = function RomanNumber(val) {
};
module.exports = romanNumber;

Now, let’s run the test again:
$ npm test

First test: passed

More to come

In Pt.2 we will add more tests based on the requirements we defined, and the code to pass those tests.

Check out the Roman Library Repository if you cannot wait for the last chapter of this blog

Node.js Passport and Terms of Use: sign in to MyPortfolio

In my application named MyPortfolio (built with Node.js/Express and MongoDB), I decided to use Passport as authentication middleware.

It is extremely flexible and modular, and it comes with a comprehensive set of strategies which support authentication using a username and password, Google-OAuth, Facebook, Twitter, and more.

I started developing a Passport authentication, by following a very interesting tutorial of Scotch.io, where it is clearly illustrated how to use Passport to perform local authentication, Facebook, Twitter, Google authentications, and even how to link all social accounts under one user account.

But there was something lacking in that tutorial, and many other Passport tutorials I found on the web: how could I use Passport middleware to perform a sign-up only after user viewed and accepted my application’s Terms of Use?

Let me show you the architecture, the models and the operations flow I used (check also the code here MyPortfolio code).

Package.json

Here are the dependencies I’m using in package.json:

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
// package.json
...
"dependencies": {
"async": "^2.5.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.18.2",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.3",
"dotenv": "^4.0.0",
"ejs": "^2.5.7",
"express": "^4.14.0",
"express-ejs-layouts": "^2.3.1",
"express-session": "^1.15.6",
"express-validator": "^4.2.1",
"fs-extra": "^4.0.2",
"mongoose": "^4.11.13",
"multer": "^1.3.0",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
"passport-local": "^1.0.0",
"passport-twitter": "^1.0.4",
"summernote-nodejs": "^1.0.4"
}
...

Because of I decided to let users log in only with their Google accounts in MyPortfolio, the dependencies of interest in this post are:

  • express is the framework.
  • express-session to handle the session in Express framework.
  • ejs is the templating engine.
  • mongoose is the object modeling for our MongoDB database.
  • passport and passport-google-oauth for Google authentication.
  • connect-flash allows to pass session flashdata messages.
  • async lets easily perform asynchronous operations.
  • fs-extra empowers Node.js fs library with Promises.

User model

Here is the user model schema, which resembles the one in the Scotch.io tutorial.

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
// user.js
...
var userSchema = mongoose.Schema({
local : {
email : String,
password : String,
},
facebook : {
id : String,
token : String,
email : String,
name : String
},
twitter : {
id : String,
token : String,
displayName : String,
username : String
},
google : {
id : String,
token : String,
email : {type: String, index: true},
name : {type: String, index: true},
imageUrl : String,
eslug : String
},
mustacceptterms : {
type: Boolean,
default: 'false'
}
});
...

You can notice that in the userSchema I added another boolean parameter, named mustacceptterms: this one will be true until the user will have explicitly confirmed to have read and accepted the Terms of Use.

Application setup

server.js contains the setup of the different packages, included Express, Passport, Session and Mongoose

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
// server.js
// load environment variables
require('dotenv').config();
// grab our dependencies
const express = require('express'),
app = express(),
port = process.env.PORT || 8080
expressLayouts = require('express-ejs-layouts'),
mongoose = require('mongoose'),
passport = require('passport'),
bodyParser = require('body-parser'),
session = require('express-session'),
cookieParser = require('cookie-parser'),
flash = require('connect-flash'),
expressValidator = require('express-validator');
// configure our application
// set sessions and cookie parser
app.use(cookieParser());
app.use(session({
secret: process.env.SECRET,
cookie: {maxAge: 8*60*60*1000}, // 8 hours
resave: false, // forces the session to be saved back to the store
saveUninitialized: false // don't save unmodified sessions
}));
app.use(flash());
// tell express where to look for static assets
app.use(express.static(__dirname + '/public'));
// set ejs as templating engine
app.set('view engine', 'ejs');
app.use(expressLayouts);
// connect to the database
mongoose.connect(process.env.DB_URI);
// use bodyParser to grab info from a json
app.use(bodyParser.json({limit: '50mb'}));
// use bodyParser to grab info from a form
app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
// express validator -> validate form or URL parameters
app.use(expressValidator());
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
require('./config/passport')(passport); // pass passport for configuration
// set the routes
app.use(require('./app/routes'));
// start our server
app.listen(port, () => {
console.log(`App listening on http://localhost:${port}`);
});

Passport Configuration

The passport object is passed as parameter to config/passport.js where the Google OAuth strategy will be configured:

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
// config/passport.js
// load only the Google strategy, because the login to My-Portfolio
// will be only possible by using the Google account
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
// load up the user model
var User = require('../app/models/user');
const initUserFolderStructure = require('../app/utilities').initUserFolderStructure;
module.exports = function(passport) {
// =========================================================================
// passport session setup ==================================================
// =========================================================================
// required for persistent login sessions
// passport needs ability to serialize and unserialize users out of session
// used to serialize the user for the session
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// used to deserialize the user
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
// =========================================================================
// GOOGLE ==================================================================
// =========================================================================
passport.use(new GoogleStrategy({
clientID : process.env.GOOGLEAUTH_clientID,
clientSecret : process.env.GOOGLEAUTH_clientSecret,
callbackURL : process.env.GOOGLEAUTH_callbackURL,
passReqToCallback : true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
},
...

As shown above, passport will use only the Google Oauth strategy, and the interesting part regards the callback invoked after authentication succeeds:

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
...
function(req, token, refreshToken, profile, done) {
// check if the user is already logged in
if (!req.user) {
User.findOne({ 'google.id' : profile.id }, function(err, user) {
if (err) {
return done(err);
}
if (user) {
// if there is a user id already but no token (user was linked at one point and then removed)
if (!user.google.token) {
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
if(!user.mustacceptterms) {
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
else {
return done(null, user);
}
}
// brand new user
else {
var newUser = new User();
newUser.google.id = profile.id;
newUser.google.token = token;
newUser.google.name = profile.displayName;
newUser.google.email = profile.emails[0].value; // pull the first email
newUser.google.imageUrl = profile.photos[0].value; // pull the first image
newUser.mustacceptterms = true; // the new user must accept terms of use before creating the account on MyPortfolio
newUser.save(function(err) {
if (err) {
throw err;
}
return done(null, newUser);
});
}
});
}
else {
// user already exists and is logged in, we have to link accounts
var user = req.user; // pull the user out of the session
user.google.id = profile.id;
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
}));

If a user logs in to MyPortfolio for the first time (‘A brand new user’), he will be saved to the database with the flag mustacceptterms set to true, and no folder structure will be created.

Even if the user had previously logged in without accepting the Terms of Use, he would just be considered authenticated, but the flag mustacceptterms would remain set to true.

So I added two different functions to determine if user is really logged-in, or if he’s just authenticated, and the flag mustacceptterms is the determiner.

Handling routing

The routes.js contains the authentication logic (and much more):

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
// routes.js
// create a new express Router
const express = require('express'),
router = express.Router(),
multer = require('multer'),
multerupload = multer({
dest: 'uploads/',
limits: { fieldSize: 25 * 1024 * 1024 }
}),
mainController = require('./controllers/main.controller'),
authController = require('./controllers/auth.controller'),
portfoliosController = require('./controllers/portfolios.controller');
const fs = require('fs');
// route middleware to make sure a user is logged in
function isLoggedIn(req, res, next) {
// if user is authenticated in the session and the terms of use have been accepted, pass the control to the "next()" handler
if (req.isAuthenticated() && !req.user.mustacceptterms) {
return next();
}
// otherwise redirect him to the home page
res.redirect('/');
}
// route middleware to make sure a user is temporarily logged in to accept terms of use
function isLoggedInToAcceptTerms(req, res, next) {
//console.log(req.user);
// if user is authenticated in the session and the terms of use have not been accepted, pass the control to the "next()" handler
if (req.isAuthenticated() && req.user.mustacceptterms) {
return next();
}
// otherwise redirect him to the home page
res.redirect('/');
}
// export router
module.exports = router;
// define routes
// main routes
router.get('/', mainController.showHome);
// route for showing the "Terms of Use" page
router.get('/termsofuse', mainController.showTermsOfUse);
// Google OAuth authentication
router.get('/auth/google', passport.authenticate('google', { scope : ['profile', 'email'] }));
// the callback after Google has authenticated the user
router.get('/auth/google/callback',
passport.authenticate('google', {failureRedirect : '/'}), // on failure Google authentication
function(req, res) { // on correct Google authentication
// User must accept the terms of use before creating the account
if(req.user.mustacceptterms) {
fs.readFile('./public/assets/termsOfUse.html', (err, data) => {
if (err) throw err;
res.render('pages/acceptTermsOfUse', {
user : req.user,
termscontent: data
});
});
}
// User has already accepted the terms of use
else {
res.redirect('/portfolios/editPortfolioInfos');
}
});
// route for showing the profile page
router.get('/account', isLoggedIn, authController.showAccount);
// route for handling the terms of use acceptance
router.post('/accepttermsofuse', isLoggedInToAcceptTerms, authController.processAcceptTermsOfUse);
...

The utility function isLoggedIn determines if a user is logged in the application.

The utility function isLoggedInToAcceptTerms determines if a user is just authenticated (but still never accepted the Terms of Use).

The route ‘/auth/google’ uses passport authentication method (which was previously configured with the Google Oauth2 Strategy) to authenticate the user with his Google account.

The magic happens for the route ‘/auth/google/callback’, which represents the callback invoked after the Google authentication procedure. I used a ‘failureRedirect’ to the home page when the authentication fails, and a Custom function on correct Google authentication.

Thanks to this custom function I can determine whether the Terms of Use haven’t been previously accepted, and in that case redirect the user to the Terms of Use page: from there the user will have the chance to read and accept the Terms and complete the sign-in process.