350 lines
11 KiB
PHP
350 lines
11 KiB
PHP
<?php
|
|
|
|
use Bjanczak\FilamentShortUrl\Jobs\SendWebhookJob;
|
|
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
|
|
use Bjanczak\FilamentShortUrl\Models\ShortUrlPixel;
|
|
use Bjanczak\FilamentShortUrl\Services\ShortUrlSettingsManager;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
beforeEach(function () {
|
|
// Enable REST API and inject mock keys
|
|
app(ShortUrlSettingsManager::class)->set([
|
|
'api_enabled' => true,
|
|
'api_keys' => [
|
|
['name' => 'Active Test Token', 'key' => 'sh_key_active_token', 'is_active' => true],
|
|
['name' => 'Inactive Test Token', 'key' => 'sh_key_inactive_token', 'is_active' => false],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('rejects API requests without a key', function () {
|
|
$response = $this->getJson('/api/short-url/links');
|
|
|
|
$response->assertStatus(401)
|
|
->assertJsonFragment(['error' => 'Unauthorized. API Key is missing.']);
|
|
});
|
|
|
|
it('returns 503 when the REST API is disabled in settings', function () {
|
|
app(ShortUrlSettingsManager::class)->set(['api_enabled' => false]);
|
|
|
|
$response = $this->getJson('/api/short-url/links', [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(503)
|
|
->assertJsonPath('error', fn ($v) => str_contains($v, 'disabled'));
|
|
});
|
|
|
|
it('rejects API requests with an invalid key', function () {
|
|
$response = $this->getJson('/api/short-url/links', [
|
|
'X-Api-Key' => 'sh_key_invalid_value',
|
|
]);
|
|
|
|
$response->assertStatus(401)
|
|
->assertJsonFragment(['error' => 'Unauthorized. Invalid or inactive API Key.']);
|
|
});
|
|
|
|
it('rejects API requests with an inactive key', function () {
|
|
$response = $this->getJson('/api/short-url/links', [
|
|
'X-Api-Key' => 'sh_key_inactive_token',
|
|
]);
|
|
|
|
$response->assertStatus(401)
|
|
->assertJsonFragment(['error' => 'Unauthorized. Invalid or inactive API Key.']);
|
|
});
|
|
|
|
it('allows API requests with a valid key', function () {
|
|
// Seed at least one short URL
|
|
ShortUrl::create([
|
|
'destination_url' => 'https://example.com/api-test',
|
|
'url_key' => 'apitest',
|
|
]);
|
|
|
|
$response = $this->getJson('/api/short-url/links', [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'data',
|
|
'meta' => ['current_page', 'last_page', 'per_page', 'total'],
|
|
])
|
|
->assertJsonFragment(['url_key' => 'apitest']);
|
|
});
|
|
|
|
it('allows creating a short link programmatically via POST', function () {
|
|
Queue::fake([SendWebhookJob::class]);
|
|
|
|
$pixel = ShortUrlPixel::create([
|
|
'name' => 'Meta Pixel Test',
|
|
'type' => 'meta',
|
|
'pixel_id' => '12345',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$response = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'googleapi',
|
|
'notes' => 'API Generated link',
|
|
'single_use' => true,
|
|
'pixels' => [$pixel->id],
|
|
'webhook_url' => 'https://webhook.site/test',
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJsonFragment(['url_key' => 'googleapi'])
|
|
->assertJsonFragment(['notes' => 'API Generated link']);
|
|
|
|
$this->assertDatabaseHas('short_urls', [
|
|
'url_key' => 'googleapi',
|
|
'single_use' => true,
|
|
'webhook_url' => 'https://webhook.site/test',
|
|
]);
|
|
|
|
$shortUrl = ShortUrl::where('url_key', 'googleapi')->first();
|
|
$this->assertDatabaseHas('short_url_pixel', [
|
|
'short_url_id' => $shortUrl->id,
|
|
'pixel_id' => $pixel->id,
|
|
]);
|
|
|
|
// SendWebhookJob should be dispatched for 'created' event since custom webhook_url is set
|
|
Queue::assertPushed(SendWebhookJob::class, function ($job) {
|
|
return $job->url === 'https://webhook.site/test' && $job->event === 'created';
|
|
});
|
|
});
|
|
|
|
it('allows deleting a short link via DELETE', function () {
|
|
$link = ShortUrl::create([
|
|
'destination_url' => 'https://delete-me.com',
|
|
'url_key' => 'delkey',
|
|
]);
|
|
|
|
$response = $this->deleteJson("/api/short-url/links/{$link->id}", [], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonFragment(['message' => 'Short URL deleted successfully.']);
|
|
|
|
$this->assertDatabaseMissing('short_urls', [
|
|
'id' => $link->id,
|
|
]);
|
|
});
|
|
|
|
it('allows updating a short link via PUT', function () {
|
|
$link = ShortUrl::create([
|
|
'destination_url' => 'https://initial-destination.com',
|
|
'url_key' => 'initkey',
|
|
'notes' => 'Initial notes',
|
|
]);
|
|
|
|
$response = $this->putJson("/api/short-url/links/{$link->id}", [
|
|
'destination_url' => 'https://updated-destination.com',
|
|
'notes' => 'Updated notes',
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonFragment(['destination_url' => 'https://updated-destination.com'])
|
|
->assertJsonFragment(['notes' => 'Updated notes'])
|
|
->assertJsonFragment(['url_key' => 'initkey']);
|
|
|
|
$this->assertDatabaseHas('short_urls', [
|
|
'id' => $link->id,
|
|
'destination_url' => 'https://updated-destination.com',
|
|
'notes' => 'Updated notes',
|
|
'url_key' => 'initkey',
|
|
]);
|
|
});
|
|
|
|
it('allows showing a single short link via GET by ID or key', function () {
|
|
$link = ShortUrl::create([
|
|
'destination_url' => 'https://single-show.com',
|
|
'url_key' => 'showkey',
|
|
]);
|
|
|
|
// Show by ID
|
|
$response = $this->getJson("/api/short-url/links/{$link->id}", [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonFragment(['url_key' => 'showkey']);
|
|
|
|
// Show by Key
|
|
$responseKey = $this->getJson('/api/short-url/links/showkey', [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseKey->assertStatus(200)
|
|
->assertJsonFragment(['id' => $link->id]);
|
|
});
|
|
|
|
it('allows fetching link statistics via GET', function () {
|
|
$link = ShortUrl::create([
|
|
'destination_url' => 'https://stats-test.com',
|
|
'url_key' => 'statskey',
|
|
]);
|
|
|
|
$response = $this->getJson("/api/short-url/links/{$link->id}/stats", [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'totalVisits',
|
|
'uniqueVisits',
|
|
'visitsToday',
|
|
'visitsByDay',
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('validates new targeting rules in API payload', function () {
|
|
// 1. Empty filters array validation
|
|
$responseEmpty = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'rule_empty',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'filters' => [], // Empty filters array, should fail
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseEmpty->assertStatus(422)
|
|
->assertJsonValidationErrors(['targeting_rules.0.filters']);
|
|
|
|
// 2. Duplicate filter type validation
|
|
$responseDuplicate = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'rule_dup',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'filters' => [
|
|
[
|
|
'type' => 'device',
|
|
'data' => ['devices' => ['mobile']],
|
|
],
|
|
[
|
|
'type' => 'device', // Duplicate filter type, should fail
|
|
'data' => ['devices' => ['desktop']],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseDuplicate->assertStatus(422)
|
|
->assertJsonValidationErrors(['targeting_rules.0.filters']);
|
|
|
|
// 3. Valid rules should succeed
|
|
$responseValid = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'rule_valid',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'filters' => [
|
|
[
|
|
'type' => 'device',
|
|
'data' => ['devices' => ['mobile']],
|
|
],
|
|
[
|
|
'type' => 'platform',
|
|
'data' => ['platforms' => ['ios']],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseValid->assertStatus(201);
|
|
|
|
// 4. Extraneous key in targeting rules validation
|
|
$responseExtraRuleKey = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'rule_extra_key',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'random_junk_key' => 'garbage', // extraneous key, should fail
|
|
'filters' => [
|
|
[
|
|
'type' => 'device',
|
|
'data' => ['devices' => ['mobile']],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseExtraRuleKey->assertStatus(422)
|
|
->assertJsonValidationErrors(['targeting_rules']);
|
|
|
|
// 5. Extraneous key in filters validation
|
|
$responseExtraFilterKey = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'filter_extra_key',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'filters' => [
|
|
[
|
|
'type' => 'device',
|
|
'data' => ['devices' => ['mobile']],
|
|
'random_junk_key' => 'garbage', // extraneous key, should fail
|
|
],
|
|
],
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseExtraFilterKey->assertStatus(422)
|
|
->assertJsonValidationErrors(['targeting_rules.0.filters']);
|
|
|
|
// 6. Invalid values in targeting rules (e.g. invalid country code and invalid device)
|
|
$responseInvalidValues = $this->postJson('/api/short-url/links', [
|
|
'destination_url' => 'https://google.com',
|
|
'url_key' => 'rule_invalid_values',
|
|
'targeting_rules' => [
|
|
[
|
|
'match' => 'or',
|
|
'url' => 'https://mobile.com',
|
|
'filters' => [
|
|
[
|
|
'type' => 'device',
|
|
'data' => ['devices' => ['microwave']], // invalid device type
|
|
],
|
|
],
|
|
],
|
|
],
|
|
], [
|
|
'X-Api-Key' => 'sh_key_active_token',
|
|
]);
|
|
|
|
$responseInvalidValues->assertStatus(422)
|
|
->assertJsonValidationErrors(['targeting_rules.0.filters.0.data.devices.0']);
|
|
});
|