✨feature: Initial MVP
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<?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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user