Http::response('console.log("Plausible script");', 200), ]); $this->get('/js/script.js') ->assertStatus(200) ->assertHeader('Content-Type', 'application/javascript') ->assertSee('Plausible script'); $this->get('/js/script.js') ->assertStatus(200) ->assertSee('Plausible script'); Http::assertSentCount(1); }); it('fetches a custom script path', function () { config()->set('plausible-proxy.script', 'js/pa-12345ABCDE.js'); Http::fake([ 'https://plausible.io/js/pa-12345ABCDE.js' => Http::response('console.log("Custom script");', 200), ]); $this->get('/js/script.js') ->assertStatus(200) ->assertSee('Custom script'); Http::assertSent(fn ($request) => $request->url() === 'https://plausible.io/js/pa-12345ABCDE.js'); }); it('handles trailing and leading slashes in domain and script path', function () { config()->set('plausible-proxy.domain', 'https://plausible.example.com/'); config()->set('plausible-proxy.script', '/js/script.outbound-links.js'); Http::fake([ 'https://plausible.example.com/js/script.outbound-links.js' => Http::response('console.log("Outbound");', 200), ]); $this->get('/js/script.js')->assertStatus(200)->assertSee('Outbound'); }); it('does not serve a cached script after the script path changes', function () { Http::fake([ 'https://plausible.io/js/script.js' => Http::response('console.log("Old script");', 200), 'https://plausible.io/js/pa-12345ABCDE.js' => Http::response('console.log("New script");', 200), ]); $this->get('/js/script.js')->assertSee('Old script'); config()->set('plausible-proxy.script', 'js/pa-12345ABCDE.js'); $this->get('/js/script.js')->assertSee('New script'); }); it('handles failed script fetch', function () { Http::fake([ 'https://plausible.io/js/script.js' => Http::response('', 500), ]); $response = $this->get('/js/script.js'); $response->assertStatus(500) ->assertSee('Error fetching Plausible script'); }); it('rejects invalid event data', function () { $response = $this->postJson('/api/event', [ 'n' => 'pageview', 'u' => 'invalid-url', 'd' => 'example.com', ]); $response->assertStatus(422) ->assertJsonValidationErrors(['u' => 'The u field must be a valid URL.']); }); it('forwards only user agent and client ip to plausible', function () { config()->set('plausible-proxy.domain', 'https://plausible.io'); Http::fake([ 'https://plausible.io/api/event' => Http::response('ok', 202), ]); $response = $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.7']) ->postJson('/api/event', [ 'n' => 'pageview', 'u' => 'https://example.com/page', 'd' => 'example.com', ], [ 'User-Agent' => 'TestAgent', 'Cookie' => 'laravel_session=secret', 'Authorization' => 'Bearer secret', ]); $response->assertStatus(202); expect($response->getContent())->toBe('ok'); Http::assertSentCount(1); Http::assertSent(function ($request) { return $request->url() === 'https://plausible.io/api/event' && $request->header('User-Agent') === ['TestAgent'] && $request->header('X-Forwarded-For') === ['203.0.113.7'] && ! $request->hasHeader('Cookie') && ! $request->hasHeader('Authorization') && $request['n'] === 'pageview' && $request['u'] === 'https://example.com/page' && $request['d'] === 'example.com'; }); }); it('forwards additional plausible fields', function () { config()->set('plausible-proxy.domain', 'https://plausible.io'); Http::fake([ 'https://plausible.io/api/event' => Http::response('ok', 202), ]); $this->postJson('/api/event', [ 'n' => 'Purchase', 'u' => 'https://example.com/checkout', 'd' => 'example.com', 'i' => false, 'p' => '{"plan":"pro"}', '$' => ['amount' => '9.99', 'currency' => 'EUR'], ])->assertStatus(202); Http::assertSent(function ($request) { return $request['i'] === false && $request['p'] === '{"plan":"pro"}' && $request['$'] === ['amount' => '9.99', 'currency' => 'EUR']; }); }); it('rejects invalid props', function () { $this->postJson('/api/event', [ 'n' => 'pageview', 'u' => 'https://example.com/page', 'd' => 'example.com', 'p' => 'not-json', ])->assertStatus(422)->assertJsonValidationErrors(['p']); });