From 86b0104b708885caa1f39d9e53712b689ac83b30 Mon Sep 17 00:00:00 2001 From: fokosun Date: Wed, 23 Aug 2023 14:32:20 -0400 Subject: [PATCH] improve test coverage --- app/Dtos/TikTokUserDto.php | 7 +- app/Exceptions/ApiException.php | 1 - app/Exceptions/Handler.php | 1 - app/Http/Controllers/CommentController.php | 20 +-- app/Http/Controllers/UserController.php | 77 ---------- app/Http/Requests/UserUpdateRequest.php | 2 +- app/Jobs/SendEmailNotification.php | 1 + app/Services/RecipeService.php | 7 +- tests/Feature/CommentTest.php | 135 ++++++++++++++++ tests/Feature/RecipeTest.php | 66 +++----- tests/Feature/UserTest.php | 169 +++++++++++++++++++++ tests/TestCase.php | 23 +++ tests/Unit/Commands/LoginCommandTest.php | 37 +++++ tests/Unit/Dtos/TikTokUserDtoTest.php | 52 +++++++ 14 files changed, 442 insertions(+), 156 deletions(-) create mode 100644 tests/Unit/Commands/LoginCommandTest.php create mode 100644 tests/Unit/Dtos/TikTokUserDtoTest.php diff --git a/app/Dtos/TikTokUserDto.php b/app/Dtos/TikTokUserDto.php index e4aca1a3..2db30669 100644 --- a/app/Dtos/TikTokUserDto.php +++ b/app/Dtos/TikTokUserDto.php @@ -1,5 +1,7 @@ open_id; } - public function isIsVerified(): bool - { - return $this->is_verified; - } - public function getProfileDeepLink(): string { return $this->profile_deep_link; diff --git a/app/Exceptions/ApiException.php b/app/Exceptions/ApiException.php index 929fa1a5..ca7b2086 100755 --- a/app/Exceptions/ApiException.php +++ b/app/Exceptions/ApiException.php @@ -3,7 +3,6 @@ declare(strict_types=1); namespace App\Exceptions; - class ApiException extends \Exception { } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 2a2ea85c..d7560647 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -10,7 +10,6 @@ use Illuminate\Validation\ValidationException; use Throwable; use Tymon\JWTAuth\Exceptions\JWTException; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; class Handler extends ExceptionHandler { diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 25312291..f6a32cd8 100755 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -11,11 +11,10 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; use Tymon\JWTAuth\Facades\JWTAuth; -use Tymon\JWTAuth\JWT; class CommentController extends Controller { - public function addComment(Request $request, JWT $jwtAuth) + public function addComment(Request $request) { /** @phpstan-ignore-next-line */ if ($user = JWTAuth::parseToken()->user()) { @@ -45,8 +44,6 @@ public function addComment(Request $request, JWT $jwtAuth) } } } - - throw new ApiException('You are not suthorized to perfrom this action.'); } public function destroyComment(Request $request) @@ -57,23 +54,10 @@ public function destroyComment(Request $request) $comment = Comment::findOrFail($request->only(['comment-id']))->first(); if ($user->isSuper() || $user->ownsComment($payload['comment-id'])) { - try { - return response()->json(['deleted' => $comment->delete()]); - } catch (\Exception $exception) { - Log::debug( - 'comment deletion failed.', - ['error' => $exception, 'payload' => $payload] - ); - - return response()->json([ - 'error' => 'There was an error processing this request. Please try again later.' - ], 400); - } + return response()->json(['deleted' => $comment->delete()]); } else { throw new ApiException('You are not suthorized to perfrom this action.'); } } - - throw new ApiException('You are not suthorized to perfrom this action.'); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 40f1b2b6..dbdbd377 100755 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -7,9 +7,7 @@ use App\Exceptions\ApiException; use App\Http\Requests\UserStoreRequest; use App\Http\Requests\UserUpdateRequest; -use App\Jobs\TriggerEmailVerificationProcess; use App\Mail\OtpWasGenerated; -use App\Models\EmailVerification; use App\Models\Following; use App\Models\User; use App\Models\UserFeedback; @@ -17,64 +15,37 @@ use App\Services\TikTok\HttpRequestRunner; use App\Services\TikTok\Videos; use App\Services\UserService; -use Carbon\Carbon; use Ichtrojan\Otp\Otp; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Tymon\JWTAuth\Facades\JWTAuth; -use Ichtrojan\Otp\Models\Otp as OtpModel; -/** - * Class UserController - */ class UserController extends Controller { protected UserService $service; - /** - * @param \App\Services\UserService $service - */ public function __construct(UserService $service) { $this->service = $service; } - /** - * Get all users from the database - */ public function index() { return $this->service->index(); } - /** - * @param UserStoreRequest $request - * @return \Illuminate\Http\JsonResponse - */ public function store(UserStoreRequest $request): \Illuminate\Http\JsonResponse { return $this->service->store($request); } - /** - * Get one user - * - * @param mixed $username username - * @throws \App\Exceptions\CookbookModelNotFoundException - */ public function show($username) { return $this->service->show($username); } - /** - * @param $username - * @param UserUpdateRequest $request - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|Response - */ public function update($username, UserUpdateRequest $request) { if ($request->all()) { @@ -88,54 +59,6 @@ public function update($username, UserUpdateRequest $request) ]); } - /** - * Email Verification - * - * @param $token - * @return \Illuminate\Http\JsonResponse|void - */ - public function verifyEmail($token) - { - $payload = Crypt::decrypt($token); - - try { - if ($payload['secret'] != env('CRYPT_SECRET')) { //one more layer of scrutiny - Log::info('Invalid secret provided for verifying this email', ['user_id' => $payload['user_id'], 'email' => $payload['email']]); - throw new \Exception('There was a problem processing this request. Please try again later.'); - } - - $user = User::findOrFail($payload['user_id']); - - if ($user) { - $verification = EmailVerification::where('user_id', $payload['user_id']); - $verification->update([ - 'is_verified' => Carbon::now(), - ]); - - return response()->json(null, Response::HTTP_NO_CONTENT); - } - } catch (\Exception $e) { - return response()->json($e->getMessage(), Response::HTTP_CONFLICT); - } - } - - /** - * @param $token - * - * @throws \Exception - */ - public function resend($token) - { - $payload = Crypt::decrypt($token); - - if ($payload['secret'] != env('CRYPT_SECRET')) { //one more layer of scrutiny - Log::info('Invalid secret provided for resending email verification', ['user_id' => $payload['user_id'], 'email' => $payload['email']]); - throw new \Exception('There was a problem processing this request. Please try again later.'); - } else { - dispatch(new TriggerEmailVerificationProcess($payload['user_id'])); - } - } - public function followUser(Request $request) { /** @phpstan-ignore-next-line */ diff --git a/app/Http/Requests/UserUpdateRequest.php b/app/Http/Requests/UserUpdateRequest.php index 5d6b4e04..2b2ac49f 100755 --- a/app/Http/Requests/UserUpdateRequest.php +++ b/app/Http/Requests/UserUpdateRequest.php @@ -15,7 +15,7 @@ class UserUpdateRequest extends FormRequest */ public function authorize() { - return false; + return true; } /** diff --git a/app/Jobs/SendEmailNotification.php b/app/Jobs/SendEmailNotification.php index f2673180..67bf4faf 100755 --- a/app/Jobs/SendEmailNotification.php +++ b/app/Jobs/SendEmailNotification.php @@ -7,6 +7,7 @@ class SendEmailNotification extends BaseNotification { const TYPE = 'email'; + protected $tries; /** * SendEmailNotification constructor. diff --git a/app/Services/RecipeService.php b/app/Services/RecipeService.php index e34a8774..fcf4986e 100755 --- a/app/Services/RecipeService.php +++ b/app/Services/RecipeService.php @@ -22,7 +22,6 @@ use Illuminate\Http\Response; use Illuminate\Support\Arr; use Illuminate\Support\Str; -use Illuminate\Validation\UnauthorizedException; /** * Class RecipeService @@ -179,8 +178,6 @@ public function update(Request $request, $id) ], Response::HTTP_OK ); } - - throw new UnauthorizedException("You are not authorized to perform this action."); } /** @@ -200,8 +197,6 @@ public function delete(User $user, $id) ], Response::HTTP_ACCEPTED ); } - - throw new UnauthorizedException("You are not authorized to perform this action."); } /** @@ -256,7 +251,7 @@ private function validatePayload(array $payload) if ($cookbook_id = Arr::get($payload, 'cookbook_id')) { if (!Cookbook::find($cookbook_id)) { $sources[] = [ - 'cookbook_id' => $cookbook_id . ' does not exist.' + 'cookbook_id' => 'This cookbook does not exist.' ]; } } diff --git a/tests/Feature/CommentTest.php b/tests/Feature/CommentTest.php index 18af6598..32a8b0d9 100644 --- a/tests/Feature/CommentTest.php +++ b/tests/Feature/CommentTest.php @@ -227,6 +227,40 @@ public function it_cannot_destroy_user_not_owned_comment() ])->assertStatus(Response::HTTP_UNAUTHORIZED); } + /** + * @test + */ + public function it_cannot_add_comment_for_an_unauthorized_user() + { + $faker = Factory::create(); + + $cookbook = Cookbook::factory()->make([ + 'user_id' => $this->user->getKey() + ]); + + $cookbook->save(); + + $recipe = Recipe::factory()->make([ + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'user_id' => $this->user->getKey() + ]); + + $recipe->save(); + + $this->json( + 'POST', + '/api/v1/comments', + [ + 'resource-type' => 'recipe', + 'resource-id' => $recipe->refresh()->getKey(), + 'comment' => $faker->sentence + ], + [ + 'Authorization' => 'Bearer unauthorized-access-token' + ] + )->assertStatus(Response::HTTP_UNAUTHORIZED); + } + /** * @test */ @@ -243,4 +277,105 @@ public function it_cannot_destroy_comment_if_access_token_is_malformed_or_invali ] )->assertStatus(Response::HTTP_UNAUTHORIZED); } + + /** + * @test + * scnario: user does not own recipe but isSuper + */ + public function only_supers_can_destroy_a_comment() + { + $theOtherUser = User::factory()->make(); + $theOtherUser->save(); + + $this->createRoles(); + $this->createUserRole($this->user->refresh()->getKey(), 'super'); + + $token = Auth::attempt([ + 'email' => $this->user->email, + 'password' => 'pass123' + ]); + + $cookbook = Cookbook::factory()->make([ + 'user_id' => $this->user->getKey() + ]); + + $cookbook->save(); + + $recipe = Recipe::factory()->make([ + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'user_id' => $this->user->getKey() + ]); + + $recipe->save(); + + $comment = Comment::factory()->make([ + 'user_id' => $theOtherUser->refresh()->getKey(), + 'recipe_id' => $recipe->refresh()->getKey() + ]); + + $comment->save(); + + $this->json( + 'POST', + '/api/v1/comments/destroy', + [ + 'comment-id' => $comment->refresh()->getKey() + ], + [ + 'Authorization' => 'Bearer ' . $token + ] + )->assertExactJson([ + 'deleted' => true + ])->assertStatus(Response::HTTP_OK); + } + + /** + * @test + */ + public function handles_when_not_user_and_not_own_comment() + { + $theOtherUser = User::factory()->make(); + $theOtherUser->save(); + + $this->createRoles(); + $this->createUserRole($this->user->refresh()->getKey(), 'contributor'); + + $token = Auth::attempt([ + 'email' => $this->user->email, + 'password' => 'pass123' + ]); + + $cookbook = Cookbook::factory()->make([ + 'user_id' => $this->user->getKey() + ]); + + $cookbook->save(); + + $recipe = Recipe::factory()->make([ + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'user_id' => $this->user->getKey() + ]); + + $recipe->save(); + + $comment = Comment::factory()->make([ + 'user_id' => $theOtherUser->refresh()->getKey(), + 'recipe_id' => $recipe->refresh()->getKey() + ]); + + $comment->save(); + + $this->json( + 'POST', + '/api/v1/comments/destroy', + [ + 'comment-id' => $comment->refresh()->getKey() + ], + [ + 'Authorization' => 'Bearer ' . $token + ] + )->assertExactJson([ + 'error' => 'You are not suthorized to perfrom this action.' + ])->assertStatus(Response::HTTP_UNAUTHORIZED); + } } diff --git a/tests/Feature/RecipeTest.php b/tests/Feature/RecipeTest.php index 285fa08b..6d4a5460 100755 --- a/tests/Feature/RecipeTest.php +++ b/tests/Feature/RecipeTest.php @@ -7,13 +7,11 @@ use App\Models\Cookbook; use App\Models\Flag; use App\Models\Recipe; -use App\Models\Role; use App\Models\User; use Faker\Factory; use Illuminate\Hashing\BcryptHasher; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; class RecipeTest extends \TestCase @@ -407,7 +405,7 @@ public function if_you_are_not_a_super_you_cannot_destroy_a_recipe() 'id' => $recipe->refresh()->getKey() ]); - $this->json( + $res = $this->json( 'POST', '/api/v1/recipes/' . $recipe->refresh()->getKey() . '/destroy', [ @@ -614,10 +612,8 @@ public function it_can_create_a_new_recipe() /** * @test */ - public function handles_error_creating_a_new_recipe() + public function handles_invalid_payload_when_updating_existing_cookbook() { - $faker = Factory::create(); - $user = User::factory()->make([ 'email' => 'evan.reid@123.com', 'password' => (new BcryptHasher)->make('pass123'), @@ -641,53 +637,29 @@ public function handles_error_creating_a_new_recipe() ]); $flag->save(); + $recipe = Recipe::factory()->make([ + 'cookbook_id' => $cookbook->refresh()->getKey(), + 'user_id' => $user->refresh()->getKey() + ]); + + $recipe->save(); + $this->json( 'POST', - '/api/v1/recipes/', + '/api/v1/recipes/' . $recipe->refresh()->getKey() . '/edit', [ - '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' + 'cookbook_id' => rand(100,105), + 'nationality' => 'fake', + 'imgUrl' => 'not-a-valid-image-url', + 'description' => 'less than 100', + 'summary' => 'less than 50', + 'ingredients' => [], + 'cuisine' => 'spanich', + 'tags' => 'not a list' ], [ 'Authorization' => 'Bearer ' . $token ] - )->assertStatus(Response::HTTP_BAD_REQUEST) - ->assertExactJson([ - "error" => 'There was an error processing this request, please try again later.' - ]); - } - - private function createRoles() - { - DB::table('roles')->insert([ - [ - 'role_id' => 'super', - ], [ - 'role_id' => 'contributor', - ] - ]); - } - - private function createUserRole($user_id, $role_id) - { - $role_id = DB::table('roles')->where(['role_id' => $role_id])->first()->id; - - $role = new Role(); - $role->user_id = $user_id; - $role->role_id = $role_id; - $role->save(); + )->assertStatus(Response::HTTP_BAD_REQUEST); } } diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 43737b82..c498b4dd 100755 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -5,7 +5,10 @@ namespace Feature; use App\Jobs\SendEmailNotification; +use App\Models\User; +use Illuminate\Hashing\BcryptHasher; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Queue; class UserTest extends \TestCase @@ -210,4 +213,170 @@ public function testCanGetOneUser() $this->assertEquals(Response::HTTP_OK, $response->status()); } + + /** + * @test + */ + public function it_can_retrieve_all_users() + { + $users = User::factory()->count(5)->make(); + + $users->map(function ($user) { + $user->save(); + }); + + $this + ->json('GET', '/api/v1/users') + ->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + [ + 'name', + 'cookbooks', + 'recipes', + 'contact', + 'contributions', + 'email', + 'following', + 'followers', + 'created_at', + 'updated_at', + 'name_slug', + 'pronouns', + 'avatar', + 'expertise_level', + 'about', + 'can_take_orders', + 'email_verified' + ] + ] + ]); + } + + /** + * @test + */ + public function it_can_update_user_detail() + { + $user = User::factory()->make(); + $user->save(); + $username = $user->refresh()->name_slug; + + $this + ->json( + 'POST', + '/api/v1/users/' . $username . '/edit', + [ + 'pronouns' => 'They/Them/ze' + ] + ) + ->assertStatus(200) + ->assertExactJson([ + 'updated' => true, + 'status' => 'success' + ]); + } + + /** + * @test + */ + public function when_nothing_to_update() + { + $user = User::factory()->make(); + $user->save(); + $username = $user->refresh()->name_slug; + + $this + ->json( + 'POST', + '/api/v1/users/' . $username . '/edit' + ) + ->assertStatus(200) + ->assertExactJson([ + 'message' => 'nothing to update.' + ]); + } + + /** + * @test + */ + public function it_can_handle_follow_user() + { + $user = User::factory()->make([ + 'email' => 'me@test.com', + 'password' => (new BcryptHasher)->make('pass123'), + ]); + $user->save(); + + $userToFollow = User::factory()->make([ + 'email' => 'them@test.com', + 'password' => (new BcryptHasher)->make('pass123'), + ]); + $userToFollow->save(); + + $otherUsers = User::factory()->count(5)->make([ + 'password' => (new BcryptHasher)->make('pass123'), + ]); + + $otherUsers->map(function ($user) { + $user->save(); + }); + + $myBearertoken = Auth::attempt([ + 'email' => 'me@test.com', + 'password' => 'pass123' + ]); + + $this->json( + 'POST', + '/api/v1/follow', + [ + 'toFollow' => $userToFollow->refresh()->getKey() + ], + [ + 'Authorization' => 'Bearer ' . $myBearertoken + ] + )->assertStatus(200) + ->assertJsonStructure([ + [ + 'followers', 'author', 'avatar', 'handle' + ] + ]); + } + + public function test_who_to_folow() + { + $user = User::factory()->make([ + 'email' => 'me@test.com', + 'password' => (new BcryptHasher)->make('pass123'), + ]); + $user->save(); + + $myBearertoken = Auth::attempt([ + 'email' => 'me@test.com', + 'password' => 'pass123' + ]); + + $usersToFollow = User::factory()->count(30)->make([ + 'password' => (new BcryptHasher)->make('pass123'), + ]); + + $usersToFollow->map(function ($user) { + $user->save(); + }); + + $this->json( + 'GET', + '/api/v1/who-to-follow', + [], + [ + 'Authorization' => 'Bearer ' . $myBearertoken + ] + )->assertStatus(200) + ->assertJsonStructure([ + [ + 'followers', 'author', 'avatar', 'handle' + ] + ]); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index fc616d16..b948dcec 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,9 +1,11 @@ insert([ + [ + 'role_id' => 'super', + ], [ + 'role_id' => 'contributor', + ] + ]); + } + + protected function createUserRole($user_id, $role_id) + { + $role_id = DB::table('roles')->where(['role_id' => $role_id])->first()->id; + + $role = new Role(); + $role->user_id = $user_id; + $role->role_id = $role_id; + $role->save(); + } } diff --git a/tests/Unit/Commands/LoginCommandTest.php b/tests/Unit/Commands/LoginCommandTest.php new file mode 100644 index 00000000..617f12c6 --- /dev/null +++ b/tests/Unit/Commands/LoginCommandTest.php @@ -0,0 +1,37 @@ +artisan('auth:token') + ->expectsOutput('Loading User from Cache ...') + ->expectsOutput('===========================') + ->expectsOutput('User not found in Cache, creating new User ...') + ->expectsOutput('==============================================') + ->expectsOutput('====================================') + ->expectsOutput('Here you go! Use this token to access protected resources.!') + ->assertExitCode(0); + } + + public function test_creates_token_when_test_user_is_cached(): void + { + $user = User::factory()->make(); + $user->save(); + + Cache::put('testUser', $user->refresh()); + $this->artisan('auth:token') + ->expectsOutput('Loading User from Cache ...') + ->expectsOutput('===========================') + ->expectsOutput('====================================') + ->expectsOutput('Here you go! Use this token to access protected resources.!') + ->assertExitCode(0); + } +} diff --git a/tests/Unit/Dtos/TikTokUserDtoTest.php b/tests/Unit/Dtos/TikTokUserDtoTest.php new file mode 100644 index 00000000..aaf0f88e --- /dev/null +++ b/tests/Unit/Dtos/TikTokUserDtoTest.php @@ -0,0 +1,52 @@ +tiktokUserDto = new TikTokUserDto( + 1, + 'test', + 'test', + false, + 'test', + 'test', + 'test', + 'test', + 'test', + 'test', + 'test', + 0 + ); + } + + /** + * @test + */ + public function it_can_get() + { + $this->assertSame([ + 'user_id' => 1, + 'open_id' => 'test', + 'code' => 'test', + 'is_verified' => false, + 'profile_deep_link' => 'test', + 'bio_description' => 'test', + 'display_name' => 'test', + 'avatar_large_url' => 'test', + 'avatar_url_100' => 'test', + 'avatar_url' => 'test', + 'union_id' => 'test', + 'video_count' => 0 + ], $this->tiktokUserDto->toArray()); + } +}