Question

I'm using BCrypt to encrypt passwords, and I have a model that I'm trying to write tests for. I have made a YAML file with some basic test data in it, but I can't seem to get the code right.

My Account model:

package models;

import javax.persistence.*;
import org.mindrot.jbcrypt.BCrypt;
import play.data.validation.Constraints.Required;
import play.db.ebean.Model;
import play.db.ebean.Model.Finder;

@Entity
public class Account extends Model {

    @Id
    public Long id;
    public String email;
    public String username;
    public String passwordhash;

    public Account(String email, String username, String password) {
        this.email = email;
        this.username = username;
        this.passwordhash = BCrypt.hashpw(password, BCrypt.gensalt()); 
    }

    public static Finder<Long, Account> find = new Finder<Long, Account>(Long.class, Account.class);

    public static Account create(
            String username, String email, String password, 
            String confirmPassword
    ) {
        Account account = new Account(username, email, password);
        account.save();
        return account;
    }

    public static Account authenticate (String username, String password) {
        Account account = Account.find.where().eq("username", username).findUnique();
        if (account != null && BCrypt.checkpw(password, account.passwordhash)) {
            return account;
        } else {
            return null;
        }
    }

}

My Model Test:

package ModelTests;

import models.*;
import org.junit.*;
import com.avaje.ebean.Ebean;
import static org.junit.Assert.*;
import play.libs.Yaml;
import play.test.WithApplication;
import static play.test.Helpers.*;

import java.util.List;

public class AccountTest extends WithApplication {

    @Before
    public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
        Ebean.save((List) Yaml.load("test-data.yml"));
    }

    @Test
    public void authentication() {
        assertNotNull(Account.authenticate("palug", "Oxford4EVA"));
        assertNotNull(Account.authenticate("isthatastrawhat", "Supernatural"));
        assertNull(Account.authenticate("MelvIsntNormal", "meaplesss"));
        assertNull(Account.authenticate("SomeStupidHacker", "dogewow"));
    }

}

And my test data YAML:

# Accounts

- &melvin !!models.Account
    email: mxtcapps@maxtrace.co.uk
    username: MelvIsntNormal
    password: HashedPassword

- &emma !!models.Account
    email: isthatastrawhat@maxtrace.co.uk
    username: isthatastrawhat
    password: Supernatural

- &paul !!models.Account
    email: palug@maxtrace.co.uk
    username: palug
    password: Oxford4EVA

When I run the test, I get this error:

[error] Test ModelTests.AccountTest.authentication failed: org.yaml.snakeyaml.constructor.ConstructorException: null; Can't construct a java object for tag:yaml.org,2002:models.Account; exception=Cannot create property=password for JavaBean=models.Account@1c8d2d9; Unable to find property 'password' on class: models.Account;  in 'reader', line 3, column 3:
[error]     - &melvin !!models.Account
[error]       ^
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:333)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.constructSequenceStep2(BaseConstructor.java:276)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.constructSequence(BaseConstructor.java:247)
[error]     at org.yaml.snakeyaml.constructor.SafeConstructor$ConstructYamlSeq.construct(SafeConstructor.java:442)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:182)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:141)
[error]     at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:127)
[error]     at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:481)
[error]     at org.yaml.snakeyaml.Yaml.load(Yaml.java:412)
[error]     at play.libs.Yaml.load(Yaml.java:31)
[error]     at play.libs.Yaml.load(Yaml.java:18)
[error]     at ModelTests.AccountTest.setUp(AccountTest.java:18)
[error]     ...
[error] Caused by: org.yaml.snakeyaml.error.YAMLException: Cannot create property=password for JavaBean=models.Account@1c8d2d9; Unable to find property 'password' on class: models.Account
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:299)
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:189)
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
[error]     ... 48 more
[error] Caused by: org.yaml.snakeyaml.error.YAMLException: Unable to find property 'password' on class: models.Account
[error]     at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:132)
[error]     at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:121)
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.getProperty(Constructor.java:308)
[error]     at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:240)
[error]     ... 50 more
[info] ModelTests.AccountTest
[info] x authentication
[info] 
[info] 
[info] Total for test ModelTests.AccountTest
[info] Finished in 0.039 seconds
[info] 1 tests, 1 failures, 0 errors
[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error]     ModelTests.AccountTest
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 26 s, completed 01-Feb-2014 07:59:02

I understand the error is caused by the fact the the password field does not exist. However, I'm not quite sure how to go about getting the tests to invoke the Account method so the passwords would be encrypted. Can anyone help with this?

QUESTION ANSWER (Thanks @Joe)

I had to add a setter method for the password field:

public void setPassword(String password) {
        this.passwordhash = BCrypt.hashpw(password, BCrypt.gensalt());
}

My Account model now looks like this: package models;

import javax.persistence.*;
import org.mindrot.jbcrypt.BCrypt;
import play.data.validation.Constraints.Required;
import play.db.ebean.Model;
import play.db.ebean.Model.Finder;

@Entity
public class Account extends Model {

    @Id
    public Long id;
    public String email;
    public String username;
    public String passwordhash;

    public Account(String email, String username, String password) {
        this.email = email;
        this.username = username;
        this.passwordhash = BCrypt.hashpw(password, BCrypt.gensalt()); 
    }

    public static Finder<Long, Account> find = new Finder<Long, Account>(Long.class, Account.class);

    public static Account create(
            String username, String email, String password, 
            String confirmPassword
    ) {
        Account account = new Account(username, email, password);
        account.save();
        return account;
    }

    public void setPassword(String password) {
        this.passwordhash = BCrypt.hashpw(password, BCrypt.gensalt());
    }

    public static Account authenticate (String username, String password) {
        Account account = Account.find.where().eq("username", username).findUnique();
        if (account != null && BCrypt.checkpw(password, account.passwordhash)) {
            return account;
        } else {
            return null;
        }
    }

}
Was it helpful?

Solution

The error indicates that your models.Account class does not have a password property. SnakeYAML is looking for either a public field called password or a JavaBeans setter method called setPassword:

By default standard JavaBean properties and public fields are included.

It is finding neither:

[error] Caused by: org.yaml.snakeyaml.error.YAMLException: Unable to find property 'password' on class: models.Account

Instead, it has a passwordHash field. This suggests two possibilities:

  1. Define passwordHash in your test data.
  2. Define a setPassword(String) method on models.Account that will hash the password and store it in passwordHash.

A test should do one thing, so I'd suggest the first. You're testing authentication against a known password. Defining the hash as part of your test means you can see the hash of the known password in your test data, and avoid tests that pass in the face of bugs where (say) the hashpw method always returns the empty string.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top