Files
project-manager/tests/CardOrderTest.php
T

257 lines
9.5 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Tests;
/**
* `position` is a dense rank within a column — the cards sharing a
* (project, status). PUT /api/projects/{id}/cards/order sets one column's
* contents and order.
*/
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 */
private function addCard(int $projectId, string $text, array $auth): int
{
return $this->decode(
$this->request('POST', "/api/projects/{$projectId}/cards", ['text' => $text], $auth),
)['card']['id'];
}
/** @param array<string, string> $auth @return list<array{text: string, status_id: int|null, position: int}> */
private function cards(int $projectId, array $auth): array
{
return array_map(
static fn (array $c): array => [
'text' => $c['text'],
'status_id' => $c['status_id'],
'position' => $c['position'],
],
$this->decode($this->request('GET', "/api/projects/{$projectId}/cards", null, $auth))['cards'],
);
}
public function test_new_cards_are_ranked_densely_within_the_inbox(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
foreach (['A', 'B', 'C'] as $text) {
$this->addCard($projectId, $text, $auth);
}
self::assertSame(
[['text' => 'A', 'status_id' => null, 'position' => 0],
['text' => 'B', 'status_id' => null, 'position' => 1],
['text' => 'C', 'status_id' => null, 'position' => 2]],
$this->cards($projectId, $auth),
);
}
public function test_reorder_within_the_inbox_column(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$ids = [];
foreach (['A', 'B', 'C'] as $text) {
$ids[$text] = $this->addCard($projectId, $text, $auth);
}
$response = $this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => null,
'card_ids' => [$ids['C'], $ids['A'], $ids['B']],
], $auth);
self::assertSame(200, $response->getStatusCode());
self::assertSame(['C', 'A', 'B'], array_column($this->decode($response)['cards'], 'text'));
self::assertSame([0, 1, 2], array_column($this->cards($projectId, $auth), 'position'));
}
public function test_ordering_a_status_column_moves_cards_into_it_and_repacks_the_inbox(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$todo = $this->statuses($projectId, $auth)[0]['id'];
$ids = [];
foreach (['A', 'B', 'C'] as $text) {
$ids[$text] = $this->addCard($projectId, $text, $auth);
}
// Move B and C into "To do" (C first), leaving A alone in the inbox.
$this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => $todo,
'card_ids' => [$ids['C'], $ids['B']],
], $auth);
$byText = [];
foreach ($this->cards($projectId, $auth) as $c) {
$byText[$c['text']] = $c;
}
self::assertSame(['status_id' => null, 'position' => 0], ['status_id' => $byText['A']['status_id'], 'position' => $byText['A']['position']]);
self::assertSame(['status_id' => $todo, 'position' => 0], ['status_id' => $byText['C']['status_id'], 'position' => $byText['C']['position']]);
self::assertSame(['status_id' => $todo, 'position' => 1], ['status_id' => $byText['B']['status_id'], 'position' => $byText['B']['position']]);
}
public function test_moving_a_card_out_of_the_inbox_repacks_the_survivors(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$todo = $this->statuses($projectId, $auth)[0]['id'];
$ids = [];
foreach (['A', 'B', 'C'] as $text) {
$ids[$text] = $this->addCard($projectId, $text, $auth); // inbox 0,1,2
}
// Pull the middle card into "To do".
$this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => $todo,
'card_ids' => [$ids['B']],
], $auth);
$inbox = array_values(array_filter($this->cards($projectId, $auth), static fn ($c) => $c['status_id'] === null));
self::assertSame(
[['text' => 'A', 'status_id' => null, 'position' => 0],
['text' => 'C', 'status_id' => null, 'position' => 1]],
$inbox,
);
}
public function test_a_new_inbox_card_lands_after_the_repacked_survivors(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$todo = $this->statuses($projectId, $auth)[0]['id'];
$a = $this->addCard($projectId, 'A', $auth);
$b = $this->addCard($projectId, 'B', $auth); // inbox: A@0, B@1
$this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => $todo,
'card_ids' => [$a],
], $auth); // inbox now: B@0
$newId = $this->addCard($projectId, 'C', $auth);
$byId = [];
foreach ($this->decode($this->request('GET', "/api/projects/{$projectId}/cards", null, $auth))['cards'] as $c) {
$byId[$c['id']] = $c;
}
self::assertSame(0, $byId[$b]['position']);
self::assertSame(1, $byId[$newId]['position']);
self::assertNull($byId[$newId]['status_id']);
}
public function test_patch_status_appends_the_card_to_the_destination_column(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$todo = $this->statuses($projectId, $auth)[0]['id'];
$first = $this->addCard($projectId, 'first', $auth);
$second = $this->addCard($projectId, 'second', $auth);
$this->request('PATCH', "/api/projects/{$projectId}/cards/{$first}", ['status_id' => $todo], $auth);
$moved = $this->decode(
$this->request('PATCH', "/api/projects/{$projectId}/cards/{$second}", ['status_id' => $todo], $auth),
)['card'];
self::assertSame($todo, $moved['status_id']);
self::assertSame(1, $moved['position']); // after `first`, which took slot 0
}
public function test_reorder_rejects_a_foreign_status(): void
{
$auth = $this->authHeader();
$mine = $this->newProject($auth, 'Mine');
$other = $this->newProject($auth, 'Other');
$foreignStatus = $this->statuses($other, $auth)[0]['id'];
$card = $this->addCard($mine, 'x', $auth);
$response = $this->request('PUT', "/api/projects/{$mine}/cards/order", [
'status_id' => $foreignStatus,
'card_ids' => [$card],
], $auth);
self::assertSame(422, $response->getStatusCode());
}
public function test_reorder_rejects_a_card_from_another_project(): void
{
$auth = $this->authHeader();
$mine = $this->newProject($auth, 'Mine');
$other = $this->newProject($auth, 'Other');
$foreignCard = $this->addCard($other, 'x', $auth);
$response = $this->request('PUT', "/api/projects/{$mine}/cards/order", [
'status_id' => null,
'card_ids' => [$foreignCard],
], $auth);
self::assertSame(422, $response->getStatusCode());
}
public function test_reorder_must_list_every_card_already_in_the_target_column(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$todo = $this->statuses($projectId, $auth)[0]['id'];
$a = $this->addCard($projectId, 'A', $auth);
$b = $this->addCard($projectId, 'B', $auth);
// Put both in "To do".
$this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => $todo,
'card_ids' => [$a, $b],
], $auth);
// Now try to reorder "To do" mentioning only one of them.
$response = $this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => $todo,
'card_ids' => [$b],
], $auth);
self::assertSame(422, $response->getStatusCode());
}
public function test_reorder_rejects_duplicate_ids(): void
{
$auth = $this->authHeader();
$projectId = $this->newProject($auth);
$a = $this->addCard($projectId, 'A', $auth);
$response = $this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => null,
'card_ids' => [$a, $a],
], $auth);
self::assertSame(422, $response->getStatusCode());
}
public function test_reorder_is_scoped_to_the_owner(): void
{
$owner = $this->authHeader('owner@example.com');
$other = $this->authHeader('other@example.com');
$projectId = $this->newProject($owner);
$card = $this->addCard($projectId, 'x', $owner);
self::assertSame(404, $this->request('PUT', "/api/projects/{$projectId}/cards/order", [
'status_id' => null,
'card_ids' => [$card],
], $other)->getStatusCode());
}
}