Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#220 - Create tests for API controllers #222

Merged
merged 12 commits into from
May 8, 2024
15 changes: 15 additions & 0 deletions app/Exceptions/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Illuminate\Foundation\Exceptions\Handler;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Sentry\Laravel\Integration;
Expand All @@ -29,6 +30,13 @@ public function register(): void
public function render($request, Throwable $exception)
{
if ($exception instanceof ValidationException) {
if (Request::is("api/*")) {
return response()->json([
"message" => $exception->getMessage(),
"errors" => $exception->errors(),
], Response::HTTP_UNPROCESSABLE_ENTITY);
}

return back()->withErrors($exception->errors());
}

Expand Down Expand Up @@ -74,6 +82,13 @@ public function render($request, Throwable $exception)
break;
}

if (Request::is("api/*")) {
return response()->json([
"statusTitle" => $statusTitle,
"statusDescription" => $statusDescription,
], $statusCode);
}

return Inertia::render("Error", [
"statusTitle" => $statusTitle,
"statusDescription" => $statusDescription,
Expand Down
94 changes: 58 additions & 36 deletions app/Http/Controllers/Api/Admin/ProviderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,44 @@
namespace App\Http\Controllers\Api\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\ProviderRequest;
use App\Http\Requests\ApiProviderRequest;
use App\Http\Resources\ProviderResource;
use App\Models\Provider;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;

class ProviderController extends Controller
{
public const ITEMS_PER_PAGE = 15;

public function index(): JsonResponse
{
$providers = Provider::query()
->search("name")
->orderByName()
->orderByTimeRange()
->paginate(self::ITEMS_PER_PAGE)
->withQueryString();
$providers = Provider::all();

return response()->json([
"providers" => ProviderResource::collection($providers),
]);
}

public function store(ProviderRequest $request): JsonResponse
public function store(ApiProviderRequest $request): JsonResponse
{
Provider::query()->create($request->validated());

$fileName = $this->getFilename($request->name, $request->file("file"));
$fileContents = $request->file("file")->get();
$provider = Provider::create($request->validated());

Storage::disk("public")->put("providers/" . $fileName, $fileContents);
if ($request->has("file")) {
if (!$this->processProviderImage($request->file, $provider->name)) {
return response()->json(["message" => __("The image must be 150x100 pixels.")], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}

return response()->json(["message" => __("Provider created successfully.")], 201);
return response()->json(["message" => __("Provider created successfully.")], Response::HTTP_CREATED);
}

public function update(ProviderRequest $request, Provider $provider): JsonResponse
public function update(ApiProviderRequest $request, Provider $provider): JsonResponse
{
$provider->update($request->validated());

$imageName = $this->getFilename($request->name, $request->file("file"));
$storageImagePath = storage_path("app/public/providers/" . $imageName);
$resourceImagePath = resource_path("providers/" . $imageName);
$imageContents = $request->file("file")->get();

if (file_exists($resourceImagePath)) {
file_put_contents($resourceImagePath, $imageContents);
Storage::put($storageImagePath, file_get_contents($imageContents));
} else {
Storage::put($storageImagePath, file_get_contents($imageContents));
if ($request->has("file")) {
$this->replaceProviderImage($request->file, $provider->name);
}

return response()->json(["message" => __("Provider updated successfully.")]);
Expand All @@ -65,19 +51,15 @@ public function update(ProviderRequest $request, Provider $provider): JsonRespon
public function destroy(Provider $provider): JsonResponse
{
$provider->delete();
$imagePath = storage_path("app/public/providers/" . strtolower($provider["name"]) . ".png");
$imagePath = $this->providerImagePath($provider->name);
File::delete($imagePath);

return response()->json(["message" => __("Provider deleted successfully.")]);
}

public function showLogo(string $filename): JsonResponse
{
$imagePath = storage_path("app/public/providers/" . $filename);

if (!file_exists($imagePath)) {
$imagePath = storage_path("app/public/providers/unknown.png");
}
$imagePath = $this->providerImagePath($filename, true);

$imageData = base64_encode(file_get_contents($imagePath));

Expand All @@ -87,8 +69,48 @@ public function showLogo(string $filename): JsonResponse
]);
}

private function getFilename(string $name, UploadedFile $file): string
private function providerImagePath(string $name, bool $useDefault = false): string
{
return strtolower($name) . "." . $file->getClientOriginalExtension();
$path = storage_path("app/public/providers/" . strtolower($name) . ".png");

return file_exists($path) || !$useDefault ? $path : storage_path("app/public/providers/unknown.png");
}

private function processProviderImage(string $encodedFile, string $name): bool
{
[$decodedFile, $mimeType] = $this->decodeFile($encodedFile);

if (!$this->validateImageDimensions($decodedFile)) {
return false;
}

Storage::disk("public")->put("providers/" . strtolower($name) . "." . $mimeType, $decodedFile);

return true;
}

private function replaceProviderImage(string $encodedFile, string $name): void
{
$oldImagePath = $this->providerImagePath($name);
Storage::disk("public")->delete($oldImagePath);
$this->processProviderImage($encodedFile, $name);
}

private function decodeFile(string $encodedFile): array
{
preg_match('/^data:image\/(\w+);base64,/', $encodedFile, $matches);
$decodedFile = base64_decode(explode(",", $encodedFile)[1] ?? "", true);

return [$decodedFile, $matches[1] ?? "png"];
}

private function validateImageDimensions(string $decodedFile): bool
{
$imageResource = imagecreatefromstring($decodedFile);
$width = imagesx($imageResource);
$height = imagesy($imageResource);
imagedestroy($imageResource);

return $width === 150 && $height === 100;
}
}
16 changes: 9 additions & 7 deletions app/Http/Controllers/Api/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function store(RegisterRequest $request): JsonResponse

return response()->json([
"message" => __("User created."),
]);
], Response::HTTP_CREATED);
}

public function login(LoginRequest $request): JsonResponse
Expand All @@ -42,14 +42,15 @@ public function login(LoginRequest $request): JsonResponse
"password" => $request->password,
], $remember)) {
$user = Auth::user();
$user_id = (string)Auth::id();
$userId = (string)Auth::id();

$token_abilities = $this->getUserAbilities($user);

$token = $user->createToken($user_id, $token_abilities)->plainTextToken;
$token = $user->createToken($userId, $token_abilities)->plainTextToken;

return response()->json([
$token_abilities,
"abilities" => $token_abilities,
"userId" => $userId,
"access_token" => $token,
]);
}
Expand Down Expand Up @@ -90,16 +91,17 @@ public function handleProviderRedirect(string $provider): JsonResponse
]);
$token_abilities = $this->getUserAbilities($user);

