Skip to content

Commit

Permalink
use test-blurts db with fixture data for db tests
Browse files Browse the repository at this point in the history
cover "public" DB methods with tests
  • Loading branch information
groovecoder committed Aug 8, 2018
1 parent f806dd1 commit 4be9673
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ node_js:
services:
- postgresql
env:
- TESTING_ENVIRONMENT=1
- NODE_ENV=tests
before_script:
- cp .env-dist .env
- psql -c 'create database blurts;' -U postgres
- createdb test-blurts
45 changes: 30 additions & 15 deletions db/DB.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const AppConstants = require("../app-constants");

// eslint-disable-next-line node/no-extraneous-require
const uuidv4 = require("uuid/v4");
const Knex = require("knex");
Expand All @@ -8,22 +10,29 @@ const knexConfig = require("./knexfile");
const HIBP = require("../hibp");
const getSha1 = require("../sha1-utils");

const knex = Knex(knexConfig);
const knex = Knex(knexConfig[AppConstants.NODE_ENV]);


const DB = {
async getSubscriberByEmailAndToken(email, token) {
const res = await knex("subscribers")
.where("verification_token", "=", token)
.andWhere("email", "=", email);

return res[0];
},

async addSubscriberUnverifiedEmailHash(email) {
const res = await knex("subscribers").insert(
{ email: email, sha1: getSha1(email), verification_token: uuidv4(), verified: false }
).returning("verification_token");
).returning("*");
return res[0];
},

async verifyEmailHash(token, email) {
const emailHash = await knex("subscribers")
.where("verification_token", "=", token)
.andWhere("email", "=", email);
return await this.verifySubscriber(emailHash[0]);
const unverifiedSubscriber = await this.getSubscriberByEmailAndToken(email, token);
const verifiedSubscriber = await this._verifySubscriber(unverifiedSubscriber);
return verifiedSubscriber[0];
},

// Used internally, ideally should not be called by consumers.
Expand Down Expand Up @@ -63,33 +72,39 @@ const DB = {

async addSubscriber(email) {
const emailHash = await this._addEmailHash(getSha1(email), email, true);
return this.verifySubscriber(emailHash);
const verifiedSubscriber = await this._verifySubscriber(emailHash);
return verifiedSubscriber[0];
},

async verifySubscriber(emailHash) {
async _verifySubscriber(emailHash) {
await HIBP.subscribeHash(emailHash.sha1);
return await knex("subscribers")
.where("verification_token", "=", emailHash.verification_token)
const verifiedSubscriber = await knex("subscribers")
.where("email", "=", emailHash.email)
.update({ verified: true })
.returning("*");
return verifiedSubscriber;
},

async removeSubscriber(email) {
const sha1 = getSha1(email);

return await this._getSha1EntryAndDo(sha1, async aEntry => {
// Patch out the email from the entry.
return await aEntry
.$query()
.patch({ email: null })
.returning("*"); // Postgres trick to return the updated row as model.
const removedSubscriber = await knex("subscribers")
.update({ email: null })
.where("id", "=", aEntry.id)
.returning("*");
return removedSubscriber[0];
});
},

async getSubscribersByHashes(hashes) {
return await knex("subscribers").whereIn("sha1", hashes).andWhere("verified", "=", true);
},

async destroyConnection() {
await knex.destroy();
},

};

module.exports = DB;
18 changes: 14 additions & 4 deletions db/knexfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@

const AppConstants = require("../app-constants");

module.exports = {

// For runtime, use DATABASE_URL
const RUNTIME_CONFIG = {
client: "postgresql",
connection: AppConstants.DATABASE_URL,
pool: {
min: 2,
max: 10,
};
// For tests, use test-DATABASE
const TEST_DATABASE_URL = AppConstants.DATABASE_URL.replace(/\/(\w*)$/, "/test-$1");

module.exports = {
dev: RUNTIME_CONFIG,
stage: RUNTIME_CONFIG,
prod: RUNTIME_CONFIG,
tests: {
client: "postgresql",
connection: TEST_DATABASE_URL,
},
};
32 changes: 32 additions & 0 deletions db/seeds/test_subscribers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use strict";

const getSha1 = require("../../sha1-utils");


exports.seed = function(knex) {
// Deletes ALL existing entries
return knex("subscribers").del()
.then(function () {
// Inserts seed entries
return knex("subscribers").insert([
{
sha1: getSha1("firefoxaccount@test.com"),
email: "firefoxaccount@test.com",
verification_token: "",
verified: true,
},
{
sha1: getSha1("unverifiedemail@test.com"),
email: "unverifiedemail@test.com",
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf0",
verified: false,
},
{
sha1: getSha1("verifiedemail@test.com"),
email: "verifiedemail@test.com",
verification_token: "54010800-6c3c-4186-971a-76dc92874941",
verified: true,
},
]);
});
};
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@
"uuid": "^3.2.1"
},
"devDependencies": {
"blue-tape": "^1.0.0",
"coveralls": "^3.0.1",
"eslint": "^4.18.1",
"eslint-plugin-node": "^6.0.1",
"faucet": "0.0.1",
"faucet": "^0.0.1",
"htmllint-cli": "^0.0.6",
"npm-run-all": "^4.1.3",
"nsp": "^3.2.1",
"nyc": "^11.8.0",
"stylelint": "^9.2.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.9.1"
"tape": "^4.9.1",
"tape-async": "^2.3.0",
"tape-promise": "^3.0.0"
},
"engines": {
"node": ">=8"
Expand All @@ -65,6 +68,9 @@
"pretest": "npm run lint",
"get-hashsets": "node scripts/get-hashsets",
"start": "node server.js",
"test": "nyc tape tests/**/test*.js | faucet && nyc report --reporter=text-lcov | coveralls"
"test:db:migrate": "knex migrate:latest --knexfile db/knexfile.js --env tests",
"test:db:seed": "knex seed:run --knexfile db/knexfile.js --env tests",
"test:tests": "NODE_ENV=tests nyc tape tests/**/test*.js | faucet && nyc report --reporter=text-lcov | coveralls",
"test": "run-s test:db:migrate test:db:seed test:tests"
}
}
7 changes: 3 additions & 4 deletions scripts/make-sha1-hashes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

const crypto = require("crypto");
const getSha1 = require("../sha1-utils");
const stdin = process.openStdin();

const PROMPT = "\nEnter an email address to get the SHA1 hash as it would appear in a HIBP hashset file:";
Expand All @@ -9,8 +9,7 @@ console.log(PROMPT);

stdin.addListener("data", data => {
const trimmedString = data.toString().trim();
const shasum = crypto.createHash("sha1");
shasum.update(trimmedString.toLowerCase());
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${shasum.digest("hex")}`);
const sha1 = getSha1(trimmedString);
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${sha1}`);
console.log(PROMPT);
});
2 changes: 0 additions & 2 deletions tests/fixtures/TestBreach.txt

