Skip to content

Commit c6dd4c9

Browse files
authored
Merge pull request #10 from llm-agents-php/feature/session-tests
feat: Add test suite for Session classes
2 parents 4d183ec + 2ceb18f commit c6dd4c9

File tree

8 files changed

+1744
-5
lines changed

8 files changed

+1744
-5
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -965,15 +965,13 @@ composer test
965965
We follow PSR-12 coding standards:
966966

967967
```bash
968-
composer cs-fix # Fix code style
969-
composer cs-check # Check code style
970-
composer refactor # Run rector
968+
composer cs:fix
971969
```
972970

973971
### Testing
974972

975973
```bash
976-
composer test # Run all tests
974+
composer test
977975
```
978976

979977
## License

context.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,14 @@ documents:
7575
- vendor/react/http/src/HttpServer.php
7676
- vendor/react/http/src/Io/MiddlewareRunner.php
7777
- vendor/react/http/src/Middleware
78-
- src/Transports/Middleware
78+
- src/Transports/Middleware
79+
80+
- description: Session
81+
outputPath: src/session.md
82+
sources:
83+
- type: file
84+
sourcePaths:
85+
- src/Session
86+
- src/Contracts/SessionHandlerInterface.php
87+
- src/Contracts/SessionIdGeneratorInterface.php
88+
- src/Contracts/SessionInterface.php
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Tests\Unit\Session;
6+
7+
use Mcp\Server\Session\ArraySessionHandler;
8+
use PHPUnit\Framework\TestCase;
9+
use Psr\Clock\ClockInterface;
10+
11+
final class ArraySessionHandlerTest extends TestCase
12+
{
13+
private ClockInterface $clock;
14+
private ArraySessionHandler $handler;
15+
16+
public function test_read_returns_false_when_session_not_exists(): void
17+
{
18+
$result = $this->handler->read('non-existent-id');
19+
20+
$this->assertFalse($result);
21+
}
22+
23+
public function test_write_and_read_session_data(): void
24+
{
25+
$dateTime = new \DateTimeImmutable('2025-01-01 12:00:00');
26+
$this->clock->shouldReceive('now')->andReturn($dateTime);
27+
28+
$sessionId = 'test-session-id';
29+
$sessionData = '{"user_id": 123, "username": "test"}';
30+
31+
$writeResult = $this->handler->write($sessionId, $sessionData);
32+
$this->assertTrue($writeResult);
33+
34+
$readResult = $this->handler->read($sessionId);
35+
$this->assertEquals($sessionData, $readResult);
36+
}
37+
38+
public function test_read_returns_false_when_session_expired(): void
39+
{
40+
$writeTime = new \DateTimeImmutable('2025-01-01 12:00:00');
41+
$readTime = new \DateTimeImmutable('2025-01-01 14:01:00'); // 2 hours 1 minute later
42+
43+
$sessionId = 'expired-session';
44+
$sessionData = '{"data": "test"}';
45+
46+
// Write session
47+
$this->clock->shouldReceive('now')->andReturn($writeTime)->once();
48+
$this->handler->write($sessionId, $sessionData);
49+
50+
// Try to read after expiration (ttl = 3600 seconds = 1 hour)
51+
$this->clock->shouldReceive('now')->andReturn($readTime)->once();
52+
$result = $this->handler->read($sessionId);
53+
54+
$this->assertFalse($result);
55+
}
56+
57+
public function test_read_returns_data_when_session_not_expired(): void
58+
{
59+
$writeTime = new \DateTimeImmutable('2025-01-01 12:00:00');
60+
$readTime = new \DateTimeImmutable('2025-01-01 12:30:00'); // 30 minutes later
61+
62+
$sessionId = 'valid-session';
63+
$sessionData = '{"data": "test"}';
64+
65+
// Write session
66+
$this->clock->shouldReceive('now')->andReturn($writeTime)->once();
67+
$this->handler->write($sessionId, $sessionData);
68+
69+
// Read before expiration
70+
$this->clock->shouldReceive('now')->andReturn($readTime)->once();
71+
$result = $this->handler->read($sessionId);
72+
73+
$this->assertEquals($sessionData, $result);
74+
}
75+
76+
public function test_destroy_removes_session(): void
77+
{
78+
$dateTime = new \DateTimeImmutable('2025-01-01 12:00:00');
79+
$this->clock->shouldReceive('now')->andReturn($dateTime)->times(2);
80+
81+
$sessionId = 'test-session';
82+
$sessionData = '{"data": "test"}';
83+
84+
// Write session
85+
$this->handler->write($sessionId, $sessionData);
86+
$this->assertEquals($sessionData, $this->handler->read($sessionId));
87+
88+
// Destroy session
89+
$result = $this->handler->destroy($sessionId);
90+
$this->assertTrue($result);
91+
92+
// Verify session is removed
93+
$this->assertFalse($this->handler->read($sessionId));
94+
}
95+
96+
public function test_destroy_returns_true_for_non_existent_session(): void
97+
{
98+
$result = $this->handler->destroy('non-existent-session');
99+
100+
$this->assertTrue($result);
101+
}
102+
103+
public function test_gc_removes_expired_sessions(): void
104+
{
105+
$writeTime = new \DateTimeImmutable('2025-01-01 12:00:00');
106+
$gcTime = new \DateTimeImmutable('2025-01-01 14:31:00'); // 2 hours 31 minutes later
107+
108+
// Write multiple sessions
109+
$this->clock->shouldReceive('now')->andReturn($writeTime)->times(3);
110+
$this->handler->write('session1', '{"data": "test1"}');
111+
$this->handler->write('session2', '{"data": "test2"}');
112+
$this->handler->write('session3', '{"data": "test3"}');
113+
114+
// Run garbage collection with maxLifetime = 1800 (30 minutes)
115+
$this->clock->shouldReceive('now')->andReturn($gcTime)->once();
116+
$deletedSessions = $this->handler->gc(1800);
117+
118+
$this->assertCount(3, $deletedSessions);
119+
$this->assertContains('session1', $deletedSessions);
120+
$this->assertContains('session2', $deletedSessions);
121+
$this->assertContains('session3', $deletedSessions);
122+
123+
// Verify sessions are actually removed
124+
$this->assertFalse($this->handler->read('session1'));
125+
$this->assertFalse($this->handler->read('session2'));
126+
$this->assertFalse($this->handler->read('session3'));
127+
}
128+
129+
public function test_gc_keeps_non_expired_sessions(): void
130+
{
131+
$writeTime = new \DateTimeImmutable('2025-01-01 12:00:00');
132+
$gcTime = new \DateTimeImmutable('2025-01-01 12:30:00'); // 30 minutes later
133+
134+
// Write session
135+
$this->clock->shouldReceive('now')->andReturn($writeTime)->once();
136+
$this->handler->write('valid-session', '{"data": "test"}');
137+
138+
// Run garbage collection with maxLifetime = 3600 (1 hour)
139+
$this->clock->shouldReceive('now')->andReturn($gcTime)->times(2); // once for gc, once for read
140+
$deletedSessions = $this->handler->gc(3600);
141+
142+
$this->assertEmpty($deletedSessions);
143+
$this->assertEquals('{"data": "test"}', $this->handler->read('valid-session'));
144+
}
145+
146+
public function test_gc_returns_empty_array_when_no_sessions(): void
147+
{
148+
$this->clock->shouldReceive('now')->andReturn(new \DateTimeImmutable())->once();
149+
150+
$deletedSessions = $this->handler->gc(3600);
151+
152+
$this->assertEmpty($deletedSessions);
153+
}
154+
155+
public function test_constructor_with_default_parameters(): void
156+
{
157+
$handler = new ArraySessionHandler();
158+
159+
$this->assertInstanceOf(ArraySessionHandler::class, $handler);
160+
$this->assertEquals(3600, $handler->ttl);
161+
}
162+
163+
public function test_constructor_with_custom_ttl(): void
164+
{
165+
$handler = new ArraySessionHandler(ttl: 7200);
166+
167+
$this->assertEquals(7200, $handler->ttl);
168+
}
169+
170+
protected function setUp(): void
171+
{
172+
$this->clock = \Mockery::mock(ClockInterface::class);
173+
$this->handler = new ArraySessionHandler(ttl: 3600, clock: $this->clock);
174+
}
175+
176+
protected function tearDown(): void
177+
{
178+
\Mockery::close();
179+
}
180+
}

0 commit comments

Comments
 (0)