Skip to content

Commit 2bf25bf

Browse files
committed
wip
1 parent e903e51 commit 2bf25bf

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

app/Http/Controllers/Api/LicenseController.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Http\Controllers\Controller;
88
use App\Http\Resources\Api\LicenseResource;
99
use App\Jobs\CreateAnystackLicenseJob;
10+
use App\Jobs\UpdateAnystackLicenseExpiryJob;
1011
use App\Models\License;
1112
use App\Models\User;
1213
use Illuminate\Http\Request;
@@ -78,4 +79,19 @@ public function show(string $key)
7879

7980
return new LicenseResource($license);
8081
}
82+
83+
public function renew(string $key): LicenseResource
84+
{
85+
$license = License::where('key', $key)
86+
->with('user')
87+
->firstOrFail();
88+
89+
// Update Anystack first, then update database with new expiry date
90+
UpdateAnystackLicenseExpiryJob::dispatchSync($license);
91+
92+
// Refresh to get the updated expiry date
93+
$license->refresh();
94+
95+
return new LicenseResource($license);
96+
}
8197
}

routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Route::post('/licenses', [LicenseController::class, 'store']);
2121
Route::get('/licenses/{key}', [LicenseController::class, 'show']);
2222
Route::get('/licenses', [LicenseController::class, 'index']);
23+
Route::patch('/licenses/{key}/renew', [LicenseController::class, 'renew']);
2324
Route::post('/temp-links', [TemporaryLinkController::class, 'store']);
2425
});
2526

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Tests\Feature\Api;
4+
5+
use App\Models\License;
6+
use App\Models\User;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Illuminate\Support\Facades\Http;
9+
use Tests\TestCase;
10+
11+
class LicenseRenewalTest extends TestCase
12+
{
13+
use RefreshDatabase;
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
// Mock the Anystack API renew endpoint
20+
Http::fake([
21+
'https://api.anystack.sh/v1/products/*/licenses/*/renew' => Http::response([
22+
'data' => [
23+
'id' => 'license_123',
24+
'key' => 'TEST-LICENSE-KEY',
25+
'expires_at' => now()->addYear()->toISOString(),
26+
'updated_at' => now()->toISOString(),
27+
],
28+
], 200),
29+
]);
30+
}
31+
32+
public function test_requires_authentication(): void
33+
{
34+
$user = User::factory()->create();
35+
$license = License::factory()->create([
36+
'user_id' => $user->id,
37+
'key' => 'TEST-KEY-123',
38+
]);
39+
40+
$response = $this->patchJson('/api/licenses/'.$license->key.'/renew');
41+
42+
$response->assertStatus(401);
43+
}
44+
45+
public function test_returns_404_for_non_existent_license(): void
46+
{
47+
$token = config('services.bifrost.api_key');
48+
49+
$response = $this->withHeaders([
50+
'Authorization' => 'Bearer '.$token,
51+
])->patchJson('/api/licenses/NON-EXISTENT-KEY/renew');
52+
53+
$response->assertStatus(404);
54+
}
55+
56+
public function test_successfully_renews_license_and_returns_new_expiry_date(): void
57+
{
58+
$user = User::factory()->create([
59+
'email' => '[email protected]',
60+
'name' => 'Test User',
61+
]);
62+
63+
$currentExpiry = now()->addMonths(3);
64+
65+
$license = License::factory()->create([
66+
'user_id' => $user->id,
67+
'key' => 'TEST-LICENSE-KEY',
68+
'policy_name' => 'pro',
69+
'source' => 'bifrost',
70+
'anystack_id' => 'license_123',
71+
'expires_at' => $currentExpiry,
72+
]);
73+
74+
$token = config('services.bifrost.api_key');
75+
76+
$response = $this->withHeaders([
77+
'Authorization' => 'Bearer '.$token,
78+
])->patchJson('/api/licenses/'.$license->key.'/renew');
79+
80+
$response->assertStatus(200)
81+
->assertJsonStructure([
82+
'data' => [
83+
'id',
84+
'anystack_id',
85+
'key',
86+
'policy_name',
87+
'source',
88+
'expires_at',
89+
'created_at',
90+
'updated_at',
91+
'email',
92+
],
93+
])
94+
->assertJson([
95+
'data' => [
96+
'id' => $license->id,
97+
'anystack_id' => 'license_123',
98+
'key' => 'TEST-LICENSE-KEY',
99+
'policy_name' => 'pro',
100+
'source' => 'bifrost',
101+
'email' => '[email protected]',
102+
],
103+
]);
104+
105+
// Verify the expiry date was updated in the database
106+
$license->refresh();
107+
$this->assertNotNull($license->expires_at);
108+
$this->assertTrue(
109+
$license->expires_at->greaterThan($currentExpiry),
110+
'New expiry date should be after the current expiry date'
111+
);
112+
113+
// Verify Anystack API was called
114+
Http::assertSent(function ($request) use ($license) {
115+
return $request->url() === "https://api.anystack.sh/v1/products/{$license->anystack_product_id}/licenses/{$license->anystack_id}/renew"
116+
&& $request->method() === 'PATCH';
117+
});
118+
}
119+
120+
public function test_renews_license_without_anystack_id_logs_error(): void
121+
{
122+
$user = User::factory()->create();
123+
124+
$license = License::factory()->create([
125+
'user_id' => $user->id,
126+
'key' => 'TEST-LICENSE-KEY',
127+
'anystack_id' => null, // No Anystack ID
128+
'expires_at' => now()->addMonths(3),
129+
]);
130+
131+
$token = config('services.bifrost.api_key');
132+
133+
$response = $this->withHeaders([
134+
'Authorization' => 'Bearer '.$token,
135+
])->patchJson('/api/licenses/'.$license->key.'/renew');
136+
137+
// Should still return 200 but the expiry won't be updated
138+
$response->assertStatus(200);
139+
140+
// Verify the expiry date was NOT updated
141+
$license->refresh();
142+
$this->assertEquals(
143+
now()->addMonths(3)->format('Y-m-d H:i'),
144+
$license->expires_at->format('Y-m-d H:i'),
145+
'Expiry date should remain unchanged when no anystack_id'
146+
);
147+
148+
// Verify Anystack API was NOT called
149+
Http::assertNothingSent();
150+
}
151+
}

0 commit comments

Comments
 (0)