Files
filament-short-url/tests/Feature/ShortUrlCustomDomainTest.php

139 lines
4.7 KiB
PHP
Raw Normal View History

<?php
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
use Bjanczak\FilamentShortUrl\Models\ShortUrlCustomDomain;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('can create a custom domain and find short URLs mapped to it', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'links.acme.com',
'is_verified' => true,
'is_active' => true,
]);
$shortUrl = ShortUrl::create([
'destination_url' => 'https://example.com/target',
'url_key' => 'promo',
'custom_domain_id' => $domain->id,
]);
expect($shortUrl->customDomain->domain)->toBe('links.acme.com');
// Finding URL on the custom domain
$found = ShortUrl::findByKey('promo', 'links.acme.com');
expect($found)->not->toBeNull()
->and($found->id)->toBe($shortUrl->id);
// Finding URL on standard domain should fail since it's scoped to the custom domain
$notFound = ShortUrl::findByKey('promo', 'app.com');
expect($notFound)->toBeNull();
});
it('routes requests on custom domains at root-level fallback', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'links.acme.com',
'is_verified' => true,
'is_active' => true,
]);
$shortUrl = ShortUrl::create([
'destination_url' => 'https://example.com/target',
'url_key' => 'promo',
'custom_domain_id' => $domain->id,
]);
// Request to root of custom domain key should trigger redirect
$response = $this->get('http://links.acme.com/promo');
$response->assertRedirect('https://example.com/target');
});
it('returns 404 for fallback requests on unregistered custom domains', function () {
// Attempting access on unregistered domain
$response = $this->get('http://unregistered.com/somekey');
$response->assertStatus(404);
});
it('returns 404 for path segments / subfolders under custom domains fallback', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'links.acme.com',
'is_verified' => true,
'is_active' => true,
]);
$response = $this->get('http://links.acme.com/not-registered/subfolder');
$response->assertStatus(404);
});
it('returns correct short URL link string based on custom domain association', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'links.acme.com',
'is_verified' => true,
'is_active' => true,
]);
$shortUrl = ShortUrl::create([
'destination_url' => 'https://example.com/target',
'url_key' => 'promo',
'custom_domain_id' => $domain->id,
]);
$standardUrl = ShortUrl::create([
'destination_url' => 'https://example.com/standard',
'url_key' => 'stdkey',
]);
config(['app.url' => 'https://maindomain.com']);
config(['filament-short-url.route_prefix' => 's']);
expect($shortUrl->getShortUrl())->toBe('https://links.acme.com/s/promo');
expect($standardUrl->getShortUrl())->toBe('https://maindomain.com/s/stdkey');
});
feat: v5.0.0 — Live Feed page, Folders & Tags, Link Archiving, API scopes ## Analytics — Live Activity Feed - Dedicated /stats/live standalone page (mirrors /stats/logs pattern) - Consistent 3-tab navigation across Statistics / Live Feed / Visit Logs - checkForUpdates() with skipRender(): O(1) MAX(id) poll — ~100-byte response on no-change - latestVisitId sentinel (-1) prevents unnecessary first-poll re-render - Cache key versioned by latestVisitId — fixes bug where concurrent users could permanently miss a visit due to stale thundering-herd cache - 3-second thundering-herd cache (key: url_id + date + filters + latestVisitId) - Country flags via flagcdn.com (precomputed in PHP, Alpine.js CSP-safe onerror) - time_ago precomputed in PHP — no Carbon::parse() double-instantiation in Blade - SELECT only 12 needed columns instead of SELECT *; limit 25 rows - wire:poll.5s.visible — polling stops when widget is off-screen ## Link Organization - Folders: one folder per link, clickable → filtered link list, link count badge - Tags: up to 5 tags per link, clickable → filtered link list, link count badge - Archiving: soft-archive links instead of permanent deletion; restorable ## REST API - Per-key API scopes: links:read-only (GET) / links:read-write (full CRUD) - Per-key rate limiting: individual requests/minute per API key ## Translations - Added stats_live_feed_poll_interval (EN: 'Updates every 5s', PL: 'Aktualizacja co 5s') ## Docs - README.md: corrected API scopes/rate-limiting docs (features were already implemented) - README.md: added Folders, Tags, Archiving to features list - README.md: added v5.0.0 changelog entry
2026-06-06 02:16:07 +02:00
it('invalidates both old and new custom domain cache keys when domain name is updated', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'old-domain.com',
'is_verified' => true,
'is_active' => true,
]);
cache()->remember('filament-short-url:custom-domain:old-domain.com', 300, fn () => $domain);
expect(cache()->has('filament-short-url:custom-domain:old-domain.com'))->toBeTrue();
// Now update domain name
$domain->update(['domain' => 'new-domain.com']);
// Both old and new domain cache keys must be cleared
expect(cache()->has('filament-short-url:custom-domain:old-domain.com'))->toBeFalse();
expect(cache()->has('filament-short-url:custom-domain:new-domain.com'))->toBeFalse();
});
it('invalidates short URL redirect caches when its custom domain is updated or toggled', function () {
$domain = ShortUrlCustomDomain::create([
'domain' => 'old-domain.com',
'is_verified' => true,
'is_active' => true,
]);
$shortUrl = ShortUrl::create([
'destination_url' => 'https://example.com/target',
'url_key' => 'promo',
'custom_domain_id' => $domain->id,
]);
cache()->remember('filament-short-url:promo:old-domain.com', 3600, fn () => $shortUrl);
expect(cache()->has('filament-short-url:promo:old-domain.com'))->toBeTrue();
// Update the domain name
$domain->update(['domain' => 'new-domain.com']);
// Redirect cache for the old domain should be invalidated
expect(cache()->has('filament-short-url:promo:old-domain.com'))->toBeFalse();
expect(cache()->has('filament-short-url:promo:new-domain.com'))->toBeFalse();
});