Skip to content

Commit 2bfe861

Browse files
vijaythecoderclaude
andcommitted
Add comprehensive test coverage and fix middleware test conflicts
- Add CheckOnboarding middleware tests (6 tests) - Add API key store endpoint tests (7 tests) - Add open-external URL endpoint tests (10 tests) - Fix existing tests by mocking ApiKeyService for middleware - All 116 tests now passing with 333 assertions - Complete coverage for security, validation, and error handling - Preserve existing functionality while adding onboarding protection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent b47411b commit 2bfe861

File tree

6 files changed

+494
-0
lines changed

6 files changed

+494
-0
lines changed

tests/Feature/ApiKeyStoreTest.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Services\ApiKeyService;
6+
use Mockery;
7+
use Tests\TestCase;
8+
9+
class ApiKeyStoreTest extends TestCase
10+
{
11+
12+
protected function setUp(): void
13+
{
14+
parent::setUp();
15+
}
16+
17+
protected function tearDown(): void
18+
{
19+
Mockery::close();
20+
parent::tearDown();
21+
}
22+
23+
public function test_stores_valid_api_key_successfully(): void
24+
{
25+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
26+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
27+
28+
$validApiKey = 'sk-1234567890abcdef1234567890abcdef1234567890abcdef';
29+
30+
$mockApiKeyService->shouldReceive('validateApiKey')
31+
->once()
32+
->with($validApiKey)
33+
->andReturn(true);
34+
35+
$mockApiKeyService->shouldReceive('setApiKey')
36+
->once()
37+
->with($validApiKey);
38+
39+
$response = $this->postJson('/api/openai/api-key', [
40+
'api_key' => $validApiKey
41+
]);
42+
43+
$response->assertStatus(200)
44+
->assertJson([
45+
'success' => true,
46+
'message' => 'API key saved successfully.'
47+
]);
48+
}
49+
50+
public function test_rejects_invalid_api_key(): void
51+
{
52+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
53+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
54+
55+
$invalidApiKey = 'sk-1234567890abcdef1234567890abcdef1234567890invalid';
56+
57+
$mockApiKeyService->shouldReceive('validateApiKey')
58+
->once()
59+
->with($invalidApiKey)
60+
->andReturn(false);
61+
62+
$mockApiKeyService->shouldNotReceive('setApiKey');
63+
64+
$response = $this->postJson('/api/openai/api-key', [
65+
'api_key' => $invalidApiKey
66+
]);
67+
68+
$response->assertStatus(422)
69+
->assertJson([
70+
'success' => false,
71+
'message' => 'The provided API key is invalid. Please check and try again.'
72+
]);
73+
}
74+
75+
public function test_validates_required_api_key_field(): void
76+
{
77+
$response = $this->postJson('/api/openai/api-key', []);
78+
79+
$response->assertStatus(422)
80+
->assertJsonValidationErrors(['api_key']);
81+
}
82+
83+
public function test_validates_api_key_minimum_length(): void
84+
{
85+
$response = $this->postJson('/api/openai/api-key', [
86+
'api_key' => 'sk-short'
87+
]);
88+
89+
$response->assertStatus(422)
90+
->assertJsonValidationErrors(['api_key']);
91+
}
92+
93+
public function test_validates_api_key_is_string(): void
94+
{
95+
$response = $this->postJson('/api/openai/api-key', [
96+
'api_key' => 123456
97+
]);
98+
99+
$response->assertStatus(422)
100+
->assertJsonValidationErrors(['api_key']);
101+
}
102+
103+
public function test_handles_api_key_service_exception(): void
104+
{
105+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
106+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
107+
108+
$validApiKey = 'sk-1234567890abcdef1234567890abcdef1234567890abcdef';
109+
110+
$mockApiKeyService->shouldReceive('validateApiKey')
111+
->once()
112+
->with($validApiKey)
113+
->andThrow(new \Exception('Service error'));
114+
115+
$response = $this->postJson('/api/openai/api-key', [
116+
'api_key' => $validApiKey
117+
]);
118+
119+
// Controller doesn't handle exceptions, so it returns 500
120+
$response->assertStatus(500);
121+
}
122+
123+
public function test_api_key_endpoint_accessible_without_existing_api_key(): void
124+
{
125+
// This test ensures the middleware allows access to the API key store endpoint
126+
// even when no API key is configured
127+
128+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
129+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
130+
131+
$validApiKey = 'sk-1234567890abcdef1234567890abcdef1234567890abcdef';
132+
133+
$mockApiKeyService->shouldReceive('validateApiKey')
134+
->once()
135+
->andReturn(true);
136+
137+
$mockApiKeyService->shouldReceive('setApiKey')
138+
->once();
139+
140+
$response = $this->postJson('/api/openai/api-key', [
141+
'api_key' => $validApiKey
142+
]);
143+
144+
$response->assertStatus(200);
145+
}
146+
}

tests/Feature/Controllers/ConversationControllerTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
use App\Models\ConversationSession;
44
use App\Models\ConversationTranscript;
55
use App\Models\ConversationInsight;
6+
use App\Services\ApiKeyService;
67

