Skip to content

Commit d39a790

Browse files
authored
Close issues as 'not_planned' (#261)
* Close issues as 'not_planned' * Revert Constants and hard-code "not_planned" * Add test for GithubIssueApi
1 parent ea075a0 commit d39a790

File tree

6 files changed

+250
-11
lines changed

6 files changed

+250
-11
lines changed

src/Api/Issue/GithubIssueApi.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace App\Api\Issue;
44

55
use App\Model\Repository;
6+
use App\Service\TaskHandler\CloseDraftHandler;
7+
use App\Service\TaskHandler\CloseStaleIssuesHandler;
68
use Github\Api\Issue;
79
use Github\Api\Issue\Comments;
810
use Github\Api\Search;
@@ -54,9 +56,23 @@ public function show(Repository $repository, $issueNumber): array
5456
return $this->issueApi->show($repository->getVendor(), $repository->getName(), $issueNumber);
5557
}
5658

57-
public function close(Repository $repository, $issueNumber)
59+
/**
60+
* Close an issue and mark it as "not_planned".
61+
*
62+
* @see CloseDraftHandler
63+
* @see CloseStaleIssuesHandler
64+
*/
65+
public function close(Repository $repository, $issueNumber): void
5866
{
59-
$this->issueApi->update($repository->getVendor(), $repository->getName(), $issueNumber, ['state' => 'closed']);
67+
$this->issueApi->update(
68+
$repository->getVendor(),
69+
$repository->getName(),
70+
$issueNumber,
71+
[
72+
'state' => 'closed',
73+
'state_reason' => 'not_planned',
74+
],
75+
);
6076
}
6177

6278
/**

src/Api/Issue/IssueApi.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function lastCommentWasMadeByBot(Repository $repository, $number): bool;
2626
public function findStaleIssues(Repository $repository, \DateTimeImmutable $noUpdateAfter): iterable;
2727

2828
/**
29-
* Close an issue or a pull request.
29+
* Close an issue and mark it as "not_planned".
3030
*/
31-
public function close(Repository $repository, $issueNumber);
31+
public function close(Repository $repository, $issueNumber): void;
3232
}

src/Api/Issue/NullIssueApi.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function findStaleIssues(Repository $repository, \DateTimeImmutable $noUp
2929
return [];
3030
}
3131