$user_id = $user->id->toString();
$token = $user->createToken($user_id, $token_abilities)->plainTextToken;
$userId = (string)$user->id;
$token = $user->createToken($userId, $token_abilities)->plainTextToken;

return response()->json([
"access_token" => $token,
"userId" => $userId,
]);
} catch (Exception $e) {
return response()->json([
"message" => __("Login failed."),
]);
], Response::HTTP_UNAUTHORIZED);
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/Api/ChangeLocaleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ChangeLocaleController extends Controller
{
Expand All @@ -22,6 +23,6 @@ public function __invoke(Request $request, string $locale): JsonResponse

return response()->json([
"message" => __("Error changing the language."),
]);
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
3 changes: 2 additions & 1 deletion app/Http/Controllers/Api/CityOpinionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Http\Requests\CityOpinionRequest;
use App\Models\CityOpinion;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class CityOpinionController extends Controller
{
Expand All @@ -19,7 +20,7 @@ public function store(CityOpinionRequest $request): JsonResponse

return response()->json([
"message" => __("Opinion added successfully."),
]);
], Response::HTTP_CREATED);
}

public function update(CityOpinionRequest $request, CityOpinion $cityOpinion): JsonResponse
Expand Down
8 changes: 8 additions & 0 deletions app/Http/Controllers/Api/CityProviderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Services\DataImporterService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class CityProviderController extends Controller
{
Expand Down Expand Up @@ -47,6 +48,13 @@ public function index(): JsonResponse

public function update(CityProviderService $service, CityProviderRequest $request, City $city): JsonResponse
{
foreach ($request->providerNames as $providerName) {
if (Provider::query()->where("name", $providerName)->doesntExist()) {
return response()->json([
"message" => __("Provider does not exist."),
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
$service->updateProvider($request->providerNames, $city);

return response()->json([
Expand Down
8 changes: 8 additions & 0 deletions app/Http/Controllers/Api/FavoritesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use App\Http\Controllers\Controller;
use App\Http\Resources\CityResource;
use App\Http\Resources\ProviderResource;
use App\Models\City;
use App\Models\Favorites;
use App\Models\Provider;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class FavoritesController extends Controller
{
Expand All @@ -35,6 +37,12 @@ public function store(Request $request): JsonResponse
$cityId = $request->input("city_id");
$userId = $request->user()?->id;

if (City::query()->where("id", $cityId)->doesntExist() || !$cityId) {
return response()->json([
"message" => "City not found.",
], Response::HTTP_BAD_REQUEST);
}

$favorite = Favorites::firstOrCreate([
"user_id" => $userId,
"city_id" => $cityId,
Expand Down
35 changes: 35 additions & 0 deletions app/Http/Requests/ApiProviderRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Unique;

class ApiProviderRequest extends FormRequest
{
public function rules(): array
{
return [
"name" => ["required", "string", "regex:/^[A-Z\s]/", "max:100", $this->uniqueRuleForProvider("name")],
"color" => ["required", "string", "size:7"],
"url" => ["nullable", "url"],
"android_url" => ["nullable", "url"],
"ios_url" => ["nullable", "url"],
"file" => [
"required",
"string",
"regex:/^data:image\/(png);base64,[a-zA-Z0-9+\/]+=*$/",
kamilpiech97 marked this conversation as resolved.
Show resolved Hide resolved
],
];
}

protected function uniqueRuleForProvider(string $column): Unique
{
$currentProviderId = $this->route(param: "provider");

return Rule::unique(table: "providers", column: $column)->ignore($currentProviderId);
}
}
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
"require": {
"php": "^8.2",
"ext-dom": "*",
"ext-gd": "*",
"ext-pdo": "*",
"dedoc/scramble": "^0.9.0",
"guzzlehttp/guzzle": "^7.7",
"inertiajs/inertia-laravel": "^1.0.0",
"inertiajs/inertia-laravel": "^0.6.9",
"intervention/image": "2.*",

"laravel/framework": "^10.13.0",
"laravel/sanctum": "^3.2.5",
"laravel/socialite": "^5.10",
Expand Down
Loading
Loading