diff --git a/src/ApiResources/Requests/MenuItemOptionRequest.php b/src/ApiResources/Requests/MenuItemOptionRequest.php index a42af28..bd85cfe 100644 --- a/src/ApiResources/Requests/MenuItemOptionRequest.php +++ b/src/ApiResources/Requests/MenuItemOptionRequest.php @@ -9,13 +9,13 @@ class MenuItemOptionRequest extends FormRequest public function attributes() { return [ - 'menu_id', lang('igniter.cart::default.menus.label_option'), - 'option_id', lang('igniter.cart::default.menus.label_option_id'), - 'priority', lang('igniter.cart::default.menus.label_option'), - 'required', lang('igniter.cart::default.menus.label_option_required'), - 'min_selected', lang('igniter.cart::default.menus.label_min_selected'), - 'max_selected', lang('igniter.cart::default.menus.label_max_selected'), - 'menu_option_values.*', lang('admin::lang.label_option_value_id'), + 'menu_id' => lang('igniter.cart::default.menus.label_option'), + 'option_id' => lang('igniter.cart::default.menus.label_option_id'), + 'priority' => lang('igniter.cart::default.menus.label_option'), + 'required' => lang('igniter.cart::default.menus.label_option_required'), + 'min_selected' => lang('igniter.cart::default.menus.label_min_selected'), + 'max_selected' => lang('igniter.cart::default.menus.label_max_selected'), + 'menu_option_values.*' => lang('admin::lang.label_option_value_id'), ]; } diff --git a/src/ApiResources/Requests/OrderRequest.php b/src/ApiResources/Requests/OrderRequest.php index 8499eff..97fd54f 100644 --- a/src/ApiResources/Requests/OrderRequest.php +++ b/src/ApiResources/Requests/OrderRequest.php @@ -3,7 +3,6 @@ namespace Igniter\Api\ApiResources\Requests; use Igniter\System\Classes\FormRequest; -use Illuminate\Support\Facades\Request; class OrderRequest extends FormRequest { @@ -30,8 +29,6 @@ public function attributes() public function rules() { - $method = Request::method(); - $rules = [ 'first_name' => ['between:1,48'], 'last_name' => ['between:1,48'], @@ -47,14 +44,14 @@ public function rules() 'is_processed' => ['integer'], ]; - if ($method == 'post') { + if ($this->method() == 'post') { $rules['first_name'][] = 'required'; $rules['last_name'][] = 'required'; $rules['order_type'][] = 'required'; $rules['customer_id'][] = 'required'; } - if (Request::input('order_type', 'collection') == 'delivery') { + if ($this->input('order_type', 'collection') == 'delivery') { $rules['address_id'] = ['integer']; $rules['address.address_1'] = ['required', 'min:3', 'max:128']; $rules['address.address_2'] = ['sometimes', 'min:3', 'max:128']; diff --git a/src/Classes/ApiManager.php b/src/Classes/ApiManager.php index 1508aca..9e9456a 100644 --- a/src/Classes/ApiManager.php +++ b/src/Classes/ApiManager.php @@ -4,7 +4,6 @@ use Igniter\Api\Models\Resource; use Igniter\Flame\Igniter; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; @@ -46,26 +45,6 @@ public function getCurrentAction() return Str::afterLast(Route::currentRouteAction(), '@'); } - public function buildResource($name, $model, $meta = []) - { - $controller = $this->parseName($name); - $singularController = str_singular($controller); - $namespace = '\\Igniter\\Api\\ApiResources'; - - Artisan::call('create:apiresource', [ - 'extension' => 'Igniter.Api', - 'controller' => $controller, - '--model' => $model, - '--meta' => $meta, - ]); - - if (!class_exists($controllerName = $namespace."\\{$controller}")) { - return [null, null]; - } - - return [$controllerName, $namespace."\\Transformers\\{$singularController}Transformer"]; - } - protected function loadResources() { Resource::syncAll(); diff --git a/src/Database/Factories/ResourceFactory.php b/src/Database/Factories/ResourceFactory.php new file mode 100644 index 0000000..f4986de --- /dev/null +++ b/src/Database/Factories/ResourceFactory.php @@ -0,0 +1,29 @@ + $this->faker->sentence(2), + 'endpoint' => $this->faker->word(), + 'description' => $this->faker->paragraph(), + 'meta' => [ + 'actions' => ['index', 'show', 'store', 'update', 'destroy'], + 'authorization' => [ + 'index' => 'all', + 'show' => 'admin', + 'store' => 'admin', + 'update' => 'admin', + 'destroy' => 'admin', + ], + ], + ]; + } +} diff --git a/src/Database/Factories/TokenFactory.php b/src/Database/Factories/TokenFactory.php new file mode 100644 index 0000000..3120d2f --- /dev/null +++ b/src/Database/Factories/TokenFactory.php @@ -0,0 +1,21 @@ + 'users', + 'tokenable_id' => 1, + 'name' => $this->faker->sentence(2), + 'token' => $this->faker->sha256, + 'abilities' => ['*'], + ]; + } +} diff --git a/src/Http/Requests/ResourceRequest.php b/src/Http/Requests/ResourceRequest.php index b330447..8679716 100644 --- a/src/Http/Requests/ResourceRequest.php +++ b/src/Http/Requests/ResourceRequest.php @@ -6,7 +6,18 @@ class ResourceRequest extends FormRequest { - public function rules() + public function attributes(): array + { + return [ + 'name' => lang('igniter.api::default.label_name'), + 'description' => lang('igniter.api::default.label_description'), + 'endpoint' => lang('igniter.api::default.label_endpoint'), + 'meta.actions' => lang('igniter.api::default.label_actions'), + 'meta.authorization' => lang('igniter.api::default.label_authorization'), + ]; + } + + public function rules(): array { return [ 'name' => ['required', 'min:2', 'max:128', 'string'], diff --git a/src/Models/Resource.php b/src/Models/Resource.php index ecd4a98..b2d74f8 100644 --- a/src/Models/Resource.php +++ b/src/Models/Resource.php @@ -2,6 +2,7 @@ namespace Igniter\Api\Models; +use Igniter\Flame\Database\Factories\HasFactory; use Igniter\Flame\Database\Model; use Igniter\Flame\Database\Traits\HasPermalink; use Igniter\Flame\Mail\Markdown; @@ -13,6 +14,7 @@ */ class Resource extends Model { + use HasFactory; use HasPermalink; /** diff --git a/src/Models/Token.php b/src/Models/Token.php index 26b0e29..15fe167 100644 --- a/src/Models/Token.php +++ b/src/Models/Token.php @@ -2,6 +2,7 @@ namespace Igniter\Api\Models; +use Igniter\Flame\Database\Factories\HasFactory; use Igniter\User\Models\Customer; use Igniter\User\Models\User; use Illuminate\Support\Str; @@ -13,6 +14,8 @@ */ class Token extends PersonalAccessToken { + use HasFactory; + /** * @var string The database table used by the model. */ diff --git a/tests/ApiResources/Requests/MenuItemOptionRequestTest.php b/tests/ApiResources/Requests/MenuItemOptionRequestTest.php new file mode 100644 index 0000000..33d8510 --- /dev/null +++ b/tests/ApiResources/Requests/MenuItemOptionRequestTest.php @@ -0,0 +1,40 @@ +attributes(); + + expect($attributes)->toHaveKey('menu_id', lang('igniter.cart::default.menus.label_option')) + ->and($attributes)->toHaveKey('option_id', lang('igniter.cart::default.menus.label_option_id')) + ->and($attributes)->toHaveKey('priority', lang('igniter.cart::default.menus.label_option')) + ->and($attributes)->toHaveKey('required', lang('igniter.cart::default.menus.label_option_required')) + ->and($attributes)->toHaveKey('min_selected', lang('igniter.cart::default.menus.label_min_selected')) + ->and($attributes)->toHaveKey('max_selected', lang('igniter.cart::default.menus.label_max_selected')) + ->and($attributes)->toHaveKey('menu_option_values.*', lang('admin::lang.label_option_value_id')); +}); + +it('returns correct validation rules', function() { + $request = new MenuItemOptionRequest(); + + $rules = $request->rules(); + + expect($rules)->toHaveKey('menu_id') + ->and($rules)->toHaveKey('option_id') + ->and($rules)->toHaveKey('priority') + ->and($rules)->toHaveKey('required') + ->and($rules)->toHaveKey('min_selected') + ->and($rules)->toHaveKey('max_selected') + ->and($rules)->toHaveKey('menu_option_values.*') + ->and($rules['menu_id'])->toContain('nullable', 'integer') + ->and($rules['option_id'])->toContain('required', 'integer') + ->and($rules['priority'])->toContain('integer') + ->and($rules['required'])->toContain('boolean') + ->and($rules['min_selected'])->toContain('integer', 'lte:max_selected') + ->and($rules['max_selected'])->toContain('integer', 'gte:min_selected') + ->and($rules['menu_option_values.*'])->toContain('array'); +}); diff --git a/tests/ApiResources/Requests/MenuOptionRequestTest.php b/tests/ApiResources/Requests/MenuOptionRequestTest.php new file mode 100644 index 0000000..9bff2cc --- /dev/null +++ b/tests/ApiResources/Requests/MenuOptionRequestTest.php @@ -0,0 +1,49 @@ +attributes(); + + expect($attributes)->toHaveKey('option_name', lang('admin::lang.menu_options.label_option_group_name')) + ->and($attributes)->toHaveKey('display_type', lang('admin::lang.menu_options.label_display_type')) + ->and($attributes)->toHaveKey('priority', lang('admin::lang.menu_options.label_priority')) + ->and($attributes)->toHaveKey('locations.*', lang('admin::lang.label_location')) + ->and($attributes)->toHaveKey('option_values.*.option_value_id', lang('admin::lang.label_option_value_id')) + ->and($attributes)->toHaveKey('option_values.*.option_id', lang('admin::lang.label_option_id')) + ->and($attributes)->toHaveKey('option_values.*.value', lang('admin::lang.menu_options.label_option_value')) + ->and($attributes)->toHaveKey('option_values.*.price', lang('admin::lang.menu_options.label_option_price')) + ->and($attributes)->toHaveKey('option_values.*.priority', lang('admin::lang.menu_options.label_option_price')) + ->and($attributes)->toHaveKey('option_values.*.allergens.*', lang('igniter.cart::default.menus.label_allergens')); +}); + +it('returns correct validation rules', function() { + $request = new MenuOptionRequest(); + + $rules = $request->rules(); + + expect($rules)->toHaveKey('option_name') + ->and($rules)->toHaveKey('display_type') + ->and($rules)->toHaveKey('priority') + ->and($rules)->toHaveKey('locations.*') + ->and($rules)->toHaveKey('option_values.*.option_value_id') + ->and($rules)->toHaveKey('option_values.*.option_id') + ->and($rules)->toHaveKey('option_values.*.value') + ->and($rules)->toHaveKey('option_values.*.price') + ->and($rules)->toHaveKey('option_values.*.priority') + ->and($rules)->toHaveKey('option_values.*.allergens.*') + ->and($rules['option_name'])->toContain('required', 'min:2', 'max:32') + ->and($rules['display_type'])->toContain('required', 'alpha') + ->and($rules['priority'])->toContain('integer') + ->and($rules['locations.*'])->toContain('integer') + ->and($rules['option_values.*.option_value_id'])->toContain('integer') + ->and($rules['option_values.*.option_id'])->toContain('integer') + ->and($rules['option_values.*.value'])->toContain('min:2', 'max:128') + ->and($rules['option_values.*.price'])->toContain('numeric', 'min:0') + ->and($rules['option_values.*.priority'])->toContain('integer') + ->and($rules['option_values.*.allergens.*'])->toContain('integer'); +}); diff --git a/tests/ApiResources/Requests/OrderRequestTest.php b/tests/ApiResources/Requests/OrderRequestTest.php new file mode 100644 index 0000000..47e3cff --- /dev/null +++ b/tests/ApiResources/Requests/OrderRequestTest.php @@ -0,0 +1,68 @@ +attributes(); + + expect($attributes)->toHaveKey('first_name', lang('igniter.cart::default.checkout.label_first_name')) + ->and($attributes)->toHaveKey('last_name', lang('igniter.cart::default.checkout.label_last_name')) + ->and($attributes)->toHaveKey('email', lang('igniter.cart::default.checkout.label_email')) + ->and($attributes)->toHaveKey('telephone', lang('igniter.cart::default.checkout.label_telephone')) + ->and($attributes)->toHaveKey('comment', lang('igniter.cart::default.checkout.label_comment')) + ->and($attributes)->toHaveKey('payment', lang('igniter.cart::default.checkout.label_payment_method')) + ->and($attributes)->toHaveKey('order_type', lang('igniter.cart::default.checkout.label_order_type')) + ->and($attributes)->toHaveKey('address_id', lang('igniter.cart::default.checkout.label_address')) + ->and($attributes)->toHaveKey('address.address_1', lang('igniter.cart::default.checkout.label_address_1')) + ->and($attributes)->toHaveKey('address.address_2', lang('igniter.cart::default.checkout.label_address_2')) + ->and($attributes)->toHaveKey('address.city', lang('igniter.cart::default.checkout.label_city')) + ->and($attributes)->toHaveKey('address.state', lang('igniter.cart::default.checkout.label_state')) + ->and($attributes)->toHaveKey('address.postcode', lang('igniter.cart::default.checkout.label_postcode')) + ->and($attributes)->toHaveKey('address.country_id', lang('igniter.cart::default.checkout.label_country')); +}); + +it('returns correct validation rules', function() { + $request = new OrderRequest(); + $request->setMethod('post'); + $request->merge([ + 'order_type' => 'delivery', + ]); + + $rules = $request->rules(); + + expect($rules)->toHaveKey('first_name') + ->and($rules)->toHaveKey('last_name') + ->and($rules)->toHaveKey('email') + ->and($rules)->toHaveKey('telephone') + ->and($rules)->toHaveKey('comment') + ->and($rules)->toHaveKey('payment') + ->and($rules)->toHaveKey('order_type') + ->and($rules)->toHaveKey('address_id') + ->and($rules)->toHaveKey('address.address_1') + ->and($rules)->toHaveKey('address.address_2') + ->and($rules)->toHaveKey('address.city') + ->and($rules)->toHaveKey('address.state') + ->and($rules)->toHaveKey('address.postcode') + ->and($rules)->toHaveKey('address.country_id') + ->and($rules)->toHaveKey('customer_id') + ->and($rules)->toHaveKey('order_menus') + ->and($rules)->toHaveKey('order_totals') + ->and($rules)->toHaveKey('status_id') + ->and($rules)->toHaveKey('is_processed') + ->and($rules['first_name'])->toContain('between:1,48') + ->and($rules['last_name'])->toContain('between:1,48') + ->and($rules['email'])->toContain('sometimes', 'required', 'email:filter', 'max:96') + ->and($rules['telephone'])->toContain('string') + ->and($rules['comment'])->toContain('max:500') + ->and($rules['order_type'])->toContain('alpha_dash') + ->and($rules['payment'])->toContain('sometimes', 'required', 'alpha_dash') + ->and($rules['customer_id'])->toContain('integer') + ->and($rules['order_menus'])->toContain('array') + ->and($rules['order_totals'])->toContain('array') + ->and($rules['status_id'])->toContain('integer') + ->and($rules['is_processed'])->toContain('integer'); +}); diff --git a/tests/ApiResources/Requests/ReservationRequestTest.php b/tests/ApiResources/Requests/ReservationRequestTest.php new file mode 100644 index 0000000..1452196 --- /dev/null +++ b/tests/ApiResources/Requests/ReservationRequestTest.php @@ -0,0 +1,49 @@ +attributes(); + + expect($attributes)->toHaveKey('table_id', lang('igniter.reservation::default.column_table')) + ->and($attributes)->toHaveKey('location_id', lang('igniter.reservation::default.label_location')) + ->and($attributes)->toHaveKey('guest_num', lang('igniter.reservation::default.label_guest_num')) + ->and($attributes)->toHaveKey('reserve_date', lang('igniter.reservation::default.label_date')) + ->and($attributes)->toHaveKey('reserve_time', lang('igniter.reservation::default.label_time')) + ->and($attributes)->toHaveKey('first_name', lang('igniter.reservation::default.label_first_name')) + ->and($attributes)->toHaveKey('last_name', lang('igniter.reservation::default.label_last_name')) + ->and($attributes)->toHaveKey('email', lang('igniter.reservation::default.label_email')) + ->and($attributes)->toHaveKey('telephone', lang('igniter.reservation::default.label_telephone')) + ->and($attributes)->toHaveKey('comment', lang('igniter.reservation::default.label_comment')); +}); + +it('returns correct validation rules', function() { + $request = new ReservationRequest(); + + $rules = $request->rules(); + + expect($rules)->toHaveKey('table_id') + ->and($rules)->toHaveKey('location_id') + ->and($rules)->toHaveKey('guest_num') + ->and($rules)->toHaveKey('reserve_date') + ->and($rules)->toHaveKey('reserve_time') + ->and($rules)->toHaveKey('first_name') + ->and($rules)->toHaveKey('last_name') + ->and($rules)->toHaveKey('email') + ->and($rules)->toHaveKey('telephone') + ->and($rules)->toHaveKey('comment') + ->and($rules['table_id'])->toContain('sometimes', 'required', 'integer') + ->and($rules['location_id'])->toContain('required', 'integer') + ->and($rules['guest_num'])->toContain('required', 'integer') + ->and($rules['reserve_date'])->toContain('required', 'date_format:Y-m-d') + ->and($rules['reserve_time'])->toContain('required', 'date_format:H:i') + ->and($rules['first_name'])->toContain('required', 'between:1,48') + ->and($rules['last_name'])->toContain('required', 'between:1,48') + ->and($rules['email'])->toContain('required', 'email:filter', 'max:96') + ->and($rules['telephone'])->toContain('required') + ->and($rules['comment'])->toContain('max:520'); +}); diff --git a/tests/Classes/AbstractRepositoryTest.php b/tests/Classes/AbstractRepositoryTest.php new file mode 100644 index 0000000..4897caf --- /dev/null +++ b/tests/Classes/AbstractRepositoryTest.php @@ -0,0 +1,89 @@ +repository = Mockery::mock(AbstractRepository::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $this->model = Mockery::mock(Model::class); +}); + +it('finds a record by id', function() { + $this->repository->shouldReceive('createModel')->andReturn($this->model); + $this->repository->shouldReceive('prepareQuery')->andReturn($this->model); + $this->model->shouldReceive('find')->with(1, ['*'])->andReturn($this->model); + + $result = $this->repository->find(1); + + expect($result)->toBe($this->model); +}); + +it('throws exception when record not found by id', function() { + $this->repository->shouldReceive('createModel')->andReturn($this->model); + $this->repository->shouldReceive('prepareQuery')->andReturn($this->model); + $this->model->shouldReceive('find')->with(1, ['*'])->andReturn(null); + + $this->expectException(NotFoundHttpException::class); + + $this->repository->find(1); +}); + +it('finds a record by attribute', function() { + $this->repository->shouldReceive('createModel')->andReturn($this->model); + $this->repository->shouldReceive('prepareQuery')->andReturn($this->model); + $this->model->shouldReceive('where')->with('name', 'test')->andReturnSelf(); + $this->model->shouldReceive('first')->with(['*'])->andReturn($this->model); + + $result = $this->repository->findBy('name', 'test'); + + expect($result)->toBe($this->model); +}); + +it('creates a new record', function() { + $attributes = ['name' => 'test']; + $this->repository->shouldReceive('createModel')->andReturn($this->model); + $this->repository->shouldReceive('setModelAttributes')->with($this->model, $attributes); + $this->repository->shouldReceive('setCustomerAwareAttributes')->with($this->model); + $this->repository->shouldReceive('fireSystemEvent')->with('api.repository.beforeCreate', [$this->model, $attributes]); + $this->repository->shouldReceive('fireSystemEvent')->with('api.repository.afterCreate', [$this->model, true]); + $this->model->shouldReceive('reload'); + + DB::shouldReceive('transaction')->andReturnUsing(function($callback) { + $callback(); + }); + + $result = $this->repository->create($this->model, $attributes); + + expect($result)->toBe($this->model); +}); + +it('updates an existing record', function() { + $attributes = ['name' => 'updated']; + $this->repository->shouldReceive('find')->with(1)->andReturn($this->model); + $this->repository->shouldReceive('setModelAttributes')->with($this->model, $attributes); + $this->repository->shouldReceive('fireSystemEvent')->with('api.repository.beforeUpdate', [$this->model, $attributes]); + $this->repository->shouldReceive('fireSystemEvent')->with('api.repository.afterUpdate', [$this->model, true]); + + DB::shouldReceive('transaction')->andReturnUsing(function($callback) { + $callback(); + }); + + $result = $this->repository->update(1, $attributes); + + expect($result)->toBe($this->model); +}); + +it('deletes a record by id', function() { + $this->repository->shouldReceive('find')->with(1)->andReturn($this->model); + $this->model->shouldReceive('delete')->andReturn(true); + $this->repository->shouldReceive('fireSystemEvent')->with('api.repository.afterDelete', [$this->model, true]); + + $result = $this->repository->delete(1); + + expect($result)->toBe($this->model); +}); diff --git a/tests/Classes/ApiManagerTest.php b/tests/Classes/ApiManagerTest.php new file mode 100644 index 0000000..03eeaf1 --- /dev/null +++ b/tests/Classes/ApiManagerTest.php @@ -0,0 +1,36 @@ +apiManager = new ApiManager(); +}); + +it('returns resources when they are loaded', function() { + expect($this->apiManager->getResources())->toHaveCount(12); +}); + +it('returns empty array when resource is not found', function() { + $resource = $this->apiManager->getResource('non-existent-endpoint'); + + expect($resource)->toBe([]); +}); + +it('returns current resource based on route name', function() { + Route::shouldReceive('currentRouteName')->andReturn('api.categories.index'); + + $currentResource = $this->apiManager->getCurrentResource(); + + expect($currentResource->endpoint)->toBe('categories'); +}); + +it('returns current action based on route action', function() { + Route::shouldReceive('currentRouteAction')->andReturn('TestController@index'); + + $currentAction = $this->apiManager->getCurrentAction(); + + expect($currentAction)->toBe('index'); +}); diff --git a/tests/ExtensionTest.php b/tests/ExtensionTest.php index 92c05c4..22cec5c 100644 --- a/tests/ExtensionTest.php +++ b/tests/ExtensionTest.php @@ -10,7 +10,18 @@ it('loads registered api resources', function() { $resources = Resource::listRegisteredResources(); - expect($resources)->toHaveKey('categories'); + expect($resources) + ->toHaveKey('categories') + ->toHaveKey('currencies') + ->toHaveKey('customers') + ->toHaveKey('locations') + ->toHaveKey('menus') + ->toHaveKey('menu_options') + ->toHaveKey('menu_item_options') + ->toHaveKey('orders') + ->toHaveKey('reservations') + ->toHaveKey('reviews') + ->toHaveKey('tables'); }); it('replaces fractal.fractal_class config item', function() { diff --git a/tests/Fixtures/TestModel.php b/tests/Fixtures/TestModel.php new file mode 100644 index 0000000..491b7e2 --- /dev/null +++ b/tests/Fixtures/TestModel.php @@ -0,0 +1,10 @@ + [], + 'repository' => TestRepository::class, + 'transformer' => TestTransformer::class + ]; +} diff --git a/tests/Fixtures/TestTransformer.php b/tests/Fixtures/TestTransformer.php new file mode 100644 index 0000000..dae6536 --- /dev/null +++ b/tests/Fixtures/TestTransformer.php @@ -0,0 +1,8 @@ +controller = new CreateToken(); +}); + +it('creates token for valid credentials', function(bool $isAdmin, Model $model) { + $this->post(route('igniter.api.token.create'), [ + 'email' => $model->email, + 'password' => 'password', + 'is_admin' => $isAdmin, + 'device_name' => 'device', + 'abilities' => ['*'], + ]); + + expect(Token::where('tokenable_type', $model->getMorphClass()) + ->where('tokenable_id', $model->getKey()) + ->exists())->toBeTrue(); +})->with([ + [true, fn() => User::factory()->superUser()->create()], + [false, fn() => Customer::factory()->create([ + 'is_activated' => true, + ])] +]); + +it('throws validation exception for invalid admin credentials', function(bool $isAdmin, Model $model) { + $response = $this->post(route('igniter.api.token.create'), [ + 'email' => $model->email, + 'password' => 'wrongpassword', + 'is_admin' => $isAdmin, + 'device_name' => 'device', + 'abilities' => ['*'], + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors('email') + ->assertJsonFragment(['The provided credentials are incorrect.']); +})->with([ + [true, fn() => User::factory()->superUser()->create()], + [false, fn() => Customer::factory()->create([ + 'is_activated' => true, + ])] +]); + +it('throws validation exception for inactive user', function(bool $isAdmin, Model $model) { + $response = $this->post(route('igniter.api.token.create'), [ + 'email' => $model->email, + 'password' => 'password', + 'is_admin' => $isAdmin, + 'device_name' => 'device', + 'abilities' => ['*'], + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors('email') + ->assertJsonFragment(['Inactive user account']); +})->with([ + [true, fn() => User::factory()->superUser()->create([ + 'is_activated' => false, + ])], + [false, fn() => Customer::factory()->create([ + 'is_activated' => false, + ])] +]); diff --git a/tests/Http/Controllers/ResourcesTest.php b/tests/Http/Controllers/ResourcesTest.php new file mode 100644 index 0000000..3f920f1 --- /dev/null +++ b/tests/Http/Controllers/ResourcesTest.php @@ -0,0 +1,76 @@ +get(route('igniter.api.resources')) + ->assertOk(); +}); + +it('fails to load create resource page', function() { + actingAsSuperUser() + ->get(route('igniter.api.resources', ['slug' => 'create'])) + ->assertStatus(500); + + actingAsSuperUser() + ->post(route('igniter.api.resources', ['slug' => 'create']), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]) + ->assertStatus(500); +}); + +it('loads edit resource page', function() { + $resource = Resource::factory()->create([ + 'endpoint' => 'categories', + ]); + + actingAsSuperUser() + ->get(route('igniter.api.resources', ['slug' => 'edit/'.$resource->getKey()])) + ->assertOk(); +}); + +it('loads resource preview page', function() { + $resource = Resource::factory()->create([ + 'endpoint' => 'categories', + ]); + + actingAsSuperUser() + ->get(route('igniter.api.resources', ['slug' => 'preview/'.$resource->getKey()])) + ->assertOk(); +}); + +it('updates resource', function() { + $resource = Resource::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.api.resources', ['slug' => 'edit/'.$resource->getKey()]), [ + 'Resource' => [ + 'name' => 'Updated Resource', + 'endpoint' => 'updated-endpoint', + 'description' => 'Updated Resource Description', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Resource::find($resource->getKey()))->name->toBe('Updated Resource') + ->endpoint->not->toBe('updated-endpoint') + ->endpoint->toBe($resource->endpoint); +}); + +it('deletes resource', function() { + $resource = Resource::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.api.resources', ['slug' => 'edit/'.$resource->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Resource::find($resource->getKey()))->toBeNull(); +}); diff --git a/tests/Http/Controllers/TokensTest.php b/tests/Http/Controllers/TokensTest.php new file mode 100644 index 0000000..81b5ae0 --- /dev/null +++ b/tests/Http/Controllers/TokensTest.php @@ -0,0 +1,22 @@ +get(route('igniter.api.tokens')) + ->assertOk(); +}); + +it('deletes token on tokens page', function() { + $token = \Igniter\Api\Models\Token::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.api.tokens'), [ + 'checked' => [$token->getKey()], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]) + ->assertOk(); +}); diff --git a/tests/Http/Middleware/AuthenticateTest.php b/tests/Http/Middleware/AuthenticateTest.php new file mode 100644 index 0000000..3913711 --- /dev/null +++ b/tests/Http/Middleware/AuthenticateTest.php @@ -0,0 +1,49 @@ +middleware = Mockery::mock(Authenticate::class) + ->makePartial() + ->shouldAllowMockingProtectedMethods(); + $this->request = Mockery::mock(Request::class); + $this->next = function($request) { + return 'next'; + }; +}); + +it('handles request with default guard', function() { + config()->set('igniter.api.guard', null); + $this->middleware->shouldReceive('authenticate')->with($this->request, [])->andReturn(true); + + $response = $this->middleware->handle($this->request, $this->next); + + expect($response)->toBe('next'); +}); + +it('handles request with custom guard', function() { + config()->set('igniter.api.guard', 'api'); + $this->middleware->shouldReceive('authenticate')->with($this->request, ['api'])->andReturn(true); + + $response = $this->middleware->handle($this->request, $this->next); + + expect($response)->toBe('next'); +}); + +it('throws custom authentication exception on failure', function() { + config()->set('igniter.api.guard', 'api'); + $this->middleware->shouldReceive('authenticate') + ->with($this->request, ['api']) + ->andThrow(new IlluminateAuthenticationException('Unauthenticated.', ['api'])); + + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Unauthenticated.'); + + $this->middleware->handle($this->request, $this->next); +}); diff --git a/tests/Http/Requests/ResourceRequestTest.php b/tests/Http/Requests/ResourceRequestTest.php new file mode 100644 index 0000000..7b76262 --- /dev/null +++ b/tests/Http/Requests/ResourceRequestTest.php @@ -0,0 +1,36 @@ +attributes(); + + expect($attributes)->toHaveKey('name', lang('igniter.api::default.label_name')) + ->and($attributes)->toHaveKey('description', lang('igniter.api::default.label_description')) + ->and($attributes)->toHaveKey('endpoint', lang('igniter.api::default.label_endpoint')) + ->and($attributes)->toHaveKey('meta.actions', lang('igniter.api::default.label_actions')) + ->and($attributes)->toHaveKey('meta.authorization', lang('igniter.api::default.label_authorization')); +}); + +it('returns correct validation rules', function() { + $request = new ResourceRequest(); + + $rules = $request->rules(); + + expect($rules)->toHaveKey('name') + ->and($rules)->toHaveKey('description') + ->and($rules)->toHaveKey('endpoint') + ->and($rules)->toHaveKey('meta') + ->and($rules)->toHaveKey('meta.actions.*') + ->and($rules)->toHaveKey('meta.authorization.*') + ->and($rules['name'])->toContain('required', 'min:2', 'max:128', 'string') + ->and($rules['description'])->toContain('required', 'min:2', 'max:255') + ->and($rules['endpoint'])->toContain('max:255', 'regex:/^[a-z0-9\-_\/]+$/i', 'unique:igniter_api_resources,endpoint,') + ->and($rules['meta'])->toContain('array') + ->and($rules['meta.actions.*'])->toContain('alpha') + ->and($rules['meta.authorization.*'])->toContain('alpha'); +}); diff --git a/tests/Listeners/TokenEventSubscriberTest.php b/tests/Listeners/TokenEventSubscriberTest.php new file mode 100644 index 0000000..94713eb --- /dev/null +++ b/tests/Listeners/TokenEventSubscriberTest.php @@ -0,0 +1,72 @@ +subscriber = new TokenEventSubscriber(); + $this->token = Token::factory()->create(); + $this->event = new TokenAuthenticated($this->token); + $this->route = Mockery::mock(Route::class); + request()->setRouteResolver(function() { + return $this->route; + }); +}); + +it('returns access token for allowed group all', function() { + RouteFacade::shouldReceive('currentRouteName')->andReturn('api.categories.index'); + $this->route->shouldReceive('currentRouteName')->andReturn('api.categories.index'); + $this->route->shouldReceive('getActionMethod')->andReturn('index'); + + $result = $this->subscriber->handleTokenAuthenticated($this->event); + + expect($result)->token->toBe($this->token->token); +}); + +it('throws unauthorized exception for missing access token', function() { + RouteFacade::shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('getActionMethod')->andReturn('store'); + + $this->event->token = null; + + $this->expectException(UnauthorizedHttpException::class); + $this->expectExceptionMessage('No valid API token provided.'); + + $this->subscriber->handleTokenAuthenticated($this->event); +}); + +it('throws access denied exception for restricted group', function() { + RouteFacade::shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('getActionMethod')->andReturn('store'); + + $this->event->token->tokenable_type = 'customers'; + + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('The API token doesn\'t have permissions to perform the request.'); + + $this->subscriber->handleTokenAuthenticated($this->event); +}); + +it('returns tokenable for valid access token', function() { + RouteFacade::shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('currentRouteName')->andReturn('api.categories.store'); + $this->route->shouldReceive('getActionMethod')->andReturn('store'); + + $this->event->token = $this->token; + $this->event->token->tokenable = User::factory()->create(); + + $tokenable = $this->subscriber->handleTokenAuthenticated($this->event); + + expect($tokenable)->toBeInstanceOf(User::class); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 9b0c66e..f8986fd 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,3 +1,10 @@ in(__DIR__); + +function actingAsSuperUser() +{ + return test()->actingAs(User::factory()->superUser()->create(), 'igniter-admin'); +}