Skip to content

Commit

Permalink
Merge remote-tracking branch 'remotes/origin/master' into feature/app…
Browse files Browse the repository at this point in the history
…-user-registration

* remotes/origin/master: (23 commits)
  Adds credentials to user seeder
  Adds tests for more login scenarios
  Fixes problem with password_verify() and hhvm
  Implements additinonal token assertions
  Adds method to get source from specified message
  Updates getLastMessage() to get complete message data
  Removes unnecessary setUp() method
  Uses the responder for the reset password response
  Allow customization of the status code for noContent
  Moves the dispatch mail logic from repository to controller
  Sets the sync driver as default, correctly this time
  Updates API documentation
  Restores previous driver after test finishes
  Restores to explicitly set the queue driver to sync
  Sets travis env to use sync queue driver as default
  Sets synchronous as default queue driver for phpunit
  Adds the DispatchesJob declaration
  Use synchronous queue driver so we can test email queue jobs.
  Adds assertion for email being sent
  Implements accessor for full name attribute
  ...
  • Loading branch information
zakhenry committed Jul 26, 2015
2 parents b8d7b66 + 136ae10 commit 0e592e8
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 12 deletions.
2 changes: 1 addition & 1 deletion api/.travis.env
Expand Up @@ -20,7 +20,7 @@ DB_PASSWORD=

CACHE_DRIVER=redis
SESSION_DRIVER=array
QUEUE_DRIVER=beanstalkd
QUEUE_DRIVER=sync
BEANSTALKD_HOST=127.0.0.1

MAIL_DRIVER=smtp
Expand Down
26 changes: 26 additions & 0 deletions api/app/Http/Controllers/UserController.php
Expand Up @@ -6,14 +6,18 @@
use Illuminate\Http\Request;
use App\Models\UserCredential;
use Illuminate\Support\MessageBag;
use App\Jobs\SendPasswordResetEmail;
use App\Exceptions\ValidationException;
use Laravel\Lumen\Routing\DispatchesJobs;
use App\Extensions\Lock\Manager as Lock;
use App\Repositories\UserRepository as Repository;
use App\Http\Validators\UserValidator as Validator;
use Spira\Responder\Contract\ApiResponderInterface as Responder;

class UserController extends ApiController
{
use DispatchesJobs;

/**
* Permission Lock Manager.
*
Expand Down Expand Up @@ -100,4 +104,26 @@ public function putOne($id, Request $request)

return $this->responder->createdItem($model);
}

/**
* Reset user password.
*
* @param string $id
* @return Response
*/
public function resetPassword($id)
{
$this->validateId($id);

try {
$user = $this->repository->find($id);
} catch (ModelNotFoundException $e) {
$this->responder->errorNotFound();
}

$token = $this->repository->makeLoginToken($id);
$this->dispatch(new SendPasswordResetEmail($user, $token));

return $this->responder->noContent(202);
}
}
1 change: 1 addition & 0 deletions api/app/Http/routes.php
Expand Up @@ -26,6 +26,7 @@
$app->put('{id}', ['uses' => 'UserController@putOne']);
$app->patch('{id}', ['uses' => 'UserController@patchOne']);
$app->delete('{id}', ['uses' => 'UserController@deleteOne']);
$app->delete('{id}/password', ['uses' => 'UserController@resetPassword']);
});


Expand Down
53 changes: 53 additions & 0 deletions api/app/Jobs/SendPasswordResetEmail.php
@@ -0,0 +1,53 @@
<?php namespace App\Jobs;

use App\Models\User;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendPasswordResetEmail extends Job implements SelfHandling, ShouldQueue
{
/**
* User to email.
*
* @var \App\Models\User
*/
protected $user;

/**
* Token for confirmation.
*
* @var string
*/
protected $token;

/**
* Create a new job instance.
*
* @param User $user
* @param string $token
* @return void
*/
public function __construct(User $user, $token)
{
$this->user = $user;
$this->token = $token;
}

/**
* Execute the job.
*
* @param Mailer $mailer
* @return void
*/
public function handle(Mailer $mailer)
{
$mailer->send('emails.resetPassword', [
'user' => $this->user,
'token' => $this->token
], function ($m) {
$m->to($this->user->email, $this->user->full_name)
->subject('Password Reset');
});
}
}
19 changes: 18 additions & 1 deletion api/app/Models/User.php
Expand Up @@ -76,14 +76,31 @@ public function setCredential(UserCredential $credential)
return $this;
}

/**
* Accessor to get full name attribute for the user.
*
* @return string
*/
public function getFullNameAttribute()
{
return sprintf('%s %s', $this->first_name, $this->last_name);
}

/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->userCredential ? $this->userCredential->password : false;
// If no user credential is associated with the user, just return an
// empty string which will trigger a ValidationException during
// password_verify()
if (!$this->userCredential) {
return '';
}

return $this->userCredential->password;
}

/**
Expand Down
24 changes: 20 additions & 4 deletions api/database/seeds/UserStorySeeder.php
@@ -1,5 +1,7 @@
<?php

use App\Models\User;
use App\Models\UserCredential;
use Illuminate\Database\Seeder;

class UserStorySeeder extends Seeder
Expand All @@ -11,10 +13,24 @@ class UserStorySeeder extends Seeder
*/
public function run()
{
factory(App\Models\User::class)->create([
'email' => 'john.smith@example.com',
]);
$this->createUser(['email' => 'john.smith@example.com']);

factory(App\Models\User::class, 99)->create();
for ($i=0; $i < 99; $i++) {
$this->createUser();
}
}

