diff --git a/app/Http/ApiControllers/HoldingController.php b/app/Http/ApiControllers/HoldingController.php index ee345df..483fbc3 100644 --- a/app/Http/ApiControllers/HoldingController.php +++ b/app/Http/ApiControllers/HoldingController.php @@ -5,6 +5,8 @@ namespace App\Http\ApiControllers; use App\Models\Holding; use App\Models\Portfolio; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; +use App\Http\Requests\HoldingRequest; use App\Http\Resources\HoldingResource; use HackerEsq\FilterModels\FilterModels; use App\Http\ApiControllers\Controller as ApiController; @@ -25,12 +27,22 @@ class HoldingController extends ApiController public function show(Portfolio $portfolio, string $symbol) { - // + Gate::authorize('readOnly', $portfolio); + + $holding = $portfolio->holdings()->symbol($symbol)->firstOrFail(); + + return HoldingResource::make($holding); } - public function put(FilterModels $filters) + public function update(HoldingRequest $request, Portfolio $portfolio, string $symbol) { - // + Gate::authorize('fullAccess', $portfolio); + + $holding = $portfolio->holdings()->symbol($symbol)->firstOrFail(); + + $holding->update($request->validated()); + + return HoldingResource::make($holding); } } \ No newline at end of file diff --git a/app/Http/Requests/HoldingRequest.php b/app/Http/Requests/HoldingRequest.php new file mode 100644 index 0000000..a75d82d --- /dev/null +++ b/app/Http/Requests/HoldingRequest.php @@ -0,0 +1,24 @@ +|string> + */ + public function rules(): array + { + + $rules = [ + 'reinvest_dividends' => ['sometimes', 'boolean'] + ]; + + return $rules; + } +} diff --git a/routes/api.php b/routes/api.php index 949c05b..32395b5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,6 +20,8 @@ Route::middleware(['auth:sanctum'])->name('api.')->group(function () { // holding Route::get('/holding', [HoldingController::class, 'index'])->name('holding.index'); + Route::get('/holding/{portfolio}/{symbol}', [HoldingController::class, 'show'])->name('holding.show')->scopeBindings(); + Route::put('/holding/{portfolio}/{symbol}', [HoldingController::class, 'update'])->name('holding.update')->scopeBindings(); // market data Route::get('/market-data/{symbol}', [MarketDataController::class, 'show'])->name('market-data.show'); diff --git a/tests/Api/HoldingsTest.php b/tests/Api/HoldingsTest.php new file mode 100644 index 0000000..ef1a24b --- /dev/null +++ b/tests/Api/HoldingsTest.php @@ -0,0 +1,117 @@ +user = User::factory()->create(); + } + + public function test_can_list_holdings() + { + $this->actingAs($this->user); + + Transaction::factory(10)->create(); + + $this->actingAs($this->user) + ->getJson(route('api.holding.index', ['page' => 1, 'itemsPerPage' => 5])) + ->assertOk() + ->assertJsonStructure([ + 'data' => [['id', 'symbol', 'portfolio_id', 'total_market_value', 'dividends_earned']], + 'meta' => ['current_page', 'last_page', 'total'], + 'links' => ['first', 'last', 'prev', 'next'] + ]); + } + + public function test_cannot_list_others_holdings() + { + // create transactions with existing user + $this->actingAs($this->user); + Transaction::factory(10)->create(); + + // Create a new user + $this->actingAs($user = User::factory()->create()); + Transaction::factory(1)->create(); + $this->actingAs($user) + ->getJson(route('api.holding.index', ['page' => 1, 'itemsPerPage' => 5])) + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_cannot_access_holdings_when_unauthenticated() + { + $this->getJson(route('api.holding.index'))->assertUnauthorized(); + } + + public function test_can_show_a_holding() + { + $this->actingAs($this->user); + + $transaction = Transaction::factory()->create(); + + $holding = Holding::where(['portfolio_id' => $transaction->portfolio->id, 'symbol' => $transaction->symbol])->firstOrFail(); + + $this->getJson(route('api.holding.show', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol])) + ->assertOk() + ->assertJsonFragment([ + 'id' => $holding->id, + ]); + } + + public function test_cannot_show_nonexistent_holdings() + { + $this->actingAs($this->user) + ->getJson(route('api.holding.show', ['portfolio' => 'abc-123-foo-BAR', 'symbol' => 'AAPL'])) + ->assertNotFound(); + } + + public function test_can_update_holding_options() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $data = [ + 'reinvest_dividends' => true + ]; + + $this->actingAs($this->user) + ->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data) + ->assertOk() + ->assertJsonFragment([ + 'reinvest_dividends' => true + ]); + } + + public function test_cannot_update_holding_without_permission() + { + $this->actingAs($this->user); + $transaction = Transaction::factory()->create(); + + $data = [ + 'reinvest_dividends' => true + ]; + + $otherUser = User::factory()->create(); + $this->actingAs($otherUser) + ->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data) + ->assertForbidden(); + } + +} \ No newline at end of file