feature: add api for categories
feature: add initial docker build and compose for development mode
This commit is contained in:
parent
6bf486e52b
commit
f980be8e28
14 changed files with 520 additions and 0 deletions
28
.dockerignore
Normal file
28
.dockerignore
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Ignore .env files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
||||
# Ignore vendor folder
|
||||
vendor/
|
||||
|
||||
# Ignore log files
|
||||
*.log
|
||||
|
||||
# Ignore IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Ignore system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Ignore Laravel storage
|
||||
/storage/*.key
|
||||
/storage/*.log
|
||||
/storage/framework/cache/*
|
||||
/storage/framework/sessions/*
|
||||
/storage/framework/views/*
|
||||
/storage/logs/*
|
||||
89
Dockerfile
Normal file
89
Dockerfile
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
FROM php:8.5-fpm-alpine
|
||||
|
||||
# Copy composer.lock and composer.json
|
||||
COPY composer.lock composer.json /var/www/
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www
|
||||
|
||||
# # Install dependencies
|
||||
RUN apk update && apk add --no-cache \
|
||||
build-base \
|
||||
libpng-dev \
|
||||
libjpeg-turbo-dev \
|
||||
freetype-dev \
|
||||
zip \
|
||||
jpegoptim optipng pngquant gifsicle \
|
||||
vim \
|
||||
unzip \
|
||||
git \
|
||||
curl \
|
||||
libzip-dev \
|
||||
oniguruma-dev \
|
||||
icu-dev \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
nodejs \
|
||||
npm
|
||||
|
||||
|
||||
# # Clear cache
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
# # Install extensions
|
||||
RUN docker-php-ext-install mbstring zip exif pcntl intl pdo_sqlite
|
||||
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
# # Install composer
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
# Add user for laravel application
|
||||
RUN addgroup -g 1000 www
|
||||
RUN adduser -u 1000 -G www -s /bin/sh -D www
|
||||
|
||||
# # Copy existing application directory contents
|
||||
COPY . /var/www
|
||||
|
||||
# # Copy existing application directory permissions
|
||||
COPY --chown=www-data:www-data . /var/www
|
||||
|
||||
# # Install PHP dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN npm install
|
||||
|
||||
# Build assets
|
||||
RUN npm run build
|
||||
|
||||
# Change ownership of /var/www to www-data
|
||||
RUN chmod -R u+rw /var/www && chown -R www:www /var/www
|
||||
|
||||
# # RUN php artisan optimize
|
||||
# # removed as it calls all 4 commands below and fails du to
|
||||
# # multi-site configuration
|
||||
|
||||
# # Clear any cached configurations
|
||||
RUN php artisan config:clear
|
||||
# RUN php artisan cache:clear
|
||||
RUN php artisan route:clear
|
||||
RUN php artisan view:clear
|
||||
|
||||
# # Build optimizations
|
||||
RUN php artisan config:cache
|
||||
RUN php artisan event:cache
|
||||
RUN php artisan view:cache
|
||||
|
||||
# # RUN php artisan route:cache
|
||||
|
||||
# # Run Laravel artisan command
|
||||
RUN php artisan storage:link
|
||||
|
||||
# # RUN composer install --optimize-autoloader --no-dev
|
||||
|
||||
# # Change current user to www
|
||||
USER www
|
||||
|
||||
# # Expose port 9000 and start php-fpm server
|
||||
# EXPOSE 9000
|
||||
61
app/Http/Controllers/CategoryController.php
Normal file
61
app/Http/Controllers/CategoryController.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreCategoryRequest;
|
||||
use App\Http\Requests\UpdateCategoryRequest;
|
||||
use App\Http\Resources\CategoryResource;
|
||||
use App\Models\Category;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return CategoryResource::collection(Category::all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreCategoryRequest $request)
|
||||
{
|
||||
return new CategoryResource(Category::create($request->validated()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Category $category)
|
||||
{
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateCategoryRequest $request, Category $category)
|
||||
{
|
||||
$category->update($request->validated());
|
||||
|
||||
return new CategoryResource($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Category $category)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user || $user->email !== config('app.admin_email')) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$category->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/StoreCategoryRequest.php
Normal file
28
app/Http/Requests/StoreCategoryRequest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreCategoryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255|unique:categories,name',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/UpdateCategoryRequest.php
Normal file
28
app/Http/Requests/UpdateCategoryRequest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCategoryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'sometimes|required|string|max:255|unique:categories,name,'.$this->category->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/CategoryResource.php
Normal file
24
app/Http/Resources/CategoryResource.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CategoryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
}
|
||||
|
|
|
|||
11
cmd/build_container.sh
Executable file
11
cmd/build_container.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
|
||||
CONTAINER_LABEL="0.0.1"
|
||||
CACHE="--no-cache"
|
||||
CACHE=""
|
||||
|
||||
docker build \
|
||||
$CACHE \
|
||||
-t ${LARAVEL_CONTAINER_NAME}:${CONTAINER_LABEL} \
|
||||
-f Dockerfile .
|
||||
15
cmd/curl_get_categories.sh
Executable file
15
cmd/curl_get_categories.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/categories'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
14
cmd/curl_get_categories_anon.sh
Executable file
14
cmd/curl_get_categories_anon.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# TOKEN="your_api_token_here"
|
||||
# ensure to have set TOKEN to a valid value before running
|
||||
# ideally add this to an .envrc file and source it
|
||||
# tokens need to be created with tinker or similar method
|
||||
|
||||
|
||||
URL='http://127.0.0.1:8000/api/categories'
|
||||
|
||||
curl -s -X GET \
|
||||
-H "Accept: application/json" \
|
||||
$URL
|
||||
|
||||
23
database/factories/CategoryFactory.php
Normal file
23
database/factories/CategoryFactory.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Category>
|
||||
*/
|
||||
class CategoryFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->unique()->words(2, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
docker-compose-dev.yml
Normal file
18
docker-compose-dev.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
services:
|
||||
#PHP Service
|
||||
app:
|
||||
image: quay.io/marshyon/share-lt:0.0.1
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command: sh -c "php artisan optimize:clear && php artisan serve --host=0.0.0.0 --port=8000"
|
||||
volumes:
|
||||
- ./.env:/var/www/.env
|
||||
- ./database/database.sqlite:/var/www/database/database.sqlite
|
||||
- ./storage:/var/www/storage
|
||||
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
working_dir: /var/www
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\CategoryController;
|
||||
use App\Http\Controllers\EntryController;
|
||||
use App\Http\Controllers\TextWidgetController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/categories', [CategoryController::class, 'index']);
|
||||
Route::post('/categories', [CategoryController::class, 'store']);
|
||||
Route::get('/categories/{category}', [CategoryController::class, 'show']);
|
||||
Route::put('/categories/{category}', [CategoryController::class, 'update']);
|
||||
Route::delete('/categories/{category}', [CategoryController::class, 'destroy']);
|
||||
|
||||
Route::get('/entries', [EntryController::class, 'index']);
|
||||
Route::post('/entries', [EntryController::class, 'store']);
|
||||
Route::get('/entries/{entry}', [EntryController::class, 'show']);
|
||||
|
|
|
|||
171
tests/Feature/CategoryApiTest.php
Normal file
171
tests/Feature/CategoryApiTest.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
use function Pest\Laravel\deleteJson;
|
||||
use function Pest\Laravel\getJson;
|
||||
use function Pest\Laravel\postJson;
|
||||
use function Pest\Laravel\putJson;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('index returns all categories', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Category::factory()->count(3)->create();
|
||||
|
||||
actingAs($user)
|
||||
->getJson('/api/categories')
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(3, 'data');
|
||||
});
|
||||
|
||||
test('index requires authentication', function () {
|
||||
getJson('/api/categories')->assertUnauthorized();
|
||||
});
|
||||
|
||||
test('store creates a new category when user is admin', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
|
||||
actingAs($admin)
|
||||
->postJson('/api/categories', ['name' => 'Technology'])
|
||||
->assertSuccessful()
|
||||
->assertJsonPath('data.name', 'Technology');
|
||||
|
||||
expect(Category::where('name', 'Technology')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('store fails when name is missing', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
|
||||
actingAs($admin)
|
||||
->postJson('/api/categories', [])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['name']);
|
||||
});
|
||||
|
||||
test('store fails when name is not unique', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
|
||||
Category::factory()->create(['name' => 'Existing']);
|
||||
|
||||
actingAs($admin)
|
||||
->postJson('/api/categories', ['name' => 'Existing'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['name']);
|
||||
});
|
||||
|
||||
test('store fails when user is not admin', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
actingAs($user)
|
||||
->postJson('/api/categories', ['name' => 'Technology'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('store requires authentication', function () {
|
||||
postJson('/api/categories', ['name' => 'Technology'])->assertUnauthorized();
|
||||
});
|
||||
|
||||
test('show returns a single category', function () {
|
||||
$user = User::factory()->create();
|
||||
$category = Category::factory()->create(['name' => 'Science']);
|
||||
|
||||
actingAs($user)
|
||||
->getJson("/api/categories/{$category->id}")
|
||||
->assertSuccessful()
|
||||
->assertJsonPath('data.name', 'Science')
|
||||
->assertJsonPath('data.id', $category->id);
|
||||
});
|
||||
|
||||
test('show requires authentication', function () {
|
||||
$category = Category::factory()->create();
|
||||
|
||||
getJson("/api/categories/{$category->id}")->assertUnauthorized();
|
||||
});
|
||||
|
||||
test('update modifies an existing category when user is admin', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
$category = Category::factory()->create(['name' => 'Old Name']);
|
||||
|
||||
actingAs($admin)
|
||||
->putJson("/api/categories/{$category->id}", ['name' => 'New Name'])
|
||||
->assertSuccessful()
|
||||
->assertJsonPath('data.name', 'New Name');
|
||||
|
||||
expect($category->refresh()->name)->toBe('New Name');
|
||||
});
|
||||
|
||||
test('update allows partial updates', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
$category = Category::factory()->create(['name' => 'Original']);
|
||||
|
||||
actingAs($admin)
|
||||
->putJson("/api/categories/{$category->id}", [])
|
||||
->assertSuccessful();
|
||||
|
||||
expect($category->refresh()->name)->toBe('Original');
|
||||
});
|
||||
|
||||
test('update fails when name is not unique', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
|
||||
Category::factory()->create(['name' => 'Existing']);
|
||||
$category = Category::factory()->create(['name' => 'Original']);
|
||||
|
||||
actingAs($admin)
|
||||
->putJson("/api/categories/{$category->id}", ['name' => 'Existing'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['name']);
|
||||
});
|
||||
|
||||
test('update fails when user is not admin', function () {
|
||||
$user = User::factory()->create();
|
||||
$category = Category::factory()->create();
|
||||
|
||||
actingAs($user)
|
||||
->putJson("/api/categories/{$category->id}", ['name' => 'New Name'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('update requires authentication', function () {
|
||||
$category = Category::factory()->create();
|
||||
|
||||
putJson("/api/categories/{$category->id}", ['name' => 'New Name'])->assertUnauthorized();
|
||||
});
|
||||
|
||||
test('destroy deletes a category when user is admin', function () {
|
||||
$adminEmail = config('app.admin_email');
|
||||
$admin = User::factory()->create(['email' => $adminEmail]);
|
||||
$category = Category::factory()->create();
|
||||
|
||||
actingAs($admin)
|
||||
->deleteJson("/api/categories/{$category->id}")
|
||||
->assertNoContent();
|
||||
|
||||
expect(Category::find($category->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy fails when user is not admin', function () {
|
||||
$user = User::factory()->create();
|
||||
$category = Category::factory()->create();
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson("/api/categories/{$category->id}")
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('destroy requires authentication', function () {
|
||||
$category = Category::factory()->create();
|
||||
|
||||
deleteJson("/api/categories/{$category->id}")->assertUnauthorized();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue