diff --git a/src/Contracts/CheckRunQueryInterface.php b/src/Contracts/CheckRunQueryInterface.php new file mode 100644 index 0000000..320225e --- /dev/null +++ b/src/Contracts/CheckRunQueryInterface.php @@ -0,0 +1,51 @@ + + */ + public function get(): Collection; + + /** + * @return Collection + */ + public function wherePassing(): Collection; + + /** + * @return Collection + */ + public function whereFailing(): Collection; + + /** + * @return Collection + */ + public function wherePending(): Collection; + + /** + * @return Collection + */ + public function whereNeutral(): Collection; + + /** + * @return Collection + */ + public function whereSkipped(): Collection; + + public function latest(): ?CheckRun; + + public function byName(string $name): ?CheckRun; + + public function summary(): CheckSummary; +} diff --git a/src/Contracts/CommentManagerInterface.php b/src/Contracts/CommentManagerInterface.php new file mode 100644 index 0000000..51b0a57 --- /dev/null +++ b/src/Contracts/CommentManagerInterface.php @@ -0,0 +1,25 @@ + + */ + public function get(): Collection; + + public function create(string $body): Comment; + + public function update(int $commentId, string $body): Comment; + + public function delete(int $commentId): bool; +} diff --git a/src/Contracts/FileQueryInterface.php b/src/Contracts/FileQueryInterface.php new file mode 100644 index 0000000..c5dc302 --- /dev/null +++ b/src/Contracts/FileQueryInterface.php @@ -0,0 +1,53 @@ + + */ + public function get(): Collection; + + /** + * @return Collection + */ + public function whereAdded(): Collection; + + /** + * @return Collection + */ + public function whereModified(): Collection; + + /** + * @return Collection + */ + public function whereRemoved(): Collection; + + /** + * @return Collection + */ + public function whereRenamed(): Collection; + + /** + * @param string $pattern Glob pattern for file paths + * @return Collection + */ + public function wherePath(string $pattern): Collection; + + /** + * @return Collection + */ + public function whereExtension(string $extension): Collection; + + public function stats(): FileStats; +} diff --git a/src/Contracts/MergeManagerInterface.php b/src/Contracts/MergeManagerInterface.php new file mode 100644 index 0000000..44accfe --- /dev/null +++ b/src/Contracts/MergeManagerInterface.php @@ -0,0 +1,23 @@ + + */ + public function get(): Collection; + + /** + * @return Collection + */ + public function whereApproved(): Collection; + + /** + * @return Collection + */ + public function whereChangesRequested(): Collection; + + /** + * @return Collection + */ + public function wherePending(): Collection; + + /** + * @return Collection + */ + public function byUser(string $username): Collection; + + public function latest(): ?Review; +} diff --git a/src/DataTransferObjects/CheckSummary.php b/src/DataTransferObjects/CheckSummary.php new file mode 100644 index 0000000..dac1cd4 --- /dev/null +++ b/src/DataTransferObjects/CheckSummary.php @@ -0,0 +1,62 @@ +total > 0 && $this->failing === 0 && $this->pending === 0; + } + + public function hasFailures(): bool + { + return $this->failing > 0; + } + + public function hasPending(): bool + { + return $this->pending > 0; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'total' => $this->total, + 'passing' => $this->passing, + 'failing' => $this->failing, + 'pending' => $this->pending, + 'neutral' => $this->neutral, + 'skipped' => $this->skipped, + ]; + } +} diff --git a/src/DataTransferObjects/FileStats.php b/src/DataTransferObjects/FileStats.php new file mode 100644 index 0000000..b512779 --- /dev/null +++ b/src/DataTransferObjects/FileStats.php @@ -0,0 +1,53 @@ + + */ + public function toArray(): array + { + return [ + 'total' => $this->total, + 'added' => $this->added, + 'modified' => $this->modified, + 'removed' => $this->removed, + 'renamed' => $this->renamed, + 'total_additions' => $this->totalAdditions, + 'total_deletions' => $this->totalDeletions, + 'total_changes' => $this->totalChanges, + ]; + } +} diff --git a/tests/Unit/ContractsTest.php b/tests/Unit/ContractsTest.php new file mode 100644 index 0000000..93beca0 --- /dev/null +++ b/tests/Unit/ContractsTest.php @@ -0,0 +1,176 @@ +hasMethod('find'))->toBeTrue() + ->and($reflection->hasMethod('get'))->toBeTrue() + ->and($reflection->hasMethod('query'))->toBeTrue() + ->and($reflection->hasMethod('create'))->toBeTrue(); + }); + + it('returns PullRequestQueryInterface from query method', function () { + $reflection = new ReflectionClass(PullRequestManagerInterface::class); + $method = $reflection->getMethod('query'); + + expect($method->getReturnType()?->getName())->toBe(PullRequestQueryInterface::class); + }); + + it('returns PullRequestBuilderInterface from create method', function () { + $reflection = new ReflectionClass(PullRequestManagerInterface::class); + $method = $reflection->getMethod('create'); + + expect($method->getReturnType()?->getName())->toBe(PullRequestBuilderInterface::class); + }); +}); + +describe('PullRequestBuilderInterface', function () { + it('defines all required builder methods', function () { + $reflection = new ReflectionClass(PullRequestBuilderInterface::class); + + expect($reflection->hasMethod('title'))->toBeTrue() + ->and($reflection->hasMethod('body'))->toBeTrue() + ->and($reflection->hasMethod('head'))->toBeTrue() + ->and($reflection->hasMethod('base'))->toBeTrue() + ->and($reflection->hasMethod('draft'))->toBeTrue() + ->and($reflection->hasMethod('maintainerCanModify'))->toBeTrue() + ->and($reflection->hasMethod('create'))->toBeTrue(); + }); + + it('has chainable methods returning self', function () { + $reflection = new ReflectionClass(PullRequestBuilderInterface::class); + + $title = $reflection->getMethod('title'); + expect($title->getReturnType()?->getName())->toBe('self'); + + $body = $reflection->getMethod('body'); + expect($body->getReturnType()?->getName())->toBe('self'); + + $head = $reflection->getMethod('head'); + expect($head->getReturnType()?->getName())->toBe('self'); + }); +}); + +describe('ReviewBuilderInterface', function () { + it('defines all required review builder methods', function () { + $reflection = new ReflectionClass(ReviewBuilderInterface::class); + + expect($reflection->hasMethod('approve'))->toBeTrue() + ->and($reflection->hasMethod('requestChanges'))->toBeTrue() + ->and($reflection->hasMethod('comment'))->toBeTrue() + ->and($reflection->hasMethod('addInlineComment'))->toBeTrue() + ->and($reflection->hasMethod('addSuggestion'))->toBeTrue() + ->and($reflection->hasMethod('submit'))->toBeTrue(); + }); + + it('has chainable methods returning self', function () { + $reflection = new ReflectionClass(ReviewBuilderInterface::class); + + $approve = $reflection->getMethod('approve'); + expect($approve->getReturnType()?->getName())->toBe('self'); + + $comment = $reflection->getMethod('comment'); + expect($comment->getReturnType()?->getName())->toBe('self'); + }); +}); + +describe('ReviewQueryInterface', function () { + it('defines all required review query methods', function () { + $reflection = new ReflectionClass(ReviewQueryInterface::class); + + expect($reflection->hasMethod('get'))->toBeTrue() + ->and($reflection->hasMethod('whereApproved'))->toBeTrue() + ->and($reflection->hasMethod('whereChangesRequested'))->toBeTrue() + ->and($reflection->hasMethod('wherePending'))->toBeTrue() + ->and($reflection->hasMethod('byUser'))->toBeTrue() + ->and($reflection->hasMethod('latest'))->toBeTrue(); + }); +}); + +describe('CheckRunQueryInterface', function () { + it('defines all required check run query methods', function () { + $reflection = new ReflectionClass(CheckRunQueryInterface::class); + + expect($reflection->hasMethod('get'))->toBeTrue() + ->and($reflection->hasMethod('wherePassing'))->toBeTrue() + ->and($reflection->hasMethod('whereFailing'))->toBeTrue() + ->and($reflection->hasMethod('wherePending'))->toBeTrue() + ->and($reflection->hasMethod('whereNeutral'))->toBeTrue() + ->and($reflection->hasMethod('whereSkipped'))->toBeTrue() + ->and($reflection->hasMethod('latest'))->toBeTrue() + ->and($reflection->hasMethod('byName'))->toBeTrue() + ->and($reflection->hasMethod('summary'))->toBeTrue(); + }); +}); + +describe('MergeManagerInterface', function () { + it('defines all required merge manager methods', function () { + $reflection = new ReflectionClass(MergeManagerInterface::class); + + expect($reflection->hasMethod('merge'))->toBeTrue() + ->and($reflection->hasMethod('squash'))->toBeTrue() + ->and($reflection->hasMethod('rebase'))->toBeTrue() + ->and($reflection->hasMethod('canMerge'))->toBeTrue() + ->and($reflection->hasMethod('deleteBranch'))->toBeTrue(); + }); +}); + +describe('FileQueryInterface', function () { + it('defines all required file query methods', function () { + $reflection = new ReflectionClass(FileQueryInterface::class); + + expect($reflection->hasMethod('get'))->toBeTrue() + ->and($reflection->hasMethod('whereAdded'))->toBeTrue() + ->and($reflection->hasMethod('whereModified'))->toBeTrue() + ->and($reflection->hasMethod('whereRemoved'))->toBeTrue() + ->and($reflection->hasMethod('whereRenamed'))->toBeTrue() + ->and($reflection->hasMethod('wherePath'))->toBeTrue() + ->and($reflection->hasMethod('whereExtension'))->toBeTrue() + ->and($reflection->hasMethod('stats'))->toBeTrue(); + }); +}); + +describe('CommentManagerInterface', function () { + it('defines all required comment manager methods', function () { + $reflection = new ReflectionClass(CommentManagerInterface::class); + + expect($reflection->hasMethod('get'))->toBeTrue() + ->and($reflection->hasMethod('create'))->toBeTrue() + ->and($reflection->hasMethod('update'))->toBeTrue() + ->and($reflection->hasMethod('delete'))->toBeTrue(); + }); +}); + +describe('Contract Type Safety', function () { + it('ensures ReviewQueryInterface methods return Collection', function () { + $reflection = new ReflectionClass(ReviewQueryInterface::class); + + $get = $reflection->getMethod('get'); + expect($get->getReturnType()?->getName())->toBe('Illuminate\Support\Collection'); + + $whereApproved = $reflection->getMethod('whereApproved'); + expect($whereApproved->getReturnType()?->getName())->toBe('Illuminate\Support\Collection'); + }); + + it('ensures nullable return types are properly defined', function () { + $reflection = new ReflectionClass(PullRequestQueryInterface::class); + $method = $reflection->getMethod('first'); + $returnType = $method->getReturnType(); + + expect($returnType)->not->toBeNull() + ->and($returnType->allowsNull())->toBeTrue(); + }); +}); diff --git a/tests/Unit/DataTransferObjects/CheckSummaryTest.php b/tests/Unit/DataTransferObjects/CheckSummaryTest.php new file mode 100644 index 0000000..ad9dd99 --- /dev/null +++ b/tests/Unit/DataTransferObjects/CheckSummaryTest.php @@ -0,0 +1,95 @@ + 10, + 'passing' => 8, + 'failing' => 1, + 'pending' => 1, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($data); + + expect($summary->total)->toBe(10) + ->and($summary->passing)->toBe(8) + ->and($summary->failing)->toBe(1) + ->and($summary->pending)->toBe(1) + ->and($summary->neutral)->toBe(0) + ->and($summary->skipped)->toBe(0); +}); + +it('can check if all checks are passing', function () { + $passingData = [ + 'total' => 5, + 'passing' => 5, + 'failing' => 0, + 'pending' => 0, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($passingData); + expect($summary->allPassing())->toBeTrue(); + + $failingData = [ + 'total' => 5, + 'passing' => 4, + 'failing' => 1, + 'pending' => 0, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($failingData); + expect($summary->allPassing())->toBeFalse(); +}); + +it('can check if has failures', function () { + $data = [ + 'total' => 5, + 'passing' => 4, + 'failing' => 1, + 'pending' => 0, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($data); + expect($summary->hasFailures())->toBeTrue(); +}); + +it('can check if has pending', function () { + $data = [ + 'total' => 5, + 'passing' => 4, + 'failing' => 0, + 'pending' => 1, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($data); + expect($summary->hasPending())->toBeTrue(); +}); + +it('can serialize to array', function () { + $data = [ + 'total' => 10, + 'passing' => 8, + 'failing' => 1, + 'pending' => 1, + 'neutral' => 0, + 'skipped' => 0, + ]; + + $summary = CheckSummary::fromArray($data); + $array = $summary->toArray(); + + expect($array)->toBe($data); +}); diff --git a/tests/Unit/DataTransferObjects/FileStatsTest.php b/tests/Unit/DataTransferObjects/FileStatsTest.php new file mode 100644 index 0000000..0be858a --- /dev/null +++ b/tests/Unit/DataTransferObjects/FileStatsTest.php @@ -0,0 +1,66 @@ + 10, + 'added' => 3, + 'modified' => 5, + 'removed' => 2, + 'renamed' => 0, + 'total_additions' => 150, + 'total_deletions' => 75, + 'total_changes' => 225, + ]; + + $stats = FileStats::fromArray($data); + + expect($stats->total)->toBe(10) + ->and($stats->added)->toBe(3) + ->and($stats->modified)->toBe(5) + ->and($stats->removed)->toBe(2) + ->and($stats->renamed)->toBe(0) + ->and($stats->totalAdditions)->toBe(150) + ->and($stats->totalDeletions)->toBe(75) + ->and($stats->totalChanges)->toBe(225); +}); + +it('can serialize to array', function () { + $data = [ + 'total' => 10, + 'added' => 3, + 'modified' => 5, + 'removed' => 2, + 'renamed' => 0, + 'total_additions' => 150, + 'total_deletions' => 75, + 'total_changes' => 225, + ]; + + $stats = FileStats::fromArray($data); + $array = $stats->toArray(); + + expect($array)->toBe($data); +}); + +it('handles zero values correctly', function () { + $data = [ + 'total' => 0, + 'added' => 0, + 'modified' => 0, + 'removed' => 0, + 'renamed' => 0, + 'total_additions' => 0, + 'total_deletions' => 0, + 'total_changes' => 0, + ]; + + $stats = FileStats::fromArray($data); + + expect($stats->total)->toBe(0) + ->and($stats->added)->toBe(0) + ->and($stats->totalChanges)->toBe(0); +});