git-auth-helper.test.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247
  1. import * as core from '@actions/core'
  2. import * as fs from 'fs'
  3. import * as gitAuthHelper from '../lib/git-auth-helper'
  4. import * as io from '@actions/io'
  5. import * as os from 'os'
  6. import * as path from 'path'
  7. import * as stateHelper from '../lib/state-helper'
  8. import {IGitCommandManager} from '../lib/git-command-manager'
  9. import {IGitSourceSettings} from '../lib/git-source-settings'
  10. const isWindows = process.platform === 'win32'
  11. const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
  12. const originalRunnerTemp = process.env['RUNNER_TEMP']
  13. const originalHome = process.env['HOME']
  14. let workspace: string
  15. let localGitConfigPath: string
  16. let globalGitConfigPath: string
  17. let runnerTemp: string
  18. let tempHomedir: string
  19. let git: IGitCommandManager & {env: {[key: string]: string}}
  20. let settings: IGitSourceSettings
  21. let sshPath: string
  22. let githubServerUrl: string
  23. describe('git-auth-helper tests', () => {
  24. beforeAll(async () => {
  25. // SSH
  26. sshPath = await io.which('ssh')
  27. // Clear test workspace
  28. await io.rmRF(testWorkspace)
  29. })
  30. beforeEach(() => {
  31. // Mock setSecret
  32. jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
  33. // Mock error/warning/info/debug
  34. jest.spyOn(core, 'error').mockImplementation(jest.fn())
  35. jest.spyOn(core, 'warning').mockImplementation(jest.fn())
  36. jest.spyOn(core, 'info').mockImplementation(jest.fn())
  37. jest.spyOn(core, 'debug').mockImplementation(jest.fn())
  38. // Mock state helper
  39. jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
  40. jest
  41. .spyOn(stateHelper, 'setSshKnownHostsPath')
  42. .mockImplementation(jest.fn())
  43. })
  44. afterEach(() => {
  45. // Unregister mocks
  46. jest.restoreAllMocks()
  47. // Restore HOME
  48. if (originalHome) {
  49. process.env['HOME'] = originalHome
  50. } else {
  51. delete process.env['HOME']
  52. }
  53. })
  54. afterAll(() => {
  55. // Restore RUNNER_TEMP
  56. delete process.env['RUNNER_TEMP']
  57. if (originalRunnerTemp) {
  58. process.env['RUNNER_TEMP'] = originalRunnerTemp
  59. }
  60. })
  61. async function testAuthHeader(
  62. testName: string,
  63. serverUrl: string | undefined = undefined
  64. ) {
  65. // Arrange
  66. let expectedServerUrl = 'https://github.com'
  67. if (serverUrl) {
  68. githubServerUrl = serverUrl
  69. expectedServerUrl = githubServerUrl
  70. }
  71. await setup(testName)
  72. expect(settings.authToken).toBeTruthy() // sanity check
  73. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  74. // Act
  75. await authHelper.configureAuth()
  76. // Assert config - check that .git/config contains includeIf entries
  77. const localConfigContent = (
  78. await fs.promises.readFile(localGitConfigPath)
  79. ).toString()
  80. expect(
  81. localConfigContent.indexOf('includeIf.gitdir:')
  82. ).toBeGreaterThanOrEqual(0)
  83. // Assert credentials config file contains the actual credentials
  84. const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  85. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  86. )
  87. expect(credentialsFiles.length).toBe(1)
  88. const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
  89. const credentialsContent = (
  90. await fs.promises.readFile(credentialsConfigPath)
  91. ).toString()
  92. const basicCredential = Buffer.from(
  93. `x-access-token:${settings.authToken}`,
  94. 'utf8'
  95. ).toString('base64')
  96. expect(
  97. credentialsContent.indexOf(
  98. `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
  99. )
  100. ).toBeGreaterThanOrEqual(0)
  101. }
  102. const configureAuth_configuresAuthHeader =
  103. 'configureAuth configures auth header'
  104. it(configureAuth_configuresAuthHeader, async () => {
  105. await testAuthHeader(configureAuth_configuresAuthHeader)
  106. })
  107. const configureAuth_AcceptsGitHubServerUrl =
  108. 'inject https://my-ghes-server.com as github server url'
  109. it(configureAuth_AcceptsGitHubServerUrl, async () => {
  110. await testAuthHeader(
  111. configureAuth_AcceptsGitHubServerUrl,
  112. 'https://my-ghes-server.com'
  113. )
  114. })
  115. const configureAuth_AcceptsGitHubServerUrlSetToGHEC =
  116. 'inject https://github.com as github server url'
  117. it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
  118. await testAuthHeader(
  119. configureAuth_AcceptsGitHubServerUrlSetToGHEC,
  120. 'https://github.com'
  121. )
  122. })
  123. const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
  124. 'configureAuth configures auth header even when persist credentials false'
  125. it(
  126. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
  127. async () => {
  128. // Arrange
  129. await setup(
  130. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
  131. )
  132. expect(settings.authToken).toBeTruthy() // sanity check
  133. settings.persistCredentials = false
  134. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  135. // Act
  136. await authHelper.configureAuth()
  137. // Assert config - check credentials config file (not local .git/config)
  138. const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  139. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  140. )
  141. expect(credentialsFiles.length).toBe(1)
  142. const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
  143. const credentialsContent = (
  144. await fs.promises.readFile(credentialsConfigPath)
  145. ).toString()
  146. expect(
  147. credentialsContent.indexOf(
  148. `http.https://github.com/.extraheader AUTHORIZATION`
  149. )
  150. ).toBeGreaterThanOrEqual(0)
  151. }
  152. )
  153. const configureAuth_copiesUserKnownHosts =
  154. 'configureAuth copies user known hosts'
  155. it(configureAuth_copiesUserKnownHosts, async () => {
  156. if (!sshPath) {
  157. process.stdout.write(
  158. `Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  159. )
  160. return
  161. }
  162. // Arange
  163. await setup(configureAuth_copiesUserKnownHosts)
  164. expect(settings.sshKey).toBeTruthy() // sanity check
  165. // Mock fs.promises.readFile
  166. const realReadFile = fs.promises.readFile
  167. jest
  168. .spyOn(fs.promises, 'readFile')
  169. .mockImplementation(async (file: any, options: any): Promise<Buffer> => {
  170. const userKnownHostsPath = path.join(
  171. os.homedir(),
  172. '.ssh',
  173. 'known_hosts'
  174. )
  175. if (file === userKnownHostsPath) {
  176. return Buffer.from('some-domain.com ssh-rsa ABCDEF')
  177. }
  178. return await realReadFile(file, options)
  179. })
  180. // Act
  181. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  182. await authHelper.configureAuth()
  183. // Assert known hosts
  184. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  185. const actualSshKnownHostsContent = (
  186. await fs.promises.readFile(actualSshKnownHostsPath)
  187. ).toString()
  188. expect(actualSshKnownHostsContent).toMatch(
  189. /some-domain\.com ssh-rsa ABCDEF/
  190. )
  191. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  192. })
  193. const configureAuth_registersBasicCredentialAsSecret =
  194. 'configureAuth registers basic credential as secret'
  195. it(configureAuth_registersBasicCredentialAsSecret, async () => {
  196. // Arrange
  197. await setup(configureAuth_registersBasicCredentialAsSecret)
  198. expect(settings.authToken).toBeTruthy() // sanity check
  199. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  200. // Act
  201. await authHelper.configureAuth()
  202. // Assert secret
  203. const setSecretSpy = core.setSecret as jest.Mock<any, any>
  204. expect(setSecretSpy).toHaveBeenCalledTimes(1)
  205. const expectedSecret = Buffer.from(
  206. `x-access-token:${settings.authToken}`,
  207. 'utf8'
  208. ).toString('base64')
  209. expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
  210. })
  211. const setsSshCommandEnvVarWhenPersistCredentialsFalse =
  212. 'sets SSH command env var when persist-credentials false'
  213. it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
  214. if (!sshPath) {
  215. process.stdout.write(
  216. `Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n`
  217. )
  218. return
  219. }
  220. // Arrange
  221. await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse)
  222. settings.persistCredentials = false
  223. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  224. // Act
  225. await authHelper.configureAuth()
  226. // Assert git env var
  227. const actualKeyPath = await getActualSshKeyPath()
  228. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  229. const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
  230. actualKeyPath
  231. )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
  232. actualKnownHostsPath
  233. )}"`
  234. expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
  235. 'GIT_SSH_COMMAND',
  236. expectedSshCommand
  237. )
  238. // Assert git config
  239. const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
  240. .toString()
  241. .split('\n')
  242. .filter(x => x)
  243. // Should have includeIf entries pointing to credentials file
  244. expect(gitConfigLines.length).toBeGreaterThan(0)
  245. expect(
  246. gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
  247. ).toBeTruthy()
  248. })
  249. const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
  250. 'sets SSH command when persist-credentials true'
  251. it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => {
  252. if (!sshPath) {
  253. process.stdout.write(
  254. `Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n`
  255. )
  256. return
  257. }
  258. // Arrange
  259. await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue)
  260. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  261. // Act
  262. await authHelper.configureAuth()
  263. // Assert git env var
  264. const actualKeyPath = await getActualSshKeyPath()
  265. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  266. const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
  267. actualKeyPath
  268. )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
  269. actualKnownHostsPath
  270. )}"`
  271. expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
  272. 'GIT_SSH_COMMAND',
  273. expectedSshCommand
  274. )
  275. // Asserty git config
  276. expect(git.config).toHaveBeenCalledWith(
  277. 'core.sshCommand',
  278. expectedSshCommand
  279. )
  280. })
  281. const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
  282. it(configureAuth_writesExplicitKnownHosts, async () => {
  283. if (!sshPath) {
  284. process.stdout.write(
  285. `Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  286. )
  287. return
  288. }
  289. // Arrange
  290. await setup(configureAuth_writesExplicitKnownHosts)
  291. expect(settings.sshKey).toBeTruthy() // sanity check
  292. settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123'
  293. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  294. // Act
  295. await authHelper.configureAuth()
  296. // Assert known hosts
  297. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  298. const actualSshKnownHostsContent = (
  299. await fs.promises.readFile(actualSshKnownHostsPath)
  300. ).toString()
  301. expect(actualSshKnownHostsContent).toMatch(
  302. /my-custom-host\.com ssh-rsa ABC123/
  303. )
  304. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  305. })
  306. const configureAuth_writesSshKeyAndImplicitKnownHosts =
  307. 'writes SSH key and implicit known hosts'
  308. it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => {
  309. if (!sshPath) {
  310. process.stdout.write(
  311. `Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  312. )
  313. return
  314. }
  315. // Arrange
  316. await setup(configureAuth_writesSshKeyAndImplicitKnownHosts)
  317. expect(settings.sshKey).toBeTruthy() // sanity check
  318. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  319. // Act
  320. await authHelper.configureAuth()
  321. // Assert SSH key
  322. const actualSshKeyPath = await getActualSshKeyPath()
  323. expect(actualSshKeyPath).toBeTruthy()
  324. const actualSshKeyContent = (
  325. await fs.promises.readFile(actualSshKeyPath)
  326. ).toString()
  327. expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
  328. if (!isWindows) {
  329. // Assert read/write for user, not group or others.
  330. // Otherwise SSH client will error.
  331. expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
  332. 0o600
  333. )
  334. }
  335. // Assert known hosts
  336. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  337. const actualSshKnownHostsContent = (
  338. await fs.promises.readFile(actualSshKnownHostsPath)
  339. ).toString()
  340. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  341. })
  342. const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet =
  343. 'configureGlobalAuth configures URL insteadOf when SSH key not set'
  344. it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => {
  345. // Arrange
  346. await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet)
  347. settings.sshKey = ''
  348. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  349. // Act
  350. await authHelper.configureAuth()
  351. await authHelper.configureGlobalAuth()
  352. // Assert temporary global config
  353. expect(git.env['HOME']).toBeTruthy()
  354. const configContent = (
  355. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  356. ).toString()
  357. expect(
  358. configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`)
  359. ).toBeGreaterThanOrEqual(0)
  360. })
  361. const configureGlobalAuth_copiesGlobalGitConfig =
  362. 'configureGlobalAuth copies global git config'
  363. it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
  364. // Arrange
  365. await setup(configureGlobalAuth_copiesGlobalGitConfig)
  366. await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
  367. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  368. // Act
  369. await authHelper.configureAuth()
  370. await authHelper.configureGlobalAuth()
  371. // Assert original global config not altered
  372. let configContent = (
  373. await fs.promises.readFile(globalGitConfigPath)
  374. ).toString()
  375. expect(configContent).toBe('value-from-global-config')
  376. // Assert temporary global config
  377. expect(git.env['HOME']).toBeTruthy()
  378. const basicCredential = Buffer.from(
  379. `x-access-token:${settings.authToken}`,
  380. 'utf8'
  381. ).toString('base64')
  382. configContent = (
  383. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  384. ).toString()
  385. expect(
  386. configContent.indexOf('value-from-global-config')
  387. ).toBeGreaterThanOrEqual(0)
  388. // Global config should have include.path pointing to credentials file
  389. expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
  390. // Check credentials in the separate config file
  391. const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  392. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  393. )
  394. expect(credentialsFiles.length).toBeGreaterThan(0)
  395. const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
  396. const credentialsContent = (
  397. await fs.promises.readFile(credentialsConfigPath)
  398. ).toString()
  399. expect(
  400. credentialsContent.indexOf(
  401. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  402. )
  403. ).toBeGreaterThanOrEqual(0)
  404. })
  405. const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
  406. 'configureGlobalAuth creates new git config when global does not exist'
  407. it(
  408. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
  409. async () => {
  410. // Arrange
  411. await setup(
  412. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
  413. )
  414. await io.rmRF(globalGitConfigPath)
  415. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  416. // Act
  417. await authHelper.configureAuth()
  418. await authHelper.configureGlobalAuth()
  419. // Assert original global config not recreated
  420. try {
  421. await fs.promises.stat(globalGitConfigPath)
  422. throw new Error(
  423. `Did not expect file to exist: '${globalGitConfigPath}'`
  424. )
  425. } catch (err) {
  426. if ((err as any)?.code !== 'ENOENT') {
  427. throw err
  428. }
  429. }
  430. // Assert temporary global config
  431. expect(git.env['HOME']).toBeTruthy()
  432. const basicCredential = Buffer.from(
  433. `x-access-token:${settings.authToken}`,
  434. 'utf8'
  435. ).toString('base64')
  436. const configContent = (
  437. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  438. ).toString()
  439. // Global config should have include.path pointing to credentials file
  440. expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
  441. // Check credentials in the separate config file
  442. const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  443. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  444. )
  445. expect(credentialsFiles.length).toBeGreaterThan(0)
  446. const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
  447. const credentialsContent = (
  448. await fs.promises.readFile(credentialsConfigPath)
  449. ).toString()
  450. expect(
  451. credentialsContent.indexOf(
  452. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  453. )
  454. ).toBeGreaterThanOrEqual(0)
  455. }
  456. )
  457. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
  458. 'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
  459. it(
  460. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
  461. async () => {
  462. // Arrange
  463. await setup(
  464. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
  465. )
  466. settings.persistCredentials = false
  467. settings.sshKey = ''
  468. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  469. await authHelper.configureAuth()
  470. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  471. mockSubmoduleForeach.mockClear() // reset calls
  472. // Act
  473. await authHelper.configureSubmoduleAuth()
  474. // Assert
  475. expect(mockSubmoduleForeach).toBeCalledTimes(1)
  476. expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
  477. /unset-all.*insteadOf/
  478. )
  479. }
  480. )
  481. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
  482. 'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
  483. it(
  484. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
  485. async () => {
  486. if (!sshPath) {
  487. process.stdout.write(
  488. `Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
  489. )
  490. return
  491. }
  492. // Arrange
  493. await setup(
  494. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
  495. )
  496. settings.persistCredentials = false
  497. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  498. await authHelper.configureAuth()
  499. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  500. mockSubmoduleForeach.mockClear() // reset calls
  501. // Act
  502. await authHelper.configureSubmoduleAuth()
  503. // Assert
  504. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
  505. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  506. /unset-all.*insteadOf/
  507. )
  508. }
  509. )
  510. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
  511. 'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
  512. it(
  513. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
  514. async () => {
  515. // Arrange
  516. await setup(
  517. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
  518. )
  519. settings.sshKey = ''
  520. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  521. await authHelper.configureAuth()
  522. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  523. mockSubmoduleForeach.mockClear() // reset calls
  524. // Act
  525. await authHelper.configureSubmoduleAuth()
  526. // Assert
  527. // Should configure insteadOf (2 calls for two values)
  528. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
  529. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  530. /unset-all.*insteadOf/
  531. )
  532. expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
  533. /url.*insteadOf.*git@github.com:/
  534. )
  535. expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
  536. /url.*insteadOf.*org-123456@github.com:/
  537. )
  538. }
  539. )
  540. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
  541. 'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
  542. it(
  543. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
  544. async () => {
  545. if (!sshPath) {
  546. process.stdout.write(
  547. `Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
  548. )
  549. return
  550. }
  551. // Arrange
  552. await setup(
  553. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
  554. )
  555. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  556. await authHelper.configureAuth()
  557. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  558. mockSubmoduleForeach.mockClear() // reset calls
  559. // Act
  560. await authHelper.configureSubmoduleAuth()
  561. // Assert
  562. // Should configure sshCommand (1 call)
  563. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
  564. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  565. /unset-all.*insteadOf/
  566. )
  567. expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
  568. }
  569. )
  570. const removeAuth_removesSshCommand = 'removeAuth removes SSH command'
  571. it(removeAuth_removesSshCommand, async () => {
  572. if (!sshPath) {
  573. process.stdout.write(
  574. `Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n`
  575. )
  576. return
  577. }
  578. // Arrange
  579. await setup(removeAuth_removesSshCommand)
  580. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  581. await authHelper.configureAuth()
  582. let gitConfigContent = (
  583. await fs.promises.readFile(localGitConfigPath)
  584. ).toString()
  585. expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual(
  586. 0
  587. ) // sanity check
  588. const actualKeyPath = await getActualSshKeyPath()
  589. expect(actualKeyPath).toBeTruthy()
  590. await fs.promises.stat(actualKeyPath)
  591. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  592. expect(actualKnownHostsPath).toBeTruthy()
  593. await fs.promises.stat(actualKnownHostsPath)
  594. // Act
  595. await authHelper.removeAuth()
  596. // Assert git config
  597. gitConfigContent = (
  598. await fs.promises.readFile(localGitConfigPath)
  599. ).toString()
  600. expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0)
  601. // Assert SSH key file
  602. try {
  603. await fs.promises.stat(actualKeyPath)
  604. throw new Error('SSH key should have been deleted')
  605. } catch (err) {
  606. if ((err as any)?.code !== 'ENOENT') {
  607. throw err
  608. }
  609. }
  610. // Assert known hosts file
  611. try {
  612. await fs.promises.stat(actualKnownHostsPath)
  613. throw new Error('SSH known hosts should have been deleted')
  614. } catch (err) {
  615. if ((err as any)?.code !== 'ENOENT') {
  616. throw err
  617. }
  618. }
  619. })
  620. const removeAuth_removesToken = 'removeAuth removes token'
  621. it(removeAuth_removesToken, async () => {
  622. // Arrange
  623. await setup(removeAuth_removesToken)
  624. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  625. await authHelper.configureAuth()
  626. // Verify includeIf entries exist in local config
  627. let localConfigContent = (
  628. await fs.promises.readFile(localGitConfigPath)
  629. ).toString()
  630. expect(
  631. localConfigContent.indexOf('includeIf.gitdir:')
  632. ).toBeGreaterThanOrEqual(0)
  633. // Verify both host and container includeIf entries are present
  634. const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
  635. expect(
  636. localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
  637. ).toBeGreaterThanOrEqual(0)
  638. expect(
  639. localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
  640. ).toBeGreaterThanOrEqual(0)
  641. // Verify credentials file exists
  642. let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  643. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  644. )
  645. expect(credentialsFiles.length).toBe(1)
  646. const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
  647. // Verify credentials file contains the auth token
  648. let credentialsContent = (
  649. await fs.promises.readFile(credentialsFilePath)
  650. ).toString()
  651. const basicCredential = Buffer.from(
  652. `x-access-token:${settings.authToken}`,
  653. 'utf8'
  654. ).toString('base64')
  655. expect(
  656. credentialsContent.indexOf(
  657. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  658. )
  659. ).toBeGreaterThanOrEqual(0)
  660. // Verify the includeIf entries point to the credentials file
  661. const containerCredentialsPath = path.posix.join(
  662. '/github/runner_temp',
  663. path.basename(credentialsFilePath)
  664. )
  665. expect(
  666. localConfigContent.indexOf(credentialsFilePath)
  667. ).toBeGreaterThanOrEqual(0)
  668. expect(
  669. localConfigContent.indexOf(containerCredentialsPath)
  670. ).toBeGreaterThanOrEqual(0)
  671. // Act
  672. await authHelper.removeAuth()
  673. // Assert all includeIf entries removed from local git config
  674. localConfigContent = (
  675. await fs.promises.readFile(localGitConfigPath)
  676. ).toString()
  677. expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
  678. expect(
  679. localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
  680. ).toBeLessThan(0)
  681. expect(
  682. localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
  683. ).toBeLessThan(0)
  684. expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
  685. expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
  686. // Assert credentials config file deleted
  687. credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  688. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  689. )
  690. expect(credentialsFiles.length).toBe(0)
  691. // Verify credentials file no longer exists on disk
  692. try {
  693. await fs.promises.stat(credentialsFilePath)
  694. throw new Error('Credentials file should have been deleted')
  695. } catch (err) {
  696. if ((err as any)?.code !== 'ENOENT') {
  697. throw err
  698. }
  699. }
  700. })
  701. const removeAuth_removesTokenFromSubmodules =
  702. 'removeAuth removes token from submodules'
  703. it(removeAuth_removesTokenFromSubmodules, async () => {
  704. // Arrange
  705. await setup(removeAuth_removesTokenFromSubmodules)
  706. // Create fake submodule config paths
  707. const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
  708. const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
  709. const submodule1ConfigPath = path.join(submodule1Dir, 'config')
  710. const submodule2ConfigPath = path.join(submodule2Dir, 'config')
  711. await fs.promises.mkdir(submodule1Dir, {recursive: true})
  712. await fs.promises.mkdir(submodule2Dir, {recursive: true})
  713. await fs.promises.writeFile(submodule1ConfigPath, '')
  714. await fs.promises.writeFile(submodule2ConfigPath, '')
  715. // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
  716. const mockGetSubmoduleConfigPaths =
  717. git.getSubmoduleConfigPaths as jest.Mock<any, any>
  718. mockGetSubmoduleConfigPaths.mockResolvedValue([
  719. submodule1ConfigPath,
  720. submodule2ConfigPath
  721. ])
  722. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  723. await authHelper.configureAuth()
  724. await authHelper.configureSubmoduleAuth()
  725. // Verify credentials file exists
  726. let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  727. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  728. )
  729. expect(credentialsFiles.length).toBe(1)
  730. const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
  731. // Verify submodule 1 config has includeIf entries
  732. let submodule1Content = (
  733. await fs.promises.readFile(submodule1ConfigPath)
  734. ).toString()
  735. const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
  736. expect(
  737. submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
  738. ).toBeGreaterThanOrEqual(0)
  739. expect(
  740. submodule1Content.indexOf(credentialsFilePath)
  741. ).toBeGreaterThanOrEqual(0)
  742. // Verify submodule 2 config has includeIf entries
  743. let submodule2Content = (
  744. await fs.promises.readFile(submodule2ConfigPath)
  745. ).toString()
  746. const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
  747. expect(
  748. submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
  749. ).toBeGreaterThanOrEqual(0)
  750. expect(
  751. submodule2Content.indexOf(credentialsFilePath)
  752. ).toBeGreaterThanOrEqual(0)
  753. // Verify both host and container paths are in each submodule config
  754. const containerCredentialsPath = path.posix.join(
  755. '/github/runner_temp',
  756. path.basename(credentialsFilePath)
  757. )
  758. expect(
  759. submodule1Content.indexOf(containerCredentialsPath)
  760. ).toBeGreaterThanOrEqual(0)
  761. expect(
  762. submodule2Content.indexOf(containerCredentialsPath)
  763. ).toBeGreaterThanOrEqual(0)
  764. // Act - ensure mock persists for removeAuth
  765. mockGetSubmoduleConfigPaths.mockResolvedValue([
  766. submodule1ConfigPath,
  767. submodule2ConfigPath
  768. ])
  769. await authHelper.removeAuth()
  770. // Assert submodule 1 includeIf entries removed
  771. submodule1Content = (
  772. await fs.promises.readFile(submodule1ConfigPath)
  773. ).toString()
  774. expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
  775. expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
  776. expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
  777. // Assert submodule 2 includeIf entries removed
  778. submodule2Content = (
  779. await fs.promises.readFile(submodule2ConfigPath)
  780. ).toString()
  781. expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
  782. expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
  783. expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
  784. // Assert credentials config file deleted
  785. credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
  786. f => f.startsWith('git-credentials-') && f.endsWith('.config')
  787. )
  788. expect(credentialsFiles.length).toBe(0)
  789. // Verify credentials file no longer exists on disk
  790. try {
  791. await fs.promises.stat(credentialsFilePath)
  792. throw new Error('Credentials file should have been deleted')
  793. } catch (err) {
  794. if ((err as any)?.code !== 'ENOENT') {
  795. throw err
  796. }
  797. }
  798. })
  799. const removeGlobalConfig_removesOverride =
  800. 'removeGlobalConfig removes override'
  801. it(removeGlobalConfig_removesOverride, async () => {
  802. // Arrange
  803. await setup(removeGlobalConfig_removesOverride)
  804. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  805. await authHelper.configureAuth()
  806. await authHelper.configureGlobalAuth()
  807. const homeOverride = git.env['HOME'] // Sanity check
  808. expect(homeOverride).toBeTruthy()
  809. await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
  810. // Act
  811. await authHelper.removeGlobalConfig()
  812. // Assert
  813. expect(git.env['HOME']).toBeUndefined()
  814. try {
  815. await fs.promises.stat(homeOverride)
  816. throw new Error(`Should have been deleted '${homeOverride}'`)
  817. } catch (err) {
  818. if ((err as any)?.code !== 'ENOENT') {
  819. throw err
  820. }
  821. }
  822. })
  823. const testCredentialsConfigPath_matchesCredentialsConfigPaths =
  824. 'testCredentialsConfigPath matches credentials config paths'
  825. it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
  826. // Arrange
  827. await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
  828. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  829. // Get a real credentials config path
  830. const credentialsConfigPath = await (
  831. authHelper as any
  832. ).getCredentialsConfigPath()
  833. // Act & Assert
  834. expect(
  835. (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
  836. ).toBe(true)
  837. expect(
  838. (authHelper as any).testCredentialsConfigPath(
  839. '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
  840. )
  841. ).toBe(true)
  842. expect(
  843. (authHelper as any).testCredentialsConfigPath(
  844. '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
  845. )
  846. ).toBe(true)
  847. // Test invalid paths
  848. expect(
  849. (authHelper as any).testCredentialsConfigPath(
  850. '/some/path/other-config.config'
  851. )
  852. ).toBe(false)
  853. expect(
  854. (authHelper as any).testCredentialsConfigPath(
  855. '/some/path/git-credentials-invalid.config'
  856. )
  857. ).toBe(false)
  858. expect(
  859. (authHelper as any).testCredentialsConfigPath(
  860. '/some/path/git-credentials-.config'
  861. )
  862. ).toBe(false)
  863. expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
  864. })
  865. const includeIfCleanupRegex_matchesBothVariants =
  866. 'includeIf cleanup regex matches both gitdir: and gitdir/i: keys'
  867. it(includeIfCleanupRegex_matchesBothVariants, async () => {
  868. // The cleanup regex must match both variants so credential
  869. // removal works regardless of which was written
  870. const regex = /^includeIf\.gitdir(\/i)?:/
  871. expect(regex.test('includeIf.gitdir:D:/workspaces/repo/.git.path')).toBe(
  872. true
  873. )
  874. expect(regex.test('includeIf.gitdir/i:D:/Workspaces/repo/.git.path')).toBe(
  875. true
  876. )
  877. expect(regex.test('includeIf.gitdir/i:/github/workspace/.git.path')).toBe(
  878. true
  879. )
  880. expect(regex.test('includeIf.gitdir:~/projects/foo/.git.path')).toBe(true)
  881. expect(regex.test('includeIf.onbranch:main.path')).toBe(false)
  882. expect(regex.test('include.path')).toBe(false)
  883. })
  884. const includeIfDirective_usesCorrectVariantForPlatform =
  885. 'includeIf directive uses gitdir/i on Windows and gitdir on other platforms'
  886. it(includeIfDirective_usesCorrectVariantForPlatform, async () => {
  887. await setup(includeIfDirective_usesCorrectVariantForPlatform)
  888. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  889. await authHelper.configureAuth()
  890. const localConfigContent = (
  891. await fs.promises.readFile(localGitConfigPath)
  892. ).toString()
  893. if (isWindows) {
  894. expect(localConfigContent).toContain('includeIf.gitdir/i:')
  895. expect(localConfigContent).not.toContain('includeIf.gitdir:')
  896. } else {
  897. expect(localConfigContent).toContain('includeIf.gitdir:')
  898. expect(localConfigContent).not.toContain('includeIf.gitdir/i:')
  899. }
  900. })
  901. })
  902. async function setup(testName: string): Promise<void> {
  903. testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
  904. // Directories
  905. workspace = path.join(testWorkspace, testName, 'workspace')
  906. runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
  907. tempHomedir = path.join(testWorkspace, testName, 'home-dir')
  908. await fs.promises.mkdir(workspace, {recursive: true})
  909. await fs.promises.mkdir(runnerTemp, {recursive: true})
  910. await fs.promises.mkdir(tempHomedir, {recursive: true})
  911. process.env['RUNNER_TEMP'] = runnerTemp
  912. process.env['HOME'] = tempHomedir
  913. process.env['GITHUB_WORKSPACE'] = workspace
  914. // Create git config
  915. globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
  916. await fs.promises.writeFile(globalGitConfigPath, '')
  917. localGitConfigPath = path.join(workspace, '.git', 'config')
  918. await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
  919. await fs.promises.writeFile(localGitConfigPath, '')
  920. git = {
  921. branchDelete: jest.fn(),
  922. branchExists: jest.fn(),
  923. branchList: jest.fn(),
  924. disableSparseCheckout: jest.fn(),
  925. sparseCheckout: jest.fn(),
  926. sparseCheckoutNonConeMode: jest.fn(),
  927. checkout: jest.fn(),
  928. checkoutDetach: jest.fn(),
  929. config: jest.fn(
  930. async (
  931. key: string,
  932. value: string,
  933. globalConfig?: boolean,
  934. add?: boolean,
  935. configFile?: string
  936. ) => {
  937. const configPath =
  938. configFile ||
  939. (globalConfig
  940. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  941. : localGitConfigPath)
  942. // Ensure directory exists
  943. await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
  944. await fs.promises.appendFile(configPath, `\n${key} ${value}`)
  945. }
  946. ),
  947. configExists: jest.fn(
  948. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  949. const configPath = globalConfig
  950. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  951. : localGitConfigPath
  952. const content = await fs.promises.readFile(configPath)
  953. const lines = content
  954. .toString()
  955. .split('\n')
  956. .filter(x => x)
  957. return lines.some(x => x.startsWith(key))
  958. }
  959. ),
  960. env: {},
  961. fetch: jest.fn(),
  962. getDefaultBranch: jest.fn(),
  963. getSubmoduleConfigPaths: jest.fn(async () => []),
  964. getWorkingDirectory: jest.fn(() => workspace),
  965. init: jest.fn(),
  966. isDetached: jest.fn(),
  967. lfsFetch: jest.fn(),
  968. lfsInstall: jest.fn(),
  969. log1: jest.fn(),
  970. remoteAdd: jest.fn(),
  971. removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
  972. revParse: jest.fn(),
  973. setEnvironmentVariable: jest.fn((name: string, value: string) => {
  974. git.env[name] = value
  975. }),
  976. shaExists: jest.fn(),
  977. submoduleForeach: jest.fn(async () => {
  978. return ''
  979. }),
  980. submoduleSync: jest.fn(),
  981. submoduleStatus: jest.fn(async () => {
  982. return true
  983. }),
  984. submoduleUpdate: jest.fn(),
  985. tagExists: jest.fn(),
  986. tryClean: jest.fn(),
  987. tryConfigUnset: jest.fn(
  988. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  989. const configPath = globalConfig
  990. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  991. : localGitConfigPath
  992. let content = await fs.promises.readFile(configPath)
  993. let lines = content
  994. .toString()
  995. .split('\n')
  996. .filter(x => x)
  997. .filter(x => !x.startsWith(key))
  998. await fs.promises.writeFile(configPath, lines.join('\n'))
  999. return true
  1000. }
  1001. ),
  1002. tryConfigUnsetValue: jest.fn(
  1003. async (
  1004. key: string,
  1005. value: string,
  1006. globalConfig?: boolean,
  1007. configPath?: string
  1008. ): Promise<boolean> => {
  1009. const targetConfigPath =
  1010. configPath ||
  1011. (globalConfig
  1012. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  1013. : localGitConfigPath)
  1014. let content = await fs.promises.readFile(targetConfigPath)
  1015. let lines = content
  1016. .toString()
  1017. .split('\n')
  1018. .filter(x => x)
  1019. .filter(x => !(x.startsWith(key) && x.includes(value)))
  1020. await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
  1021. return true
  1022. }
  1023. ),
  1024. tryDisableAutomaticGarbageCollection: jest.fn(),
  1025. tryGetFetchUrl: jest.fn(),
  1026. tryGetConfigValues: jest.fn(
  1027. async (
  1028. key: string,
  1029. globalConfig?: boolean,
  1030. configPath?: string
  1031. ): Promise<string[]> => {
  1032. const targetConfigPath =
  1033. configPath ||
  1034. (globalConfig
  1035. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  1036. : localGitConfigPath)
  1037. const content = await fs.promises.readFile(targetConfigPath)
  1038. const lines = content
  1039. .toString()
  1040. .split('\n')
  1041. .filter(x => x && x.startsWith(key))
  1042. .map(x => x.substring(key.length).trim())
  1043. return lines
  1044. }
  1045. ),
  1046. tryGetConfigKeys: jest.fn(
  1047. async (
  1048. pattern: string,
  1049. globalConfig?: boolean,
  1050. configPath?: string
  1051. ): Promise<string[]> => {
  1052. const targetConfigPath =
  1053. configPath ||
  1054. (globalConfig
  1055. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  1056. : localGitConfigPath)
  1057. const content = await fs.promises.readFile(targetConfigPath)
  1058. const lines = content
  1059. .toString()
  1060. .split('\n')
  1061. .filter(x => x)
  1062. const keys = lines
  1063. .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
  1064. .map(x => x.split(' ')[0])
  1065. return [...new Set(keys)] // Remove duplicates
  1066. }
  1067. ),
  1068. tryReset: jest.fn(),
  1069. version: jest.fn()
  1070. }
  1071. settings = {
  1072. authToken: 'some auth token',
  1073. clean: true,
  1074. commit: '',
  1075. filter: undefined,
  1076. sparseCheckout: [],
  1077. sparseCheckoutConeMode: true,
  1078. fetchDepth: 1,
  1079. fetchTags: false,
  1080. showProgress: true,
  1081. lfs: false,
  1082. submodules: false,
  1083. nestedSubmodules: false,
  1084. persistCredentials: true,
  1085. ref: 'refs/heads/main',
  1086. repositoryName: 'my-repo',
  1087. repositoryOwner: 'my-org',
  1088. repositoryPath: '',
  1089. sshKey: sshPath ? 'some ssh private key' : '',
  1090. sshKnownHosts: '',
  1091. sshStrict: true,
  1092. sshUser: '',
  1093. workflowOrganizationId: 123456,
  1094. setSafeDirectory: true,
  1095. githubServerUrl: githubServerUrl
  1096. }
  1097. }
  1098. async function getActualSshKeyPath(): Promise<string> {
  1099. let actualTempFiles = (await fs.promises.readdir(runnerTemp))
  1100. .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
  1101. .sort()
  1102. .map(x => path.join(runnerTemp, x))
  1103. if (actualTempFiles.length === 0) {
  1104. return ''
  1105. }
  1106. expect(actualTempFiles).toHaveLength(2)
  1107. expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy()
  1108. return actualTempFiles[0]
  1109. }
  1110. async function getActualSshKnownHostsPath(): Promise<string> {
  1111. let actualTempFiles = (await fs.promises.readdir(runnerTemp))
  1112. .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
  1113. .sort()
  1114. .map(x => path.join(runnerTemp, x))
  1115. if (actualTempFiles.length === 0) {
  1116. return ''
  1117. }
  1118. expect(actualTempFiles).toHaveLength(2)
  1119. expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy()
  1120. expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy()
  1121. return actualTempFiles[1]
  1122. }