use test-blurts db with fixture data for db tests

cover "public" DB methods with tests
This commit is contained in:
Luke Crouch 2018-08-07 11:03:38 -05:00
Родитель f806dd12ee
Коммит 4be9673dfe
11 изменённых файлов: 210 добавлений и 34 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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");
@ -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.
@ -63,26 +72,28 @@ 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];
});
},
@ -90,6 +101,10 @@ const DB = {
return await knex("subscribers").whereIn("sha1", hashes).andWhere("verified", "=", true);
},
async destroyConnection() {
await knex.destroy();
},
};
module.exports = DB;

Просмотреть файл

@ -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,
},
};

Просмотреть файл

@ -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
package-lock.json сгенерированный
Просмотреть файл

@ -435,6 +435,15 @@
"tweetnacl": "0.14.5"
}
},
"blue-tape": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/blue-tape/-/blue-tape-1.0.0.tgz",
"integrity": "sha1-dYHQTAc5XJXEJrLtbR7bRUp2uSs=",
"dev": true,
"requires": {
"tape": "4.9.1"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@ -3177,6 +3186,12 @@
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"is-generator": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz",
"integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=",
"dev": true
},
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
@ -9198,6 +9213,28 @@
}
}
},
"tape-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tape-async/-/tape-async-2.3.0.tgz",
"integrity": "sha1-F1nHLm1TrF2OW4WYJygYrMVKiH0=",
"dev": true,
"requires": {
"co": "4.6.0",
"is-generator": "1.0.3",
"is-promise": "2.1.0",
"tape": "4.9.1"
}
},
"tape-promise": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tape-promise/-/tape-promise-3.0.0.tgz",
"integrity": "sha512-3LZy5ieZleqn9PbZgr2Aqkw5ikxdJT5DhnNxWH8tCL6yzNQyv0jPZH8nkpboTPDRO5COYmYbjKhn9Fl2GchOKg==",
"dev": true,
"requires": {
"is-promise": "2.1.0",
"onetime": "2.0.1"
}
},
"tarn": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.4.tgz",

Просмотреть файл

@ -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"
@ -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"
}
}

Просмотреть файл

@ -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:";
@ -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
tests/fixtures/TestBreach.txt поставляемый
Просмотреть файл

@ -1,2 +0,0 @@
69020cadd01494505872663c38151bd6ffb8bbb6
12eeb998fe0c7600b3a3aacf518aa760b1dbd775

Двоичные данные
tests/fixtures/TestBreach.zip поставляемый

Двоичный файл не отображается.

73
tests/test-db.js Normal file
Просмотреть файл

@ -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();
});

Просмотреть файл

@ -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);
});