diff --git a/.env.example b/.env.example index e1a8ffde..9c4580ad 100755 --- a/.env.example +++ b/.env.example @@ -52,3 +52,5 @@ INSTAGRAM_REDIRECT_URI= MAILGUN_DOMAIN= MAILGUN_SECRET= + +GH_PAT= diff --git a/.github/workflows/feature-test.yml b/.github/workflows/feature-test.yml index c4c714ff..c3bba071 100755 --- a/.github/workflows/feature-test.yml +++ b/.github/workflows/feature-test.yml @@ -1,4 +1,4 @@ -name: Feature test +name: Run Feature tests on: push: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9c537192..f426c044 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,4 +1,4 @@ -name: Static analysis +name: Run Static analysis on: push: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 6b174ae4..760ebdb1 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -1,4 +1,4 @@ -name: Unit test +name: Run Unit tests on: push: diff --git a/Dockerfile b/Dockerfile index 5215e3f0..29b61cf4 100755 --- a/Dockerfile +++ b/Dockerfile @@ -42,5 +42,8 @@ WORKDIR /var/www USER $user +RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/php.ini +RUN echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/php.ini +RUN echo "xdebug.idekey=PHPSTORM" >> /usr/local/etc/php/conf.d/php.ini RUN echo "memory_limit=1024M" >> /usr/local/etc/php/conf.d/php.ini RUN echo "allow_url_fopen=on" >> /usr/local/etc/php/conf.d/php.ini diff --git a/Makefile b/Makefile index c47a7040..2ed3b5ce 100755 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ db_migrate: ## run db migrations db_schemefy: ## Display the db schema in table format @php artisan schema:show -setup: composer generate_key +setup: composer generate_key jwt_key db_connection copy_env: #todo: figure out a way to not override env vars if file_exists already @cp .env.example .env @@ -38,6 +38,12 @@ composer: ## Install project dependencies generate_key: ## Generate APP_KEY and set in .env @docker-compose exec app php artisan key:generate +jwt_key: ## Generate JWT_SECRET and set in .env + @docker-compose exec app php artisan jwt:secret + +db_connection: ## Generate DB Connection details and set in .env + @docker-compose exec app php artisan db:connection + login: ## Creates a new user/token or generate new token for given user @php artisan auth:token diff --git a/readme.md b/README.md similarity index 53% rename from readme.md rename to README.md index 012875e7..107ea5dd 100755 --- a/readme.md +++ b/README.md @@ -61,3 +61,36 @@ Examples: ![Alt text](docs/images/help.png?raw=true "help") +## Setup PHPStorm + + Docker + Xdebug + postman +- Open settings by pressing `(cmd + ,)` button +- Under PHP, add a new CLI interpreter + +![add-new-cli-interpreter1.png](docs%2Fimages%2Fadd-new-cli-interpreter1.png) + +- Select From Docker, Vagrant ... option +- Select Docker Compose, set the configuration file to ./docker-compose.yml and the service select app +![add-new-cli-interpreter2.png](docs%2Fimages%2Fadd-new-cli-interpreter2.png) + +- Now your PHP interpreter settings hosuld look like this +![xdebug-php.png](docs%2Fimages%2Fxdebug-php.png) + +- Next, you want to set up the test framework, click the plus sign and select PHPUnit by remote interpreter +![test-framework1.png](docs%2Fimages%2Ftest-framework1.png) + +- Select the interpreter you just created from the dropdown list +![test-framework2.png](docs%2Fimages%2Ftest-framework2.png) + +- Now you can start debugging, set a break point in any controller class and run the test associated with it in debug mode + +### Listening for requests from postman +- First step is to set up a server +![server.png](docs%2Fimages%2Fserver.png) +- On postman add this parameter. When postman detects this in a request, it creates a cookie with the value of XDEBUG_SESSION_START. This has an expiry time of 30 minutes so you dont have to include it in your requests all the time. + +```angular2html +XDEBUG_SESSION_START=PHPSTORM +``` +- Finally, tell postman to listen for PHP Debug Connections +![php-debug-connections.png](docs%2Fimages%2Fphp-debug-connections.png) + +- Set a break point in the code called by the endpoint you are consuming on postman, hit send to start debugging diff --git a/app/Console/Commands/DatabaseConnectionDetails.php b/app/Console/Commands/DatabaseConnectionDetails.php new file mode 100644 index 00000000..084f0952 --- /dev/null +++ b/app/Console/Commands/DatabaseConnectionDetails.php @@ -0,0 +1,75 @@ + "ao9moanwus0rjiex.cbetxkdyhwsb.us-east-1.rds.amazonaws.com", + "DB_PORT" => "3306", + "DB_DATABASE" => "athiftsxpmxaj82c", + "DB_USERNAME" => "w7dydvcjsog985xj", + "DB_PASSWORD" => "iliqkyv8vbbtw603" + ]; + + $filepath = $this->envPath(); + + foreach($connection as $key => $value) { + $fileContents = $this->getFileContents($filepath); + + if (Str::contains($fileContents, $key)) { + $this->putFileContents( + $filepath, + preg_replace( + "/{$key}=.*/", + "{$key}={$value}", + $fileContents + ) + ); + } + } + + $this->info("db connection details set successfully."); + } + + protected function envPath(): string + { + if (method_exists($this->laravel, 'environmentFilePath')) { + return $this->laravel->environmentFilePath(); + } + + return $this->laravel->basePath('.env'); + } + + protected function getFileContents(string $filepath): string + { + return file_get_contents($filepath); + } + + protected function putFileContents(string $filepath, string $data): void + { + file_put_contents($filepath, $data); + } +} diff --git a/app/Console/Commands/LoginCommand.php b/app/Console/Commands/LoginCommand.php index 2649a4d2..ba9bc5b5 100755 --- a/app/Console/Commands/LoginCommand.php +++ b/app/Console/Commands/LoginCommand.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; +use App\Models\User; use App\Services\AuthService; use App\Services\UserService; use Illuminate\Console\Command; @@ -39,17 +40,19 @@ public function handle(UserService $userService, AuthService $authService) $fromCache = Cache::get('testUser'); + $email = Str::random(5) . '@console.com'; + if (!$fromCache) { $this->line('User not found in Cache, creating new User ...'); $this->line('=============================================='); $response = $userService->store(new Request([ 'name' => 'test user', - 'email' => Str::random(5) . '@console.com', + 'email' => $email, 'password' => 'testing123' ])); - $user = json_decode($response->getContent(), true)["response"]["data"]; + $user = User::where('email', '=', $email)->first(); Cache::put('testUser', $user); } @@ -61,7 +64,7 @@ public function handle(UserService $userService, AuthService $authService) 'password' => 'testing123' ])); - $this->info($token->getContent()); + $this->info($token); $this->line('===================================='); $this->info("Here you go! Use this token to access protected resources.!"); diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index a39f1395..73b3db7b 100755 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -17,6 +17,7 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; use Tymon\JWTAuth\Exceptions\JWTException; /** @@ -36,21 +37,27 @@ public function __construct(AuthService $service) $this->service = $service; } - /** - * @param SignInRequest $request - * @return \Illuminate\Http\JsonResponse - */ public function login(SignInRequest $request): \Illuminate\Http\JsonResponse { - return $this->service->login($request); + if (!$token = $this->service->login($request)) { + return response()->json( + [ + 'Not found or Invalid Credentials.', + ], ResponseAlias::HTTP_NOT_FOUND + ); + } + + return $this->successResponse(['token' => $token]); } /** - * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse|Response */ public function logout() { - return $this->service->logout(); + return $this->service->logout() ? + $this->noContentResponse() : + $this->errorResponse(['Not found or Invalid Credentials.']); } /** @@ -69,7 +76,7 @@ public function loginViaMagicLink(Request $request, LocationService $locationSer 'required' => [ 'email' => 'Looks like this is your first time signing in with magiclink! Kindly provide your registered email for verification.', ] - ], Response::HTTP_UNPROCESSABLE_ENTITY); + ], ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); } try { @@ -83,7 +90,7 @@ public function loginViaMagicLink(Request $request, LocationService $locationSer ] ]); - return response()->json($locationService->getErrors(), Response::HTTP_UNAUTHORIZED); + return response()->json($locationService->getErrors(), ResponseAlias::HTTP_UNAUTHORIZED); } $locationUserEmail = $location->getUser()->email; @@ -95,7 +102,7 @@ public function loginViaMagicLink(Request $request, LocationService $locationSer ] ]); - return response()->json($locationService->getErrors(), Response::HTTP_UNAUTHORIZED); + return response()->json($locationService->getErrors(), ResponseAlias::HTTP_UNAUTHORIZED); } else { $location->update([ 'ip' => $request->ipinfo->ip, @@ -124,7 +131,7 @@ public function loginViaMagicLink(Request $request, LocationService $locationSer } catch (\Throwable $e) { $m = array_merge($locationService->getErrors(), [$e->getMessage()]); - return response()->json($m, Response::HTTP_UNAUTHORIZED); + return response()->json($m, ResponseAlias::HTTP_UNAUTHORIZED); } } @@ -263,7 +270,7 @@ public function tikTokHandleCallback(Request $request, Client $client, UserServi public function validateToken(Request $request) { if (!$request->bearerToken() || !Auth::check()) { - throw new JWTException('Expired or Tnvalid token.'); + throw new JWTException('Expired or Invalid token.'); } return response()->json( diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index f6a32cd8..4f934ef9 100755 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -16,9 +16,8 @@ class CommentController extends Controller { public function addComment(Request $request) { - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line */ if ($user = JWTAuth::parseToken()->user()) { - $payload = $request->only([ 'resource-type', 'resource-id', 'comment' ]); @@ -44,11 +43,13 @@ public function addComment(Request $request) } } } + + return $this->unauthorizedResponse(); } public function destroyComment(Request $request) { - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line */ if ($user = JWTAuth::parseToken()->user()) { $payload = $request->only(['comment-id']); $comment = Comment::findOrFail($request->only(['comment-id']))->first(); @@ -56,8 +57,10 @@ public function destroyComment(Request $request) if ($user->isSuper() || $user->ownsComment($payload['comment-id'])) { return response()->json(['deleted' => $comment->delete()]); } else { - throw new ApiException('You are not suthorized to perfrom this action.'); + throw new ApiException('You are not authorized to perform this action.'); } } + + return $this->unauthorizedResponse(); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 70ebcf20..db73e1f3 100755 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -9,6 +9,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\Response; use Illuminate\Routing\Controller as BaseController; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; class Controller extends BaseController { @@ -16,9 +17,9 @@ class Controller extends BaseController use AuthorizesRequests, DispatchesJobs, ValidatesRequests; - public function successResponse(array $data = []): \Illuminate\Http\JsonResponse + public function successResponse(array $data = [], $code = ResponseAlias::HTTP_OK): \Illuminate\Http\JsonResponse { - return response()->json($data); + return response()->json($data, $code); } public function noContentResponse(): Response @@ -28,13 +29,13 @@ public function noContentResponse(): Response public function errorResponse(array $data = []): \Illuminate\Http\JsonResponse { - return response()->json($data, Response::HTTP_BAD_REQUEST); + return response()->json($data, ResponseAlias::HTTP_BAD_REQUEST); } public function unauthorizedResponse(): \Illuminate\Http\JsonResponse { return response()->json([ 'error' => 'Your login session has expired. Please login.' - ], Response::HTTP_UNAUTHORIZED); + ], ResponseAlias::HTTP_UNAUTHORIZED); } } diff --git a/app/Http/Controllers/CookbookController.php b/app/Http/Controllers/CookbookController.php index 2901664e..9159b84c 100755 --- a/app/Http/Controllers/CookbookController.php +++ b/app/Http/Controllers/CookbookController.php @@ -13,8 +13,8 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Routing\ResponseFactory; use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; /** * Class UserController @@ -34,23 +34,21 @@ public function __construct(CookbookService $service) } /** - * All cookbooks - * @return JsonResponse + * Get all cookbooks */ public function index(): JsonResponse { - return $this->service->index(); + return response()->json(['data' => $this->service->index()]); } /** * @param mixed $id - * @return Response|ResponseFactory - * + * @return JsonResponse * @throws CookbookModelNotFoundException */ - public function show($id) + public function show(mixed $id): JsonResponse { - return $this->service->show($id); + return response()->json(['data' => $this->service->show($id)]); } /** @@ -59,7 +57,7 @@ public function show($id) */ public function myCookbooks(Request $request): JsonResponse { - return $this->service->index($request->get('user_id')); + return response()->json(['data' => $this->service->index($request->get('user_id'))]); } /** @@ -67,61 +65,59 @@ public function myCookbooks(Request $request): JsonResponse * @return JsonResponse * @throws Exception */ - public function store(CookbookStoreRequest $request) + public function store(CookbookStoreRequest $request): JsonResponse { - try { - //todo: creation of cookbooks will not be publicly accessible until later releases -// if (!Auth::user()->isEarlyBird()) { -// throw new UnauthorizedException("You are not authorized to perform this action."); -// } - - $request->merge([ - 'user_id' => Auth::user()->id, - 'alt_text' => $request->get("alt_text") ?? 'cookbook cover image', - 'flag_id' => Flag::where(["flag" => $request->get("flag_id")])->first()->getKey(), - 'tags' => $request->get("tags") ?? "" + $request->merge([ + 'user_id' => Auth::user()->id, + 'alt_text' => $request->get("alt_text") ?? 'cookbook cover image', + 'flag_id' => Flag::where(["flag" => $request->get("flag_id")])->first()->getKey(), + 'tags' => $request->get("tags") ?? "" + ]); + + if ($this->service->store($request)) { + return $this->successResponse([ + 'response' => [ + 'created' => true, + 'data' => [] + ] ]); - - return $this->service->store($request); - - } catch (Exception $exception) { - return response()->json([ - 'error' => $exception->getMessage() - ], Response::HTTP_BAD_REQUEST); } + + return $this->errorResponse([ + 'error'=> 'There was an error processing this request, please try again.' + ]); } /** * @param int $id * @param Request $request - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|JsonResponse|Response + * @return JsonResponse * @throws CookbookModelNotFoundException - * @throws \Tymon\JWTAuth\Exceptions\JWTException */ - public function update(int $id, Request $request) + public function update(int $id, Request $request): JsonResponse { - if (Auth::user()->ownCookbook($id)) { - return $this->service->update($request, (string) $id); + if (Auth::user()->ownCookbook($id) && $this->service->update($request, (string) $id)) { + return $this->successResponse(['updated' => true]); } return response()->json([ 'error' => 'You are not authorized to access this resource.' - ], 401); + ], ResponseAlias::HTTP_UNAUTHORIZED); } /** * @param $cookbookId - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|JsonResponse|Response + * @return JsonResponse|Response * @throws CookbookModelNotFoundException */ - public function destroy($cookbookId) + public function destroy($cookbookId): JsonResponse|Response { - if (Auth::user()->ownCookbook($cookbookId)) { - return $this->service->delete($cookbookId); + if (Auth::user()->ownCookbook($cookbookId) && $this->service->delete($cookbookId)) { + return $this->noContentResponse(); } return response()->json([ 'error' => 'You are not authorized to perform this action.' - ], 401); + ], ResponseAlias::HTTP_UNAUTHORIZED); } } diff --git a/app/Http/Controllers/RecipeController.php b/app/Http/Controllers/RecipeController.php index 703e2a15..f5c6af6f 100755 --- a/app/Http/Controllers/RecipeController.php +++ b/app/Http/Controllers/RecipeController.php @@ -4,19 +4,21 @@ namespace App\Http\Controllers; +use AllowDynamicProperties; use App\Http\Requests\RecipeStoreRequest; use App\Models\Recipe; use App\Services\RecipeService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; +use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\JWT; /** * Class UserController */ -class RecipeController extends Controller +#[AllowDynamicProperties] class RecipeController extends Controller { protected RecipeService $service; @@ -33,26 +35,30 @@ public function __construct(RecipeService $service) } /** - * Get all recipes belonging to a user - * - * @return \Illuminate\Http\JsonResponse + * @return JsonResponse */ - public function index(): \Illuminate\Http\JsonResponse + public function index() { - return $this->service->index(); + return $this->successResponse(['data' => $this->service->index()]); } /** * @param $recipeId - * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object + * @return JsonResponse * @throws \App\Exceptions\CookbookModelNotFoundException */ - public function show($recipeId) + public function show($recipeId): JsonResponse { - return $this->service->show($recipeId); + return $this->successResponse(['data' => $this->service->show($recipeId)]); } - public function addClap(Request $request) + /** + * @param Request $request + * @return JsonResponse + * @throws \App\Exceptions\CookbookModelNotFoundException + * @throws \Illuminate\Validation\ValidationException + */ + public function addClap(Request $request): JsonResponse { $this->validate( $request, [ @@ -60,36 +66,51 @@ public function addClap(Request $request) ] ); - return $this->service->addClap($request->get('recipe_id')); + return ($recipe = $this->service->addClap($request->get('recipe_id'))) ? + /** @phpstan-ignore-next-line */ + $this->successResponse(['updated' => true, 'claps' => $recipe->claps]) : + $this->errorResponse(['error' => 'There was an error processing this request. Please try again.']); } - public function myRecipes(Request $request, JWT $jwtAuth): \Illuminate\Http\JsonResponse + /** + * @param Request $request + * @param JWT $jwtAuth + * @return JsonResponse + * @throws JWTException + */ + public function myRecipes(Request $request, JWT $jwtAuth): JsonResponse { if ($jwtAuth->parseToken()->check()) { - return $this->service->index($request->get('user_id')); + return $this->successResponse([ + 'data' => $this->service->index($request->get('user_id')) + ]); } return response()->json([ 'error', 'You are not authorized to access this resource.' - ], 401); + ], ResponseAlias::HTTP_UNAUTHORIZED); } public function store(RecipeStoreRequest $request, JWT $jwtAuth) { try { $jwtAuth->parseToken()->check(); - return $this->service->store($request); + + return $this->service->store($request) ? + $this->successResponse(['created' => true],ResponseAlias::HTTP_CREATED) : + $this->errorResponse(['created' => false]); + } catch (\Exception $exception) { - Log::debug('An error occured while creating this recipe', [ + Log::debug('An error occurred while creating this recipe', [ 'resource' => self::RECIPE_RESOURCE, 'exception' => $exception ]); $message = "There was an error processing this request, please try again later."; - $code = Response::HTTP_BAD_REQUEST; + $code = ResponseAlias::HTTP_BAD_REQUEST; if ($exception->getCode() == 401) { - $code = Response::HTTP_UNAUTHORIZED; + $code = ResponseAlias::HTTP_UNAUTHORIZED; $message = "You are not authorized to perform this action."; } @@ -101,33 +122,32 @@ public function store(RecipeStoreRequest $request, JWT $jwtAuth) public function update(Request $request, $recipeId, JWT $jwtAuth) { - if ( - $request->user()->ownRecipe($recipeId) - ) { - if ( - $jwtAuth->parseToken()->check() - ) { - return $this->service->update($request, $recipeId); + if ($jwtAuth->parseToken()->check() && $request->user()->ownRecipe($recipeId)) { + if ($this->service->update($request, $recipeId)) { + return $this->successResponse(['updated' => true]); } + + return $this->errorResponse(['updated' => false]); } return response()->json([ 'error' => 'You are not authorized to access this resource.' - ], 401); + ], ResponseAlias::HTTP_UNAUTHORIZED); } public function destroy(Request $request, $recipeId, JWT $jwtAuth) { - if ( - $jwtAuth->parseToken()->check() && - $request->user()->isSuper() - ) { - return $this->service->delete($request->user(), $recipeId); + if ($jwtAuth->parseToken()->check() && $request->user()->isSuper()) { + if ($this->service->delete($request->user(), $recipeId)) { + return $this->successResponse(['deleted' => true]); + } + + return $this->errorResponse(['deleted' => false]); } return response()->json([ 'error' => 'You are not authorized to perform this action.' - ], Response::HTTP_UNAUTHORIZED); + ], ResponseAlias::HTTP_UNAUTHORIZED); } public function report(Request $request, JWT $jwtAuth): JsonResponse @@ -155,6 +175,6 @@ public function report(Request $request, JWT $jwtAuth): JsonResponse return response()->json([ 'error' => 'You are not authorized to perform this action.' - ], Response::HTTP_UNAUTHORIZED); + ], ResponseAlias::HTTP_UNAUTHORIZED); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 4200d421..6ab22e2b 100755 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -7,9 +7,9 @@ use App\Http\Requests\SearchRequest; use App\Services\SearchService; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Validator; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; class SearchController extends Controller { @@ -132,7 +132,7 @@ public function getSearchResults(Request $request): \Illuminate\Http\JsonRespons return response()->json([ 'error', 'Your login session has expired. Please login.' - ], Response::HTTP_UNAUTHORIZED); + ], ResponseAlias::HTTP_UNAUTHORIZED); } if (str_starts_with($searchQuery, ":me|for-you")) { @@ -144,7 +144,7 @@ public function getSearchResults(Request $request): \Illuminate\Http\JsonRespons return response()->json([ 'error', 'Your login session has expired. Please login.' - ], Response::HTTP_UNAUTHORIZED); + ], ResponseAlias::HTTP_UNAUTHORIZED); } if ($searchQuery === "cookbooks") { diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index af760920..ef524a06 100755 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -6,6 +6,7 @@ use App\Models\Subscriber; use Illuminate\Http\Response; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; class SubscriptionController extends Controller { @@ -31,7 +32,7 @@ public function store(\Illuminate\Http\Request $request) 'created' => true, 'data' => $subscriber, ], - ], Response::HTTP_CREATED + ], ResponseAlias::HTTP_CREATED ); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 748fc28d..f35df16d 100755 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers; -use App\Exceptions\ApiException; use App\Http\Requests\UserStoreRequest; use App\Http\Requests\UserUpdateRequest; use App\Mail\OtpWasGenerated; @@ -16,11 +15,13 @@ use App\Services\TikTok\Videos; use App\Services\UserService; use Ichtrojan\Otp\Otp; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; use Tymon\JWTAuth\Facades\JWTAuth; +use Tymon\JWTAuth\JWT; class UserController extends Controller { @@ -31,32 +32,50 @@ public function __construct(UserService $service) $this->service = $service; } - public function index() + /** + * @return \Illuminate\Http\JsonResponse + */ + public function index(): JsonResponse { - return $this->service->index(); + return $this->successResponse(['data' => $this->service->index()]); } - public function store(UserStoreRequest $request): \Illuminate\Http\JsonResponse + /** + * @param UserStoreRequest $request + * @return JsonResponse + */ + public function store(UserStoreRequest $request) { - return $this->service->store($request); + return $this->service->store($request) ? $this->successResponse( + [ + 'response' => [ + 'created' => true, + 'data' => [], + 'status' => 'success' + ] + ], + ResponseAlias::HTTP_CREATED + ) : $this->errorResponse(['error' => 'There was an error processing this request. Please try again.']); } public function show($username) { - return $this->service->show($username); + return $this->successResponse(['data' => ['user' => $this->service->show($username)]]); } public function update($username, UserUpdateRequest $request) { if ($request->all()) { - $request->merge(['username']); + $request->merge(['username' => $username]); + + if ($this->service->update($request, $username)) { + return $this->successResponse(['updated' => true, 'status' => 'success']); + } - return $this->service->update($request, $username); + return $this->errorResponse(['updated' => false, 'status' => 'failed']); } - return response()->json([ - 'message' => 'nothing to update.', - ]); + return response()->json(['message' => 'nothing to update.',]); } public function followUser(Request $request) @@ -83,14 +102,14 @@ public function followUser(Request $request) $following->save(); - return response()->json($this->getWhoToFollowData($user), Response::HTTP_OK); + return response()->json($this->getWhoToFollowData($user), ResponseAlias::HTTP_OK); } - return response()->noContent(Response::HTTP_OK); + return response()->noContent(ResponseAlias::HTTP_OK); } } - return response()->json(['error', 'Bad request.'], Response::HTTP_BAD_REQUEST); + return response()->json(['error', 'Bad request.'], ResponseAlias::HTTP_BAD_REQUEST); } return $this->unauthorizedResponse(); @@ -101,14 +120,11 @@ public function followUser(Request $request) * The logic to get who to follow is undecided yet * For now, this just returns the latest five unfollowed users in the database */ - public function getWhoToFollow() + public function getWhoToFollow(Request $request, JWT $jwtAuth) { - /** @phpstan-ignore-next-line */ - if ($user = JWTAuth::parseToken()->user()) { - return $this->getWhoToFollowData($user); - } - - return $this->unauthorizedResponse(); + return ($jwtAuth->parseToken()->check()) ? + $this->getWhoToFollowData($request->user()) : + $this->unauthorizedResponse(); } private function getWhoToFollowData(User $user) diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index 0607de90..91be171a 100755 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -5,36 +5,27 @@ namespace App\Services; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; class AuthService { - /** - * Authenticate the user - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function login(Request $request): \Illuminate\Http\JsonResponse + public function login(Request $request) { if (!$token = Auth::attempt($request->only('email', 'password'))) { - return response()->json( - [ - 'Not found or Invalid Credentials.', - ], Response::HTTP_NOT_FOUND - ); + return false; } - return response()->json(['token' => $token], Response::HTTP_OK); + return $token; } - public function logout(): \Illuminate\Http\JsonResponse|Response + /** + * @return bool + */ + public function logout() { try { Auth::logout(); - return response()->noContent(); } catch (\Exception $exception) { Log::info( 'Not found or Invalid Credentials.', @@ -43,11 +34,9 @@ public function logout(): \Illuminate\Http\JsonResponse|Response ] ); - return response()->json( - [ - 'Not found or Invalid Credentials.' - ], Response::HTTP_BAD_REQUEST - ); + return false; } + + return true; } } diff --git a/app/Services/CookbookService.php b/app/Services/CookbookService.php index cc645203..cfd62cb8 100755 --- a/app/Services/CookbookService.php +++ b/app/Services/CookbookService.php @@ -10,7 +10,7 @@ use App\Models\Cookbook; use App\Utils\DbHelper; use Illuminate\Http\Request; -use Illuminate\Http\Response; +use Illuminate\Support\Facades\DB; /** * Class CookbookService @@ -22,48 +22,41 @@ public function __construct() $this->serviceModel = new Cookbook(); } - /** - * Return all cookbooks - */ public function index($user_id = null) { - $cookbooks = Cookbook::with([ - 'categories', - 'flag', - 'recipes', - 'users', - ]); + $cookbooks = DB::table('cookbooks')->get(); - if ($user_id) { - return response()->json( - [ - 'data' => $cookbooks - ->where('user_id', '=', $user_id) - ->take(15) - ->orderByDesc('created_at') - ->get(), - ], Response::HTTP_OK - ); + if ($user_id !== null) { + return $cookbooks->where('user_id', '=', $user_id); } - return response()->json( - [ - 'data' => $cookbooks->take(15) - ->orderByDesc('created_at') - ->get(), - ], Response::HTTP_OK - ); + return $cookbooks->map(function($cookbook) { + $category_ids = DB::table('category_cookbook') + ->where('cookbook_id', '=', $cookbook->id) + ->pluck('category_id'); + $categories = DB::table('categories')->whereIn('id', $category_ids->toArray())->get(); + + $flag = DB::table('flags')->where('id', '=', $cookbook->flag_id)->pluck('flag', 'nationality'); + $recipes = DB::table('recipes')->where('cookbook_id', '=', $cookbook->id)->get(); + $user_ids = DB::table('cookbook_user')->where('cookbook_id', '=', $cookbook->id)->pluck('user_id'); + $users = DB::table('users')->whereIn('id', $user_ids->toArray())->get(); + + $cookbook->categories = $categories; + $cookbook->flag = $flag; + $cookbook->recipes = $recipes; + $cookbook->recipes_count = $recipes->count(); + $cookbook->users = $users; + $cookbook->author = DB::table('users')->where('id', $cookbook->user_id)->get(); + + return $cookbook; + }); } /** - * Create cookbook resource - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - * - * @throws \Exception + * @param Request $request + * @return bool */ - public function store(Request $request): \Illuminate\Http\JsonResponse + public function store(Request $request): bool { $categories = explode(",", $request->get('categories')); $categories = Category::whereIn('slug', $categories)->pluck('id')->toArray(); @@ -90,45 +83,32 @@ public function store(Request $request): \Illuminate\Http\JsonResponse $cookbook->categories()->attach($category); } - return response()->json( - [ - 'response' => [ - 'created' => true, - 'data' => $cookbook, - ], - ], Response::HTTP_CREATED - ); + return true; } - return response()->json( - [ - 'error' => 'There was an error prcessing this request, please try again.' - ], Response::HTTP_BAD_REQUEST - ); + return false; } /** - * Update cookbook resource - * * @param $request * @param string $id - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|Response + * @return bool|int * @throws CookbookModelNotFoundException */ - public function update($request, string $id) + public function update($request, string $id): bool|int { $cookbook = $this->findWhere($id); $data = $request->only([ - 'name', 'description', 'bookCoverImg', 'categories', 'alt_text', 'tags' + 'name', 'description', 'bookCoverImg', 'categories', 'alt_text', 'tags', 'slug' ]); if (isset($data['tags'])) { - $exisintgTags = $cookbook->tags; + $existingTags = $cookbook->tags; - if ($exisintgTags) { - $exisintgTags = array_merge($exisintgTags, explode(",", $data["tags"])); - $data["tags"] = array_unique($exisintgTags); + if ($existingTags) { + $existingTags = array_merge($existingTags, explode(",", $data["tags"])); + $data["tags"] = array_unique($existingTags); } } @@ -146,34 +126,23 @@ public function update($request, string $id) } } - return response( - [ - 'updated' => $cookbook->update($data), - ], Response::HTTP_OK - ); + return $cookbook->update($data); } /** - * Delete Cookbook resource - * - * @param int $id identofier - * + * @param $id + * @return bool|null * @throws CookbookModelNotFoundException */ - public function delete($id) + public function delete($id): bool|null { $cookbook = $this->findWhere($id); - return response( - [ - 'deleted' => $cookbook->delete(), - ], Response::HTTP_ACCEPTED - ); + return $cookbook->delete(); } /** * @param mixed $option - * * @throws CookbookModelNotFoundException */ public function show($option) @@ -184,16 +153,10 @@ public function show($option) throw new CookbookModelNotFoundException(); } - return response( - [ - 'data' => $cookbook, - ], Response::HTTP_OK - ); + return $cookbook; } /** - * Find cookbook record - * * @param $q * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object * diff --git a/app/Services/RecipeService.php b/app/Services/RecipeService.php index 6ed353e9..0e2585e9 100755 --- a/app/Services/RecipeService.php +++ b/app/Services/RecipeService.php @@ -16,12 +16,11 @@ use App\Utils\DbHelper; use App\Utils\IngredientMaker; use Carbon\Carbon; -use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; /** * Class RecipeService @@ -33,11 +32,7 @@ public function __construct() $this->serviceModel = new Recipe(); } - /** - * @param $user_id - * @return \Illuminate\Http\JsonResponse - */ - public function index($user_id = null): \Illuminate\Http\JsonResponse + public function index($user_id = null) { $recipes = Recipe::paginate(100); @@ -45,39 +40,22 @@ public function index($user_id = null): \Illuminate\Http\JsonResponse return !$recipe->is_draft; }); - if ($user_id) { - return response()->json( - [ - 'data' => $recipes->where('user_id', '=', $user_id), - ], Response::HTTP_OK - ); - } - - return response()->json(['data' => $recipes]); + return $user_id ? $recipes->where('user_id', '=', $user_id) : $recipes; } /** - * Retrieve one Recipe - * * @param $id - * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model - * + * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object|null * @throws CookbookModelNotFoundException */ public function show($id) { - $recipe = $this->get($id); - - if (!$recipe) { - throw new CookbookModelNotFoundException(); - } - - return $recipe; + return $this->get($id) ?: null; } /** * @param $request - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|Response + * @return bool * @throws ApiException */ public function store($request) @@ -130,12 +108,10 @@ public function store($request) 'resource_type' => 'recipe' ]); - $draft->save(); + return $draft->save(); } - return response([ - 'created' => $created, - ], Response::HTTP_CREATED); + return $created; } catch (\Exception $e) { throw new ApiException($e->getMessage()); } @@ -144,7 +120,7 @@ public function store($request) /** * @param Request $request * @param $id - * @return Application|ResponseFactory|\Illuminate\Foundation\Application|Response|void + * @return bool|int|void * @throws CookbookModelNotFoundException * @throws InvalidPayloadException */ @@ -172,18 +148,14 @@ public function update(Request $request, $id) ->getKey(); } - return response( - [ - 'updated' => $recipe->update($payload), - ], Response::HTTP_OK - ); + return $recipe->update($payload); } } /** * @param User $user * @param $id - * @return Application|ResponseFactory|\Illuminate\Foundation\Application|Response|void + * @return bool|mixed|void|null * @throws CookbookModelNotFoundException */ public function delete(User $user, $id) @@ -191,17 +163,13 @@ public function delete(User $user, $id) if ($user->isSuper()) { $recipe = $this->get($id); - return response( - [ - 'deleted' => $recipe->delete(), - ], Response::HTTP_ACCEPTED - ); + return $recipe->delete(); } } /** * @param $recipeId - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|Response + * @return false|\Illuminate\Database\Eloquent\Model * @throws CookbookModelNotFoundException */ public function addClap($recipeId) @@ -209,14 +177,12 @@ public function addClap($recipeId) $recipe = $this->get($recipeId); $recipe->claps = $recipe->claps + 1; - $recipe->save(); - - return response( - [ - 'updated' => true, - 'claps' => $recipe->refresh()->claps, - ], Response::HTTP_OK - ); + + if ($recipe->save()) { + return $recipe->refresh(); + } + + return false; } /** @@ -279,9 +245,9 @@ private function validatePayload(array $payload) } } - //descriptin length + //description length If ($description = Arr::get($payload, 'description')) { - //todo: ai enabled giberrish detection + //todo: ai enabled gibberish detection if (Str::wordCount($description) < 100) { $sources[] = [ 'description' =>'Description must not be less than 100 words.' @@ -291,7 +257,7 @@ private function validatePayload(array $payload) //summary length If ($summary = Arr::get($payload, 'summary')) { - //todo: ai enabled giberrish detection + //todo: ai enabled gibberish detection if (Str::wordCount($summary) < 50) { $sources[] = [ 'summary' => 'Summary must not be less than 50 words.' diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 7cbe0791..ae3f2051 100755 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -4,14 +4,12 @@ namespace App\Services; -use App\Exceptions\CookbookModelNotFoundException; use App\Interfaces\serviceInterface; use App\Models\User; use App\Models\UserContactDetail; use App\Utils\DbHelper; use Illuminate\Hashing\BcryptHasher; use Illuminate\Http\Request; -use Illuminate\Http\Response; /** * Class UserService @@ -28,20 +26,13 @@ public function __construct() */ public function index() { - $users = User::with('cookbooks', 'recipes', 'contact')->get(); - - return response([ - 'data' => $users, - ], Response::HTTP_OK); + return User::with('cookbooks', 'recipes', 'contact')->get(); } /** - * Create a new user - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse + * Create a new user resource */ - public function store(Request $request): \Illuminate\Http\JsonResponse + public function store(Request $request) { $user = new User([ 'name' => $request->name, @@ -54,43 +45,31 @@ public function store(Request $request): \Illuminate\Http\JsonResponse ]); $created = $user->save(); - $serialized = $request->merge(['user_id' => $user->id]); - $contact = new UserContactDetailsService(); - $contact->store(new Request($serialized->all())); - - // dispatch(new SendEmailNotification($user->id)); - - return response()->json( - [ - 'response' => [ - 'created' => $created, - 'data' => $user, - 'status' => 'success', - ], - ], Response::HTTP_CREATED - ); + + if ($created) { + $serialized = $request->merge(['user_id' => $user->id]); + + //TODO: hand this over to a job to handle asynchronously + $contact = new UserContactDetailsService(); + $contact->store(new Request($serialized->all())); + + // dispatch(new SendEmailNotification($user->id)); + return true; + } + + //TODO: log some debugging info here + return false; } - /** - * @param $q - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|Response - * @throws CookbookModelNotFoundException - */ public function show($q) { - return response( - [ - 'data' => [ - 'user' => $this->findWhere($q)->get()->append(['tiktok_videos']), - ], - ], Response::HTTP_OK - ); + return $this->findWhere($q)->get()->append(['tiktok_videos']); } /** * @param Request $request * @param string $option - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|Response + * @return bool */ public function update(Request $request, string $option) { @@ -115,21 +94,12 @@ public function update(Request $request, string $option) } } - if ($updated = $userRecord->save()) { - return response( - [ - 'updated' => (bool)$updated, - 'status' => 'success', - ], Response::HTTP_OK - ); - } - - throw new \Exception('Not saved.'); + return $userRecord->save(); } catch (\Exception $e) { - return response([ - 'errors' => $e->getMessage(), - ], Response::HTTP_BAD_REQUEST); + //TODO: log debugging message here } + + return false; } /** @@ -139,8 +109,8 @@ public function update(Request $request, string $option) public function findWhere($q) { return User::with(['cookbooks', 'recipes']) - ->where('id', $q) - ->orWhere('email', $q) - ->orWhere('name_slug', $q); + ->where('id', '=', $q) + ->orWhere('email', '=', $q) + ->orWhere('name_slug', '=', $q); } } diff --git a/docker-compose.yml b/docker-compose.yml index 40cfe4a8..b27d60d0 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,9 @@ services: - ./:/var/www networks: - cookbooks + environment: + PHP_IDE_CONFIG: "serverName=localhost" + XDEBUG_CONFIG: "idekey=PHPSTORM" db: image: mysql:latest container_name: db diff --git a/docs/images/add-new-cli-interpreter1.png b/docs/images/add-new-cli-interpreter1.png new file mode 100644 index 00000000..2c484259 Binary files /dev/null and b/docs/images/add-new-cli-interpreter1.png differ diff --git a/docs/images/add-new-cli-interpreter2.png b/docs/images/add-new-cli-interpreter2.png new file mode 100644 index 00000000..366c3151 Binary files /dev/null and b/docs/images/add-new-cli-interpreter2.png differ diff --git a/docs/images/php-debug-connections.png b/docs/images/php-debug-connections.png new file mode 100644 index 00000000..f85c1823 Binary files /dev/null and b/docs/images/php-debug-connections.png differ diff --git a/docs/images/server.png b/docs/images/server.png new file mode 100644 index 00000000..7afb07fc Binary files /dev/null and b/docs/images/server.png differ diff --git a/docs/images/test-framework1.png b/docs/images/test-framework1.png new file mode 100644 index 00000000..a4e84902 Binary files /dev/null and b/docs/images/test-framework1.png differ diff --git a/docs/images/test-framework2.png b/docs/images/test-framework2.png new file mode 100644 index 00000000..62610bae Binary files /dev/null and b/docs/images/test-framework2.png differ diff --git a/docs/images/xdebug-php.png b/docs/images/xdebug-php.png new file mode 100644 index 00000000..7f0ffb97 Binary files /dev/null and b/docs/images/xdebug-php.png differ diff --git a/tests/Feature/AuthTest.php b/tests/Feature/AuthTest.php index b2d73ca2..4fc0a883 100755 --- a/tests/Feature/AuthTest.php +++ b/tests/Feature/AuthTest.php @@ -6,8 +6,8 @@ use App\Models\User; use Carbon\Carbon; -use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; /** * Class UserTest @@ -24,7 +24,7 @@ public function it_responds_with_an_error_if_the_user_email_is_empty() 'email' => '', 'password' => 'mypassword', ] - )->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + )->assertStatus(ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); $decoded = json_decode($response->getContent(), true); @@ -41,7 +41,7 @@ public function it_responds_with_an_error_if_the_user_email_is_null() 'POST', '/api/v1/auth/login', [ 'password' => 'mypassword', ] - )->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + )->assertStatus(ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); $decoded = json_decode($response->getContent(), true); @@ -59,7 +59,7 @@ public function it_responds_with_an_error_if_the_user_password_is_empty() 'email' => 'sally@foo.com', 'password' => '', ] - )->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + )->assertStatus(ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); $decoded = json_decode($response->getContent(), true); @@ -76,7 +76,7 @@ public function it_responds_with_an_error_if_the_user_password_is_null() 'POST', '/api/v1/auth/login', [ 'email' => 'sally@foo.com', ] - )->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + )->assertStatus(ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); $decoded = json_decode($response->getContent(), true); @@ -91,7 +91,7 @@ public function it_responds_with_an_error_if_the_request_does_not_contain_email_ { $response = $this->json( 'POST', '/api/v1/auth/login', [] - )->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + )->assertStatus(ResponseAlias::HTTP_UNPROCESSABLE_ENTITY); $decoded = json_decode($response->getContent(), true); @@ -176,7 +176,7 @@ public function it_responds_with_a_404_when_attempting_to_signin_a_user_that_doe 'email' => 'sally@foo.com', 'password' => 'invalidpassword', ] - )->assertStatus(Response::HTTP_NOT_FOUND); + )->assertStatus(ResponseAlias::HTTP_NOT_FOUND); } /** @@ -220,14 +220,14 @@ public function it_can_logout_an_existing_user() ] ); - $respose = $this->json( + $response = $this->json( 'POST', '/api/v1/auth/login', [ 'email' => $email, 'password' => $password, ] ); - $decoded = json_decode($respose->getContent(), true); + $decoded = json_decode($response->getContent(), true); $this->json( 'GET', '/api/v1/auth/logout', [], [ @@ -260,7 +260,7 @@ public function logout_responds_with_an_error_if_token_is_invalid() ] ] ) - ->assertStatus(Response::HTTP_BAD_REQUEST) + ->assertStatus(ResponseAlias::HTTP_BAD_REQUEST) ->assertExactJson([ 'Not found or Invalid Credentials.' ]); @@ -282,14 +282,14 @@ public function it_can_validate_access_token_success() ] ); - $respose = $this->json( + $response = $this->json( 'POST', '/api/v1/auth/login', [ 'email' => $email, 'password' => $password, ] ); - $decoded = json_decode($respose->getContent(), true); + $decoded = json_decode($response->getContent(), true); $this ->json( @@ -297,7 +297,7 @@ public function it_can_validate_access_token_success() 'Authorization' => 'Bearer ' . $decoded['token'] ] ) - ->assertStatus(Response::HTTP_OK) + ->assertStatus(ResponseAlias::HTTP_OK) ->assertExactJson([ 'validated' => true ]); @@ -314,9 +314,9 @@ public function it_can_validate_access_token_fails() 'Authorization' => 'Bearer invalid-token' ] ) - ->assertStatus(Response::HTTP_UNAUTHORIZED) + ->assertStatus(ResponseAlias::HTTP_UNAUTHORIZED) ->assertExactJson([ - 'error' => 'Expired or Tnvalid token.' + 'error' => 'Expired or Invalid token.' ]); } } diff --git a/tests/Feature/CommentTest.php b/tests/Feature/CommentTest.php index 32a8b0d9..d83d8ec9 100644 --- a/tests/Feature/CommentTest.php +++ b/tests/Feature/CommentTest.php @@ -223,7 +223,7 @@ public function it_cannot_destroy_user_not_owned_comment() 'Authorization' => 'Bearer ' . $token ] )->assertExactJson([ - 'error' => 'You are not suthorized to perfrom this action.' + 'error' => 'You are not authorized to perform this action.' ])->assertStatus(Response::HTTP_UNAUTHORIZED); } @@ -375,7 +375,7 @@ public function handles_when_not_user_and_not_own_comment() 'Authorization' => 'Bearer ' . $token ] )->assertExactJson([ - 'error' => 'You are not suthorized to perfrom this action.' + 'error' => 'You are not authorized to perform this action.' ])->assertStatus(Response::HTTP_UNAUTHORIZED); } } diff --git a/tests/Feature/CookbookTest.php b/tests/Feature/CookbookTest.php index 8beef875..ac61f228 100755 --- a/tests/Feature/CookbookTest.php +++ b/tests/Feature/CookbookTest.php @@ -6,11 +6,13 @@ use App\Models\Cookbook; use App\Models\User; +use Illuminate\Hashing\BcryptHasher; use Illuminate\Http\Response; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; class CookbookTest extends \TestCase { - protected string $bookcoverImageUrl = + protected string $bookCoverImageUrl = "https://www.glamox.com/public/images/image-default.png?scale=canvas&width=640&height=480"; public function setUp(): void @@ -25,7 +27,7 @@ public function setUp(): void public function it_can_retrieve_all_cookbooks_and_respond_with_a_200_status_code() { $this->json('GET', '/api/v1/cookbooks') - ->assertStatus(Response::HTTP_OK) + ->assertStatus(ResponseAlias::HTTP_OK) ->assertJsonStructure([ 'data' => [ [ @@ -41,9 +43,11 @@ public function it_can_retrieve_all_cookbooks_and_respond_with_a_200_status_code 'is_locked', 'alt_text', 'tags', - '_links', + 'recipes', 'recipes_count', 'categories', + 'users', + 'flag', 'author' ] ] @@ -56,7 +60,7 @@ public function it_can_retrieve_all_cookbooks_and_respond_with_a_200_status_code public function it_responds_with_a_404_when_retrieving_a_cookbook_that_does_not_exist() { $this->json('GET', '/api/v1/cookbooks/0') - ->assertStatus(Response::HTTP_NOT_FOUND) + ->assertStatus(ResponseAlias::HTTP_NOT_FOUND) ->assertExactJson([ "error" => "Record Not found." ]); @@ -86,9 +90,11 @@ public function it_responds_with_a_200_when_retrieving_a_cookbook_by_id() 'is_locked', 'alt_text', 'tags', - '_links', + 'recipes', 'recipes_count', 'categories', + 'users', + 'flag', 'author' ] ] @@ -119,9 +125,11 @@ public function it_responds_with_a_200_when_retrieving_a_cookbook_by_slug() 'is_locked', 'alt_text', 'tags', - '_links', + 'recipes', 'recipes_count', 'categories', + 'users', + 'flag', 'author' ] ] @@ -152,7 +160,7 @@ public function it_responds_with_a_200_if_the_user_is_authorized_to_view_their_c $this->json('GET', '/api/v1/my/cookbooks', [], [ 'HTTP_Authorization' => 'Bearer ' . $decoded['token'] - ])->assertStatus(Response::HTTP_OK); + ])->assertStatus(ResponseAlias::HTTP_OK); } /** @@ -180,14 +188,14 @@ public function it_allows_a_user_with_valid_token_to_create_a_cookbook_resource( $this->json('POST', '/api/v1/cookbooks', [ 'name' => 'test cookbook', 'description' => fake()->sentence(150), - 'bookCoverImg' => $this->bookcoverImageUrl, + 'bookCoverImg' => $this->bookCoverImageUrl, 'category_id' => 1, 'categories' => 'keto,vegan,test', 'flag_id' => 'ng', 'slug' => 'test-cookbook' ], [ 'HTTP_Authorization' => 'Bearer ' . $decoded['token'] - ]); + ])->assertOk()->assertJsonStructure(['response' => ['created', 'data']]); $this->assertDatabaseHas('cookbooks', [ 'name' => 'test cookbook', @@ -223,7 +231,7 @@ public function it_does_not_allow_a_user_with_valid_token_update_a_cookbook_reso $cookbook = Cookbook::factory()->make([ 'user_id' => $otherUser->id, - 'bookCoverImg' => $this->bookcoverImageUrl, + 'bookCoverImg' => $this->bookCoverImageUrl, ]); $cookbook->save(); @@ -235,7 +243,7 @@ public function it_does_not_allow_a_user_with_valid_token_update_a_cookbook_reso "alt_text" => "this is an updated alt text" ], [ 'HTTP_Authorization' => 'Bearer ' . $decoded['token'] - ]); + ])->assertStatus(Response::HTTP_UNAUTHORIZED); $decoded = json_decode($response->getContent(), true); @@ -246,7 +254,49 @@ public function it_does_not_allow_a_user_with_valid_token_update_a_cookbook_reso /** * @test */ - public function it_forbids_lesser_beings_from_deleting_a_cookbook_resource() + public function it_allows_a_user_with_valid_token_update_a_cookbook_resource_they_own() + { + $user = User::factory() + ->make([ + 'email' => 'sally@example.com', + 'password' => (new BcryptHasher)->make('saltyL@k3'), + ]); + $user->save(); + $user = $user->refresh(); + + $loginResponse = $this->json( + 'POST', '/api/v1/auth/login', [ + 'email' => 'sally@example.com', + 'password' => 'saltyL@k3', + ] + ); + + $decoded = json_decode($loginResponse->getContent(), true); + + $cookbook = Cookbook::factory()->make([ + 'user_id' => $user->id, + 'bookCoverImg' => $this->bookCoverImageUrl, + 'slug' => 'my-new-cookbook' + ]); + + $cookbook->save(); + $cookbook = $cookbook->refresh(); + + //update the cookbook + $this->json('POST', '/api/v1/cookbooks/' . $cookbook->id . '/edit', [ + 'slug' => 'updated-slug' + ], [ + 'HTTP_Authorization' => 'Bearer ' . $decoded['token'] + ])->assertOK(); + + $this->assertDatabaseHas('cookbooks', ['id' => $cookbook->id, 'slug' => 'updated-slug']); + } + + /** + * This is intentional and a temporary measure + * @test + */ + public function it_forbids_everyone_from_deleting_a_cookbook_resource() { $this->json( 'POST', '/api/v1/auth/register', [ diff --git a/tests/Feature/RecipeTest.php b/tests/Feature/RecipeTest.php index 6d4a5460..acf989b6 100755 --- a/tests/Feature/RecipeTest.php +++ b/tests/Feature/RecipeTest.php @@ -13,6 +13,7 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response as ResponseAlias; class RecipeTest extends \TestCase { @@ -166,6 +167,50 @@ public function it_cannot_clap_for_a_recipe_that_does_not_exist() ]); } + /** + * @test + */ + public function it_can_show_a_recipe_by_id_or_slug() + { + $user = User::factory()->make([ + 'email' => 'evan.reid@123.com', + 'password' => (new BcryptHasher)->make('pass123'), + ]); + $user->save(); + + $token = Auth::attempt([ + 'email' => 'evan.reid@123.com', + 'password' => 'pass123' + ]); + + $cookbook = Cookbook::factory()->make([ + 'user_id' => $user->getKey() + ]); + + $cookbook->save(); + + $recipe = Recipe::factory()->make([ + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'user_id' => $user->getKey() + ]); + + $recipe->save(); + $recipe = $recipe->refresh(); + + $searchBy = ['id', 'slug']; + + foreach ($searchBy as $key) { + $this->json( + 'GET', + '/api/v1/recipes/' . $recipe->$key, + [], + [ + 'Authorization' => 'Bearer ' . $token + ] + )->assertStatus(ResponseAlias::HTTP_OK)->assertJsonStructure(['data']); + } + } + /** * @test */ @@ -366,7 +411,7 @@ public function only_supers_can_destroy_a_recipe() [ 'Authorization' => 'Bearer ' . $token ] - )->assertStatus(Response::HTTP_ACCEPTED) + )->assertStatus(ResponseAlias::HTTP_OK) ->assertExactJson([ "deleted" => true ]); @@ -579,31 +624,33 @@ public function it_can_create_a_new_recipe() ]); $flag->save(); + $payload = [ + 'is_draft' => 'false', + 'name' => $faker->jobTitle, + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'description' => implode(" ", $faker->words(150)), + 'summary' => implode(" ", $faker->words(55)), + 'imgUrl' => $faker->imageUrl(), + 'ingredients' => [ + [ + 'name' => $faker->jobTitle, + 'unit' => '2', + 'thumbnail' => $faker->imageUrl(), + ] + ], + 'nationality' => $flag->refresh()->flag, + 'cuisine' => 'spanich', + 'tags' => [] + ]; + $this->json( 'POST', '/api/v1/recipes/', - [ - 'is_draft' => 'false', - 'name' => $faker->jobTitle, - 'cookbook_id' => $cookbook->refresh()->getKey(), - 'description' => implode(" ", $faker->words(150)), - 'summary' => implode(" ", $faker->words(55)), - 'imgUrl' => $faker->imageUrl(), - 'ingredients' => [ - [ - 'name' => $faker->jobTitle, - 'unit' => '2', - 'thumbnail' => $faker->imageUrl(), - ] - ], - 'nationality' => $flag->refresh()->flag, - 'cuisine' => 'spanich', - 'tags' => [] - ], + $payload, [ 'Authorization' => 'Bearer ' . $token ] - )->assertStatus(Response::HTTP_CREATED) + )->assertStatus(ResponseAlias::HTTP_CREATED) ->assertExactJson([ "created" => true ]); diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php index 72c4c21d..e9e714a6 100644 --- a/tests/Unit/Controllers/ControllerTest.php +++ b/tests/Unit/Controllers/ControllerTest.php @@ -18,7 +18,7 @@ public function test_it_is_instantiable() $this->assertInstanceOf(Controller::class, $cookbookController); } - public function test_it_can_retrieve_all_cokbooks() + public function test_it_can_retrieve_all_cookbooks() { $service = $this->mock(CookbookService::class); $expectedResponse = new JsonResponse([]); @@ -29,7 +29,7 @@ public function test_it_can_retrieve_all_cokbooks() $controller = new CookbookController($service); - $this->assertSame($expectedResponse, $controller->index()); + $this->assertSame($expectedResponse->getStatusCode(), $controller->index()->getStatusCode()); } public function test_it_will_throw_cookbook_model_not_found_exception_if_cookbook_does_not_exist() @@ -58,6 +58,6 @@ public function test_it_can_retrieve_cookbook_by_id() ->andReturn($expectedResponse); $controller = new CookbookController($service); - $this->assertSame($expectedResponse, $controller->show($cookbookId)); + $this->assertSame($expectedResponse->getStatusCode(), $controller->show($cookbookId)->getStatusCode()); } }