This file was deleted.

Binary file removed tests/fixtures/TestBreach.zip
Binary file not shown.
73 changes: 73 additions & 0 deletions tests/test-db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use strict";

const test = require("tape-async");

const DB = require("../db/DB");
const getSha1 = require("../sha1-utils");

test("getSubscriberByEmailAndToken accepts email and token and returns subscriber", async t => {
const testEmail = "unverifiedemail@test.com";
const testToken = "0e2cb147-2041-4e5b-8ca9-494e773b2cf0";
const subscriber = await DB.getSubscriberByEmailAndToken(testEmail, testToken);

t.ok(subscriber.email === testEmail);
t.ok(subscriber.verification_token === testToken);
});

test("getSubscribersByHashes accepts hashes and only returns verified subscribers", async t => {
const testHashes = [
"firefoxaccount@test.com",
"unverifiedemail@test.com",
"verifiedemail@test.com",
].map(email => getSha1(email));
const subscribers = await DB.getSubscribersByHashes(testHashes);
for (const subscriber of subscribers) {
t.ok(subscriber.verified);
}
});

test("addSubscriberUnverifiedEmailHash accepts email and returns unverified subscriber with sha1 hash and verification token", async t => {
const testEmail = "test@test.com";
// https://stackoverflow.com/a/13653180
const uuidRE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

const subscriber = await DB.addSubscriberUnverifiedEmailHash(testEmail);
t.ok(subscriber.sha1 === getSha1(testEmail));
t.ok(uuidRE.test(subscriber.verification_token));
t.notOk(subscriber.verified);
});

test("verifyEmailHash accepts token and email and returns verified subscriber", async t => {
const testEmail = "verifyEmailHash@test.com";

const unverifiedSubscriber = await DB.addSubscriberUnverifiedEmailHash(testEmail);
t.notOk(unverifiedSubscriber.verified);

const verifiedSubscriber = await DB.verifyEmailHash(unverifiedSubscriber.verification_token, unverifiedSubscriber.email);
t.ok(verifiedSubscriber.sha1 === getSha1(testEmail));
t.ok(verifiedSubscriber.verified);
});

test("addSubscriber accepts email and returns verified subscriber", async t => {
const testEmail = "newFirefoxAccount@test.com";

const verifiedSubscriber = await DB.addSubscriber(testEmail);

t.ok(verifiedSubscriber.email === testEmail);
t.ok(verifiedSubscriber.verified);
t.ok(verifiedSubscriber.sha1 === getSha1(testEmail));
});

test("removeSubscriber accepts email and removes the email address", async t => {
const testEmail = "removingFirefoxAccount@test.com";

const verifiedSubscriber = await DB.addSubscriber(testEmail);
const removedSubscriber = await DB.removeSubscriber(verifiedSubscriber.email);
console.log("removedSubscriber: ", removedSubscriber);

t.ok(removedSubscriber.email === null);
});

test("teardown", async t => {
DB.destroyConnection();
});
14 changes: 10 additions & 4 deletions tests/test-sha1.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
"use strict";

const test = require("tape");
const test = require("tape-async");
const getSha1 = require("../sha1-utils");


function isHexString(hashDigest) {
for (const character of hashDigest) {
if (parseInt(character, 16).toString(16) != character.toLowerCase()) {
if (parseInt(character, 16).toString(16) !== character.toLowerCase()) {
return false;
}
}
return true;
}

test("getSha1 returns hex digest", function (t) {
t.plan(1);
test("getSha1 returns hex digest", t => {
t.ok(isHexString(getSha1("test@test.com")));
t.end();
});

test("tape-async example", async t => {
t.ok(isHexString(getSha1("test@test.com")));
const a = await Promise.resolve(42);
t.equal(a, 42);
});

0 comments on commit 4be9673

Please sign in to comment.