|
- <?php
-
- declare(strict_types=1);
-
- use App\Controllers\CardsController;
- use Core\Database;
- use Core\Request;
-
- spl_autoload_register(static function (string $class): void {
- $root = str_starts_with($class, 'App\\') ? 'app' : 'core';
- $path = __DIR__ . '/../' . $root . '/' . str_replace('\\', '/', substr($class, strpos($class, '\\') + 1)) . '.php';
- if (is_file($path)) {
- require $path;
- }
- });
-
- function database(): Database
- {
- static $database = null;
- if ($database === null) {
- $database = new Database([
- 'dsn' => 'sqlite::memory:',
- 'options' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
- ]);
- $database->pdo()->exec('PRAGMA foreign_keys = ON');
- }
-
- return $database;
- }
-
- function check(bool $condition, string $message): void
- {
- if (!$condition) {
- throw new RuntimeException($message);
- }
- }
-
- $db = database();
- foreach ([
- '20260522_000001_create_boards_table.php',
- '20260522_000002_create_board_columns_table.php',
- '20260522_000003_create_swim_lanes_table.php',
- '20260522_000004_create_cards_table.php',
- '20260615_000003_add_cell_entered_at_to_cards.php',
- '20260917_000001_create_card_movements_table.php',
- ] as $migrationFile) {
- (require __DIR__ . '/../database/migrations/' . $migrationFile)->up($db);
- }
-
- $db->execute("INSERT INTO boards (id, name, slug) VALUES (1, 'Main', 'main'), (2, 'Other', 'other')");
- $db->execute("INSERT INTO board_columns (id, board_id, name, position) VALUES
- (1, 1, 'Queued', 0), (2, 1, 'Printing', 1), (3, 2, 'Other column', 0)");
- $db->execute("INSERT INTO swim_lanes (id, board_id, name, position) VALUES
- (1, 1, 'Standard', 0), (2, 1, 'Urgent', 1), (3, 2, 'Other row', 0)");
- $db->execute("INSERT INTO cards (id, board_id, column_id, swim_lane_id, position, cell_entered_at)
- VALUES (1, 1, 1, 1, 0, '2026-01-01 00:00:00')");
-
- putenv('APP_DEBUG=0');
- date_default_timezone_set('America/New_York');
- $_SESSION['auth_user'] = ['preferred_username' => 'test-user'];
- $controller = new CardsController();
- $move = static function (int $columnId, int $laneId, int $position = 0) use ($controller): Core\Response {
- return $controller->move(new Request([], [
- 'column_id' => (string) $columnId,
- 'swim_lane_id' => (string) $laneId,
- 'position' => (string) $position,
- ], ['REQUEST_METHOD' => 'POST']), 1);
- };
-
- check($move(2, 2)->status() === 200, 'Move should succeed.');
- $card = $db->first('SELECT column_id, swim_lane_id FROM cards WHERE id = 1');
- check((int) $card['column_id'] === 2 && (int) $card['swim_lane_id'] === 2, 'Card destination was not saved.');
- $history = $db->first('SELECT * FROM card_movements WHERE card_id = 1');
- check($history !== null, 'Movement was not recorded.');
- check($history['from_column_name'] === 'Queued' && $history['to_column_name'] === 'Printing', 'Column history is incorrect.');
- check($history['from_swim_lane_name'] === 'Standard' && $history['to_swim_lane_name'] === 'Urgent', 'Row history is incorrect.');
- check($history['moved_by'] === 'test-user', 'Move user was not recorded.');
- check(abs(strtotime($history['moved_at'] . ' UTC') - time()) < 10, 'Movement time was not saved in UTC.');
- $response = $controller->history(1);
- $recorded = json_decode($response->content(), true, flags: JSON_THROW_ON_ERROR)['movements'][0];
- check($recorded['to_column_name'] === 'Printing' && $recorded['to_swim_lane_name'] === 'Urgent', 'History API omitted the destination.');
-
- check($move(2, 2, 1)->status() === 200, 'Reordering should succeed.');
- check(count($db->query('SELECT id FROM card_movements WHERE card_id = 1')) === 1, 'Reordering added a movement.');
-
- check($move(2, 1)->status() === 200, 'Row-only move should succeed.');
- check(count($db->query('SELECT id FROM card_movements WHERE card_id = 1')) === 2, 'Row-only move was not recorded.');
-
- check($move(3, 1)->status() === 422, 'Cross-board column should be rejected.');
- check($move(2, 3)->status() === 422, 'Cross-board row should be rejected.');
- $card = $db->first('SELECT column_id, swim_lane_id FROM cards WHERE id = 1');
- check((int) $card['column_id'] === 2 && (int) $card['swim_lane_id'] === 1, 'Rejected move changed the card.');
- check(count($db->query('SELECT id FROM card_movements WHERE card_id = 1')) === 2, 'Rejected move changed history.');
-
- $db->execute("CREATE TRIGGER reject_movement BEFORE INSERT ON card_movements
- BEGIN SELECT RAISE(ABORT, 'forced history failure'); END");
- $logFile = tempnam(sys_get_temp_dir(), 'card_move_');
- $previousLog = ini_get('error_log');
- ini_set('error_log', $logFile);
- try {
- check($move(1, 1)->status() === 500, 'History failure should reject the move.');
- $card = $db->first('SELECT column_id, swim_lane_id FROM cards WHERE id = 1');
- check((int) $card['column_id'] === 2 && (int) $card['swim_lane_id'] === 1, 'Failed history write did not roll back the move.');
- check(count($db->query('SELECT id FROM card_movements WHERE card_id = 1')) === 2, 'Failed move changed history.');
- } finally {
- ini_set('error_log', $previousLog ?: '');
- unlink($logFile);
- $db->execute('DROP TRIGGER reject_movement');
- }
-
- echo "PASS: card moves record column and row history\n";
|