32-
public function close(Repository $repository, $issueNumber)
32+
public function close(Repository $repository, $issueNumber): void
3333
{
3434
}
3535
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
namespace App\Tests\Api\Issue;
4+
5+
use App\Api\Issue\GithubIssueApi;
6+
use App\Model\Repository;
7+
use Github\Api\Issue;
8+
use Github\Api\Issue\Comments;
9+
use Github\Api\Search;
10+
use Github\ResultPager;
11+
use PHPUnit\Framework\MockObject\MockObject;
12+
use PHPUnit\Framework\TestCase;
13+
14+
/**
15+
* @author Simon André <[email protected]>
16+
*/
17+
class GithubIssueApiTest extends TestCase
18+
{
19+
public const USER_NAME = 'weaverryan';
20+
21+
public const REPO_NAME = 'carson';
22+
23+
public const BOT_USERNAME = 'carsonbot';
24+
25+
private GithubIssueApi $api;
26+
27+
private ResultPager|MockObject $resultPager;
28+
29+
private Comments|MockObject $issueCommentApi;
30+
31+
private Issue|MockObject $backendApi;
32+
33+
private Search|MockObject $searchApi;
34+
35+
private Repository $repository;
36+
37+
protected function setUp(): void
38+
{
39+
$this->backendApi = $this->getMockBuilder(Issue::class)
40+
->disableOriginalConstructor()
41+
->getMock();
42+
43+
$this->resultPager = $this->getMockBuilder(ResultPager::class)
44+
->disableOriginalConstructor()
45+
->getMock();
46+
47+
$this->issueCommentApi = $this->getMockBuilder(Comments::class)
48+
->disableOriginalConstructor()
49+
->getMock();
50+
51+
$this->searchApi = $this->getMockBuilder(Search::class)
52+
->disableOriginalConstructor()
53+
->getMock();
54+
55+
$this->api = new GithubIssueApi(
56+
$this->resultPager,
57+
$this->issueCommentApi,
58+
$this->backendApi,
59+
$this->searchApi,
60+
self::BOT_USERNAME
61+
);
62+
63+
$this->repository = new Repository(
64+
self::USER_NAME,
65+
self::REPO_NAME,
66+
null
67+
);
68+
}
69+
70+
public function testClose()
71+
{
72+
$this->backendApi->expects($this->once())
73+
->method('update')
74+
->with(
75+
self::USER_NAME,
76+
self::REPO_NAME,
77+
1234,
78+
[
79+
'state' => 'closed',
80+
'state_reason' => 'not_planned',
81+
]
82+
);
83+
84+
$this->api->close($this->repository, 1234);
85+
}
86+
87+
public function testCommentOnIssue()
88+
{
89+
$commentBody = 'This is a test comment';
90+
91+
$this->issueCommentApi->expects($this->once())
92+
->method('create')
93+
->with(
94+
self::USER_NAME,
95+
self::REPO_NAME,
96+
1234,
97+
['body' => $commentBody]
98+
);
99+
100+
$this->api->commentOnIssue($this->repository, 1234, $commentBody);
101+
}
102+
103+
public function testShow()
104+
{
105+
$issueData = ['title' => 'Test Issue', 'body' => 'Issue description'];
106+
107+
$this->backendApi->expects($this->once())
108+
->method('show')
109+
->with(self::USER_NAME, self::REPO_NAME, 1234)
110+
->willReturn($issueData);
111+
112+
$this->assertSame($issueData, $this->api->show($this->repository, 1234));
113+
}
114+
115+
public function testLastCommentWasMadeByBot()
116+
{
117+
$comments = [
118+
['user' => ['login' => 'user1']],
119+
['user' => ['login' => self::BOT_USERNAME]],
120+
];
121+
122+
$this->issueCommentApi->expects($this->once())
123+
->method('all')
124+
->with(self::USER_NAME, self::REPO_NAME, 1234, ['per_page' => 100])
125+
->willReturn($comments);
126+
127+
$this->assertTrue($this->api->lastCommentWasMadeByBot($this->repository, 1234));
128+
}
129+
130+
public function testLastCommentWasNotMadeByBot()
131+
{
132+
$comments = [
133+
['user' => ['login' => self::BOT_USERNAME]],
134+
['user' => ['login' => 'user1']],
135+
];
136+
137+
$this->issueCommentApi->expects($this->once())
138+
->method('all')
139+
->with(self::USER_NAME, self::REPO_NAME, 1234, ['per_page' => 100])
140+
->willReturn($comments);
141+
142+
$this->assertFalse($this->api->lastCommentWasMadeByBot($this->repository, 1234));
143+
}
144+
145+
public function testOpenCreatesNewIssue()
146+
{
147+
$title = 'Test Issue';
148+
$body = 'Issue description';
149+
$labels = ['bug', 'help wanted'];
150+
151+
$this->resultPager->expects($this->once())
152+
->method('fetchAllLazy')
153+
->with($this->searchApi, 'issues', [
154+
sprintf('repo:%s/%s "%s" is:open author:%s', self::USER_NAME, self::REPO_NAME, $title, self::BOT_USERNAME),
155+
'updated',
156+
'desc',
157+
])
158+
->willReturn((fn () => yield from [])());
159+
160+
$this->backendApi->expects($this->once())
161+
->method('create')
162+
->with(
163+
self::USER_NAME,
164+
self::REPO_NAME,
165+
[
166+
'title' => $title,
167+
'labels' => $labels,
168+
'body' => $body,
169+
]
170+
);
171+
172+
$this->api->open($this->repository, $title, $body, $labels);
173+
}
174+
175+
public function testOpenUpdatesExistingIssue()
176+
{
177+
$title = 'Test Issue';
178+
$body = 'Updated description';
179+
$labels = ['bug', 'help wanted'];
180+
$issueNumber = 1234;
181+
182+
$this->resultPager->expects($this->once())
183+
->method('fetchAllLazy')
184+
->willReturn((fn () => yield ['number' => 1234])());
185+
186+
$this->backendApi->expects($this->once())
187+
->method('update')
188+
->with(
189+
self::USER_NAME,
190+
self::REPO_NAME,
191+
$issueNumber,
192+
[
193+
'title' => $title,
194+
'body' => $body,
195+
]
196+
);
197+
198+
$this->api->open($this->repository, $title, $body, $labels);
199+
}
200+
201+
public function testFindStaleIssues()
202+
{
203+
$date = new \DateTimeImmutable('2022-01-01');
204+
$expectedResults = [
205+
['number' => 1234, 'title' => 'Stale issue 1'],
206+
['number' => 5678, 'title' => 'Stale issue 2'],
207+
];
208+
209+
$this->resultPager->expects($this->once())
210+
->method('fetchAllLazy')
211+
->with(
212+
$this->searchApi,
213+
'issues',
214+
[
215+
sprintf('repo:%s/%s is:issue -label:"Keep open" -label:"Missing translations" is:open -linked:pr updated:<2022-01-01', self::USER_NAME, self::REPO_NAME),
216+
'updated',
217+
'desc',
218+
]
219+
)
220+
->willReturn((fn () => yield from $expectedResults)());
221+
222+
$this->assertSame($expectedResults, iterator_to_array($this->api->findStaleIssues($this->repository, $date)));
223+
}
224+
}