/**
* Create a new user with credentials.
*
* @param array $attributes
* @return void
*/
protected function createUser(array $attributes = [])
{
$user = factory(User::class)->create($attributes);
$credential = factory(UserCredential::class)->make();
$credential->user_id = $user->user_id;
$credential->save();
}
}
2 changes: 1 addition & 1 deletion api/phpunit.xml
Expand Up @@ -24,6 +24,6 @@
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="redis"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="beanstalkd"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>
16 changes: 15 additions & 1 deletion api/resources/views/documentation/sections/user.blade.apib
Expand Up @@ -49,7 +49,7 @@ with a third party through OAuth)
* Note that for this entity replacement of entity is *not* permitted
#### Restrictions
* Allowed - [all]
Note that in this request, a child `#userCredentials` object is provided. This represents an associated model that must
Note that in this request, a child `_userCredentials` object is provided. This represents an associated model that must
be created when the user is first registered.

The request *must* provide at least one authentication method when the user is created. In this example we use standard
Expand Down Expand Up @@ -93,6 +93,20 @@ password credentials - social signin is described later in this document (@todo)

+ Response 204

### Reset user password [DELETE /users/{userId}/password]
#### Restrictions
* Allowed - [admin, self]

When the request is performed, the password is not actually deleted, but an
email is sent to the user with a reset token.

+ Request
+ Headers

Authorization: Bearer {!! $factory->make(\App\Models\AuthToken::class)->token !!}

+ Response 202

### Delete user entity [DELETE]
#### Restrictions
* Allowed - [admin]
Expand Down
5 changes: 5 additions & 0 deletions api/resources/views/emails/resetPassword.blade.php
@@ -0,0 +1,5 @@
{{ $user->first_name }},

Please reset your password by following this link

http://somelink/{{ $token }}
5 changes: 3 additions & 2 deletions api/src/Responder/Responder/ApiResponder.php
Expand Up @@ -47,13 +47,14 @@ public function created($location = null)
/**
* Respond with a no content response.
*
* @param int $code
* @return Response
*/
public function noContent()
public function noContent($code = 204)
{
$response = $this->getResponse();
$response->setContent(null);
return $response->setStatusCode(204);
return $response->setStatusCode($code);
}

/**
Expand Down
32 changes: 31 additions & 1 deletion api/tests/Traits/MailcatcherTrait.php
Expand Up @@ -40,8 +40,11 @@ public function getLastMessage()
if (empty($messages)) {
return $this->fail('No messages received');
}

// messages are in descending order
return reset($messages);
$id = reset($messages)->id;

return $this->getMessage($id);
}

/**
Expand All @@ -55,4 +58,31 @@ public function getMessages()

return json_decode($jsonResponse->getBody());
}

/**
* Get a message by its id.
*
* @param int $id
* @param string $type
* @return mixed
*/
public function getMessage($id, $type = 'json')
{
$jsonResponse = $this->mailcatcher->get(sprintf('/messages/%s.%s', $id, $type));

return json_decode($jsonResponse->getBody());
}

/**
* Get a message source by its id.
*
* @param int $id
* @return string
*/
public function getMessageSource($id)
{
$response = $this->mailcatcher->get(sprintf('/messages/%s.html', $id));

return (string) $response->getBody();
}
}
30 changes: 30 additions & 0 deletions api/tests/integration/AuthTest.php
Expand Up @@ -55,6 +55,36 @@ public function testFailedLogin()
$this->assertContains('failed', $body->message);
}

public function testLoginEmptyPassword()
{
$user = factory(App\Models\User::class)->create();
$credential = factory(App\Models\UserCredential::class)->make();
$credential->user_id = $user->user_id;
$credential->save();
$this->get('/auth/jwt/login', [
'PHP_AUTH_USER' => $user->email,
'PHP_AUTH_PW' => '',
]);

$body = json_decode($this->response->getContent());
$this->assertResponseStatus(401);
$this->assertContains('failed', $body->message);
}

public function testLoginUserMissCredentials()
{
$user = factory(App\Models\User::class)->create();

$this->get('/auth/jwt/login', [
'PHP_AUTH_USER' => $user->email,
'PHP_AUTH_PW' => '',
]);

$body = json_decode($this->response->getContent());
$this->assertResponseStatus(401);
$this->assertContains('failed', $body->message);
}

public function testFailedTokenEncoding()
{
$user = factory(App\Models\User::class)->create();
Expand Down
14 changes: 14 additions & 0 deletions api/tests/integration/QueueTest.php
Expand Up @@ -6,13 +6,27 @@
class QueueTest extends TestCase
{
protected $pheanstalk;
protected $originalDriver;

public function setUp()
{
parent::setUp();

// We'll use the beanstalkd queue driver for this test.
$this->originalDriver = getenv('QUEUE_DRIVER');
putenv('QUEUE_DRIVER=beanstalkd');

$this->pheanstalk = new Pheanstalk(env('BEANSTALKD_HOST'));
}

public function teardown()
{
parent::teardown();

// Restore the original driver when the test is finished
putenv('QUEUE_DRIVER='.$this->originalDriver);
}

/**
* Test queue is listening.
*/
Expand Down

0 comments on commit 0e592e8

Please sign in to comment.