diff --git a/.gitignore b/.gitignore index 5e74ac8..7286cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /log myapp.conf hypnotoad.pid +*.db +*~ diff --git a/cpanfile b/cpanfile index 53e03fe..f41ce5c 100644 --- a/cpanfile +++ b/cpanfile @@ -2,3 +2,6 @@ requires 'Mojolicious::Lite'; requires 'Data::UUID'; requires 'Devel::Dwarn'; requires 'Mojo::JSON'; +requires 'Email::Valid'; +requires 'ORM::Date'; +requires 'Authen::Passphrase::BlowfishCrypt'; diff --git a/dropschema.sql b/dropschema.sql new file mode 100644 index 0000000..5e90f8c --- /dev/null +++ b/dropschema.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS Tokens; +DROP TABLE IF EXISTS Transactions; +DROP TABLE IF EXISTS Users; +DROP TABLE IF EXISTS Customers; +DROP TABLE IF EXISTS AgeRanges; +DROP TABLE IF EXISTS Organisations; + + + + + diff --git a/foodloopserver.conf b/foodloopserver.conf new file mode 100644 index 0000000..6810316 --- /dev/null +++ b/foodloopserver.conf @@ -0,0 +1,6 @@ +{ + dsn => "dbi:SQLite:dbname=foodloop.db", + user => undef, + pass => undef, + key => "a", +}; diff --git a/foodloopserver.development.conf b/foodloopserver.development.conf new file mode 100644 index 0000000..2add266 --- /dev/null +++ b/foodloopserver.development.conf @@ -0,0 +1,6 @@ +{ + dsn => "dbi:SQLite:dbname=foodloop-test.db", + user => undef, + pass => undef, + key => "a", +}; diff --git a/foodloopserver.pl b/foodloopserver.pl index 67a841f..ccc288a 100644 --- a/foodloopserver.pl +++ b/foodloopserver.pl @@ -1,5 +1,5 @@ -#!/usr/bin/env perl +#!/usr/bin/env perl -w # NOT READY FOR PRODUCTION use Mojolicious::Lite; @@ -7,12 +7,19 @@ use Data::UUID; use Devel::Dwarn; use Mojo::JSON; use Data::Dumper; +use Email::Valid; +use ORM::Date; +use Authen::Passphrase::BlowfishCrypt; # connect to database use DBI; -my $config = plugin Config => {file => 'myapp.conf'}; +my $config = plugin 'Config'; + my $dbh = DBI->connect($config->{dsn},$config->{user},$config->{pass}) or die "Could not connect"; +$dbh->do("PRAGMA foreign_keys = ON"); +$dbh->do("PRAGMA secure_delete = ON"); + Dwarn $config; # shortcut for use in template @@ -63,29 +70,145 @@ post '/register' => sub { my $self = shift; my $json = $self->req->json; - - my $account = $self->get_account_by_username( $json->{username} ); - - $self->app->log->debug( "Account: " . Dumper $account ); $self->app->log->debug( "JSON: " . Dumper $json ); - unless ( defined $account ) { + my $token = $json->{token}; + if ( ! $self->is_token_unused($token) ) { return $self->render( json => { success => Mojo::JSON->false, - message => 'Username not recognised, has your token expired?', - }); - } elsif ( $account->{keyused} ) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token has already been used', - }); + message => 'Token not valid or has been used.', + }, + status => 401,); #Unauthorized } - my $insert = $self->db->prepare("UPDATE accounts SET fullname = ?, email = ?, postcode = ?, age = ?, gender = ?, grouping = ?, password = ?, keyused = ? WHERE username = ?"); - $insert->execute( - @{$json}{ qw/ fullname email postcode age gender grouping password / }, 'True', $account->{username}, - ); - $self->render( json => { success => Mojo::JSON->true } ); + my $username = $json->{username}; + if ($username eq ''){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username cannot be blank.', + }, + status => 400,); #Malformed request + } + elsif ( ! ($username =~ m/^[A-Za-z0-9]+$/)){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username can only be A-Z, a-z and 0-9 characters.', + }, + status => 400,); #Malformed request + } + elsif ( $self->does_username_exist($username) ) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username exists.', + }, + status => 403,); #Forbidden + } + + my $email = $json->{email}; + if ( ! Email::Valid->address($email)){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Email is invalid.', + }, + status => 400,); #Malformed request + } + elsif($self->does_email_exist($email)) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Email exists.', + }, + status => 403,); #Forbidden + } + + #TODO test to see if post code is valid. + my $postcode = $json->{postcode}; + + #TODO should we enforce password requirements. + my $password = $json->{password}; + my $hashedPassword = $self->generate_hashed_password($password); + + my $secondsTime = time(); + my $date = ORM::Date->new_epoch($secondsTime)->mysql_date; + + my $usertype = $json->{usertype}; + + if ($usertype eq 'customer'){ + my $age = $json->{age}; + + my $ageForeignKey = $self->get_age_foreign_key($age); + if ( ! defined $ageForeignKey ){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Age range is invalid.', + }, + status => 400,); #Malformed request + } + + #TODO UNTESTED as it's hard to simulate. + #Token is no longer valid race condition. + if ( ! $self->set_token_as_used($token) ){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token no longer is accepted.', + }, + status => 500,); #Internal server error. Racecondition + } + + + my ($idToUse) = $self->db->selectrow_array("SELECT MAX(CustomerId) FROM Customers"); + if (defined $idToUse){ + $idToUse++; + } + else{ + $idToUse = 1; + } + + #TODO Race condition here. + my $insertCustomer = $self->db->prepare("INSERT INTO Customers (CustomerId, UserName, AgeRange_FK, PostCode) VALUES (?, ?, ?, ?)"); + my $rowsInsertedCustomer = $insertCustomer->execute($idToUse, $username, $ageForeignKey, $postcode); + my $insertUser = $self->db->prepare("INSERT INTO Users (CustomerId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); + my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); + + return $self->render( json => { success => Mojo::JSON->true } ); + } + elsif ($usertype eq 'organisation') { + #TODO validation on the address. Or perhaps add the organisation to a "to be inspected" list then manually check them. + my $fullAddress = $json->{fulladdress}; + + #TODO UNTESTED as it's hard to simulate. + #Token is no longer valid race condition. + if ( ! $self->set_token_as_used($token) ){ + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token no longer is accepted.', + }, + status => 500,); #Internal server error. Racecondition + } + + my $idToUse = $self->db->selectrow_array("SELECT MAX(OrganisationalId) FROM Organisations"); + if (defined $idToUse){ + $idToUse++; + } + else{ + $idToUse = 1; + } + + + #TODO Race condition here. + my $insertOrganisation = $self->db->prepare("INSERT INTO Organisations (OrganisationalId, Name, FullAddress, PostCode) VALUES (?, ?, ?, ?)"); + my $rowsInsertedOrganisation = $insertOrganisation->execute($idToUse, $username, $fullAddress, $postcode); + my $insertUser = $self->db->prepare("INSERT INTO Users (OrganisationalId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); + my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); + + return $self->render( json => { success => Mojo::JSON->true } ); + } + else{ + return $self->render( json => { + success => Mojo::JSON->false, + message => '"usertype" is invalid.', + }, + status => 400,); #Malformed request + } }; post '/edit' => sub { @@ -116,29 +239,6 @@ post '/edit' => sub { }; -post '/token' => sub { - my $self = shift; - - my $json = $self->req->json; - - my $account = $self->get_account_by_token( $json->{token} ); - - $self->app->log->debug( "Account: " . Dumper $account ); - - # TODO change to proper boolean checks - if ( ! defined $account || $account->{keyused} ) { - $self->app->log->info("unrecognised or preused token: [" . $json->{token} . "]"); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token is invalid or has already been used', - }); - } - return $self->render( json => { - username => $account->{username}, - success => Mojo::JSON->true, - }); -}; - post '/fetchuser' => sub { my $self = shift; @@ -175,4 +275,81 @@ helper get_account_by_username => sub { ); }; +#Return true if and only if the token exists and has not been used. +helper is_token_unused => sub { + my ( $self, $token ) = @_; + + my ( $out ) = $self->db->selectrow_array("SELECT COUNT(TokenId) FROM Tokens WHERE TokenName = ? AND Used = 0", undef, ($token)); + + return $out != 0; + +}; + +helper get_age_foreign_key => sub { + my ( $self, $ageString ) = @_; + + my ($out) = $self->db->selectrow_array( + "SELECT AgeRangeId FROM AgeRanges WHERE AgeRangeString = ?", + {}, + $ageString, + ); + + return $out; +}; + + +helper does_username_exist => sub { + my ( $self, $username ) = @_; + + my ($out) = $self->db->selectrow_array("SELECT COUNT(UserName) FROM Customers WHERE UserName = ?", {}, ($username)); + #print "-". Dumper($out) ."-"; + + return $out != 0; +}; + +helper does_email_exist => sub { + my ( $self, $email ) = @_; + + return defined ($self->db->selectrow_hashref( + "SELECT Email FROM Users WHERE Email = ?", + {}, + $email, + )); +}; + +helper set_token_as_used => sub { + my ( $self, $token ) = @_; + + #Return true if and only if the token exists and has not been used. + my $statement = $self->db->prepare("UPDATE Tokens SET Used = 1 WHERE TokenName = ? AND Used = 0 "); + my $rows = $statement->execute($token); + + #print '-set_token_as_used-'.(Dumper($rows))."-\n"; + + return $rows != 0; +}; + +helper generate_hashed_password => sub { + my ( $self, $password) = @_; + + my $ppr = Authen::Passphrase::BlowfishCrypt->new( + cost => 8, salt_random => 1, + passphrase => $password); + return $ppr->as_crypt; + +}; + +# We assume the user already exists. +helper check_password_email => sub{ + my ( $self, $email, $password) = @_; + + my $statement = $self->db->prepare("SELECT HashedPassword FROM Users WHERE Email = ?"); + my $result -> execute($email); + my ($hashedPassword) = $result->fetchrow_array; + + my $ppr = Authen::Passphrase::BlowfishCrypt->from_crypt($hashedPassword); + + return $ppr->match($password); +}; + app->start; diff --git a/schema-potential.sql b/schema-potential.sql new file mode 100644 index 0000000..1e5aa9a --- /dev/null +++ b/schema-potential.sql @@ -0,0 +1,53 @@ +CREATE TABLE Organisations ( + OrganisationalId INTEGER PRIMARY KEY UNIQUE NOT NULL, + Name TEXT NOT NULL, + FullAddress TEXT NOT NULL, + PostCode TEXT NOT NULL +); + +CREATE TABLE AgeRanges ( + AgeRangeId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + AgeRangeString TEXT NOT NULL UNIQUE +); + +INSERT INTO AgeRanges (AgeRangeString) VALUES ('20-35'); +INSERT INTO AgeRanges (AgeRangeString) VALUES ('35-50'); +INSERT INTO AgeRanges (AgeRangeString) VALUES ('50+'); + +CREATE TABLE Customers ( + CustomerId INTEGER PRIMARY KEY UNIQUE NOT NULL, + UserName TEXT NOT NULL UNIQUE, + AgeRange_FK INTEGER NOT NULL, + PostCode TEXT NOT NULL, + FOREIGN KEY (AgeRange_FK) REFERENCES AgeRanges (AgeRangeId) +); + +CREATE TABLE Users ( + UserId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + CustomerId_FK INTEGER UNIQUE, + OrganisationalId_FK INTEGER UNIQUE, + Email TEXT NOT NULL UNIQUE, + JoinDate INTEGER NOT NULL, + HashedPassword TEXT NOT NULL, + FOREIGN KEY (CustomerId_FK) REFERENCES Customer (CustomerId), + FOREIGN KEY (OrganisationalId_FK) REFERENCES Organisation (OrganisationalId), + CHECK ((CustomerId_FK NOTNULL AND OrganisationalId_FK ISNULL) OR (CustomerId_FK ISNULL AND OrganisationalId_FK NOTNULL)) +); + +CREATE TABLE Transactions ( + TransactionId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + BuyerUserId_FK INTEGER NOT NULL, + SellerOrganisationId_FK INTEGER NOT NULL, + Date TEXT NOT NULL, + ValueMicroCurrency INTEGER NOT NULL, + ProofImage TEXT NOT NULL UNIQUE, + FOREIGN KEY (BuyerUserId_FK) REFERENCES User (UserId), + FOREIGN KEY (SellerOrganisationId_FK) REFERENCES Organisation (OrganisationalId), + CHECK ((BuyerUserId_FK IN (SELECT UserId FROM Users WHERE UserId = BuyerUserId_FK AND CustomerId_FK IS NOT NULL)) OR (BuyerUserId_FK IN (SELECT UserId FROM Users WHERE UserId = BuyerUserId_FK AND OrganisationalId_FK IS NOT NULL AND OrganisationalId_FK IS NOT SellerOrganisationId_FK))) +); + +CREATE TABLE Tokens ( + TokenId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + TokenName TEXT UNIQUE NOT NULL, + Used INTEGER NOT NULL DEFAULT 0 +); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..54ede92 --- /dev/null +++ b/schema.sql @@ -0,0 +1,52 @@ +CREATE TABLE Organisations ( + OrganisationalId INTEGER PRIMARY KEY UNIQUE NOT NULL, + Name TEXT NOT NULL, + FullAddress TEXT NOT NULL, + PostCode TEXT NOT NULL +); + +CREATE TABLE AgeRanges ( + AgeRangeId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + AgeRangeString TEXT NOT NULL UNIQUE +); + +INSERT INTO AgeRanges (AgeRangeString) VALUES ('20-35'); +INSERT INTO AgeRanges (AgeRangeString) VALUES ('35-50'); +INSERT INTO AgeRanges (AgeRangeString) VALUES ('50+'); + +CREATE TABLE Customers ( + CustomerId INTEGER PRIMARY KEY UNIQUE NOT NULL, + UserName TEXT NOT NULL UNIQUE, + AgeRange_FK INTEGER NOT NULL, + PostCode TEXT NOT NULL, + FOREIGN KEY (AgeRange_FK) REFERENCES AgeRanges (AgeRangeId) +); + +CREATE TABLE Users ( + UserId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + CustomerId_FK INTEGER UNIQUE, + OrganisationalId_FK INTEGER UNIQUE, + Email TEXT NOT NULL UNIQUE, + JoinDate INTEGER NOT NULL, + HashedPassword TEXT NOT NULL, + FOREIGN KEY (CustomerId_FK) REFERENCES Customers (CustomerId), + FOREIGN KEY (OrganisationalId_FK) REFERENCES Organisations (OrganisationalId), + CHECK ((CustomerId_FK NOTNULL AND OrganisationalId_FK ISNULL) OR (CustomerId_FK ISNULL AND OrganisationalId_FK NOTNULL)) +); + +CREATE TABLE Transactions ( + TransactionId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + BuyerUserId_FK INTEGER NOT NULL, + SellerOrganisationId_FK INTEGER NOT NULL, + Date TEXT NOT NULL, + ValueMicroCurrency INTEGER NOT NULL, + ProofImage TEXT NOT NULL UNIQUE, + FOREIGN KEY (BuyerUserId_FK) REFERENCES Users (UserId), + FOREIGN KEY (SellerOrganisationId_FK) REFERENCES Organisations (OrganisationalId) +); + +CREATE TABLE Tokens ( + TokenId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + TokenName TEXT UNIQUE NOT NULL, + Used INTEGER NOT NULL DEFAULT 0 +); diff --git a/t/register.t b/t/register.t new file mode 100644 index 0000000..7919046 --- /dev/null +++ b/t/register.t @@ -0,0 +1,251 @@ +use Test::More; +use Test::Mojo; +use Mojo::JSON; + +use FindBin; + +$ENV{MOJO_MODE} = 'development'; +$ENV{MOJO_LOG_LEVEL} = 'debug'; + +require "$FindBin::Bin/../foodloopserver.pl"; + +my $t = Test::Mojo->new; + +my $dbh = $t->app->db; + +#Dump all pf the test tables and start again. +my $sqlDeployment = Mojo::File->new("$FindBin::Bin/../dropschema.sql")->slurp; +for (split ';', $sqlDeployment){ + $dbh->do($_) or die $dbh->errstr; +} + +my $sqlDeployment = Mojo::File->new("$FindBin::Bin/../schema.sql")->slurp; +for (split ';', $sqlDeployment){ + $dbh->do($_) or die $dbh->errstr; +} + +#Variables to be used for uniqueness when testing. +my @names = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'); +my @emails = ('a@a.com', 'b@a.com', 'c@a.com', 'd@a.com', 'e@a.com', 'f@a.com', 'g@a.com', 'h@a.com', 'i@a.com', 'j@a.com', 'k@a.com', 'l@a.com', 'm@a.com', 'n@a.com'); +my @tokens = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'); +my $tokenStatement = $dbh->prepare('INSERT INTO Tokens (TokenName) VALUES (?)'); +foreach (@tokens){ + my $rowsAdded = $tokenStatement->execute($_); +} + + +#Not valid token. +print "test1\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => ' ', + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '50+' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(401)->or(sub{ diag $t->tx->res->body}) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/token/i); + +#Blank username +print "test2\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => '', + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '50+' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/blank/i) + ->content_like(qr/username/i); + +#Not alpha numeric chars e.g. ! +print "test3\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => 'asa!', + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '50+' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/username/i); + +my $usernameToReuse = shift(@names); +my $emailToReuse = shift(@emails); + +#Valid customer +print "test4\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => $usernameToReuse, + 'email' => $emailToReuse, + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '50+' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + +#Valid customer2 +print "test5\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '35-50' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + +#Valid customer3 +print "test6\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '20-35' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + +#Username exists +print "test7\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => $usernameToReuse, + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '50+' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(403) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/exists/i); + +#invalid email 1 +print "test8\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => 'dfsd@.com', + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '35-50' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/email/i) + ->content_like(qr/invalid/i); + +#invalid email 2 +print "test9\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => 'dfsd@com', + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '35-50' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/email/i) + ->content_like(qr/invalid/i); + +#Email exists +print "test10\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => $emailToReuse, + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => '35-50' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(403) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/email/i) + ->content_like(qr/exists/i); + + +#Age is invalid +print "test11\n\n"; +my $testJson = { + 'usertype' => 'customer', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'age' => 'invalid' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/age/i) + ->content_like(qr/invalid/i); + +#Organisation valid +print "test12\n\n"; +my $testJson = { + 'usertype' => 'organisation', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'fulladdress' => 'mary lane testing....' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + +#Invalid user type +print "test13\n\n"; +my $testJson = { + 'usertype' => 'organisation1', + 'token' => shift(@tokens), + 'username' => shift(@names), + 'email' => shift(@emails), + 'postcode' => 'LA1 1AA', + 'password' => 'Meh', + 'fulladdress' => 'mary lane testing....' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/usertype/i) + ->content_like(qr/invalid/i); + +done_testing();