78
beforeEach(function () {
9+
// Mock API key service to return true (API key exists) for all conversation tests
10+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
11+
$mockApiKeyService->shouldReceive('hasApiKey')->andReturn(true);
12+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
13+
814
// Create a test conversation session for some tests
915
$this->session = ConversationSession::create([
1016
'user_id' => null,

tests/Feature/DashboardTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
<?php
22

3+
use App\Services\ApiKeyService;
4+
35
test('dashboard page is accessible', function () {
6+
// Mock API key service to return true (API key exists)
7+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
8+
$mockApiKeyService->shouldReceive('hasApiKey')->andReturn(true);
9+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
10+
411
$response = $this->get('/dashboard');
512
$response->assertStatus(200);
613
});
714

815
test('realtime agent page is accessible', function () {
16+
// Mock API key service to return true (API key exists)
17+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
18+
$mockApiKeyService->shouldReceive('hasApiKey')->andReturn(true);
19+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
20+
921
$response = $this->get('/realtime-agent');
1022
$response->assertStatus(200);
1123
});

tests/Feature/ExampleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<?php
22

3+
use App\Services\ApiKeyService;
4+
35
it('returns a successful response', function () {
6+
// Mock API key service to return true (API key exists)
7+
$mockApiKeyService = Mockery::mock(ApiKeyService::class);
8+
$mockApiKeyService->shouldReceive('hasApiKey')->andReturn(true);
9+
$this->app->instance(ApiKeyService::class, $mockApiKeyService);
10+
411
$response = $this->get('/');
512

613
$response->assertStatus(200);

tests/Feature/OpenExternalTest.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Illuminate\Foundation\Testing\RefreshDatabase;
6+
use Mockery;
7+
use Native\Laravel\Facades\Shell;
8+
use Tests\TestCase;
9+
10+
class OpenExternalTest extends TestCase
11+
{
12+
use RefreshDatabase;
13+
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
18+
// Reset Shell facade mock for each test
19+
Shell::clearResolvedInstance('Shell');
20+
}
21+
22+
protected function tearDown(): void
23+
{
24+
Mockery::close();
25+
parent::tearDown();
26+
}
27+
28+
public function test_opens_valid_url_successfully(): void
29+
{
30+
Shell::shouldReceive('openExternal')
31+
->once()
32+
->with('https://github.com/vijaythecoder/clueless');
33+
34+
$response = $this->postJson('/api/open-external', [
35+
'url' => 'https://github.com/vijaythecoder/clueless'
36+
]);
37+
38+
$response->assertStatus(200)
39+
->assertJson([
40+
'success' => true
41+
]);
42+
}
43+
44+
public function test_opens_openai_url_successfully(): void
45+
{
46+
Shell::shouldReceive('openExternal')
47+
->once()
48+
->with('https://platform.openai.com/api-keys');
49+
50+
$response = $this->postJson('/api/open-external', [
51+
'url' => 'https://platform.openai.com/api-keys'
52+
]);
53+
54+
$response->assertStatus(200)
55+
->assertJson([
56+
'success' => true
57+
]);
58+
}
59+
60+
public function test_rejects_invalid_url(): void
61+
{
62+
$response = $this->postJson('/api/open-external', [
63+
'url' => 'not-a-valid-url'
64+
]);
65+
66+
$response->assertStatus(400)
67+
->assertJson([
68+
'error' => 'Invalid URL'
69+
]);
70+
}
71+
72+
public function test_rejects_malicious_urls(): void
73+
{
74+
$maliciousUrls = [
75+
'javascript:alert("xss")',
76+
'data:text/html,<script>alert("xss")</script>',
77+
'not-a-url'
78+
];
79+
80+
foreach ($maliciousUrls as $url) {
81+
$response = $this->postJson('/api/open-external', [
82+
'url' => $url
83+
]);
84+
85+
$response->assertStatus(400)
86+
->assertJson([
87+
'error' => 'Invalid URL'
88+
]);
89+
}
90+
}
91+
92+
public function test_requires_url_parameter(): void
93+
{
94+
$response = $this->postJson('/api/open-external', []);
95+
96+
$response->assertStatus(400);
97+
}
98+
99+
public function test_handles_empty_url(): void
100+
{
101+
$response = $this->postJson('/api/open-external', [
102+
'url' => ''
103+
]);
104+
105+
$response->assertStatus(400)
106+
->assertJson([
107+
'error' => 'Invalid URL'
108+
]);
109+
}
110+
111+
public function test_handles_null_url(): void
112+
{
113+
$response = $this->postJson('/api/open-external', [
114+
'url' => null
115+
]);
116+
117+
$response->assertStatus(400)
118+
->assertJson([
119+
'error' => 'Invalid URL'
120+
]);
121+
}
122+
123+
public function test_allows_https_urls(): void
124+
{
125+
$validUrls = [
126+
'https://github.com',
127+
'https://platform.openai.com',
128+
'https://www.example.com',
129+
'https://subdomain.example.com/path?query=value'
130+
];
131+
132+
foreach ($validUrls as $url) {
133+
Shell::shouldReceive('openExternal')
134+
->once()
135+
->with($url);
136+
137+
$response = $this->postJson('/api/open-external', [
138+
'url' => $url
139+
]);
140+
141+
$response->assertStatus(200)
142+
->assertJson([
143+
'success' => true
144+
]);
145+
}
146+
}
147+
148+
public function test_allows_http_urls(): void
149+
{
150+
Shell::shouldReceive('openExternal')
151+
->once()
152+
->with('http://example.com');
153+
154+
$response = $this->postJson('/api/open-external', [
155+
'url' => 'http://example.com'
156+
]);
157+
158+
$response->assertStatus(200)
159+
->assertJson([
160+
'success' => true
161+
]);
162+
}
163+
164+
public function test_handles_shell_exception(): void
165+
{
166+
Shell::shouldReceive('openExternal')
167+
->once()
168+
->with('https://github.com')
169+
->andThrow(new \Exception('Shell error'));
170+
171+
$response = $this->postJson('/api/open-external', [
172+
'url' => 'https://github.com'
173+
]);
174+
175+
// The route doesn't handle exceptions explicitly, so it would return 500
176+
// In a real implementation, you might want to catch and handle this
177+
$response->assertStatus(500);
178+
}
179+
}

0 commit comments

Comments
 (0)