2026-09-04 13:37:17 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Tests;
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
|
|
2026-09-04 13:37:17 +01:00
|
|
|
/**
|
2026-09-04 15:22:33 +01:00
|
|
|
* `position` is a dense rank within a "column" -- the cards sharing an
|
|
|
|
|
* (owner, project, status). The inbox is the column where project and status
|
|
|
|
|
* are both null, and it's global to the owner rather than per-project.
|
|
|
|
|
* PUT /api/cards/order sets one column's contents and order.
|
2026-09-04 13:37:17 +01:00
|
|
|
*/
|
|
|
|
|
final class CardOrderTest extends ApiTestCase
|
|
|
|
|
{
|
|
|
|
|
/** @param array<string, string> $auth */
|
|
|
|
|
private function newProject(array $auth, string $title = 'Board'): int
|
|
|
|
|
{
|
|
|
|
|
return $this->decode($this->request('POST', '/api/projects', ['title' => $title], $auth))['project']['id'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<string, string> $auth @return array<int, mixed> */
|
|
|
|
|
private function statuses(int $projectId, array $auth): array
|
|
|
|
|
{
|
|
|
|
|
return $this->decode(
|
|
|
|
|
$this->request('GET', "/api/projects/{$projectId}/statuses", null, $auth),
|
|
|
|
|
)['statuses'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<string, string> $auth */
|
2026-09-04 15:22:33 +01:00
|
|
|
private function addToInbox(string $text, array $auth): int
|
|
|
|
|
{
|
|
|
|
|
return $this->decode(
|
|
|
|
|
$this->request('POST', '/api/inbox/cards', ['text' => $text], $auth),
|
|
|
|
|
)['card']['id'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<string, string> $auth */
|
|
|
|
|
private function addToProject(int $projectId, string $text, array $auth): int
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
return $this->decode(
|
|
|
|
|
$this->request('POST', "/api/projects/{$projectId}/cards", ['text' => $text], $auth),
|
|
|
|
|
)['card']['id'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
/** @param int[] $cardIds @param array<string, string> $auth */
|
|
|
|
|
private function reorder(?int $projectId, ?int $statusId, array $cardIds, array $auth): ResponseInterface
|
|
|
|
|
{
|
|
|
|
|
return $this->request('PUT', '/api/cards/order', [
|
|
|
|
|
'project_id' => $projectId,
|
|
|
|
|
'status_id' => $statusId,
|
|
|
|
|
'card_ids' => $cardIds,
|
|
|
|
|
], $auth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<string, string> $auth @return list<array{text: string, project_id: int|null, status_id: int|null, position: int}> */
|
|
|
|
|
private function inbox(array $auth): array
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
return array_map(
|
|
|
|
|
static fn (array $c): array => [
|
2026-09-04 15:22:33 +01:00
|
|
|
'text' => $c['text'], 'project_id' => $c['project_id'],
|
|
|
|
|
'status_id' => $c['status_id'], 'position' => $c['position'],
|
2026-09-04 13:37:17 +01:00
|
|
|
],
|
2026-09-04 15:22:33 +01:00
|
|
|
$this->decode($this->request('GET', '/api/inbox/cards', null, $auth))['cards'],
|
2026-09-04 13:37:17 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
// --- inbox CRUD -----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_inbox_requires_authentication(): void
|
|
|
|
|
{
|
|
|
|
|
self::assertSame(401, $this->request('GET', '/api/inbox/cards')->getStatusCode());
|
|
|
|
|
self::assertSame(401, $this->request('POST', '/api/inbox/cards', ['text' => 'x'])->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_creating_an_inbox_card(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->request('POST', '/api/inbox/cards', ['text' => 'Buy milk'], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(201, $response->getStatusCode());
|
|
|
|
|
$card = $this->decode($response)['card'];
|
|
|
|
|
self::assertSame('Buy milk', $card['text']);
|
|
|
|
|
self::assertNull($card['project_id']);
|
|
|
|
|
self::assertNull($card['status_id']);
|
|
|
|
|
self::assertNull($card['status']);
|
|
|
|
|
self::assertSame(0, $card['position']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_inbox_card_creation_validates_text(): void
|
|
|
|
|
{
|
|
|
|
|
$response = $this->request('POST', '/api/inbox/cards', ['text' => ' '], $this->authHeader());
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
self::assertArrayHasKey('text', $this->decode($response)['error']['details']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_inbox_is_isolated_per_owner(): void
|
|
|
|
|
{
|
|
|
|
|
$mine = $this->authHeader('mine@example.com');
|
|
|
|
|
$theirs = $this->authHeader('theirs@example.com');
|
|
|
|
|
$this->addToInbox('mine', $mine);
|
|
|
|
|
$this->addToInbox('theirs', $theirs);
|
|
|
|
|
|
|
|
|
|
self::assertSame(['mine'], array_column($this->inbox($mine), 'text'));
|
|
|
|
|
self::assertSame(['theirs'], array_column($this->inbox($theirs), 'text'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_new_inbox_cards_are_ranked_densely(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
2026-09-04 13:37:17 +01:00
|
|
|
foreach (['A', 'B', 'C'] as $text) {
|
2026-09-04 15:22:33 +01:00
|
|
|
$this->addToInbox($text, $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
self::assertSame([0, 1, 2], array_column($this->inbox($auth), 'position'));
|
|
|
|
|
self::assertSame([null, null, null], array_column($this->inbox($auth), 'project_id'));
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 00:10:52 +01:00
|
|
|
public function test_creating_a_project_card_in_a_given_status(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$doing = $this->statuses($projectId, $auth)[1]['id'];
|
|
|
|
|
$this->addToProject($projectId, 'Already doing this', $auth); // lands in "To do", not "Doing"
|
|
|
|
|
|
|
|
|
|
$response = $this->request(
|
|
|
|
|
'POST',
|
|
|
|
|
"/api/projects/{$projectId}/cards",
|
|
|
|
|
['text' => 'Start this', 'status_id' => $doing],
|
|
|
|
|
$auth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertSame(201, $response->getStatusCode());
|
|
|
|
|
$card = $this->decode($response)['card'];
|
|
|
|
|
self::assertSame($doing, $card['status_id']);
|
|
|
|
|
self::assertSame(0, $card['position']); // first (and only) card in "Doing" so far
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_creating_a_project_card_appends_to_the_end_of_its_status(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$doing = $this->statuses($projectId, $auth)[1]['id'];
|
|
|
|
|
$first = $this->request(
|
|
|
|
|
'POST',
|
|
|
|
|
"/api/projects/{$projectId}/cards",
|
|
|
|
|
['text' => 'First', 'status_id' => $doing],
|
|
|
|
|
$auth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$second = $this->request(
|
|
|
|
|
'POST',
|
|
|
|
|
"/api/projects/{$projectId}/cards",
|
|
|
|
|
['text' => 'Second', 'status_id' => $doing],
|
|
|
|
|
$auth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertSame(0, $this->decode($first)['card']['position']);
|
|
|
|
|
self::assertSame(1, $this->decode($second)['card']['position']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_creating_a_project_card_rejects_a_status_from_another_project(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth, 'A');
|
|
|
|
|
$otherStatus = $this->statuses($this->newProject($auth, 'B'), $auth)[0]['id'];
|
|
|
|
|
|
|
|
|
|
$response = $this->request(
|
|
|
|
|
'POST',
|
|
|
|
|
"/api/projects/{$projectId}/cards",
|
|
|
|
|
['text' => 'x', 'status_id' => $otherStatus],
|
|
|
|
|
$auth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
// --- reordering within a column --------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_reorder_within_the_inbox(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$ids = [];
|
|
|
|
|
foreach (['A', 'B', 'C'] as $text) {
|
2026-09-04 15:22:33 +01:00
|
|
|
$ids[$text] = $this->addToInbox($text, $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->reorder(null, null, [$ids['C'], $ids['A'], $ids['B']], $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
self::assertSame(['C', 'A', 'B'], array_column($this->decode($response)['cards'], 'text'));
|
2026-09-04 15:22:33 +01:00
|
|
|
self::assertSame(['C', 'A', 'B'], array_column($this->inbox($auth), 'text'));
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
public function test_reorder_within_a_project_status(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$todo = $this->statuses($projectId, $auth)[0]['id'];
|
|
|
|
|
$ids = [];
|
|
|
|
|
foreach (['A', 'B', 'C'] as $text) {
|
2026-09-04 15:22:33 +01:00
|
|
|
$ids[$text] = $this->addToProject($projectId, $text, $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->reorder($projectId, $todo, [$ids['C'], $ids['A'], $ids['B']], $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
self::assertSame(['C', 'A', 'B'], array_column($this->decode($response)['cards'], 'text'));
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
// --- moving between the inbox and a project --------------------------
|
|
|
|
|
|
|
|
|
|
public function test_moving_a_card_from_the_inbox_into_a_project_status(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
2026-09-04 15:22:33 +01:00
|
|
|
$doing = $this->statuses($projectId, $auth)[1]['id'];
|
|
|
|
|
$cardId = $this->addToInbox('Triage me', $auth);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder($projectId, $doing, [$cardId], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
$card = $this->decode($response)['cards'][0];
|
|
|
|
|
self::assertSame($projectId, $card['project_id']);
|
|
|
|
|
self::assertSame($doing, $card['status_id']);
|
|
|
|
|
self::assertSame('Doing', $card['status']['name']);
|
|
|
|
|
self::assertSame(0, $card['position']);
|
|
|
|
|
self::assertSame([], $this->inbox($auth));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_moving_a_card_out_of_a_project_back_to_the_inbox(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$cardId = $this->addToProject($projectId, 'Rethink this', $auth);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder(null, null, [$cardId], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
$card = $this->decode($response)['cards'][0];
|
|
|
|
|
self::assertNull($card['project_id']);
|
|
|
|
|
self::assertNull($card['status_id']);
|
|
|
|
|
self::assertSame(['Rethink this'], array_column($this->inbox($auth), 'text'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_a_card_moves_from_one_project_to_another_via_the_inbox(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectA = $this->newProject($auth, 'A');
|
|
|
|
|
$projectB = $this->newProject($auth, 'B');
|
|
|
|
|
$bStatus = $this->statuses($projectB, $auth)[2]['id'];
|
|
|
|
|
$cardId = $this->addToProject($projectA, 'Reassign me', $auth);
|
|
|
|
|
|
|
|
|
|
// A -> inbox
|
|
|
|
|
$this->reorder(null, null, [$cardId], $auth);
|
|
|
|
|
self::assertSame(['Reassign me'], array_column($this->inbox($auth), 'text'));
|
|
|
|
|
|
|
|
|
|
// inbox -> B
|
|
|
|
|
$response = $this->reorder($projectB, $bStatus, [$cardId], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
$card = $this->decode($response)['cards'][0];
|
|
|
|
|
self::assertSame($projectB, $card['project_id']);
|
|
|
|
|
self::assertSame($bStatus, $card['status_id']);
|
|
|
|
|
self::assertSame([], $this->inbox($auth));
|
|
|
|
|
|
|
|
|
|
$projectACards = $this->decode($this->request('GET', "/api/projects/{$projectA}/cards", null, $auth))['cards'];
|
|
|
|
|
self::assertSame([], $projectACards);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_moving_a_card_repacks_the_column_it_left(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
2026-09-04 13:37:17 +01:00
|
|
|
$ids = [];
|
|
|
|
|
foreach (['A', 'B', 'C'] as $text) {
|
2026-09-04 15:22:33 +01:00
|
|
|
$ids[$text] = $this->addToInbox($text, $auth); // inbox: A@0, B@1, C@2
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
2026-09-04 15:22:33 +01:00
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$todo = $this->statuses($projectId, $auth)[0]['id'];
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$this->reorder($projectId, $todo, [$ids['B']], $auth); // pull B out
|
2026-09-04 13:37:17 +01:00
|
|
|
|
|
|
|
|
self::assertSame(
|
2026-09-04 15:22:33 +01:00
|
|
|
[['text' => 'A', 'position' => 0], ['text' => 'C', 'position' => 1]],
|
|
|
|
|
array_map(
|
|
|
|
|
static fn (array $c) => ['text' => $c['text'], 'position' => $c['position']],
|
|
|
|
|
$this->inbox($auth),
|
|
|
|
|
),
|
2026-09-04 13:37:17 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_a_new_inbox_card_lands_after_the_repacked_survivors(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
2026-09-04 15:22:33 +01:00
|
|
|
$a = $this->addToInbox('A', $auth);
|
|
|
|
|
$this->addToInbox('B', $auth); // inbox: A@0, B@1
|
2026-09-04 13:37:17 +01:00
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$todo = $this->statuses($projectId, $auth)[0]['id'];
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$this->reorder($projectId, $todo, [$a], $auth); // inbox now: B@0
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$newId = $this->addToInbox('C', $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
$byId = [];
|
2026-09-04 15:22:33 +01:00
|
|
|
foreach ($this->inbox($auth) as $c) {
|
|
|
|
|
$byId[$c['text']] = $c;
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
self::assertSame(0, $byId['B']['position']);
|
|
|
|
|
self::assertSame(1, $byId['C']['position']);
|
|
|
|
|
self::assertNotSame($newId, null); // sanity: card was actually created
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
// --- validation --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_order_rejects_status_id_without_project_id(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
2026-09-04 15:22:33 +01:00
|
|
|
$status = $this->statuses($projectId, $auth)[0]['id'];
|
|
|
|
|
$cardId = $this->addToInbox('x', $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->reorder(null, $status, [$cardId], $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
self::assertSame(422, $response->getStatusCode());
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
public function test_order_requires_status_id_when_project_id_is_set(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$cardId = $this->addToInbox('x', $auth);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder($projectId, null, [$cardId], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_order_rejects_a_status_from_another_project(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$mine = $this->newProject($auth, 'Mine');
|
|
|
|
|
$other = $this->newProject($auth, 'Other');
|
|
|
|
|
$foreignStatus = $this->statuses($other, $auth)[0]['id'];
|
2026-09-04 15:22:33 +01:00
|
|
|
$cardId = $this->addToProject($mine, 'x', $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->reorder($mine, $foreignStatus, [$cardId], $auth);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
public function test_order_rejects_a_project_owned_by_someone_else(): void
|
2026-09-04 13:37:17 +01:00
|
|
|
{
|
|
|
|
|
$owner = $this->authHeader('owner@example.com');
|
|
|
|
|
$other = $this->authHeader('other@example.com');
|
|
|
|
|
$projectId = $this->newProject($owner);
|
2026-09-04 15:22:33 +01:00
|
|
|
$status = $this->statuses($projectId, $owner)[0]['id'];
|
|
|
|
|
$cardId = $this->addToInbox('x', $other);
|
2026-09-04 13:37:17 +01:00
|
|
|
|
2026-09-04 15:22:33 +01:00
|
|
|
$response = $this->reorder($projectId, $status, [$cardId], $other);
|
|
|
|
|
|
|
|
|
|
self::assertSame(404, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_order_rejects_a_card_owned_by_someone_else(): void
|
|
|
|
|
{
|
|
|
|
|
$owner = $this->authHeader('owner@example.com');
|
|
|
|
|
$other = $this->authHeader('other@example.com');
|
|
|
|
|
$foreignCardId = $this->addToInbox('not yours', $owner);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder(null, null, [$foreignCardId], $other);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_order_must_list_every_card_already_in_the_target_column(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$a = $this->addToInbox('A', $auth);
|
|
|
|
|
$this->addToInbox('B', $auth);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder(null, null, [$a], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_order_rejects_duplicate_ids(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$a = $this->addToInbox('A', $auth);
|
|
|
|
|
|
|
|
|
|
$response = $this->reorder(null, null, [$a, $a], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- single-card routes are global -------------------------------------
|
|
|
|
|
|
|
|
|
|
public function test_a_card_is_only_reachable_by_its_owner(): void
|
|
|
|
|
{
|
|
|
|
|
$owner = $this->authHeader('owner@example.com');
|
|
|
|
|
$other = $this->authHeader('other@example.com');
|
|
|
|
|
$cardId = $this->addToInbox('mine', $owner);
|
|
|
|
|
|
|
|
|
|
self::assertSame(200, $this->request('GET', "/api/cards/{$cardId}", null, $owner)->getStatusCode());
|
|
|
|
|
self::assertSame(404, $this->request('GET', "/api/cards/{$cardId}", null, $other)->getStatusCode());
|
|
|
|
|
self::assertSame(404, $this->request('PATCH', "/api/cards/{$cardId}", ['text' => 'x'], $other)->getStatusCode());
|
|
|
|
|
self::assertSame(404, $this->request('DELETE', "/api/cards/{$cardId}", null, $other)->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_patch_requires_a_recognised_field(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$projectId = $this->newProject($auth);
|
|
|
|
|
$status = $this->statuses($projectId, $auth)[1]['id'];
|
|
|
|
|
$cardId = $this->addToProject($projectId, 'x', $auth);
|
|
|
|
|
|
|
|
|
|
// status_id is no longer a PATCH field -- moves go through /cards/order.
|
|
|
|
|
$response = $this->request('PATCH', "/api/cards/{$cardId}", ['status_id' => $status], $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(422, $response->getStatusCode());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_deleting_an_inbox_card(): void
|
|
|
|
|
{
|
|
|
|
|
$auth = $this->authHeader();
|
|
|
|
|
$cardId = $this->addToInbox('gone soon', $auth);
|
|
|
|
|
|
|
|
|
|
self::assertSame(204, $this->request('DELETE', "/api/cards/{$cardId}", null, $auth)->getStatusCode());
|
|
|
|
|
self::assertSame([], $this->inbox($auth));
|
2026-09-04 13:37:17 +01:00
|
|
|
}
|
|
|
|
|
}
|