Files
IronKanban/tests/Api/BoardApiTest.php
T
Keith Solomon 812e5c2f2a feature: Initial MVP
2026-04-05 16:20:39 -05:00

323 lines
10 KiB
PHP

<?php
// phpcs:disable PEAR.Commenting.FileComment,PEAR.Commenting.ClassComment
/**
* API integration tests.
*/
declare(strict_types=1);
namespace Tests\Api;
use IronKanban\Service\BoardService;
use Tests\Support\IntegrationTestCase;
/**
* Covers JSON endpoint behavior and request validation.
*/
final class BoardApiTest extends IntegrationTestCase
{
/**
* Ensure the board-state endpoint returns project and board data.
*
* @return void
*/
public function testBoardStateEndpointReturnsProjectState(): void
{
$projectId = $this->createProject();
$response = $this->callApi(
'public/api/board-state.php',
'GET',
['project' => $projectId]
);
self::assertSame(200, $response['status']);
self::assertSame('Demo Project', $response['payload']['project']['title']);
self::assertContains('trash', array_column($response['payload']['columns'], 'id'));
}
/**
* Ensure state-changing endpoints reject missing CSRF tokens.
*
* @return void
*/
public function testCreateTaskEndpointRejectsMissingCsrfToken(): void
{
$projectId = $this->createProject();
$response = $this->callApi(
'public/api/create-task.php',
'POST',
[],
[
'projectId' => $projectId,
'title' => 'Write docs',
'column' => 'backlog',
]
);
self::assertSame(403, $response['status']);
self::assertFalse($response['payload']['success']);
self::assertSame('invalid_csrf', $response['payload']['error']);
}
/**
* Ensure create-task returns a successful JSON state payload.
*
* @return void
*/
public function testCreateTaskEndpointCreatesTaskWithValidCsrfToken(): void
{
$projectId = $this->createProject();
$token = csrf_token();
$response = $this->callApi(
'public/api/create-task.php',
'POST',
[],
[
'projectId' => $projectId,
'title' => 'Write docs',
'column' => 'backlog',
'body' => 'Add the release checklist.',
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame('Write docs', $response['payload']['state']['task']['title']);
self::assertSame('backlog', $response['payload']['state']['task']['column']);
self::assertCount(1, $response['payload']['state']['tasks']);
}
/**
* Ensure stale revisions return a conflict response.
*
* @return void
*/
public function testCreateTaskEndpointReturnsConflictForStaleRevision(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$current = $service->getBoardState($projectId);
$token = csrf_token();
$service->createTask($projectId, 'Existing task', 'backlog');
$response = $this->callApi(
'public/api/create-task.php',
'POST',
[],
[
'projectId' => $projectId,
'title' => 'Conflicting task',
'column' => 'ready',
'revision' => (string) $current['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(409, $response['status']);
self::assertFalse($response['payload']['success']);
self::assertSame('conflict', $response['payload']['error']);
}
/**
* Ensure move-task updates the task column and preserves a success response.
*
* @return void
*/
public function testMoveTaskEndpointMovesTaskToRequestedColumn(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$created = $service->createTask($projectId, 'Move me', 'backlog');
$token = csrf_token();
$response = $this->callApi(
'public/api/move-task.php',
'POST',
[],
[
'projectId' => $projectId,
'taskId' => $created['task']['id'],
'column' => 'done',
'index' => 0,
'revision' => (string) $created['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame('done', $response['payload']['state']['tasks'][0]['column']);
}
/**
* Ensure delete-task trashes a task by default.
*
* @return void
*/
public function testDeleteTaskEndpointMovesTaskToTrash(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$created = $service->createTask($projectId, 'Archive me', 'review');
$token = csrf_token();
$response = $this->callApi(
'public/api/delete-task.php',
'POST',
[],
[
'projectId' => $projectId,
'taskId' => $created['task']['id'],
'revision' => (string) $created['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame('trash', $response['payload']['state']['tasks'][0]['column']);
}
/**
* Ensure save-note creates a note and returns it in the response state.
*
* @return void
*/
public function testSaveNoteEndpointCreatesNote(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$state = $service->getBoardState($projectId);
$token = csrf_token();
$response = $this->callApi(
'public/api/save-note.php',
'POST',
[],
[
'projectId' => $projectId,
'title' => 'Sprint Notes',
'body' => 'Capture follow-up items.',
'revision' => (string) $state['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame('Sprint Notes', $response['payload']['state']['note']['title']);
self::assertCount(1, $response['payload']['state']['notes']);
}
/**
* Ensure create-column appends a new column to the board.
*
* @return void
*/
public function testCreateColumnEndpointCreatesNewColumn(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$state = $service->getBoardState($projectId);
$token = csrf_token();
$response = $this->callApi(
'public/api/create-column.php',
'POST',
[],
[
'projectId' => $projectId,
'label' => 'Blocked',
'revision' => (string) $state['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertContains('blocked', array_column($response['payload']['state']['columns'], 'id'));
}
/**
* Ensure save-task updates persisted task fields.
*
* @return void
*/
public function testSaveTaskEndpointUpdatesTaskFields(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$created = $service->createTask($projectId, 'Initial title', 'backlog');
$token = csrf_token();
$response = $this->callApi(
'public/api/save-task.php',
'POST',
[],
[
'projectId' => $projectId,
'taskId' => $created['task']['id'],
'title' => 'Updated title',
'body' => 'Updated body',
'priority' => 'urgent',
'completed' => true,
'revision' => (string) $created['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame('Updated title', $response['payload']['state']['tasks'][0]['title']);
self::assertSame("Updated body\n", $response['payload']['state']['tasks'][0]['body']);
self::assertSame('urgent', $response['payload']['state']['tasks'][0]['priority']);
self::assertTrue($response['payload']['state']['tasks'][0]['completed']);
}
/**
* Ensure update-board accepts column replacements and keeps trash available.
*
* @return void
*/
public function testUpdateBoardEndpointReplacesColumnDefinitions(): void
{
$projectId = $this->createProject();
$service = new BoardService();
$state = $service->getBoardState($projectId);
$token = csrf_token();
$response = $this->callApi(
'public/api/update-board.php',
'POST',
[],
[
'projectId' => $projectId,
'columns' => [
['id' => 'ideas', 'label' => 'Ideas'],
['id' => 'doing', 'label' => 'Doing'],
],
'revision' => (string) $state['revision'],
'_token' => $token,
],
['HTTP_X_CSRF_TOKEN' => $token]
);
self::assertSame(200, $response['status']);
self::assertTrue($response['payload']['success']);
self::assertSame(['ideas', 'doing', 'trash'], array_column($response['payload']['state']['columns'], 'id'));
}
}