$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 $auth @return array */ private function statuses(int $projectId, array $auth): array { return $this->decode( $this->request('GET', "/api/projects/{$projectId}/statuses", null, $auth), )['statuses']; } /** @param array $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 $auth @return list */ 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()); } }