use test-blurts db with fixture data for db tests
cover "public" DB methods with tests
This commit is contained in:
Родитель
f806dd12ee
Коммит
4be9673dfe
|
@ -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
db/DB.js
45
db/DB.js
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
|
@ -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",
|
||||
|
|
12
package.json
12
package.json
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
69020cadd01494505872663c38151bd6ffb8bbb6
|
||||
12eeb998fe0c7600b3a3aacf518aa760b1dbd775
|
Двоичный файл не отображается.
|
@ -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);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче