Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New/user static tests #1

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
130 changes: 101 additions & 29 deletions server/data_sources/models/user/tests/user-static_methods.test.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,132 @@
const f = require("faker");
const { testError } = require("../../../../test_utils");
const staticMethods = require("../user-static_methods");

const mockUser = {
id: f.random.number(),
email: f.internet.email(),
plainPassword: f.internet.password(),
};

describe("User Model: static methods", () => {
test("hashPassword(): hashes a plain text password", async () => {
const password = "some password";
const hashedPassword = await staticMethods.hashPassword(password);
expect(hashedPassword).not.toEqual(password);
});

describe("verifyPassword(): validates the password", () => {
const User = { verifyPassword: staticMethods.verifyPassword };

test("throws an error if the length is < 6", () => {
try {
User.verifyPassword("abc");
} catch (error) {
expect(error.message).toBe("Password must be at least 6 characters");
}
});

test("throws an error if there are no uppercase characters", () => {
try {
User.verifyPassword("abcdefg");
} catch (error) {
expect(error.message).toBe(
"Password must include one uppercase character",
);
}
});

test("throws an error if there are no numeric characters", () => {
try {
User.verifyPassword("Abcdefg");
} catch (error) {
expect(error.message).toBe(
"Password must include one numeric character",
);
}
});

test("throws an error if there are no non-alphanumeric characters", () => {
try {
User.verifyPassword("Abcdefg5");
} catch (error) {
expect(error.message).toBe(
"Password must include one non-alphanumeric character",
);
}
});
});

test("verifyAndHashPassword(): composes verifyPassword() and hashPassword()", async () => {
const User = {
verifyPassword: () => {},
hashPassword: () => {},
verifyAndHashPassword: staticMethods.verifyAndHashPassword,
};

const password = "password";
const verifySpy = jest.spyOn(User, "verifyPassword");
const verifyHash = jest.spyOn(User, "hashPassword");

await User.verifyAndHashPassword(password);
expect(verifySpy).toHaveBeenCalledWith(password);
expect(verifyHash).toHaveBeenCalledWith(password);
});

describe("register(): registers a new Chingu User", () => {
test("Creates a new user given valid input", async () => {
const hashedPassword = `${mockUser.plainPassword}55`;
const input = {
email: f.internet.email(),
plainPassword: f.internet.password(),
};

test("Creates a new user given valid input", async () => {
const hashedPassword = `${input.password}55`;
const User = {
create: ({ email, password }) => ({
id: mockUser.id,
email,
password,
}),
create: () => {},
count: () => 0,
verifyAndHashPassword: password => hashedPassword,
register: staticMethods.register,
};

const newUser = await User.register({
email: mockUser.email,
password: mockUser.plainPassword,
});
const createSpy = jest.spyOn(User, "create");

const expected = {
id: mockUser.id,
email: mockUser.email,
await User.register(input);
expect(createSpy).toHaveBeenCalledWith({
email: input.email,
password: hashedPassword,
};

expect(newUser).toEqual(expected);
});
});

test("Throws a UserInputError Error when given an email that is already registered", async () => {
const User = {
count: () => 1,
register: staticMethods.register
register: staticMethods.register,
};

try {
await User.register(input);
} catch (error) {
testError({
error,
errorType: "UserInputError",
message: "A user with this email already exists",
invalidArgs: ["input.email"],
});
}
});

test("Throws a UserInputError Error when given an invalid password", async () => {
const { register, verifyPassword, verifyAndHashPassword } = staticMethods;
const User = {
count: () => 0,
register,
verifyPassword,
verifyAndHashPassword,
};

try {
await User.register({ email: mockUser.email, password: mockUser.plainPassword });
} catch(error) {
expect(error.constructor.name).toBe('UserInputError');
expect(error.message).toBe('A user with this email already exists');
expect(error.invalidArgs).toContain('input.email');
await User.register({ email: input.email, password: "abc" });
} catch (error) {
testError({
error,
errorType: "UserInputError",
message: "Password must be at least 6 characters",
invalidArgs: ["input.password"],
});
}
});
});
Expand Down
42 changes: 26 additions & 16 deletions server/data_sources/models/user/user-static_methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,28 @@ const { UserInputError } = require("apollo-server-express");
const SALT_ROUNDS = 12;

/**
* Hash a password
* Hashes a plain text password
* @param {string} password A plain text password
*/
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
};
}

/**
* Verify a password meets the requirements
* Verify a password meets the requirements:
* - 6 character minimum
* - one uppercase
* - one numeric
* - one non-alphanumeric
* @param {string} password A plain text password
* @throws {Error} if password fails validation
*/
function verifyPassword(password) {
if(password.length < 6) {
if (password.length < 6) {
throw new Error("Password must be at least 6 characters");
}

if(!/[A-Z]+/.test(password)) {
if (!/[A-Z]+/.test(password)) {
throw new Error("Password must include one uppercase character");
}

Expand All @@ -36,22 +41,24 @@ function verifyPassword(password) {

/**
* Verifies the validity of the given password through verifyPassword()
* Continues to hashing password if validation passes
* Proceeds to hashing password if validation passes
* @param {string} password A plain text text password
* @return {string} A validated and hashed password
*/
function verifyAndHashPassword(password) {
verifyPassword(password);
async function verifyAndHashPassword(password) {
this.verifyPassword(password);
return this.hashPassword(password);
}

/**
* Registers a new user
* @param {Object} input - User registration input
* @param {Object} input - UserRegistration input
* @param {string} input.email - A unique email
* @param {string} input.password - The new password
* @throws UserInputError [email: not unique, password: invalid]
* @return {object} A newly created User instance
*/
async function register (input) {
async function register(input) {
const { email, password } = input;
const existingUser = await this.count({ where: { email } });
if (existingUser) {
Expand All @@ -62,17 +69,20 @@ async function register (input) {

let hashedPassword;
try {
hashedPassword = await this.verifyAndHashPassword(password);
} catch(passwordError) {
throw new UserInputError(passwordError.message, { invalidArgs: ["input.password"] });
hashedPassword = await this.verifyAndHashPassword(password);
} catch (passwordError) {
throw new UserInputError(passwordError.message, {
invalidArgs: ["input.password"],
});
}

return this.create({ email, password: hashedPassword });
};
}

module.exports = {
verifyAndHashPassword,
hashPassword,
register,
hashPassword,
verifyPassword,
verifyAndHashPassword,
constants: { SALT_ROUNDS },
};
22 changes: 22 additions & 0 deletions server/test_utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Tests the existence and shape of a thrown Error
* @param {object} options Options to test for
* @param {Error} options.error The Error object to test
* @param {string} options.errorType The Type of Error (constructor name)
* @param {string} options.message The expected Error message
* @param {[string]} options.invalidArgs An Array of invalid arguments [Apollo Errors only]
*/
function testError(options) {
const { error, errorType = "Error", message, invalidArgs } = options;

expect(error).toBeDefined();
expect(error.constructor.name).toBe(errorType);
expect(error.message).toBe(message);
if (invalidArgs) {
expect(error.invalidArgs).toEqual(expect.arrayContaining(invalidArgs));
}
}

module.exports = {
testError,
};