Deploy: TekDek Employees Portal (Daedalus, Talos, Icarus) - Live at web.tekdek.dev
This commit is contained in:
136
tekdek-employees-api/tests/EmployeeControllerTest.php
Normal file
136
tekdek-employees-api/tests/EmployeeControllerTest.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TekDek\Employees\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use TekDek\Employees\EmployeeController;
|
||||
use TekDek\Employees\EmployeeRepository;
|
||||
|
||||
/**
|
||||
* Tests for EmployeeController.
|
||||
*
|
||||
* Uses mocked repository to test controller logic in isolation.
|
||||
*/
|
||||
class EmployeeControllerTest extends TestCase
|
||||
{
|
||||
private function createMockRepo(array $allData = [], ?array $singleData = null): EmployeeRepository
|
||||
{
|
||||
$mock = $this->createMock(EmployeeRepository::class);
|
||||
$mock->method('getAll')->willReturn($allData);
|
||||
$mock->method('getBySlug')->willReturnCallback(
|
||||
fn(string $slug) => $singleData[$slug] ?? null
|
||||
);
|
||||
return $mock;
|
||||
}
|
||||
|
||||
private function captureOutput(callable $fn): string
|
||||
{
|
||||
ob_start();
|
||||
$fn();
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
// --- Index Tests ---
|
||||
|
||||
public function testIndexReturnsSuccessWithEmployees(): void
|
||||
{
|
||||
$employees = [
|
||||
['slug' => 'daedalus', 'name' => 'Daedalus'],
|
||||
['slug' => 'talos', 'name' => 'Talos'],
|
||||
];
|
||||
$repo = $this->createMockRepo($employees);
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->index());
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertTrue($json['success']);
|
||||
$this->assertCount(2, $json['data']);
|
||||
$this->assertSame(2, $json['count']);
|
||||
}
|
||||
|
||||
public function testIndexReturnsEmptyArray(): void
|
||||
{
|
||||
$repo = $this->createMockRepo([]);
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->index());
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertTrue($json['success']);
|
||||
$this->assertSame([], $json['data']);
|
||||
$this->assertSame(0, $json['count']);
|
||||
}
|
||||
|
||||
// --- Show Tests ---
|
||||
|
||||
public function testShowReturnsEmployee(): void
|
||||
{
|
||||
$employee = ['slug' => 'talos', 'name' => 'Talos', 'role' => 'Backend Engineering'];
|
||||
$repo = $this->createMockRepo([], ['talos' => $employee]);
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->show('talos'));
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertTrue($json['success']);
|
||||
$this->assertSame('Talos', $json['data']['name']);
|
||||
}
|
||||
|
||||
public function testShowReturns404ForMissing(): void
|
||||
{
|
||||
$repo = $this->createMockRepo([], []);
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->show('nonexistent'));
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertFalse($json['success']);
|
||||
$this->assertStringContainsString('not found', $json['error']);
|
||||
}
|
||||
|
||||
public function testShowRejects400ForInvalidSlug(): void
|
||||
{
|
||||
$repo = $this->createMockRepo();
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
// Slug with uppercase
|
||||
$output = $this->captureOutput(fn() => $controller->show('INVALID'));
|
||||
$json = json_decode($output, true);
|
||||
$this->assertFalse($json['success']);
|
||||
$this->assertStringContainsString('Invalid slug', $json['error']);
|
||||
}
|
||||
|
||||
public function testShowRejectsSlugWithSpecialChars(): void
|
||||
{
|
||||
$repo = $this->createMockRepo();
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->show('sql;injection'));
|
||||
$json = json_decode($output, true);
|
||||
$this->assertFalse($json['success']);
|
||||
}
|
||||
|
||||
public function testShowRejectsEmptySlug(): void
|
||||
{
|
||||
$repo = $this->createMockRepo();
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->show(''));
|
||||
$json = json_decode($output, true);
|
||||
$this->assertFalse($json['success']);
|
||||
}
|
||||
|
||||
public function testShowAcceptsValidSlugFormats(): void
|
||||
{
|
||||
$employee = ['slug' => 'test-user-1', 'name' => 'Test'];
|
||||
$repo = $this->createMockRepo([], ['test-user-1' => $employee]);
|
||||
$controller = new EmployeeController($repo);
|
||||
|
||||
$output = $this->captureOutput(fn() => $controller->show('test-user-1'));
|
||||
$json = json_decode($output, true);
|
||||
$this->assertTrue($json['success']);
|
||||
}
|
||||
}
|
||||
179
tekdek-employees-api/tests/EmployeeRepositoryTest.php
Normal file
179
tekdek-employees-api/tests/EmployeeRepositoryTest.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TekDek\Employees\Tests;
|
||||
|
||||
use PDO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use TekDek\Employees\EmployeeRepository;
|
||||
|
||||
/**
|
||||
* Tests for EmployeeRepository using SQLite in-memory.
|
||||
*/
|
||||
class EmployeeRepositoryTest extends TestCase
|
||||
{
|
||||
private PDO $pdo;
|
||||
private EmployeeRepository $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->pdo = new PDO('sqlite::memory:');
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
|
||||
// Create table (SQLite-compatible schema)
|
||||
$this->pdo->exec("
|
||||
CREATE TABLE employees (
|
||||
id TEXT PRIMARY KEY,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
tagline TEXT,
|
||||
bio TEXT NOT NULL,
|
||||
mythology TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
accent_color TEXT,
|
||||
symbol TEXT,
|
||||
skills TEXT,
|
||||
quote TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
");
|
||||
|
||||
$this->seedTestData();
|
||||
$this->repo = new EmployeeRepository($this->pdo);
|
||||
}
|
||||
|
||||
private function seedTestData(): void
|
||||
{
|
||||
$employees = [
|
||||
[
|
||||
'id' => 'uuid-daedalus',
|
||||
'slug' => 'daedalus',
|
||||
'name' => 'Daedalus',
|
||||
'title' => 'Chief Architect',
|
||||
'role' => 'Architecture & Systems Design',
|
||||
'tagline' => 'The master builder.',
|
||||
'bio' => 'Daedalus is the architectural mind behind TekDek.',
|
||||
'mythology' => 'Named for the mythological master craftsman.',
|
||||
'avatar_url' => null,
|
||||
'accent_color' => '#C4A24E',
|
||||
'symbol' => 'labyrinth',
|
||||
'skills' => '["System Architecture", "Database Design"]',
|
||||
'quote' => 'A well-designed system is indistinguishable from inevitability.',
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 'uuid-talos',
|
||||
'slug' => 'talos',
|
||||
'name' => 'Talos',
|
||||
'title' => 'Lead Backend Engineer',
|
||||
'role' => 'Backend Engineering & Implementation',
|
||||
'tagline' => 'The bronze guardian.',
|
||||
'bio' => 'Talos is the engine room of TekDek.',
|
||||
'mythology' => 'Named for the giant bronze automaton.',
|
||||
'avatar_url' => null,
|
||||
'accent_color' => '#7B8794',
|
||||
'symbol' => 'gear',
|
||||
'skills' => '["PHP", "MySQL", "REST APIs"]',
|
||||
'quote' => 'If it compiles but can\'t survive production, it doesn\'t exist.',
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'id' => 'uuid-icarus',
|
||||
'slug' => 'icarus',
|
||||
'name' => 'Icarus',
|
||||
'title' => 'Lead Frontend Engineer',
|
||||
'role' => 'Frontend & UI Design',
|
||||
'tagline' => 'Flying close to the sun.',
|
||||
'bio' => 'Icarus is the face of TekDek.',
|
||||
'mythology' => 'Named for the son of Daedalus.',
|
||||
'avatar_url' => null,
|
||||
'accent_color' => '#E07A2F',
|
||||
'symbol' => 'wings',
|
||||
'skills' => '["UI/UX Design", "React", "CSS/Animation"]',
|
||||
'quote' => 'Users don\'t remember backends. They remember how it felt.',
|
||||
'sort_order' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO employees (id, slug, name, title, role, tagline, bio, mythology, avatar_url, accent_color, symbol, skills, quote, sort_order)
|
||||
VALUES (:id, :slug, :name, :title, :role, :tagline, :bio, :mythology, :avatar_url, :accent_color, :symbol, :skills, :quote, :sort_order)
|
||||
");
|
||||
|
||||
foreach ($employees as $emp) {
|
||||
$stmt->execute($emp);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetAllReturnsAllEmployees(): void
|
||||
{
|
||||
$result = $this->repo->getAll();
|
||||
$this->assertCount(3, $result);
|
||||
}
|
||||
|
||||
public function testGetAllReturnsSortedBySortOrder(): void
|
||||
{
|
||||
$result = $this->repo->getAll();
|
||||
$this->assertSame('daedalus', $result[0]['slug']);
|
||||
$this->assertSame('talos', $result[1]['slug']);
|
||||
$this->assertSame('icarus', $result[2]['slug']);
|
||||
}
|
||||
|
||||
public function testGetAllDecodesSkillsJson(): void
|
||||
{
|
||||
$result = $this->repo->getAll();
|
||||
$this->assertIsArray($result[0]['skills']);
|
||||
$this->assertContains('System Architecture', $result[0]['skills']);
|
||||
}
|
||||
|
||||
public function testGetAllCastsSortOrder(): void
|
||||
{
|
||||
$result = $this->repo->getAll();
|
||||
$this->assertSame(1, $result[0]['sort_order']);
|
||||
$this->assertSame(2, $result[1]['sort_order']);
|
||||
}
|
||||
|
||||
public function testGetBySlugReturnsEmployee(): void
|
||||
{
|
||||
$result = $this->repo->getBySlug('talos');
|
||||
$this->assertNotNull($result);
|
||||
$this->assertSame('Talos', $result['name']);
|
||||
$this->assertSame('#7B8794', $result['accent_color']);
|
||||
$this->assertSame('gear', $result['symbol']);
|
||||
}
|
||||
|
||||
public function testGetBySlugReturnsNullForMissing(): void
|
||||
{
|
||||
$result = $this->repo->getBySlug('nonexistent');
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testGetBySlugDecodesSkills(): void
|
||||
{
|
||||
$result = $this->repo->getBySlug('icarus');
|
||||
$this->assertIsArray($result['skills']);
|
||||
$this->assertContains('React', $result['skills']);
|
||||
}
|
||||
|
||||
public function testGetAllReturnsEmptyWhenNoData(): void
|
||||
{
|
||||
$this->pdo->exec('DELETE FROM employees');
|
||||
$result = $this->repo->getAll();
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testGetBySlugReturnsAllFields(): void
|
||||
{
|
||||
$result = $this->repo->getBySlug('daedalus');
|
||||
$expectedKeys = ['id', 'slug', 'name', 'title', 'role', 'tagline', 'bio', 'mythology', 'avatar_url', 'accent_color', 'symbol', 'skills', 'quote', 'sort_order', 'created_at', 'updated_at'];
|
||||
foreach ($expectedKeys as $key) {
|
||||
$this->assertArrayHasKey($key, $result, "Missing key: {$key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
103
tekdek-employees-api/tests/RouterTest.php
Normal file
103
tekdek-employees-api/tests/RouterTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TekDek\Employees\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use TekDek\Employees\EmployeeController;
|
||||
use TekDek\Employees\Router;
|
||||
|
||||
/**
|
||||
* Tests for Router dispatch logic.
|
||||
*
|
||||
* Note: These tests manipulate $_SERVER globals. In a real CI environment,
|
||||
* integration tests via HTTP requests are recommended alongside these.
|
||||
*/
|
||||
class RouterTest extends TestCase
|
||||
{
|
||||
private function mockRequest(string $method, string $uri): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$_SERVER['REQUEST_URI'] = $uri;
|
||||
}
|
||||
|
||||
private function captureDispatch(?EmployeeController $controller = null): string
|
||||
{
|
||||
ob_start();
|
||||
// Suppress header warnings in CLI test context
|
||||
@Router::dispatch($controller);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public function testGetEmployeesRoutesToIndex(): void
|
||||
{
|
||||
$this->mockRequest('GET', '/api/employees');
|
||||
|
||||
$controller = $this->createMock(EmployeeController::class);
|
||||
$controller->expects($this->once())->method('index');
|
||||
$controller->expects($this->never())->method('show');
|
||||
|
||||
$this->captureDispatch($controller);
|
||||
}
|
||||
|
||||
public function testGetEmployeesTrailingSlash(): void
|
||||
{
|
||||
$this->mockRequest('GET', '/api/employees/');
|
||||
|
||||
$controller = $this->createMock(EmployeeController::class);
|
||||
$controller->expects($this->once())->method('index');
|
||||
|
||||
$this->captureDispatch($controller);
|
||||
}
|
||||
|
||||
public function testGetEmployeeBySlugRoutesToShow(): void
|
||||
{
|
||||
$this->mockRequest('GET', '/api/employees/talos');
|
||||
|
||||
$controller = $this->createMock(EmployeeController::class);
|
||||
$controller->expects($this->once())->method('show')->with('talos');
|
||||
|
||||
$this->captureDispatch($controller);
|
||||
}
|
||||
|
||||
public function testPostMethodReturns405(): void
|
||||
{
|
||||
$this->mockRequest('POST', '/api/employees');
|
||||
|
||||
$output = $this->captureDispatch();
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertFalse($json['success']);
|
||||
$this->assertStringContainsString('Method not allowed', $json['error']);
|
||||
}
|
||||
|
||||
public function testOptionsReturnsCorsHeaders(): void
|
||||
{
|
||||
$this->mockRequest('OPTIONS', '/api/employees');
|
||||
$output = $this->captureDispatch();
|
||||
// OPTIONS should return empty body
|
||||
$this->assertEmpty($output);
|
||||
}
|
||||
|
||||
public function testUnknownRouteReturns404(): void
|
||||
{
|
||||
$this->mockRequest('GET', '/api/unknown');
|
||||
|
||||
$output = $this->captureDispatch();
|
||||
$json = json_decode($output, true);
|
||||
|
||||
$this->assertFalse($json['success']);
|
||||
$this->assertStringContainsString('not found', $json['error']);
|
||||
}
|
||||
|
||||
public function testSlugWithQueryString(): void
|
||||
{
|
||||
$this->mockRequest('GET', '/api/employees/daedalus?format=full');
|
||||
|
||||
$controller = $this->createMock(EmployeeController::class);
|
||||
$controller->expects($this->once())->method('show')->with('daedalus');
|
||||
|
||||
$this->captureDispatch($controller);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user