Create User Zone

This commit is contained in:
omair saleh
2022-09-25 03:46:14 +08:00
parent 8143e2b9a2
commit b008ad5106
59 changed files with 1757 additions and 12 deletions
+5 -1
View File
@@ -9,7 +9,8 @@
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^9.19",
"laravel/sanctum": "^3.0",
"laravel/tinker": "^2.7"
"laravel/tinker": "^2.7",
"tymon/jwt-auth": "^1.0"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
@@ -21,6 +22,9 @@
"spatie/laravel-ignition": "^1.0"
},
"autoload": {
"files": [
"src/Common/Infrastructure/helpers.php"
],
"psr-4": {
"Src\\": "src/",
"Database\\Factories\\": "database/factories/",
+6 -1
View File
@@ -182,6 +182,11 @@ return [
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Domain Service Providers...
*/
\Src\Zone\User\Application\Providers\UserServiceProvider::class,
/*
* Package Service Providers...
*/
@@ -190,7 +195,7 @@ return [
* Application Service Providers...
*/
\Src\Common\Infrastructure\Laravel\Providers\AppServiceProvider::class,
// \Src\Auth\Application\Providers\AuthServiceProvider::class,
\Src\Auth\Application\Providers\AuthServiceProvider::class,
// Src\Common\Infrastructure\Laravel\Providers\BroadcastServiceProvider::class,
\Src\Common\Infrastructure\Laravel\Providers\EventServiceProvider::class,
\Src\Common\Infrastructure\Laravel\Providers\RouteServiceProvider::class,
+7 -2
View File
@@ -14,7 +14,7 @@ return [
*/
'defaults' => [
'guard' => 'web',
'guard' => 'internal-api',
'passwords' => 'users',
],
@@ -40,6 +40,11 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'internal-api' => [
'driver' => 'jwt',
'provider' => 'users',
'hash' => false,
]
],
/*
@@ -62,7 +67,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
'model' => \Src\Zone\User\Infrastructure\EloquentModels\UserEloquentModel::class,
],
// 'users' => [
@@ -16,9 +16,12 @@ return new class extends Migration
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('avatar')->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->timestamp('email_verified_at')->nullable();
$table->boolean('is_admin')->default(0);
$table->boolean('is_active')->default(1);
$table->rememberToken();
$table->timestamps();
});
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Src\Auth\Application;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Facades\Log;
use Src\Zone\User\Application\Mappers\UserMapper;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Infrastructure\EloquentModels\UserEloquentModel;
use Src\Auth\Domain\AuthInterface;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Facades\JWTAuth as TymonJWTAuth;
class JWTAuth implements AuthInterface
{
public function login(array $credentials): string
{
$user = UserEloquentModel::query()->where('email', $credentials['email'])->first();
if (!$user || !$user->is_active) {
throw new AuthenticationException();
} elseif (!$token = auth()->attempt($credentials)) {
throw new AuthenticationException();
}
return $token;
}
public function logout(): void
{
auth()->logout();
}
public function me(): User
{
return UserMapper::fromAuth(auth()->user());
}
public function refresh(): string
{
try {
return TymonJWTAuth::parseToken()->refresh();
} catch (JWTException $e) {
Log::error($e->getMessage());
throw new AuthenticationException($e->getMessage());
}
}
}
@@ -29,7 +29,10 @@ class AuthServiceProvider extends ServiceProvider
public function register()
{
//
$this->app->bind(
\Src\Auth\Domain\AuthInterface::class,
\Src\Auth\Application\JWTAuth::class
);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Src\Auth\Domain;
use Illuminate\Auth\AuthenticationException;
use Src\Zone\User\Domain\Model\User;
interface AuthInterface
{
/**
* @throws AuthenticationException
*/
public function login(array $credentials): string;
/**
* @throws AuthenticationException
*/
public function refresh(): string;
public function logout(): void;
public function me(): User;
}
+1
View File
@@ -0,0 +1 @@
<?php
View File
View File
@@ -0,0 +1,103 @@
<?php
namespace Src\Auth\Presentation\HTTP;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Src\Auth\Domain\AuthInterface;
use Src\Common\Infrastructure\Laravel\Controller;
use Symfony\Component\HttpFoundation\Response;
class AuthController extends Controller
{
private AuthInterface $auth;
public function __construct(AuthInterface $auth)
{
$this->auth = $auth;
}
/**
* Get a JWT via given credentials.
*
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse
{
try {
$email = $request->get('email');
$password = $request->get('password');
$credentials = ['email' => strtolower($email), 'password' => $password];
$validator = Validator::make($credentials, [
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if ($validator->fails()) {
throw new ValidationException($validator);
}
$token = $this->auth->login($credentials);
return $this->respondWithToken($token);
} catch (ValidationException $validationException) {
return response()->json($validationException->errors(), Response::HTTP_BAD_REQUEST);
} catch (AuthenticationException) {
return response()->json(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED );
}
}
/**
* Get the authenticated UserEloquentModel.
*
* @return JsonResponse
*/
public function me(): JsonResponse
{
return response()->json($this->auth->me()->toArray());
}
/**
* Log the user out (Invalidate the token).
*
* @return JsonResponse
*/
public function logout(): JsonResponse
{
$this->auth->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* @return JsonResponse
*/
public function refresh(): JsonResponse
{
try {
$token = $this->auth->refresh();
} catch (AuthenticationException $e) {
return response()->json(['status' => $e->getMessage()], Response::HTTP_FORBIDDEN);
}
return $this->respondWithToken($token);
}
/**
* Get the token array structure.
*
* @param string $token
*
* @return JsonResponse
*/
protected function respondWithToken(string $token): JsonResponse
{
return response()->json([
'accessToken' => $token,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 1,
]);
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
use Illuminate\Support\Facades\Route;
use Src\Auth\Presentation\HTTP\AuthController;
Route::group([
'prefix' => 'auth'
], function () {
Route::post('login', [AuthController::class, 'login']);
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
Route::get('me', [AuthController::class, 'me']);
});
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace Src\Common\Domain;
class AggregateRoot
{
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Src\Common\Domain;
use Src\Common\Domain\Exceptions\UnauthorizedUserException;
interface CommandInterface
{
/**
* @throws UnauthorizedUserException
*/
public function execute();
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Common\Domain\Exceptions;
class EntityNotFoundException extends \DomainException
{
public function __construct(string $message = 'Entity not found')
{
parent::__construct($message);
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Common\Domain\Exceptions;
final class IncorrectEmailFormatException extends \DomainException
{
public function __construct()
{
parent::__construct('Must be a valid email');
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Common\Domain\Exceptions;
final class MaximumValueException extends \DomainException
{
public function __construct($fieldName, $value)
{
parent::__construct(__("The maximum value for '$fieldName' is '$value"));
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Common\Domain\Exceptions;
final class RequiredException extends \DomainException
{
public function __construct($fieldName)
{
parent::__construct(trans('validation.required', ['attribute' => $fieldName]));
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Common\Domain\Exceptions;
final class UnauthorizedUserException extends \Exception
{
public function __construct()
{
parent::__construct('The user is not authorized to access this resource');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Src\Common\Domain;
use Src\Common\Domain\Exceptions\UnauthorizedUserException;
interface QueryInterface
{
/**
* @throws UnauthorizedUserException
*/
public function handle(): mixed;
}
@@ -0,0 +1,42 @@
<?php
namespace Src\Common\Infrastructure\Laravel\Middleware;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
class JwtMiddleware extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
try {
if (in_array($request->decodedPath(), ['auth/login', 'auth/refresh', 'login', 'refresh']) && $request->method() == 'POST') {
return $next($request);
}
JWTAuth::parseToken()->authenticate();
} catch (Exception $e) {
if ($e instanceof TokenInvalidException){
return response()->json(['status' => 'Token is Invalid'], Response::HTTP_UNAUTHORIZED );
}else if ($e instanceof TokenExpiredException){
return response()->json(['status' => 'Token is Expired'], Response::HTTP_UNAUTHORIZED );
}else{
return response()->json(['status' => 'Authorization Token not found'], Response::HTTP_UNAUTHORIZED );
}
}
return $next($request);
}
}
@@ -27,4 +27,12 @@ class AuthServiceProvider extends ServiceProvider
//
}
public function register()
{
$this->app->bind(
\Src\Auth\Domain\AuthInterface::class,
\Src\Auth\Application\JWTAuth::class
);
}
}
@@ -29,12 +29,17 @@ class RouteServiceProvider extends ServiceProvider
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
Route::middleware('api')
->group(base_path('routes/api.php'));
Route::middleware('internal_api')
->group(function() {
require base_path('src/Auth/Presentation/HTTP/routes.php');
require base_path('src/Zone/User/Presentation/HTTP/routes.php');
});
});
}
@@ -45,8 +50,8 @@ class RouteServiceProvider extends ServiceProvider
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
RateLimiter::for('internal_api', function (Request $request) {
return Limit::perMinute(60)->by($request->ip());
});
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Zone\User\Application\Exceptions;
final class EmailAlreadyUsedException extends \DomainException
{
public function __construct()
{
parent::__construct('El email ya está en uso');
}
}
@@ -0,0 +1,68 @@
<?php
namespace Src\Zone\User\Application\Mappers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Avatar;
use Src\Zone\User\Domain\Model\ValueObjects\CompanyId;
use Src\Zone\User\Domain\Model\ValueObjects\Email;
use Src\Zone\User\Domain\Model\ValueObjects\Name;
use Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface;
use Src\Zone\User\Infrastructure\EloquentModels\UserEloquentModel;
class UserMapper
{
public static function fromRequest(Request $request, ?int $user_id = null): User
{
return new User(
id: $user_id,
name: new Name($request->input('name')),
email: new Email($request->input('email')),
avatar: new Avatar($request->input('avatar')),
is_admin: $request->input('is_admin') ?? false,
is_active: $request->input('is_active') ?? true,
);
}
public static function fromEloquent(UserEloquentModel $userEloquent): User
{
$avatarRepository = app()->make(AvatarRepositoryInterface::class);
return new User(
id: $userEloquent->id,
name: new Name($userEloquent->name),
email: new Email($userEloquent->email),
avatar: $avatarRepository->retrieveAvatarFile(new Avatar($userEloquent->avatar)),
is_admin: $userEloquent->is_admin,
is_active: $userEloquent->is_active
);
}
public static function fromAuth(Authenticatable $userEloquent): User
{
$avatarRepository = app()->make(AvatarRepositoryInterface::class);
return new User(
id: $userEloquent->id,
name: new Name($userEloquent->name),
email: new Email($userEloquent->email),
avatar: $avatarRepository->retrieveAvatarFile(new Avatar($userEloquent->avatar)),
is_admin: $userEloquent->is_admin,
is_active: $userEloquent->is_active
);
}
public static function toEloquent(User $user): UserEloquentModel
{
$userEloquent = new UserEloquentModel();
if ($user->id) {
$userEloquent = UserEloquentModel::query()->find($user->id);
}
$userEloquent->name = $user->name;
$userEloquent->email = $user->email;
$userEloquent->avatar = $user->avatar;
$userEloquent->is_admin = $user->is_admin;
$userEloquent->is_active = $user->is_active;
return $userEloquent;
}
}
@@ -0,0 +1,22 @@
<?php
namespace Src\Zone\User\Application\Providers;
use Illuminate\Support\ServiceProvider;
class UserServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
\Src\Zone\User\Domain\Repositories\UserRepositoryInterface::class,
\Src\Zone\User\Application\Repositories\Eloquent\UserRepository::class
);
$this->app->bind(
\Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface::class,
\Src\Zone\User\Application\Repositories\Local\AvatarRepository::class
);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Src\Zone\User\Application\Repositories\Eloquent;
use Src\Zone\User\Application\Mappers\UserMapper;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Password;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Zone\User\Infrastructure\EloquentModels\UserEloquentModel;
class UserRepository implements UserRepositoryInterface
{
public function findAll(): array
{
$users = [];
foreach (UserEloquentModel::all() as $userEloquent) {
$users[] = UserMapper::fromEloquent($userEloquent);
}
return $users;
}
public function findById(string $userId): User
{
$userEloquent = UserEloquentModel::query()->findOrFail($userId);
return UserMapper::fromEloquent($userEloquent);
}
public function findByEmail(string $email): User
{
$userEloquent = UserEloquentModel::query()->where('email', $email)->firstOrFail();
return UserMapper::fromEloquent($userEloquent);
}
public function store(User $user, Password $password): User
{
$userEloquent = new UserEloquentModel();
$userEloquent->fill(array_merge($user->toArray(), ['password' => $password->value()]));
$userEloquent->save();
return UserMapper::fromEloquent($userEloquent);
}
public function update(User $user, Password $password): void
{
$userArray = $user->toArray();
if ($password->isNotEmpty()) {
$userArray['password'] = $password->value();
}
$userEloquent = UserEloquentModel::query()->findOrFail($user->id);
$userEloquent->fill($userArray);
$userEloquent->save();
}
public function delete(int $user_id): void
{
$userEloquent = UserEloquentModel::query()->findOrFail($user_id);
$userEloquent->delete();
}
}
@@ -0,0 +1,46 @@
<?php
namespace Src\Zone\User\Application\Repositories\Local;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Src\Zone\User\Domain\Model\ValueObjects\Avatar;
use Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface;
class AvatarRepository implements AvatarRepositoryInterface
{
public function __construct(
private readonly ClientInterface $guzzle = new Client
)
{}
public function getRandomAvatar($url = 'https://doodleipsum.com/300/avatar-2?shape=circle'): Avatar
{
$doodleIpsum = $this->guzzle->request('GET', $url);
$mime = $doodleIpsum->getHeader('Content-Type')[0];
$binaryImage = base64_encode($doodleIpsum->getBody()->getContents());
return new Avatar('data:' . $mime . ';base64,' . $binaryImage);
}
public function storeAvatarFile(Avatar $avatar, string $name): ?string
{
if ($avatar->isBinaryFile()) {
$fileData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $avatar->getPath()));
$filename = Str::snake($name) . '.jpg';
Storage::disk('avatars')->put($filename, $fileData);
return $filename;
}
return null;
}
public function retrieveAvatarFile(Avatar $avatar): Avatar
{
if ($avatar->fileExists()) {
$fileData = Storage::disk('avatars')->get($avatar->getPath());
$avatar->setValue('data:image/' . $avatar->getExtension() . ';base64,' . base64_encode($fileData));
}
return $avatar;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Src\Zone\User\Application\UseCases\Commands;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Common\Domain\CommandInterface;
class DestroyUserCommand implements CommandInterface
{
private UserRepositoryInterface $repository;
private UserPolicy $policy;
public function __construct(
private readonly int $id
)
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function execute(): void
{
authorize('delete', $this->policy);
$this->repository->delete($this->id);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Src\Zone\User\Application\UseCases\Commands;
use Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface;
use Src\Common\Domain\CommandInterface;
class GetRandomAvatarCommand implements CommandInterface
{
private AvatarRepositoryInterface $avatarRepository;
public function __construct()
{
$this->avatarRepository = app()->make(AvatarRepositoryInterface::class);
}
public function execute(): ?string
{
return $this->avatarRepository->getRandomAvatar()->getPath();
}
}
@@ -0,0 +1,45 @@
<?php
namespace Src\Zone\User\Application\UseCases\Commands;
use Src\Zone\User\Application\Exceptions\EmailAlreadyUsedException;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Password;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Zone\User\Infrastructure\EloquentModels\UserEloquentModel;
use Src\Common\Domain\CommandInterface;
class StoreUserCommand implements CommandInterface
{
private UserRepositoryInterface $repository;
private AvatarRepositoryInterface $avatarRepository;
private UserPolicy $policy;
public function __construct(
private readonly User $user,
private readonly Password $password
)
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->avatarRepository = app()->make(AvatarRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function execute(): User
{
authorize('store', $this->policy);
if (UserEloquentModel::query()->where('email', $this->user->email)->exists()) {
throw new EmailAlreadyUsedException();
}
$avatar = $this->user->avatar;
if ($avatar->isBinaryFile()) {
$filename = $this->avatarRepository->storeAvatarFile($avatar, $this->user->name);
$this->user->setAvatar($filename);
}
return $this->repository->store($this->user, $this->password);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Src\Zone\User\Application\UseCases\Commands;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Password;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\AvatarRepositoryInterface;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Common\Domain\CommandInterface;
class UpdateUserCommand implements CommandInterface
{
private UserRepositoryInterface $repository;
private AvatarRepositoryInterface $avatarRepository;
private UserPolicy $policy;
public function __construct(
private readonly User $user,
private readonly Password $password
)
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->avatarRepository = app()->make(AvatarRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function execute(): void
{
authorize('update', $this->policy, ['user' => $this->user]);
// if (UserEloquentModel::query()->where('email', $this->user->email)->exists()) {
// throw new EmailAlreadyUsedException();
// }
$avatar = $this->user->avatar;
if ($avatar->isBinaryFile()) {
$filename = $this->avatarRepository->storeAvatarFile($avatar, $this->user->name);
$this->user->setAvatar($filename);
}
$this->repository->update($this->user, $this->password);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Src\Zone\User\Application\UseCases\Queries;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Common\Domain\QueryInterface;
class FindAllUsersQuery implements QueryInterface
{
private UserRepositoryInterface $repository;
private UserPolicy $policy;
public function __construct()
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function handle(): array
{
authorize('findAll', $this->policy);
return $this->repository->findAll();
}
}
@@ -0,0 +1,27 @@
<?php
namespace Src\Zone\User\Application\UseCases\Queries;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Common\Domain\QueryInterface;
class FindUserByEmailQuery implements QueryInterface
{
private UserRepositoryInterface $repository;
private UserPolicy $policy;
public function __construct(
private readonly string $email
)
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function handle(): array
{
authorize('findByEmail', $this->policy);
return $this->repository->findByEmail($this->email)->toArray();
}
}
@@ -0,0 +1,28 @@
<?php
namespace Src\Zone\User\Application\UseCases\Queries;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Policies\UserPolicy;
use Src\Zone\User\Domain\Repositories\UserRepositoryInterface;
use Src\Common\Domain\QueryInterface;
class FindUserByIdQuery implements QueryInterface
{
private UserRepositoryInterface $repository;
private UserPolicy $policy;
public function __construct(
private readonly int $id
)
{
$this->repository = app()->make(UserRepositoryInterface::class);
$this->policy = new UserPolicy();
}
public function handle(): User
{
authorize('findById', $this->policy);
return $this->repository->findById($this->id);
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Zone\User\Domain\Exceptions;
final class PasswordTooShortException extends \DomainException
{
public function __construct()
{
parent::__construct('The password needs to be at least 8 characters long');
}
}
@@ -0,0 +1,11 @@
<?php
namespace Src\Zone\User\Domain\Exceptions;
final class PasswordsDoNotMatchException extends \DomainException
{
public function __construct()
{
parent::__construct('Passwords do not match');
}
}
@@ -0,0 +1,36 @@
<?php
namespace Src\Zone\User\Domain\Factories;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Avatar;
use Src\Zone\User\Domain\Model\ValueObjects\Email;
use Src\Zone\User\Domain\Model\ValueObjects\CompanyId;
use Src\Zone\User\Domain\Model\ValueObjects\Name;
class UserFactory
{
public static function new(array $attributes = null): User
{
$attributes = $attributes ?: [];
$defaults = [
'name' => fake()->name(),
'email' => fake()->safeEmail(),
'avatar' => null,
'is_admin' => true,
'is_active' => true,
];
$attributes = array_replace($defaults, $attributes);
return (new User(
id: null,
name: new Name($attributes['name']),
email: new Email($attributes['email']),
avatar: new Avatar($attributes['avatar']),
is_admin: $attributes['is_admin'],
is_active: $attributes['is_active']
));
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Src\Zone\User\Domain\Model;
use Src\Zone\User\Domain\Model\ValueObjects\Avatar;
use Src\Zone\User\Domain\Model\ValueObjects\Email;
use Src\Zone\User\Domain\Model\ValueObjects\Name;
use Src\Common\Domain\AggregateRoot;
class User extends AggregateRoot implements \JsonSerializable
{
public function __construct(
public readonly ?int $id,
public readonly Name $name,
public readonly Email $email,
public Avatar $avatar,
public readonly bool $is_admin = false,
public readonly bool $is_active = true
) {}
public function setAvatar($avatar): void
{
$this->avatar = new Avatar($avatar);
}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar' => $this->avatar,
'is_admin' => $this->is_admin,
'is_active' => $this->is_active,
];
}
public function jsonSerialize(): array
{
return $this->toArray();
}
}
@@ -0,0 +1,58 @@
<?php
namespace Src\Zone\User\Domain\Model\ValueObjects;
final class Avatar implements \JsonSerializable
{
protected ?string $avatar;
public function __construct(?string $avatar)
{
$this->avatar = $avatar;
}
public static function fromString(?string $avatar): self
{
return new self($avatar);
}
public function setValue(?string $avatar): void
{
$this->avatar = $avatar;
}
public function getPath(): ?string
{
return $this->avatar ?? '';
}
public function getExtension(): ?string
{
return pathinfo(storage_path('app/avatars/' . $this->avatar), PATHINFO_EXTENSION);
}
public function isNull(): bool
{
return $this->avatar === null;
}
public function isBinaryFile(): bool
{
return !$this->isNull() && str_starts_with($this->avatar, 'data:image');
}
public function fileExists(): bool
{
return $this->avatar && file_exists(storage_path('app/avatars/' . $this->avatar));
}
public function __toString(): string
{
return $this->getPath() ?? '';
}
public function jsonSerialize(): string
{
return $this->__toString();
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Src\Zone\User\Domain\Model\ValueObjects;
use Src\Common\Domain\Exceptions\IncorrectEmailFormatException;
use Src\Common\Domain\Exceptions\RequiredException;
final class Email implements \JsonSerializable
{
private string $email;
public function __construct(?string $email, $isOptional = false)
{
if (!$email && !$isOptional) {
throw new RequiredException('email');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new IncorrectEmailFormatException();
}
$this->email = $email;
}
public function __toString(): string
{
return $this->email;
}
public function jsonSerialize(): string
{
return $this->email;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Src\Zone\User\Domain\Model\ValueObjects;
use Src\Common\Domain\Exceptions\RequiredException;
final class Name implements \JsonSerializable
{
private string $name;
public function __construct(?string $name)
{
if (!$name) {
throw new RequiredException('nombre');
}
$this->name = $name;
}
public function __toString(): string
{
return $this->name;
}
public function jsonSerialize(): string
{
return $this->name;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Src\Zone\User\Domain\Model\ValueObjects;
use Src\Zone\User\Domain\Exceptions\PasswordsDoNotMatchException;
use Src\Zone\User\Domain\Exceptions\PasswordTooShortException;
final class Password
{
private ?string $password;
public function __construct(?string $password, ?string $confirmation)
{
if ($password && strlen($password) < 8) {
throw new PasswordTooShortException();
}
if ($password !== $confirmation) {
throw new PasswordsDoNotMatchException();
}
$this->password = $password;
}
public static function fromString(string $password, string $confirmation): self
{
return new self($password, $confirmation);
}
public function value(): string
{
return $this->password;
}
public function isNotEmpty(): bool
{
return $this->password !== null;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Src\Zone\User\Domain\Policies;
use Src\Zone\User\Domain\Model\User;
class UserPolicy
{
public function findAll(): bool
{
return auth()->user()?->is_admin ?? false;
}
public function findById(): bool
{
return auth()->user()?->is_admin ?? false;
}
public function findByEmail(): bool
{
return auth()->user()?->is_admin ?? false;
}
public function store(): bool
{
return auth()->user()?->is_admin ?? false;
}
public function update(User $user): bool
{
return auth()->user()?->is_admin || auth()->user()?->id == $user->id;
}
public function delete(): bool
{
return auth()->user()?->is_admin ?? false;
}
}
@@ -0,0 +1,15 @@
<?php
namespace Src\Zone\User\Domain\Repositories;
use Src\Zone\User\Domain\Model\ValueObjects\Avatar;
interface AvatarRepositoryInterface
{
public function getRandomAvatar(?string $url): Avatar;
public function storeAvatarFile(Avatar $avatar, string $name): ?string;
public function retrieveAvatarFile(Avatar $avatar): Avatar;
}
@@ -0,0 +1,24 @@
<?php
namespace Src\Zone\User\Domain\Repositories;
//use Src\Zone\UserEloquentModel\Infrastructure\Repositories\UserDoesNotExistException;
use Src\Zone\User\Domain\Model\User;
use Src\Zone\User\Domain\Model\ValueObjects\Password;
interface UserRepositoryInterface
{
public function findAll(): array;
public function findById(string $userId): User;
public function findByEmail(string $email): User;
public function store(User $user, Password $password): User;
public function update(User $user, Password $password): void;
public function delete(int $user_id): void;
}
@@ -0,0 +1,25 @@
<?php
namespace Src\Zone\User\Infrastructure\EloquentModels\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class PasswordCast implements CastsAttributes
{
/**
* @inheritDoc
*/
public function get($model, string $key, $value, array $attributes)
{
return $value;
}
/**
* @inheritDoc
*/
public function set($model, string $key, $value, array $attributes)
{
return bcrypt($value);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Src\Zone\User\Infrastructure\EloquentModels;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Src\Zone\User\Infrastructure\EloquentModels\Casts\PasswordCast;
use Tymon\JWTAuth\Contracts\JWTSubject;
class UserEloquentModel extends Authenticatable implements JWTSubject
{
use HasApiTokens, Notifiable;
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'avatar',
'password',
'is_admin',
'is_active'
];
public array $rules = [
'name' => 'required',
'email' => 'required',
'avatar' => 'nullable',
'password' => 'confirmed|min:8|nullable',
'is_admin' => 'boolean',
'is_active' => 'boolean',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'created_at',
'updated_at',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'is_active' => 'boolean',
'avatar' => 'string',
'password' => PasswordCast::class
];
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
@@ -0,0 +1,20 @@
<?php
namespace Src\Zone\User\Presentation\HTTP;
use Illuminate\Http\JsonResponse;
use Src\Zone\User\Application\UseCases\Commands\GetRandomAvatarCommand;
use Src\Common\Domain\Exceptions\UnauthorizedUserException;
use Symfony\Component\HttpFoundation\Response;
class GetRandomAvatarController
{
public function __invoke(): JsonResponse
{
try {
return response()->json((new GetRandomAvatarCommand())->execute());
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
}
@@ -0,0 +1,75 @@
<?php
namespace Src\Zone\User\Presentation\HTTP;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Src\Zone\User\Application\Mappers\UserMapper;
use Src\Zone\User\Application\UseCases\Commands\DestroyUserCommand;
use Src\Zone\User\Application\UseCases\Commands\StoreUserCommand;
use Src\Zone\User\Application\UseCases\Commands\UpdateUserCommand;
use Src\Zone\User\Application\UseCases\Queries\FindAllUsersQuery;
use Src\Zone\User\Application\UseCases\Queries\FindUserByIdQuery;
use Src\Zone\User\Domain\Model\ValueObjects\Password;
use Src\Common\Domain\Exceptions\UnauthorizedUserException;
use Src\Common\Infrastructure\Laravel\Controller;
use Symfony\Component\HttpFoundation\Response;
class UserController extends Controller
{
public function index(): JsonResponse
{
try {
return response()->json((new FindAllUsersQuery())->handle());
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
public function show(int $id): JsonResponse
{
try {
return response()->json((new FindUserByIdQuery($id))->handle());
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
public function store(Request $request): JsonResponse
{
try {
$userData = UserMapper::fromRequest($request);
$password = new Password($request->input('password'), $request->input('password_confirmation'));
$user = (new StoreUserCommand($userData, $password))->execute();
return response()->json($user->toArray(), Response::HTTP_CREATED);
} catch (\DomainException $domainException) {
return response()->json(['error' => $domainException->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
public function update(int $user_id, Request $request): JsonResponse
{
try {
$user = UserMapper::fromRequest($request, $user_id);
$password = new Password($request->input('password'), $request->input('password_confirmation'));
(new UpdateUserCommand($user, $password))->execute();
return response()->json($user->toArray(), Response::HTTP_OK);
} catch (\DomainException $domainException) {
return response()->json(['error' => $domainException->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
public function destroy(int $user_id): JsonResponse
{
try {
(new DestroyUserCommand($user_id))->execute();
return response()->json(null, Response::HTTP_NO_CONTENT);
} catch (UnauthorizedUserException $e) {
return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
}
@@ -0,0 +1,17 @@
<?php
use Illuminate\Support\Facades\Route;
use Src\Zone\User\Presentation\HTTP\GetRandomAvatarController;
use Src\Zone\User\Presentation\HTTP\UserController;
Route::group([
'prefix' => 'user'
], function () {
Route::get('random-avatar', GetRandomAvatarController::class);
Route::get('index', [UserController::class, 'index']);
Route::get('{id}', [UserController::class, 'show']);
Route::post('', [UserController::class, 'store']);
Route::put('{id}', [UserController::class, 'update']);
Route::delete('{id}', [UserController::class, 'destroy']);
});
+337
View File
@@ -0,0 +1,337 @@
<?php
namespace Tests\Feature;
use GuzzleHttp\Client;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Src\Zone\User\Application\Mappers\UserMapper;
use Src\Zone\User\Domain\Factories\UserFactory;
use Symfony\Component\HttpFoundation\Response;
use Tests\TestCase;
use Tests\Support\WithLogin;
class UserTest extends TestCase
{
use RefreshDatabase, WithLogin;
protected function setUp(): void
{
parent::setUp();
$this->user_uri = '/user';
$this->index_uri = $this->user_uri . '/index';
$this->random_avatar_uri = $this->user_uri . '/random-avatar';
$this->adminToken = $this->newLoggedAdmin()['token'];
$this->userData = $this->newLoggedUser();
$this->userToken = $this->userData['token'];
}
/** @test */
public function admin_can_retrieve_all_users()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->get($this->index_uri)
->assertStatus(Response::HTTP_OK)
->assertJsonCount($numberUsers + 2); // +2 because of the admin and user
}
/** @test */
public function user_cannot_retrieve_all_users()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->get($this->index_uri)
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertSee(['error' => 'The user is not authorized to access this resource']);
}
/** @test */
public function admin_can_get_specific_user_by_id()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->get($this->user_uri . '/' . $randomUserId)
->assertStatus(Response::HTTP_OK)
->assertJsonStructure(['id', 'name', 'email', 'avatar', 'is_admin', 'is_active']);
}
/** @test */
public function user_cannot_get_specific_user_by_id()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->get($this->user_uri . '/' . $randomUserId)
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertSee(['error' => 'The user is not authorized to access this resource']);
}
/** @test */
public function admin_can_create_a_user()
{
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => 'https://doodleipsum.com/300/avatar-2?shape=circle',
'password' => $password,
'password_confirmation' => $password,
];
$expectedResponse = [
'id' => 3,
'name' => $requestBody['name'],
'email' => $requestBody['email'],
'is_admin' => false,
'is_active' => true
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->post($this->user_uri, $requestBody)
->assertStatus(Response::HTTP_CREATED)
->assertJson($expectedResponse);
// Assert cannot create user with same email
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->post($this->user_uri, $requestBody)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'El email ya está en uso']);
}
/** @test */
public function user_cannot_create_user()
{
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => 'https://doodleipsum.com/300/avatar-2?shape=circle',
'password' => $password,
'password_confirmation' => $password,
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->post($this->user_uri, $requestBody)
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertSee(['error' => 'The user is not authorized to access this resource']);
}
/** @test */
public function cannot_create_user_with_invalid_email()
{
$password = $this->faker->password(8);
$requestBodyInvalidEmail = [
'name' => $this->faker->name,
'email' => 'invalid-email',
'avatar' => 'https://doodleipsum.com/300/avatar-2?shape=circle',
'password' => $password,
'password_confirmation' => $password,
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->post($this->user_uri, $requestBodyInvalidEmail)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'Must be a valid email']);
}
/** @test */
public function cannot_create_user_with_invalid_password()
{
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => 'https://doodleipsum.com/300/avatar-2?shape=circle',
'password' => $password,
'password_confirmation' => $password,
];
$requestBodyInvalidPassword = $requestBody;
$requestBodyInvalidPassword['password'] = '1234';
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->post($this->user_uri, $requestBodyInvalidPassword)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'The password needs to be at least 8 characters long']);
$requestBodyNoPasswordConfirmation = $requestBody;
unset($requestBodyNoPasswordConfirmation['password_confirmation']);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->post($this->user_uri, $requestBodyNoPasswordConfirmation)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'Passwords do not match']);
}
/** @test */
public function admin_can_update_any_user()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => false,
'is_active' => false,
'update_avatar' => true,
'password' => $password,
'password_confirmation' => $password,
];
$expectedResponse = [
'id' => $randomUserId,
'name' => $requestBody['name'],
'email' => $requestBody['email'],
'avatar' => false,
'is_admin' => false,
'is_active' => false
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->put($this->user_uri . '/' . $randomUserId, $requestBody)
->assertStatus(Response::HTTP_OK)
->assertJson($expectedResponse);
}
/** @test */
public function user_cannot_update_any_user_except_itself()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(4, 4 + $numberUsers);
// Update another user
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => false,
'is_active' => false,
'update_avatar' => true,
'password' => $password,
'password_confirmation' => $password,
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->put($this->user_uri . '/' . $randomUserId, $requestBody)
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertSee(['error' => 'The user is not authorized to access this resource']);
// Update itself
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->userData['email'],
'avatar' => false,
'is_active' => true,
'update_avatar' => false,
'password' => $password,
'password_confirmation' => $password,
];
$expectedResponse = [
'id' => $this->userData['id'],
'name' => $requestBody['name'],
'email' => $requestBody['email'],
'avatar' => $requestBody['avatar'],
'is_active' => $requestBody['is_active']
];
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->put($this->user_uri . '/' . $this->userData['id'], $requestBody)
->assertStatus(Response::HTTP_OK)
->assertJson($expectedResponse);
}
/** @test */
public function cannot_update_user_with_invalid_password()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$password = $this->faker->password(8);
$requestBody = [
'name' => $this->faker->name,
'email' => $this->faker->safeEmail,
'avatar' => 'https://doodleipsum.com/300/avatar-2?shape=circle',
'is_active' => false,
'update_avatar' => true,
'password' => $password,
'password_confirmation' => $password,
];
$requestBodyInvalidPassword = $requestBody;
$requestBodyInvalidPassword['password'] = '1234';
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->put($this->user_uri . '/' . $randomUserId, $requestBodyInvalidPassword)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'The password needs to be at least 8 characters long']);
$requestBodyNoPasswordConfirmation = $requestBody;
unset($requestBodyNoPasswordConfirmation['password_confirmation']);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->put($this->user_uri . '/' . $randomUserId, $requestBodyNoPasswordConfirmation)
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJson(['error' => 'Passwords do not match']);
}
/** @test */
public function admin_can_delete_a_user()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->delete($this->user_uri . '/' . $randomUserId)
->assertStatus(Response::HTTP_NO_CONTENT);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->get($this->user_uri . '/' . $randomUserId)
->assertStatus(Response::HTTP_NOT_FOUND);
}
/** @test */
public function user_cannot_delete_a_user()
{
$numberUsers = $this->faker->numberBetween(1, 10);
$this->createRandomUsers($numberUsers);
$randomUserId = $this->faker->numberBetween(1, $numberUsers);
$this->withHeaders(['Authorization' => 'Bearer ' . $this->userToken])
->delete($this->user_uri . '/' . $randomUserId)
->assertStatus(Response::HTTP_UNAUTHORIZED)
->assertSee(['error' => 'The user is not authorized to access this resource']);
}
/** @test */
public function cannot_delete_user_if_does_not_exists()
{
$this->withHeaders(['Authorization' => 'Bearer ' . $this->adminToken])
->delete($this->user_uri . '/' . 999)
->assertStatus(Response::HTTP_NOT_FOUND);
}
private function createRandomUsers($usersNumber = 1): void
{
foreach (range(1, $usersNumber) as $_) {
$user = UserFactory::new();
$userEloquentModel = UserMapper::toEloquent($user);
$userEloquentModel->password = $this->faker->password(8);
$userEloquentModel->save();
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Tests\Integration;
use GuzzleHttp\Client;
use Tests\TestCase;
class DoodleAPITest extends TestCase
{
/** @test */
public function get_doodle_avatar()
{
$url = 'https://doodleipsum.com/300/avatar-2?shape=circle';
$response = (new Client())->get($url);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('image/png', $response->getHeader('Content-Type')[0]);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Tests\Support;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Testing\TestResponse;
use Src\Zone\User\Application\Mappers\UserMapper;
use Src\Zone\User\Domain\Factories\UserFactory;
trait WithLogin
{
use WithFaker;
/**
* Create a new user instance.
*/
private function validCredentials(array $attributes = null): array
{
$password = $this->faker->password(8);
$user = UserFactory::new($attributes);
$userEloquentModel = UserMapper::toEloquent($user);
$userEloquentModel->password = $password;
$userEloquentModel->save();
return [
'id' => $userEloquentModel->id,
'email' => $user->email,
'password' => $password,
];
}
private function newLoggedAdmin(): array
{
$credentials = $this->validCredentials(['is_admin' => true]);
$response = $this->post('auth/login', $credentials);
return ['token' => $this->getToken($response), ...$credentials];
}
private function newLoggedUser(): array
{
$credentials = $this->validCredentials(['is_admin' => false]);
$response = $this->post('auth/login', $credentials);
return ['token' => $this->getToken($response), ...$credentials];
}
private function getToken(TestResponse $response)
{
$arResponse = json_decode($response->getContent(), true);
return $arResponse['accessToken'];
}
}