tests/Service/TaskHandler/CloseDraftHandlerTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ public function testHandleStillInDraft()
1818
{
1919
$prApi = $this->getMockBuilder(NullPullRequestApi::class)
2020
->disableOriginalConstructor()
21-
->setMethods(['show'])
2221
->getMock();
2322
$prApi->expects($this->once())->method('show')->willReturn(['draft' => true]);
2423

2524
$issueApi = $this->getMockBuilder(NullIssueApi::class)
2625
->disableOriginalConstructor()
27-
->setMethods(['close'])
2826
->getMock();
29-
$issueApi->expects($this->once())->method('close');
27+
$issueApi
28+
->expects($this->once())
29+
->method('close')
30+
->with($this->anything(), 4711);
3031

3132
$repoProvider = new RepositoryProvider(['carsonbot-playground/symfony' => []]);
3233

tests/Service/TaskHandler/CloseStaleIssuesHandlerTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,15 @@ public function testHandleStale()
6262
{
6363
$labelApi = $this->getMockBuilder(NullLabelApi::class)
6464
->disableOriginalConstructor()
65-
->setMethods(['getIssueLabels'])
6665
->getMock();
6766
$labelApi->expects($this->any())->method('getIssueLabels')->willReturn(['Bug']);
6867

6968
$issueApi = $this->getMockBuilder(NullIssueApi::class)
7069
->disableOriginalConstructor()
71-
->setMethods(['close', 'lastCommentWasMadeByBot', 'show'])
7270
->getMock();
7371
$issueApi->expects($this->any())->method('show')->willReturn(['state' => 'open']);
7472
$issueApi->expects($this->any())->method('lastCommentWasMadeByBot')->willReturn(true);
75-
$issueApi->expects($this->once())->method('close');
73+
$issueApi->expects($this->once())->method('close')->with($this->anything(), 4711);
7674

7775
$repoProvider = new RepositoryProvider(['carsonbot-playground/symfony' => []]);
7876

0 commit comments

Comments
 (0)