diff --git a/src/Laravel/Storage/SessionBag.php b/src/Laravel/Storage/SessionBag.php index 39d02f14..5edfc57b 100644 --- a/src/Laravel/Storage/SessionBag.php +++ b/src/Laravel/Storage/SessionBag.php @@ -25,10 +25,26 @@ final readonly class SessionBag implements BagInterface { $session = $this->getSession(); - /** @var Envelope[] $envelopes */ $envelopes = $session->get(self::ENVELOPES_NAMESPACE, []); - return $envelopes; + if (!\is_array($envelopes)) { + return []; + } + + $result = []; + foreach ($envelopes as $envelope) { + if ($envelope instanceof Envelope) { + $result[] = $envelope; + } elseif (\is_string($envelope)) { + $unserialized = @unserialize($envelope); + if ($unserialized instanceof Envelope) { + $result[] = $unserialized; + } + } + // Arrays and invalid data silently skipped (graceful degradation) + } + + return $result; } public function set(array $envelopes): void @@ -41,7 +57,7 @@ final readonly class SessionBag implements BagInterface return; } - $session->put(self::ENVELOPES_NAMESPACE, $envelopes); + $session->put(self::ENVELOPES_NAMESPACE, array_map(serialize(...), $envelopes)); } private function getSession(): Session|FallbackSessionInterface diff --git a/tests/Laravel/Storage/SessionBagTest.php b/tests/Laravel/Storage/SessionBagTest.php index 27757784..0625fe4c 100644 --- a/tests/Laravel/Storage/SessionBagTest.php +++ b/tests/Laravel/Storage/SessionBagTest.php @@ -9,6 +9,8 @@ use Flasher\Laravel\Storage\FallbackSessionInterface; use Flasher\Laravel\Storage\SessionBag; use Flasher\Prime\Notification\Envelope; use Flasher\Prime\Notification\Notification; +use Flasher\Prime\Stamp\DelayStamp; +use Flasher\Prime\Stamp\HopsStamp; use Flasher\Prime\Stamp\IdStamp; use Flasher\Tests\Laravel\TestCase; use Illuminate\Session\SessionManager; @@ -65,7 +67,7 @@ final class SessionBagTest extends TestCase $sessionMock = \Mockery::mock(Store::class); $sessionMock->allows()->isStarted()->andReturns(true); $sessionMock->allows()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($envelopes); - $sessionMock->expects()->put(SessionBag::ENVELOPES_NAMESPACE, $envelopes); + $sessionMock->expects()->put(SessionBag::ENVELOPES_NAMESPACE, array_map(serialize(...), $envelopes)); $this->sessionManagerMock->allows()->driver()->andReturns($sessionMock); @@ -193,4 +195,132 @@ final class SessionBagTest extends TestCase $this->assertSame($envelopes2, $this->sessionBag->get()); } + + public function testEnvelopeSurvivesJsonSerializationRoundTrip(): void + { + $notification = new Notification(); + $notification->setType('success'); + $notification->setMessage('Operation completed'); + + $original = new Envelope($notification, [ + new IdStamp('json-test-id'), + new HopsStamp(2), + new DelayStamp(500), + ]); + + $sessionMock = \Mockery::mock(Store::class); + $sessionMock->allows()->isStarted()->andReturns(true); + + $captured = null; + $sessionMock->expects('put')->with(SessionBag::ENVELOPES_NAMESPACE, \Mockery::on(function ($value) use (&$captured) { + $captured = $value; + + return true; + })); + + $this->sessionManagerMock->allows()->driver()->andReturns($sessionMock); + $this->sessionBag->set([$original]); + + $jsonEncoded = json_encode($captured, \JSON_THROW_ON_ERROR); + $jsonDecoded = json_decode($jsonEncoded, true, 512, \JSON_THROW_ON_ERROR); + + $sessionMock->allows()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($jsonDecoded); + + $restored = $this->sessionBag->get(); + + $this->assertCount(1, $restored); + $this->assertInstanceOf(Envelope::class, $restored[0]); + $this->assertSame('success', $restored[0]->getType()); + $this->assertSame('Operation completed', $restored[0]->getMessage()); + $this->assertSame('json-test-id', $restored[0]->get(IdStamp::class)->getId()); + $this->assertSame(2, $restored[0]->get(HopsStamp::class)->getAmount()); + $this->assertSame(500, $restored[0]->get(DelayStamp::class)->getDelay()); + } + + public function testGetHandlesEnvelopeObjectsForBackwardCompatibility(): void + { + $envelopes = [ + new Envelope(new Notification(), new IdStamp('legacy')), + ]; + + $envelopes[0]->setType('info'); + $envelopes[0]->setMessage('Old session'); + + $sessionMock = \Mockery::mock(Store::class); + $sessionMock->expects()->isStarted()->andReturns(true); + $sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($envelopes); + + $this->sessionManagerMock->expects()->driver()->andReturns($sessionMock); + + $result = $this->sessionBag->get(); + + $this->assertSame($envelopes, $result); + } + + public function testGetSkipsCorruptedArrayData(): void + { + $sessionMock = \Mockery::mock(Store::class); + $sessionMock->expects()->isStarted()->andReturns(true); + $sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns([ + ['title' => '', 'message' => '', 'type' => 'success', 'options' => [], 'metadata' => ['id' => 'corrupted']], + ]); + + $this->sessionManagerMock->expects()->driver()->andReturns($sessionMock); + + $result = $this->sessionBag->get(); + + $this->assertSame([], $result); + } + + public function testGetSkipsInvalidSerializedStrings(): void + { + $sessionMock = \Mockery::mock(Store::class); + $sessionMock->expects()->isStarted()->andReturns(true); + $sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns([ + 'invalid-not-serialized-data', + 'O:8:"stdClass":0:{}', // Valid serialized object but wrong type + serialize(new Envelope(new Notification(), new IdStamp('valid'))), + ]); + + $this->sessionManagerMock->expects()->driver()->andReturns($sessionMock); + + $result = $this->sessionBag->get(); + + $this->assertCount(1, $result); + $this->assertInstanceOf(Envelope::class, $result[0]); + $this->assertSame('valid', $result[0]->get(IdStamp::class)->getId()); + } + + public function testPreservesAllStampsAcrossRoundTrip(): void + { + $notification = new Notification(); + $notification->setType('warning'); + $notification->setMessage('Multi-stamp'); + + $original = new Envelope($notification, [ + new IdStamp('stamp-test'), + new HopsStamp(3), + new DelayStamp(1000), + ]); + + $serialized = serialize($original); + $jsonEncoded = json_encode([$serialized], \JSON_THROW_ON_ERROR); + $jsonDecoded = json_decode($jsonEncoded, true, 512, \JSON_THROW_ON_ERROR); + + $sessionMock = \Mockery::mock(Store::class); + $sessionMock->expects()->isStarted()->andReturns(true); + $sessionMock->expects()->get(SessionBag::ENVELOPES_NAMESPACE, [])->andReturns($jsonDecoded); + + $this->sessionManagerMock->expects()->driver()->andReturns($sessionMock); + + $restored = $this->sessionBag->get(); + + $this->assertCount(1, $restored); + $this->assertInstanceOf(Envelope::class, $restored[0]); + $this->assertSame('warning', $restored[0]->getType()); + $this->assertSame('Multi-stamp', $restored[0]->getMessage()); + $this->assertSame('stamp-test', $restored[0]->get(IdStamp::class)->getId()); + $this->assertSame(3, $restored[0]->get(HopsStamp::class)->getAmount()); + $this->assertSame(1000, $restored[0]->get(DelayStamp::class)->getDelay()); + } }