diff --git a/src/Data/Label.php b/src/Data/Label.php index a46edcd5..e627f6be 100644 --- a/src/Data/Label.php +++ b/src/Data/Label.php @@ -11,6 +11,7 @@ public function __construct( public string $name, public string $color, public ?string $description, + public bool $default = false, ) {} public static function fromArray(array $data): self @@ -20,6 +21,7 @@ public static function fromArray(array $data): self name: $data['name'], color: $data['color'], description: $data['description'] ?? null, + default: $data['default'] ?? false, ); } @@ -30,6 +32,29 @@ public function toArray(): array 'name' => $this->name, 'color' => $this->color, 'description' => $this->description, + 'default' => $this->default, ]; } + + /** + * Get full hex color with # + */ + public function hexColor(): string + { + return '#'.$this->color; + } + + /** + * Check if color is light or dark + */ + public function isLightColor(): bool + { + $r = hexdec(substr($this->color, 0, 2)); + $g = hexdec(substr($this->color, 2, 2)); + $b = hexdec(substr($this->color, 4, 2)); + + $brightness = (($r * 299) + ($g * 587) + ($b * 114)) / 1000; + + return $brightness > 155; + } } diff --git a/src/Requests/IssueLabels/AddIssueLabelsRequest.php b/src/Requests/IssueLabels/AddIssueLabelsRequest.php new file mode 100644 index 00000000..c4f703b2 --- /dev/null +++ b/src/Requests/IssueLabels/AddIssueLabelsRequest.php @@ -0,0 +1,33 @@ +fullName}/issues/{$this->issueNumber}/labels"; + } + + protected function defaultBody(): array + { + return ['labels' => $this->labels]; + } +} diff --git a/src/Requests/IssueLabels/ClearIssueLabelsRequest.php b/src/Requests/IssueLabels/ClearIssueLabelsRequest.php new file mode 100644 index 00000000..df30c0cf --- /dev/null +++ b/src/Requests/IssueLabels/ClearIssueLabelsRequest.php @@ -0,0 +1,23 @@ +fullName}/issues/{$this->issueNumber}/labels"; + } +} diff --git a/src/Requests/IssueLabels/ListIssueLabelsRequest.php b/src/Requests/IssueLabels/ListIssueLabelsRequest.php new file mode 100644 index 00000000..bdd2fe72 --- /dev/null +++ b/src/Requests/IssueLabels/ListIssueLabelsRequest.php @@ -0,0 +1,23 @@ +fullName}/issues/{$this->issueNumber}/labels"; + } +} diff --git a/src/Requests/IssueLabels/RemoveIssueLabelRequest.php b/src/Requests/IssueLabels/RemoveIssueLabelRequest.php new file mode 100644 index 00000000..176b4f76 --- /dev/null +++ b/src/Requests/IssueLabels/RemoveIssueLabelRequest.php @@ -0,0 +1,24 @@ +fullName}/issues/{$this->issueNumber}/labels/{$this->label}"; + } +} diff --git a/src/Requests/IssueLabels/SetIssueLabelsRequest.php b/src/Requests/IssueLabels/SetIssueLabelsRequest.php new file mode 100644 index 00000000..8b2d2b9f --- /dev/null +++ b/src/Requests/IssueLabels/SetIssueLabelsRequest.php @@ -0,0 +1,33 @@ +fullName}/issues/{$this->issueNumber}/labels"; + } + + protected function defaultBody(): array + { + return ['labels' => $this->labels]; + } +} diff --git a/src/Requests/RepositoryLabels/CreateLabelRequest.php b/src/Requests/RepositoryLabels/CreateLabelRequest.php new file mode 100644 index 00000000..f80eadce --- /dev/null +++ b/src/Requests/RepositoryLabels/CreateLabelRequest.php @@ -0,0 +1,38 @@ +fullName}/labels"; + } + + protected function defaultBody(): array + { + return [ + 'name' => $this->name, + 'color' => $this->color, + 'description' => $this->description, + ]; + } +} diff --git a/src/Requests/RepositoryLabels/DeleteLabelRequest.php b/src/Requests/RepositoryLabels/DeleteLabelRequest.php new file mode 100644 index 00000000..a12ec5f5 --- /dev/null +++ b/src/Requests/RepositoryLabels/DeleteLabelRequest.php @@ -0,0 +1,23 @@ +fullName}/labels/{$this->name}"; + } +} diff --git a/src/Requests/RepositoryLabels/GetLabelRequest.php b/src/Requests/RepositoryLabels/GetLabelRequest.php new file mode 100644 index 00000000..cc3fe18b --- /dev/null +++ b/src/Requests/RepositoryLabels/GetLabelRequest.php @@ -0,0 +1,23 @@ +fullName}/labels/{$this->name}"; + } +} diff --git a/src/Requests/RepositoryLabels/ListLabelsRequest.php b/src/Requests/RepositoryLabels/ListLabelsRequest.php new file mode 100644 index 00000000..d5bdeba9 --- /dev/null +++ b/src/Requests/RepositoryLabels/ListLabelsRequest.php @@ -0,0 +1,22 @@ +fullName}/labels"; + } +} diff --git a/src/Requests/RepositoryLabels/UpdateLabelRequest.php b/src/Requests/RepositoryLabels/UpdateLabelRequest.php new file mode 100644 index 00000000..ef1d1dd9 --- /dev/null +++ b/src/Requests/RepositoryLabels/UpdateLabelRequest.php @@ -0,0 +1,33 @@ +fullName}/labels/{$this->name}"; + } + + protected function defaultBody(): array + { + return $this->attributes; + } +} diff --git a/src/Services/IssueLabelManager.php b/src/Services/IssueLabelManager.php new file mode 100644 index 00000000..1dba8d2f --- /dev/null +++ b/src/Services/IssueLabelManager.php @@ -0,0 +1,96 @@ + + */ + public function all(): Collection + { + $response = $this->connector->send( + new ListIssueLabelsRequest($this->fullName, $this->issueNumber) + ); + + return collect($response->json()) + ->map(fn (mixed $label): Label => Label::fromArray((array) $label)); + } + + /** + * Add labels to issue + * + * @param string|array $labels + * @return Collection + */ + public function add(string|array $labels): Collection + { + $labels = is_array($labels) ? $labels : [$labels]; + + $response = $this->connector->send( + new AddIssueLabelsRequest($this->fullName, $this->issueNumber, $labels) + ); + + return collect($response->json()) + ->map(fn (mixed $label): Label => Label::fromArray((array) $label)); + } + + /** + * Remove a label from issue + */ + public function remove(string $label): bool + { + $response = $this->connector->send( + new RemoveIssueLabelRequest($this->fullName, $this->issueNumber, $label) + ); + + return $response->successful(); + } + + /** + * Replace all labels + * + * @param array $labels + * @return Collection + */ + public function set(array $labels): Collection + { + $response = $this->connector->send( + new SetIssueLabelsRequest($this->fullName, $this->issueNumber, $labels) + ); + + return collect($response->json()) + ->map(fn (mixed $label): Label => Label::fromArray((array) $label)); + } + + /** + * Remove all labels + */ + public function clear(): bool + { + $response = $this->connector->send( + new ClearIssueLabelsRequest($this->fullName, $this->issueNumber) + ); + + return $response->successful(); + } +} diff --git a/src/Services/LabelBuilder.php b/src/Services/LabelBuilder.php new file mode 100644 index 00000000..20aafc30 --- /dev/null +++ b/src/Services/LabelBuilder.php @@ -0,0 +1,117 @@ +name = $name; + + return $this; + } + + /** + * Set label color + */ + public function color(string $color): self + { + $this->color = ltrim($color, '#'); + + return $this; + } + + /** + * Set label description + */ + public function description(string $description): self + { + $this->description = $description; + + return $this; + } + + /** + * Use a predefined color + */ + public function red(): self + { + return $this->color('d73a4a'); + } + + public function orange(): self + { + return $this->color('d4a72c'); + } + + public function yellow(): self + { + return $this->color('fef2c0'); + } + + public function green(): self + { + return $this->color('0e8a16'); + } + + public function blue(): self + { + return $this->color('1d76db'); + } + + public function purple(): self + { + return $this->color('5319e7'); + } + + public function pink(): self + { + return $this->color('e99695'); + } + + public function gray(): self + { + return $this->color('d1d5da'); + } + + /** + * Create the label + */ + public function create(): Label + { + if ($this->name === null || $this->color === null) { + throw new \InvalidArgumentException('Name and color are required to create a label'); + } + + $response = $this->connector->send( + new CreateLabelRequest( + $this->fullName, + $this->name, + $this->color, + $this->description + ) + ); + + return Label::fromArray($response->json()); + } +} diff --git a/src/Services/RepositoryLabelManager.php b/src/Services/RepositoryLabelManager.php new file mode 100644 index 00000000..02cf65a8 --- /dev/null +++ b/src/Services/RepositoryLabelManager.php @@ -0,0 +1,138 @@ + + */ + public function all(): Collection + { + $response = $this->connector->send( + new ListLabelsRequest($this->fullName) + ); + + return collect($response->json()) + ->map(fn (mixed $label): Label => Label::fromArray((array) $label)); + } + + /** + * Get a specific label + */ + public function find(string $name): Label + { + $response = $this->connector->send( + new GetLabelRequest($this->fullName, $name) + ); + + return Label::fromArray($response->json()); + } + + /** + * Create a new label + */ + public function create(string $name, string $color, ?string $description = null): Label + { + $response = $this->connector->send( + new CreateLabelRequest( + $this->fullName, + $name, + ltrim($color, '#'), + $description + ) + ); + + return Label::fromArray($response->json()); + } + + /** + * Update an existing label + */ + public function update(string $name, array $attributes): Label + { + if (isset($attributes['color'])) { + $attributes['color'] = ltrim($attributes['color'], '#'); + } + + $response = $this->connector->send( + new UpdateLabelRequest($this->fullName, $name, $attributes) + ); + + return Label::fromArray($response->json()); + } + + /** + * Delete a label + */ + public function delete(string $name): bool + { + $response = $this->connector->send( + new DeleteLabelRequest($this->fullName, $name) + ); + + return $response->successful(); + } + + /** + * Get a label builder for fluent creation + */ + public function builder(): LabelBuilder + { + return new LabelBuilder($this->connector, $this->fullName); + } + + /** + * Sync labels from an array + * + * @param array $labels + * @return Collection + */ + public function sync(array $labels): Collection + { + $existing = $this->all()->pluck('name')->toArray(); + $desired = collect($labels)->pluck('name')->toArray(); + + // Delete labels not in desired list + $toDelete = array_diff($existing, $desired); + foreach ($toDelete as $name) { + if (is_string($name)) { + $this->delete($name); + } + } + + // Create or update labels + $results = collect(); + foreach ($labels as $label) { + if (in_array($label['name'], $existing)) { + $results->push($this->update($label['name'], $label)); + } else { + $results->push($this->create( + $label['name'], + $label['color'], + $label['description'] ?? null + )); + } + } + + return $results; + } +} diff --git a/tests/Unit/Data/LabelTest.php b/tests/Unit/Data/LabelTest.php index d04812e1..83ebc8f3 100644 --- a/tests/Unit/Data/LabelTest.php +++ b/tests/Unit/Data/LabelTest.php @@ -11,6 +11,7 @@ 'name' => 'bug', 'color' => 'fc2929', 'description' => 'Something is broken', + 'default' => false, ]; $label = Label::fromArray($data); @@ -19,6 +20,7 @@ expect($label->name)->toBe('bug'); expect($label->color)->toBe('fc2929'); expect($label->description)->toBe('Something is broken'); + expect($label->default)->toBeFalse(); }); it('can create label from array with null description', function () { @@ -68,4 +70,37 @@ expect($array['color'])->toBe('84b6eb'); expect($array['description'])->toBeNull(); }); + + it('can get hex color with hash', function () { + $label = new Label( + id: 123, + name: 'bug', + color: 'fc2929', + description: null, + ); + + expect($label->hexColor())->toBe('#fc2929'); + }); + + it('can detect light colors', function () { + $lightLabel = new Label( + id: 123, + name: 'light', + color: 'ffffff', + description: null, + ); + + expect($lightLabel->isLightColor())->toBeTrue(); + }); + + it('can detect dark colors', function () { + $darkLabel = new Label( + id: 123, + name: 'dark', + color: '000000', + description: null, + ); + + expect($darkLabel->isLightColor())->toBeFalse(); + }); }); diff --git a/tests/Unit/Services/IssueLabelManagerTest.php b/tests/Unit/Services/IssueLabelManagerTest.php new file mode 100644 index 00000000..84aac4eb --- /dev/null +++ b/tests/Unit/Services/IssueLabelManagerTest.php @@ -0,0 +1,101 @@ +mockClient = new MockClient; + $this->connector = new Connector('fake-token'); + $this->connector->withMockClient($this->mockClient); + $this->manager = new IssueLabelManager($this->connector, 'owner/repo', 123); + }); + + it('lists all issue labels', function () { + $this->mockClient->addResponse(MockResponse::make([ + ['id' => 1, 'name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug', 'default' => false], + ['id' => 2, 'name' => 'feature', 'color' => '1d76db', 'description' => 'Feature', 'default' => false], + ])); + + $labels = $this->manager->all(); + + expect($labels)->toHaveCount(2) + ->and($labels->first())->toBeInstanceOf(Label::class) + ->and($labels->first()->name)->toBe('bug') + ->and($labels->last()->name)->toBe('feature'); + }); + + it('adds a single label to issue', function () { + $this->mockClient->addResponse(MockResponse::make([ + ['id' => 1, 'name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug', 'default' => false], + ])); + + $labels = $this->manager->add('bug'); + + expect($labels)->toHaveCount(1) + ->and($labels->first()->name)->toBe('bug'); + }); + + it('adds multiple labels to issue', function () { + $this->mockClient->addResponse(MockResponse::make([ + ['id' => 1, 'name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug', 'default' => false], + ['id' => 2, 'name' => 'priority-high', 'color' => 'ff0000', 'description' => 'High priority', 'default' => false], + ])); + + $labels = $this->manager->add(['bug', 'priority-high']); + + expect($labels)->toHaveCount(2) + ->and($labels->first()->name)->toBe('bug') + ->and($labels->last()->name)->toBe('priority-high'); + }); + + it('removes a label from issue', function () { + $this->mockClient->addResponse(MockResponse::make([], 204)); + + $result = $this->manager->remove('bug'); + + expect($result)->toBeTrue(); + }); + + it('returns false when remove fails', function () { + $this->mockClient->addResponse(MockResponse::make([], 404)); + + $result = $this->manager->remove('nonexistent'); + + expect($result)->toBeFalse(); + }); + + it('replaces all labels', function () { + $this->mockClient->addResponse(MockResponse::make([ + ['id' => 1, 'name' => 'feature', 'color' => '1d76db', 'description' => 'Feature', 'default' => false], + ['id' => 2, 'name' => 'enhancement', 'color' => '0e8a16', 'description' => 'Enhancement', 'default' => false], + ])); + + $labels = $this->manager->set(['feature', 'enhancement']); + + expect($labels)->toHaveCount(2) + ->and($labels->first()->name)->toBe('feature') + ->and($labels->last()->name)->toBe('enhancement'); + }); + + it('clears all labels', function () { + $this->mockClient->addResponse(MockResponse::make([], 204)); + + $result = $this->manager->clear(); + + expect($result)->toBeTrue(); + }); + + it('returns false when clear fails', function () { + $this->mockClient->addResponse(MockResponse::make([], 404)); + + $result = $this->manager->clear(); + + expect($result)->toBeFalse(); + }); +}); diff --git a/tests/Unit/Services/LabelBuilderTest.php b/tests/Unit/Services/LabelBuilderTest.php new file mode 100644 index 00000000..fd9d4107 --- /dev/null +++ b/tests/Unit/Services/LabelBuilderTest.php @@ -0,0 +1,212 @@ +mockClient = new MockClient; + $this->connector = new Connector('fake-token'); + $this->connector->withMockClient($this->mockClient); + $this->builder = new LabelBuilder($this->connector, 'owner/repo'); + }); + + it('creates label with fluent API', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'priority-high', + 'color' => 'd73a4a', + 'description' => 'High priority issue', + 'default' => false, + ])); + + $label = $this->builder + ->name('priority-high') + ->color('d73a4a') + ->description('High priority issue') + ->create(); + + expect($label)->toBeInstanceOf(Label::class) + ->and($label->name)->toBe('priority-high') + ->and($label->color)->toBe('d73a4a') + ->and($label->description)->toBe('High priority issue'); + }); + + it('strips hash from color', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'test', + 'color' => 'ff0000', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('test') + ->color('#ff0000') + ->create(); + + expect($label->color)->toBe('ff0000'); + }); + + it('uses red color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'bug', + 'color' => 'd73a4a', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('bug') + ->red() + ->create(); + + expect($label->color)->toBe('d73a4a'); + }); + + it('uses orange color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'warning', + 'color' => 'd4a72c', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('warning') + ->orange() + ->create(); + + expect($label->color)->toBe('d4a72c'); + }); + + it('uses yellow color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'attention', + 'color' => 'fef2c0', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('attention') + ->yellow() + ->create(); + + expect($label->color)->toBe('fef2c0'); + }); + + it('uses green color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'enhancement', + 'color' => '0e8a16', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('enhancement') + ->green() + ->create(); + + expect($label->color)->toBe('0e8a16'); + }); + + it('uses blue color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'documentation', + 'color' => '1d76db', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('documentation') + ->blue() + ->create(); + + expect($label->color)->toBe('1d76db'); + }); + + it('uses purple color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'question', + 'color' => '5319e7', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('question') + ->purple() + ->create(); + + expect($label->color)->toBe('5319e7'); + }); + + it('uses pink color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'design', + 'color' => 'e99695', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('design') + ->pink() + ->create(); + + expect($label->color)->toBe('e99695'); + }); + + it('uses gray color preset', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'wontfix', + 'color' => 'd1d5da', + 'description' => null, + 'default' => false, + ])); + + $label = $this->builder + ->name('wontfix') + ->gray() + ->create(); + + expect($label->color)->toBe('d1d5da'); + }); + + it('chains multiple method calls', function () { + $this->mockClient->addResponse(MockResponse::make([ + 'id' => 123, + 'name' => 'feature', + 'color' => '1d76db', + 'description' => 'New feature request', + 'default' => false, + ])); + + $label = $this->builder + ->name('feature') + ->blue() + ->description('New feature request') + ->create(); + + expect($label->name)->toBe('feature') + ->and($label->color)->toBe('1d76db') + ->and($label->description)->toBe('New feature request'); + }); +}); diff --git a/tests/Unit/Services/RepositoryLabelManagerTest.php b/tests/Unit/Services/RepositoryLabelManagerTest.php new file mode 100644 index 00000000..baa79bca --- /dev/null +++ b/tests/Unit/Services/RepositoryLabelManagerTest.php @@ -0,0 +1,188 @@ + 123, + 'name' => 'bug', + 'color' => 'd73a4a', + 'description' => 'Something is broken', + 'default' => false, + ], $override); +} + +describe('RepositoryLabelManager', function () { + beforeEach(function () { + $this->mockClient = new MockClient; + $this->connector = new Connector('fake-token'); + $this->connector->withMockClient($this->mockClient); + $this->manager = new RepositoryLabelManager($this->connector, 'owner/repo'); + }); + + it('lists all repository labels', function () { + $this->mockClient->addResponse(MockResponse::make([ + labelResponse(['id' => 1, 'name' => 'bug', 'color' => 'd73a4a']), + labelResponse(['id' => 2, 'name' => 'feature', 'color' => '1d76db']), + ])); + + $labels = $this->manager->all(); + + expect($labels)->toHaveCount(2) + ->and($labels->first())->toBeInstanceOf(Label::class) + ->and($labels->first()->name)->toBe('bug') + ->and($labels->last()->name)->toBe('feature'); + }); + + it('finds a specific label', function () { + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'enhancement']) + )); + + $label = $this->manager->find('enhancement'); + + expect($label)->toBeInstanceOf(Label::class) + ->and($label->name)->toBe('enhancement'); + }); + + it('creates a new label', function () { + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'priority-high', 'color' => 'ff0000', 'description' => 'High priority']) + )); + + $label = $this->manager->create('priority-high', 'ff0000', 'High priority'); + + expect($label)->toBeInstanceOf(Label::class) + ->and($label->name)->toBe('priority-high') + ->and($label->color)->toBe('ff0000') + ->and($label->description)->toBe('High priority'); + }); + + it('creates a label and strips hash from color', function () { + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'test', 'color' => 'ff0000']) + )); + + $label = $this->manager->create('test', '#ff0000'); + + expect($label->color)->toBe('ff0000'); + }); + + it('updates an existing label', function () { + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'bug', 'color' => '00ff00', 'description' => 'Updated']) + )); + + $label = $this->manager->update('bug', [ + 'color' => '00ff00', + 'description' => 'Updated', + ]); + + expect($label)->toBeInstanceOf(Label::class) + ->and($label->color)->toBe('00ff00') + ->and($label->description)->toBe('Updated'); + }); + + it('updates label and strips hash from color', function () { + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['color' => 'ff0000']) + )); + + $label = $this->manager->update('bug', ['color' => '#ff0000']); + + expect($label->color)->toBe('ff0000'); + }); + + it('deletes a label', function () { + $this->mockClient->addResponse(MockResponse::make([], 204)); + + $result = $this->manager->delete('old-label'); + + expect($result)->toBeTrue(); + }); + + it('returns false when delete fails', function () { + $this->mockClient->addResponse(MockResponse::make([], 404)); + + $result = $this->manager->delete('nonexistent'); + + expect($result)->toBeFalse(); + }); + + it('returns a label builder', function () { + $builder = $this->manager->builder(); + + expect($builder)->toBeInstanceOf(LabelBuilder::class); + }); + + it('syncs labels by creating new ones', function () { + // First call: get all existing labels (empty) + $this->mockClient->addResponse(MockResponse::make([])); + + // Second call: create 'bug' label + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'bug', 'color' => 'd73a4a']) + )); + + // Third call: create 'feature' label + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'feature', 'color' => '1d76db']) + )); + + $labels = $this->manager->sync([ + ['name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug'], + ['name' => 'feature', 'color' => '1d76db', 'description' => 'Feature'], + ]); + + expect($labels)->toHaveCount(2); + }); + + it('syncs labels by updating existing ones', function () { + // First call: get all existing labels + $this->mockClient->addResponse(MockResponse::make([ + labelResponse(['name' => 'bug', 'color' => 'ff0000']), + ])); + + // Second call: update 'bug' label + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'bug', 'color' => 'd73a4a']) + )); + + $labels = $this->manager->sync([ + ['name' => 'bug', 'color' => 'd73a4a', 'description' => 'Updated bug'], + ]); + + expect($labels)->toHaveCount(1) + ->and($labels->first()->color)->toBe('d73a4a'); + }); + + it('syncs labels by deleting removed ones', function () { + // First call: get all existing labels + $this->mockClient->addResponse(MockResponse::make([ + labelResponse(['name' => 'bug']), + labelResponse(['name' => 'old-label']), + ])); + + // Second call: delete 'old-label' + $this->mockClient->addResponse(MockResponse::make([], 204)); + + // Third call: update 'bug' label + $this->mockClient->addResponse(MockResponse::make( + labelResponse(['name' => 'bug']) + )); + + $labels = $this->manager->sync([ + ['name' => 'bug', 'color' => 'd73a4a', 'description' => 'Bug'], + ]); + + expect($labels)->toHaveCount(1); + }); +});