Deploy: TekDek Employees Portal (Daedalus, Talos, Icarus) - Live at web.tekdek.dev

This commit is contained in:
ParzivalTD
2026-04-11 22:56:05 -04:00
parent ec154881ae
commit 3fc4e146d4
17 changed files with 1448 additions and 0 deletions

View 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']);
}
}

View 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}");
}
}
}

View 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);
}
}