From 2bf25bf7efd6f9321323d5d03e1df2e5a573d57c Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 7 Nov 2025 13:37:13 +0000 Subject: [PATCH] wip --- .../Controllers/Api/LicenseController.php | 16 ++ routes/api.php | 1 + tests/Feature/Api/LicenseRenewalTest.php | 151 ++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 tests/Feature/Api/LicenseRenewalTest.php diff --git a/app/Http/Controllers/Api/LicenseController.php b/app/Http/Controllers/Api/LicenseController.php index 183f1097..4715f59c 100644 --- a/app/Http/Controllers/Api/LicenseController.php +++ b/app/Http/Controllers/Api/LicenseController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Http\Resources\Api\LicenseResource; use App\Jobs\CreateAnystackLicenseJob; +use App\Jobs\UpdateAnystackLicenseExpiryJob; use App\Models\License; use App\Models\User; use Illuminate\Http\Request; @@ -78,4 +79,19 @@ public function show(string $key) return new LicenseResource($license); } + + public function renew(string $key): LicenseResource + { + $license = License::where('key', $key) + ->with('user') + ->firstOrFail(); + + // Update Anystack first, then update database with new expiry date + UpdateAnystackLicenseExpiryJob::dispatchSync($license); + + // Refresh to get the updated expiry date + $license->refresh(); + + return new LicenseResource($license); + } } diff --git a/routes/api.php b/routes/api.php index 24843d09..a818afb5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,6 +20,7 @@ Route::post('/licenses', [LicenseController::class, 'store']); Route::get('/licenses/{key}', [LicenseController::class, 'show']); Route::get('/licenses', [LicenseController::class, 'index']); + Route::patch('/licenses/{key}/renew', [LicenseController::class, 'renew']); Route::post('/temp-links', [TemporaryLinkController::class, 'store']); }); diff --git a/tests/Feature/Api/LicenseRenewalTest.php b/tests/Feature/Api/LicenseRenewalTest.php new file mode 100644 index 00000000..b4cf33d8 --- /dev/null +++ b/tests/Feature/Api/LicenseRenewalTest.php @@ -0,0 +1,151 @@ + Http::response([ + 'data' => [ + 'id' => 'license_123', + 'key' => 'TEST-LICENSE-KEY', + 'expires_at' => now()->addYear()->toISOString(), + 'updated_at' => now()->toISOString(), + ], + ], 200), + ]); + } + + public function test_requires_authentication(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'TEST-KEY-123', + ]); + + $response = $this->patchJson('/api/licenses/'.$license->key.'/renew'); + + $response->assertStatus(401); + } + + public function test_returns_404_for_non_existent_license(): void + { + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->patchJson('/api/licenses/NON-EXISTENT-KEY/renew'); + + $response->assertStatus(404); + } + + public function test_successfully_renews_license_and_returns_new_expiry_date(): void + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'Test User', + ]); + + $currentExpiry = now()->addMonths(3); + + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'TEST-LICENSE-KEY', + 'policy_name' => 'pro', + 'source' => 'bifrost', + 'anystack_id' => 'license_123', + 'expires_at' => $currentExpiry, + ]); + + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->patchJson('/api/licenses/'.$license->key.'/renew'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'anystack_id', + 'key', + 'policy_name', + 'source', + 'expires_at', + 'created_at', + 'updated_at', + 'email', + ], + ]) + ->assertJson([ + 'data' => [ + 'id' => $license->id, + 'anystack_id' => 'license_123', + 'key' => 'TEST-LICENSE-KEY', + 'policy_name' => 'pro', + 'source' => 'bifrost', + 'email' => 'test@example.com', + ], + ]); + + // Verify the expiry date was updated in the database + $license->refresh(); + $this->assertNotNull($license->expires_at); + $this->assertTrue( + $license->expires_at->greaterThan($currentExpiry), + 'New expiry date should be after the current expiry date' + ); + + // Verify Anystack API was called + Http::assertSent(function ($request) use ($license) { + return $request->url() === "https://api.anystack.sh/v1/products/{$license->anystack_product_id}/licenses/{$license->anystack_id}/renew" + && $request->method() === 'PATCH'; + }); + } + + public function test_renews_license_without_anystack_id_logs_error(): void + { + $user = User::factory()->create(); + + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'TEST-LICENSE-KEY', + 'anystack_id' => null, // No Anystack ID + 'expires_at' => now()->addMonths(3), + ]); + + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->patchJson('/api/licenses/'.$license->key.'/renew'); + + // Should still return 200 but the expiry won't be updated + $response->assertStatus(200); + + // Verify the expiry date was NOT updated + $license->refresh(); + $this->assertEquals( + now()->addMonths(3)->format('Y-m-d H:i'), + $license->expires_at->format('Y-m-d H:i'), + 'Expiry date should remain unchanged when no anystack_id' + ); + + // Verify Anystack API was NOT called + Http::assertNothingSent(); + } +}