11import '../../src/env-test' ;
2+ import { GitHubService } from '../../src/integrations/github/service' ;
3+ import jwt from 'jsonwebtoken' ;
24
35/**
46 * Mock @octokit/rest as virtual mock since module might not be installed in test environment
@@ -13,14 +15,13 @@ jest.mock('@octokit/rest', () => ({
1315 */
1416jest . mock ( 'jsonwebtoken' ) ;
1517
16- import { GitHubService } from '../../src/integrations/github/service' ;
17- import jwt from 'jsonwebtoken' ;
18-
1918describe ( 'GitHubService' , ( ) => {
2019 let githubService : GitHubService ;
2120 const testAppId = '123456' ;
2221 const testAppSlug = 'hawk-tracker' ;
2322 const testPrivateKey = '-----BEGIN RSA PRIVATE KEY-----\nTEST_KEY\n-----END RSA PRIVATE KEY-----' ;
23+ const testClientId = 'Iv1.client-id' ;
24+ const testClientSecret = 'client-secret' ;
2425 const testInstallationId = '789012' ;
2526 const testApiUrl = 'https://api.example.com' ;
2627
@@ -35,13 +36,15 @@ describe('GitHubService', () => {
3536 addAssignees : jest . Mock ;
3637 } ;
3738 } ;
39+ graphql : jest . Mock ;
3840 } ;
3941
40- const createMockOctokit = ( ) => {
42+ const createMockOctokit = ( ) : typeof mockOctokit => {
4143 const createTokenMock = jest . fn ( ) ;
4244 const getInstallationMock = jest . fn ( ) ;
4345 const createIssueMock = jest . fn ( ) ;
4446 const addAssigneesMock = jest . fn ( ) ;
47+ const graphqlMock = jest . fn ( ) ;
4548
4649 return {
4750 rest : {
@@ -54,6 +57,7 @@ describe('GitHubService', () => {
5457 addAssignees : addAssigneesMock ,
5558 } ,
5659 } ,
60+ graphql : graphqlMock ,
5761 } ;
5862 } ;
5963
@@ -69,6 +73,8 @@ describe('GitHubService', () => {
6973 process . env . GITHUB_APP_ID = testAppId ;
7074 process . env . GITHUB_APP_SLUG = testAppSlug ;
7175 process . env . GITHUB_PRIVATE_KEY = testPrivateKey ;
76+ process . env . GITHUB_APP_CLIENT_ID = testClientId ;
77+ process . env . GITHUB_APP_CLIENT_SECRET = testClientSecret ;
7278 process . env . API_URL = testApiUrl ;
7379
7480 /**
@@ -79,8 +85,10 @@ describe('GitHubService', () => {
7985 /**
8086 * Get mocked Octokit constructor and set implementation
8187 */
88+ // eslint-disable-next-line @typescript-eslint/no-var-requires
8289 const { Octokit } = require ( '@octokit/rest' ) ;
83- ( Octokit as jest . Mock ) . mockImplementation ( ( ) => mockOctokit ) ;
90+
91+ Octokit . mockImplementation ( ( ) => mockOctokit ) ;
8492
8593 /**
8694 * Create service instance
@@ -95,6 +103,8 @@ describe('GitHubService', () => {
95103 Reflect . deleteProperty ( process . env , 'GITHUB_APP_ID' ) ;
96104 Reflect . deleteProperty ( process . env , 'GITHUB_APP_SLUG' ) ;
97105 Reflect . deleteProperty ( process . env , 'GITHUB_PRIVATE_KEY' ) ;
106+ Reflect . deleteProperty ( process . env , 'GITHUB_APP_CLIENT_ID' ) ;
107+ Reflect . deleteProperty ( process . env , 'GITHUB_APP_CLIENT_SECRET' ) ;
98108 Reflect . deleteProperty ( process . env , 'API_URL' ) ;
99109 } ) ;
100110
@@ -105,7 +115,7 @@ describe('GitHubService', () => {
105115 const url = githubService . getInstallationUrl ( state ) ;
106116
107117 expect ( url ) . toBe (
108- `https://github.com/apps/${ testAppSlug } /installations/new?state=${ encodeURIComponent ( state ) } &redirect_url=${ encodeURIComponent ( `${ testApiUrl } /integration/github/callback ` ) } `
118+ `https://github.com/apps/${ testAppSlug } /installations/new?state=${ encodeURIComponent ( state ) } &redirect_url=${ encodeURIComponent ( `${ testApiUrl } /integration/github/oauth ` ) } `
109119 ) ;
110120 } ) ;
111121
@@ -126,6 +136,7 @@ describe('GitHubService', () => {
126136 it ( 'should get installation information for User account' , async ( ) => {
127137 ( jwt . sign as jest . Mock ) . mockReturnValue ( mockJwtToken ) ;
128138
139+ /* eslint-disable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
129140 mockOctokit . rest . apps . getInstallation . mockResolvedValue ( {
130141 data : {
131142 id : 12345 ,
@@ -143,6 +154,7 @@ describe('GitHubService', () => {
143154 } ,
144155 } ,
145156 } as any ) ;
157+ /* eslint-enable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
146158
147159 const result = await githubService . getInstallationForRepository ( testInstallationId ) ;
148160
@@ -152,6 +164,7 @@ describe('GitHubService', () => {
152164 login : 'octocat' ,
153165 type : 'User' ,
154166 } ,
167+ // eslint-disable-next-line @typescript-eslint/camelcase, camelcase
155168 target_type : 'User' ,
156169 permissions : {
157170 issues : 'write' ,
@@ -160,13 +173,15 @@ describe('GitHubService', () => {
160173 } ) ;
161174
162175 expect ( mockOctokit . rest . apps . getInstallation ) . toHaveBeenCalledWith ( {
176+ // eslint-disable-next-line @typescript-eslint/camelcase, camelcase
163177 installation_id : parseInt ( testInstallationId , 10 ) ,
164178 } ) ;
165179 } ) ;
166180
167181 it ( 'should get installation information for Organization account' , async ( ) => {
168182 ( jwt . sign as jest . Mock ) . mockReturnValue ( mockJwtToken ) ;
169183
184+ /* eslint-disable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
170185 mockOctokit . rest . apps . getInstallation . mockResolvedValue ( {
171186 data : {
172187 id : 12345 ,
@@ -184,6 +199,7 @@ describe('GitHubService', () => {
184199 } ,
185200 } ,
186201 } as any ) ;
202+ /* eslint-enable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
187203
188204 const result = await githubService . getInstallationForRepository ( testInstallationId ) ;
189205
@@ -193,6 +209,7 @@ describe('GitHubService', () => {
193209 login : 'my-org' ,
194210 type : 'Organization' ,
195211 } ,
212+ // eslint-disable-next-line @typescript-eslint/camelcase, camelcase
196213 target_type : 'Organization' ,
197214 permissions : {
198215 issues : 'write' ,
@@ -219,21 +236,24 @@ describe('GitHubService', () => {
219236 beforeEach ( ( ) => {
220237 ( jwt . sign as jest . Mock ) . mockReturnValue ( mockJwtToken ) ;
221238
239+ /* eslint-disable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
222240 mockOctokit . rest . apps . createInstallationAccessToken . mockResolvedValue ( {
223241 data : {
224242 token : mockInstallationToken ,
225243 expires_at : '2025-01-01T00:00:00Z' ,
226244 } ,
227245 } as any ) ;
246+ /* eslint-enable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
228247 } ) ;
229248
230249 it ( 'should create issue successfully' , async ( ) => {
231250 const issueData = {
232251 title : 'Test Issue' ,
233252 body : 'Test body' ,
234- labels : [ 'bug' ] ,
253+ labels : [ 'bug' ] ,
235254 } ;
236255
256+ /* eslint-disable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
237257 mockOctokit . rest . issues . create . mockResolvedValue ( {
238258 data : {
239259 number : 123 ,
@@ -242,11 +262,13 @@ describe('GitHubService', () => {
242262 state : 'open' ,
243263 } ,
244264 } as any ) ;
265+ /* eslint-enable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
245266
246267 const result = await githubService . createIssue ( 'owner/repo' , testInstallationId , issueData ) ;
247268
248269 expect ( result ) . toEqual ( {
249270 number : 123 ,
271+ // eslint-disable-next-line @typescript-eslint/camelcase, camelcase
250272 html_url : 'https://github.com/owner/repo/issues/123' ,
251273 title : 'Test Issue' ,
252274 state : 'open' ,
@@ -257,7 +279,7 @@ describe('GitHubService', () => {
257279 repo : 'repo' ,
258280 title : 'Test Issue' ,
259281 body : 'Test body' ,
260- labels : [ 'bug' ] ,
282+ labels : [ 'bug' ] ,
261283 } ) ;
262284 } ) ;
263285
@@ -267,6 +289,7 @@ describe('GitHubService', () => {
267289 body : 'Test body' ,
268290 } ;
269291
292+ /* eslint-disable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
270293 mockOctokit . rest . issues . create . mockResolvedValue ( {
271294 data : {
272295 number : 124 ,
@@ -275,6 +298,7 @@ describe('GitHubService', () => {
275298 state : 'open' ,
276299 } ,
277300 } as any ) ;
301+ /* eslint-enable @typescript-eslint/camelcase, camelcase, @typescript-eslint/no-explicit-any */
278302
279303 const result = await githubService . createIssue ( 'owner/repo' , testInstallationId , issueData ) ;
280304
@@ -314,45 +338,64 @@ describe('GitHubService', () => {
314338 } ) ;
315339
316340 describe ( 'assignCopilot' , ( ) => {
317- const mockJwtToken = 'mock-jwt-token' ;
318- const mockInstallationToken = 'mock-installation-token' ;
319-
320- beforeEach ( ( ) => {
321- ( jwt . sign as jest . Mock ) . mockReturnValue ( mockJwtToken ) ;
322-
323- mockOctokit . rest . apps . createInstallationAccessToken . mockResolvedValue ( {
324- data : {
325- token : mockInstallationToken ,
326- expires_at : '2025-01-01T00:00:00Z' ,
327- } ,
328- } as any ) ;
329- } ) ;
341+ const mockDelegatedUserToken = 'mock-delegated-user-token' ;
330342
331343 it ( 'should assign Copilot to issue successfully' , async ( ) => {
332344 const issueNumber = 123 ;
333345
334- mockOctokit . rest . issues . addAssignees . mockResolvedValue ( {
335- data : { } ,
336- } as any ) ;
346+ mockOctokit . graphql
347+ . mockResolvedValueOnce ( {
348+ repository : {
349+ id : 'repo-123' ,
350+ issue : { id : 'issue-456' } ,
351+ suggestedActors : {
352+ nodes : [
353+ {
354+ login : 'copilot-swe-agent' ,
355+ __typename : 'Bot' ,
356+ id : 'bot-789' ,
357+ } ,
358+ ] ,
359+ } ,
360+ } ,
361+ } )
362+ . mockResolvedValueOnce ( {
363+ addAssigneesToAssignable : {
364+ assignable : {
365+ id : 'issue-456' ,
366+ number : issueNumber ,
367+ assignees : { nodes : [ ] } ,
368+ } ,
369+ } ,
370+ } ) ;
337371
338- const result = await githubService . assignCopilot ( 'owner' , ' repo', issueNumber , testInstallationId ) ;
372+ await githubService . assignCopilot ( 'owner/ repo' , issueNumber , mockDelegatedUserToken ) ;
339373
340- expect ( result ) . toBe ( true ) ;
341- expect ( mockOctokit . rest . issues . addAssignees ) . toHaveBeenCalledWith ( {
342- owner : 'owner' ,
343- repo : 'repo' ,
344- issue_number : issueNumber ,
345- assignees : [ 'github-copilot[bot]' ] ,
346- } ) ;
374+ expect ( mockOctokit . graphql ) . toHaveBeenCalledTimes ( 2 ) ;
347375 } ) ;
348376
349377 it ( 'should throw error if assignment fails' , async ( ) => {
350378 const issueNumber = 123 ;
351379
352- mockOctokit . rest . issues . addAssignees . mockRejectedValue ( new Error ( 'Issue not found' ) ) ;
380+ mockOctokit . graphql
381+ . mockResolvedValueOnce ( {
382+ repository : {
383+ id : 'repo-123' ,
384+ issue : { id : 'issue-456' } ,
385+ suggestedActors : {
386+ nodes : [
387+ {
388+ login : 'copilot-swe-agent' ,
389+ id : 'bot-789' ,
390+ } ,
391+ ] ,
392+ } ,
393+ } ,
394+ } )
395+ . mockRejectedValue ( new Error ( 'Issue not found' ) ) ;
353396
354397 await expect (
355- githubService . assignCopilot ( 'owner' , ' repo', issueNumber , testInstallationId )
398+ githubService . assignCopilot ( 'owner/ repo' , issueNumber , mockDelegatedUserToken )
356399 ) . rejects . toThrow ( 'Failed to assign Copilot' ) ;
357400 } ) ;
358401 } ) ;
0 commit comments