2
0

git-auth-helper.test.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146
  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
  77. const configContent = (
  78. await fs.promises.readFile(localGitConfigPath)
  79. ).toString()
  80. const basicCredential = Buffer.from(
  81. `x-access-token:${settings.authToken}`,
  82. 'utf8'
  83. ).toString('base64')
  84. expect(
  85. configContent.indexOf(
  86. `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
  87. )
  88. ).toBeGreaterThanOrEqual(0)
  89. }
  90. const configureAuth_configuresAuthHeader =
  91. 'configureAuth configures auth header'
  92. it(configureAuth_configuresAuthHeader, async () => {
  93. await testAuthHeader(configureAuth_configuresAuthHeader)
  94. })
  95. const configureAuth_AcceptsGitHubServerUrl =
  96. 'inject https://my-ghes-server.com as github server url'
  97. it(configureAuth_AcceptsGitHubServerUrl, async () => {
  98. await testAuthHeader(
  99. configureAuth_AcceptsGitHubServerUrl,
  100. 'https://my-ghes-server.com'
  101. )
  102. })
  103. const configureAuth_AcceptsGitHubServerUrlSetToGHEC =
  104. 'inject https://github.com as github server url'
  105. it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
  106. await testAuthHeader(
  107. configureAuth_AcceptsGitHubServerUrl,
  108. 'https://github.com'
  109. )
  110. })
  111. const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
  112. 'configureAuth configures auth header even when persist credentials false'
  113. it(
  114. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
  115. async () => {
  116. // Arrange
  117. await setup(
  118. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
  119. )
  120. expect(settings.authToken).toBeTruthy() // sanity check
  121. settings.persistCredentials = false
  122. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  123. // Act
  124. await authHelper.configureAuth()
  125. // Assert config
  126. const configContent = (
  127. await fs.promises.readFile(localGitConfigPath)
  128. ).toString()
  129. expect(
  130. configContent.indexOf(
  131. `http.https://github.com/.extraheader AUTHORIZATION`
  132. )
  133. ).toBeGreaterThanOrEqual(0)
  134. }
  135. )
  136. const configureAuth_copiesUserKnownHosts =
  137. 'configureAuth copies user known hosts'
  138. it(configureAuth_copiesUserKnownHosts, async () => {
  139. if (!sshPath) {
  140. process.stdout.write(
  141. `Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  142. )
  143. return
  144. }
  145. // Arange
  146. await setup(configureAuth_copiesUserKnownHosts)
  147. expect(settings.sshKey).toBeTruthy() // sanity check
  148. // Mock fs.promises.readFile
  149. const realReadFile = fs.promises.readFile
  150. jest
  151. .spyOn(fs.promises, 'readFile')
  152. .mockImplementation(async (file: any, options: any): Promise<Buffer> => {
  153. const userKnownHostsPath = path.join(
  154. os.homedir(),
  155. '.ssh',
  156. 'known_hosts'
  157. )
  158. if (file === userKnownHostsPath) {
  159. return Buffer.from('some-domain.com ssh-rsa ABCDEF')
  160. }
  161. return await realReadFile(file, options)
  162. })
  163. // Act
  164. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  165. await authHelper.configureAuth()
  166. // Assert known hosts
  167. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  168. const actualSshKnownHostsContent = (
  169. await fs.promises.readFile(actualSshKnownHostsPath)
  170. ).toString()
  171. expect(actualSshKnownHostsContent).toMatch(
  172. /some-domain\.com ssh-rsa ABCDEF/
  173. )
  174. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  175. })
  176. const configureAuth_registersBasicCredentialAsSecret =
  177. 'configureAuth registers basic credential as secret'
  178. it(configureAuth_registersBasicCredentialAsSecret, async () => {
  179. // Arrange
  180. await setup(configureAuth_registersBasicCredentialAsSecret)
  181. expect(settings.authToken).toBeTruthy() // sanity check
  182. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  183. // Act
  184. await authHelper.configureAuth()
  185. // Assert secret
  186. const setSecretSpy = core.setSecret as jest.Mock<any, any>
  187. expect(setSecretSpy).toHaveBeenCalledTimes(1)
  188. const expectedSecret = Buffer.from(
  189. `x-access-token:${settings.authToken}`,
  190. 'utf8'
  191. ).toString('base64')
  192. expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
  193. })
  194. const setsSshCommandEnvVarWhenPersistCredentialsFalse =
  195. 'sets SSH command env var when persist-credentials false'
  196. it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
  197. if (!sshPath) {
  198. process.stdout.write(
  199. `Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n`
  200. )
  201. return
  202. }
  203. // Arrange
  204. await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse)
  205. settings.persistCredentials = false
  206. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  207. // Act
  208. await authHelper.configureAuth()
  209. // Assert git env var
  210. const actualKeyPath = await getActualSshKeyPath()
  211. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  212. const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
  213. actualKeyPath
  214. )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
  215. actualKnownHostsPath
  216. )}"`
  217. expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
  218. 'GIT_SSH_COMMAND',
  219. expectedSshCommand
  220. )
  221. // Asserty git config
  222. const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
  223. .toString()
  224. .split('\n')
  225. .filter(x => x)
  226. expect(gitConfigLines).toHaveLength(1)
  227. expect(gitConfigLines[0]).toMatch(/^http\./)
  228. })
  229. const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
  230. 'sets SSH command when persist-credentials true'
  231. it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => {
  232. if (!sshPath) {
  233. process.stdout.write(
  234. `Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n`
  235. )
  236. return
  237. }
  238. // Arrange
  239. await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue)
  240. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  241. // Act
  242. await authHelper.configureAuth()
  243. // Assert git env var
  244. const actualKeyPath = await getActualSshKeyPath()
  245. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  246. const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
  247. actualKeyPath
  248. )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
  249. actualKnownHostsPath
  250. )}"`
  251. expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
  252. 'GIT_SSH_COMMAND',
  253. expectedSshCommand
  254. )
  255. // Asserty git config
  256. expect(git.config).toHaveBeenCalledWith(
  257. 'core.sshCommand',
  258. expectedSshCommand
  259. )
  260. })
  261. const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
  262. it(configureAuth_writesExplicitKnownHosts, async () => {
  263. if (!sshPath) {
  264. process.stdout.write(
  265. `Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  266. )
  267. return
  268. }
  269. // Arrange
  270. await setup(configureAuth_writesExplicitKnownHosts)
  271. expect(settings.sshKey).toBeTruthy() // sanity check
  272. settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123'
  273. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  274. // Act
  275. await authHelper.configureAuth()
  276. // Assert known hosts
  277. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  278. const actualSshKnownHostsContent = (
  279. await fs.promises.readFile(actualSshKnownHostsPath)
  280. ).toString()
  281. expect(actualSshKnownHostsContent).toMatch(
  282. /my-custom-host\.com ssh-rsa ABC123/
  283. )
  284. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  285. })
  286. const configureAuth_writesSshKeyAndImplicitKnownHosts =
  287. 'writes SSH key and implicit known hosts'
  288. it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => {
  289. if (!sshPath) {
  290. process.stdout.write(
  291. `Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
  292. )
  293. return
  294. }
  295. // Arrange
  296. await setup(configureAuth_writesSshKeyAndImplicitKnownHosts)
  297. expect(settings.sshKey).toBeTruthy() // sanity check
  298. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  299. // Act
  300. await authHelper.configureAuth()
  301. // Assert SSH key
  302. const actualSshKeyPath = await getActualSshKeyPath()
  303. expect(actualSshKeyPath).toBeTruthy()
  304. const actualSshKeyContent = (
  305. await fs.promises.readFile(actualSshKeyPath)
  306. ).toString()
  307. expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
  308. if (!isWindows) {
  309. // Assert read/write for user, not group or others.
  310. // Otherwise SSH client will error.
  311. expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
  312. 0o600
  313. )
  314. }
  315. // Assert known hosts
  316. const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
  317. const actualSshKnownHostsContent = (
  318. await fs.promises.readFile(actualSshKnownHostsPath)
  319. ).toString()
  320. expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
  321. })
  322. const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet =
  323. 'configureGlobalAuth configures URL insteadOf when SSH key not set'
  324. it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => {
  325. // Arrange
  326. await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet)
  327. settings.sshKey = ''
  328. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  329. // Act
  330. await authHelper.configureAuth()
  331. await authHelper.configureGlobalAuth()
  332. // Assert temporary global config
  333. expect(git.env['HOME']).toBeTruthy()
  334. const configContent = (
  335. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  336. ).toString()
  337. expect(
  338. configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`)
  339. ).toBeGreaterThanOrEqual(0)
  340. })
  341. const configureGlobalAuth_copiesGlobalGitConfig =
  342. 'configureGlobalAuth copies global git config'
  343. it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
  344. // Arrange
  345. await setup(configureGlobalAuth_copiesGlobalGitConfig)
  346. await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
  347. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  348. // Act
  349. await authHelper.configureAuth()
  350. await authHelper.configureGlobalAuth()
  351. // Assert original global config not altered
  352. let configContent = (
  353. await fs.promises.readFile(globalGitConfigPath)
  354. ).toString()
  355. expect(configContent).toBe('value-from-global-config')
  356. // Assert temporary global config
  357. expect(git.env['HOME']).toBeTruthy()
  358. const basicCredential = Buffer.from(
  359. `x-access-token:${settings.authToken}`,
  360. 'utf8'
  361. ).toString('base64')
  362. configContent = (
  363. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  364. ).toString()
  365. expect(
  366. configContent.indexOf('value-from-global-config')
  367. ).toBeGreaterThanOrEqual(0)
  368. expect(
  369. configContent.indexOf(
  370. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  371. )
  372. ).toBeGreaterThanOrEqual(0)
  373. })
  374. const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
  375. 'configureGlobalAuth creates new git config when global does not exist'
  376. it(
  377. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
  378. async () => {
  379. // Arrange
  380. await setup(
  381. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
  382. )
  383. await io.rmRF(globalGitConfigPath)
  384. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  385. // Act
  386. await authHelper.configureAuth()
  387. await authHelper.configureGlobalAuth()
  388. // Assert original global config not recreated
  389. try {
  390. await fs.promises.stat(globalGitConfigPath)
  391. throw new Error(
  392. `Did not expect file to exist: '${globalGitConfigPath}'`
  393. )
  394. } catch (err) {
  395. if ((err as any)?.code !== 'ENOENT') {
  396. throw err
  397. }
  398. }
  399. // Assert temporary global config
  400. expect(git.env['HOME']).toBeTruthy()
  401. const basicCredential = Buffer.from(
  402. `x-access-token:${settings.authToken}`,
  403. 'utf8'
  404. ).toString('base64')
  405. const configContent = (
  406. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  407. ).toString()
  408. expect(
  409. configContent.indexOf(
  410. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  411. )
  412. ).toBeGreaterThanOrEqual(0)
  413. }
  414. )
  415. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
  416. 'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
  417. it(
  418. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
  419. async () => {
  420. // Arrange
  421. await setup(
  422. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
  423. )
  424. settings.persistCredentials = false
  425. settings.sshKey = ''
  426. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  427. await authHelper.configureAuth()
  428. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  429. mockSubmoduleForeach.mockClear() // reset calls
  430. // Act
  431. await authHelper.configureSubmoduleAuth()
  432. // Assert
  433. expect(mockSubmoduleForeach).toBeCalledTimes(1)
  434. expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
  435. /unset-all.*insteadOf/
  436. )
  437. }
  438. )
  439. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
  440. 'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
  441. it(
  442. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
  443. async () => {
  444. if (!sshPath) {
  445. process.stdout.write(
  446. `Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
  447. )
  448. return
  449. }
  450. // Arrange
  451. await setup(
  452. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
  453. )
  454. settings.persistCredentials = false
  455. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  456. await authHelper.configureAuth()
  457. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  458. mockSubmoduleForeach.mockClear() // reset calls
  459. // Act
  460. await authHelper.configureSubmoduleAuth()
  461. // Assert
  462. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
  463. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  464. /unset-all.*insteadOf/
  465. )
  466. }
  467. )
  468. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
  469. 'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
  470. it(
  471. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
  472. async () => {
  473. // Arrange
  474. await setup(
  475. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
  476. )
  477. settings.sshKey = ''
  478. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  479. await authHelper.configureAuth()
  480. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  481. mockSubmoduleForeach.mockClear() // reset calls
  482. // Act
  483. await authHelper.configureSubmoduleAuth()
  484. // Assert
  485. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
  486. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  487. /unset-all.*insteadOf/
  488. )
  489. expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
  490. expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
  491. /url.*insteadOf.*git@github.com:/
  492. )
  493. expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
  494. /url.*insteadOf.*org-123456@github.com:/
  495. )
  496. }
  497. )
  498. const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
  499. 'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
  500. it(
  501. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
  502. async () => {
  503. if (!sshPath) {
  504. process.stdout.write(
  505. `Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
  506. )
  507. return
  508. }
  509. // Arrange
  510. await setup(
  511. configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
  512. )
  513. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  514. await authHelper.configureAuth()
  515. const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
  516. mockSubmoduleForeach.mockClear() // reset calls
  517. // Act
  518. await authHelper.configureSubmoduleAuth()
  519. // Assert
  520. expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
  521. expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
  522. /unset-all.*insteadOf/
  523. )
  524. expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
  525. expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
  526. }
  527. )
  528. const removeAuth_removesSshCommand = 'removeAuth removes SSH command'
  529. it(removeAuth_removesSshCommand, async () => {
  530. if (!sshPath) {
  531. process.stdout.write(
  532. `Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n`
  533. )
  534. return
  535. }
  536. // Arrange
  537. await setup(removeAuth_removesSshCommand)
  538. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  539. await authHelper.configureAuth()
  540. let gitConfigContent = (
  541. await fs.promises.readFile(localGitConfigPath)
  542. ).toString()
  543. expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual(
  544. 0
  545. ) // sanity check
  546. const actualKeyPath = await getActualSshKeyPath()
  547. expect(actualKeyPath).toBeTruthy()
  548. await fs.promises.stat(actualKeyPath)
  549. const actualKnownHostsPath = await getActualSshKnownHostsPath()
  550. expect(actualKnownHostsPath).toBeTruthy()
  551. await fs.promises.stat(actualKnownHostsPath)
  552. // Act
  553. await authHelper.removeAuth()
  554. // Assert git config
  555. gitConfigContent = (
  556. await fs.promises.readFile(localGitConfigPath)
  557. ).toString()
  558. expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0)
  559. // Assert SSH key file
  560. try {
  561. await fs.promises.stat(actualKeyPath)
  562. throw new Error('SSH key should have been deleted')
  563. } catch (err) {
  564. if ((err as any)?.code !== 'ENOENT') {
  565. throw err
  566. }
  567. }
  568. // Assert known hosts file
  569. try {
  570. await fs.promises.stat(actualKnownHostsPath)
  571. throw new Error('SSH known hosts should have been deleted')
  572. } catch (err) {
  573. if ((err as any)?.code !== 'ENOENT') {
  574. throw err
  575. }
  576. }
  577. })
  578. const removeAuth_removesToken = 'removeAuth removes token'
  579. it(removeAuth_removesToken, async () => {
  580. // Arrange
  581. await setup(removeAuth_removesToken)
  582. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  583. await authHelper.configureAuth()
  584. let gitConfigContent = (
  585. await fs.promises.readFile(localGitConfigPath)
  586. ).toString()
  587. expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
  588. // Act
  589. await authHelper.removeAuth()
  590. // Assert git config
  591. gitConfigContent = (
  592. await fs.promises.readFile(localGitConfigPath)
  593. ).toString()
  594. expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
  595. })
  596. const removeAuth_removesV6StyleCredentials =
  597. 'removeAuth removes v6 style credentials'
  598. it(removeAuth_removesV6StyleCredentials, async () => {
  599. // Arrange
  600. await setup(removeAuth_removesV6StyleCredentials)
  601. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  602. await authHelper.configureAuth()
  603. // Manually create v6-style credentials that would be left by v6
  604. const credentialsFileName =
  605. 'git-credentials-12345678-1234-1234-1234-123456789abc.config'
  606. const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
  607. const basicCredential = Buffer.from(
  608. `x-access-token:${settings.authToken}`,
  609. 'utf8'
  610. ).toString('base64')
  611. const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
  612. await fs.promises.writeFile(credentialsFilePath, credentialsContent)
  613. // Add includeIf entries to local git config (simulating v6 configuration)
  614. const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
  615. await fs.promises.appendFile(
  616. localGitConfigPath,
  617. `[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
  618. )
  619. await fs.promises.appendFile(
  620. localGitConfigPath,
  621. `[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
  622. )
  623. // Verify v6 style config exists
  624. let gitConfigContent = (
  625. await fs.promises.readFile(localGitConfigPath)
  626. ).toString()
  627. expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
  628. expect(
  629. gitConfigContent.indexOf(credentialsFilePath)
  630. ).toBeGreaterThanOrEqual(0)
  631. await fs.promises.stat(credentialsFilePath) // Verify file exists
  632. // Mock the git methods to handle v6 cleanup
  633. const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
  634. mockTryGetConfigKeys.mockResolvedValue([
  635. `includeIf.gitdir:${hostGitDir}/.path`,
  636. 'includeIf.gitdir:/github/workspace/.git/.path'
  637. ])
  638. const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
  639. mockTryGetConfigValues.mockImplementation(async (key: string) => {
  640. if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
  641. return [credentialsFilePath]
  642. }
  643. if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
  644. return [`/github/runner_temp/${credentialsFileName}`]
  645. }
  646. return []
  647. })
  648. const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
  649. any,
  650. any
  651. >
  652. mockTryConfigUnsetValue.mockImplementation(
  653. async (
  654. key: string,
  655. value: string,
  656. globalConfig?: boolean,
  657. configPath?: string
  658. ) => {
  659. const targetPath = configPath || localGitConfigPath
  660. let content = await fs.promises.readFile(targetPath, 'utf8')
  661. // Remove the includeIf section
  662. const lines = content
  663. .split('\n')
  664. .filter(line => !line.includes('includeIf') && !line.includes(value))
  665. await fs.promises.writeFile(targetPath, lines.join('\n'))
  666. return true
  667. }
  668. )
  669. // Act
  670. await authHelper.removeAuth()
  671. // Assert includeIf entries removed from local git config
  672. gitConfigContent = (
  673. await fs.promises.readFile(localGitConfigPath)
  674. ).toString()
  675. expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
  676. expect(gitConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
  677. // Assert credentials config file deleted
  678. try {
  679. await fs.promises.stat(credentialsFilePath)
  680. throw new Error('Credentials file should have been deleted')
  681. } catch (err) {
  682. if ((err as any)?.code !== 'ENOENT') {
  683. throw err
  684. }
  685. }
  686. })
  687. const removeAuth_removesV6StyleCredentialsFromSubmodules =
  688. 'removeAuth removes v6 style credentials from submodules'
  689. it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
  690. // Arrange
  691. await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
  692. // Create fake submodule config paths
  693. const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
  694. const submodule1ConfigPath = path.join(submodule1Dir, 'config')
  695. await fs.promises.mkdir(submodule1Dir, {recursive: true})
  696. await fs.promises.writeFile(submodule1ConfigPath, '')
  697. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  698. await authHelper.configureAuth()
  699. // Create v6-style credentials file
  700. const credentialsFileName =
  701. 'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
  702. const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
  703. const basicCredential = Buffer.from(
  704. `x-access-token:${settings.authToken}`,
  705. 'utf8'
  706. ).toString('base64')
  707. const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
  708. await fs.promises.writeFile(credentialsFilePath, credentialsContent)
  709. // Add includeIf entries to submodule config
  710. const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
  711. await fs.promises.appendFile(
  712. submodule1ConfigPath,
  713. `[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
  714. )
  715. // Verify submodule config has includeIf entry
  716. let submoduleConfigContent = (
  717. await fs.promises.readFile(submodule1ConfigPath)
  718. ).toString()
  719. expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
  720. 0
  721. )
  722. expect(
  723. submoduleConfigContent.indexOf(credentialsFilePath)
  724. ).toBeGreaterThanOrEqual(0)
  725. // Mock getSubmoduleConfigPaths
  726. const mockGetSubmoduleConfigPaths =
  727. git.getSubmoduleConfigPaths as jest.Mock<any, any>
  728. mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
  729. // Mock tryGetConfigKeys for submodule
  730. const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
  731. mockTryGetConfigKeys.mockImplementation(
  732. async (pattern: string, globalConfig?: boolean, configPath?: string) => {
  733. if (configPath === submodule1ConfigPath) {
  734. return [`includeIf.gitdir:${submodule1GitDir}/.path`]
  735. }
  736. return []
  737. }
  738. )
  739. // Mock tryGetConfigValues for submodule
  740. const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
  741. mockTryGetConfigValues.mockImplementation(
  742. async (key: string, globalConfig?: boolean, configPath?: string) => {
  743. if (
  744. configPath === submodule1ConfigPath &&
  745. key === `includeIf.gitdir:${submodule1GitDir}/.path`
  746. ) {
  747. return [credentialsFilePath]
  748. }
  749. return []
  750. }
  751. )
  752. // Mock tryConfigUnsetValue for submodule
  753. const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
  754. any,
  755. any
  756. >
  757. mockTryConfigUnsetValue.mockImplementation(
  758. async (
  759. key: string,
  760. value: string,
  761. globalConfig?: boolean,
  762. configPath?: string
  763. ) => {
  764. const targetPath = configPath || localGitConfigPath
  765. let content = await fs.promises.readFile(targetPath, 'utf8')
  766. const lines = content
  767. .split('\n')
  768. .filter(line => !line.includes('includeIf') && !line.includes(value))
  769. await fs.promises.writeFile(targetPath, lines.join('\n'))
  770. return true
  771. }
  772. )
  773. // Act
  774. await authHelper.removeAuth()
  775. // Assert submodule includeIf entries removed
  776. submoduleConfigContent = (
  777. await fs.promises.readFile(submodule1ConfigPath)
  778. ).toString()
  779. expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
  780. expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
  781. // Assert credentials file deleted
  782. try {
  783. await fs.promises.stat(credentialsFilePath)
  784. throw new Error('Credentials file should have been deleted')
  785. } catch (err) {
  786. if ((err as any)?.code !== 'ENOENT') {
  787. throw err
  788. }
  789. }
  790. })
  791. const removeAuth_skipsV6CleanupWhenEnvVarSet =
  792. 'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
  793. it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
  794. // Arrange
  795. await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
  796. // Set the skip environment variable
  797. process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
  798. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  799. await authHelper.configureAuth()
  800. // Create v6-style credentials file in RUNNER_TEMP
  801. const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
  802. const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
  803. const credentialsContent =
  804. '[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
  805. await fs.promises.writeFile(credentialsFilePath, credentialsContent)
  806. // Add includeIf section to local git config (separate from http.* config)
  807. const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
  808. await fs.promises.appendFile(localGitConfigPath, includeIfSection)
  809. // Verify v6 style config exists
  810. let gitConfigContent = (
  811. await fs.promises.readFile(localGitConfigPath)
  812. ).toString()
  813. expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
  814. await fs.promises.stat(credentialsFilePath) // Verify file exists
  815. // Act
  816. await authHelper.removeAuth()
  817. // Assert v5 cleanup still happened (http.* removed)
  818. gitConfigContent = (
  819. await fs.promises.readFile(localGitConfigPath)
  820. ).toString()
  821. expect(
  822. gitConfigContent.indexOf('http.https://github.com/.extraheader')
  823. ).toBeLessThan(0)
  824. // Assert v6 cleanup was skipped - includeIf should still be present
  825. expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
  826. expect(
  827. gitConfigContent.indexOf(credentialsFilePath)
  828. ).toBeGreaterThanOrEqual(0)
  829. // Assert credentials file still exists (wasn't deleted)
  830. await fs.promises.stat(credentialsFilePath) // File should still exist
  831. // Assert debug message was logged
  832. expect(core.debug).toHaveBeenCalledWith(
  833. 'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
  834. )
  835. // Cleanup
  836. delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
  837. })
  838. const removeGlobalConfig_removesOverride =
  839. 'removeGlobalConfig removes override'
  840. it(removeGlobalConfig_removesOverride, async () => {
  841. // Arrange
  842. await setup(removeGlobalConfig_removesOverride)
  843. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  844. await authHelper.configureAuth()
  845. await authHelper.configureGlobalAuth()
  846. const homeOverride = git.env['HOME'] // Sanity check
  847. expect(homeOverride).toBeTruthy()
  848. await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
  849. // Act
  850. await authHelper.removeGlobalConfig()
  851. // Assert
  852. expect(git.env['HOME']).toBeUndefined()
  853. try {
  854. await fs.promises.stat(homeOverride)
  855. throw new Error(`Should have been deleted '${homeOverride}'`)
  856. } catch (err) {
  857. if ((err as any)?.code !== 'ENOENT') {
  858. throw err
  859. }
  860. }
  861. })
  862. })
  863. async function setup(testName: string): Promise<void> {
  864. testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
  865. // Directories
  866. workspace = path.join(testWorkspace, testName, 'workspace')
  867. runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
  868. tempHomedir = path.join(testWorkspace, testName, 'home-dir')
  869. await fs.promises.mkdir(workspace, {recursive: true})
  870. await fs.promises.mkdir(runnerTemp, {recursive: true})
  871. await fs.promises.mkdir(tempHomedir, {recursive: true})
  872. process.env['RUNNER_TEMP'] = runnerTemp
  873. process.env['HOME'] = tempHomedir
  874. // Create git config
  875. globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
  876. await fs.promises.writeFile(globalGitConfigPath, '')
  877. localGitConfigPath = path.join(workspace, '.git', 'config')
  878. await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
  879. await fs.promises.writeFile(localGitConfigPath, '')
  880. git = {
  881. branchDelete: jest.fn(),
  882. branchExists: jest.fn(),
  883. branchList: jest.fn(),
  884. disableSparseCheckout: jest.fn(),
  885. sparseCheckout: jest.fn(),
  886. sparseCheckoutNonConeMode: jest.fn(),
  887. checkout: jest.fn(),
  888. checkoutDetach: jest.fn(),
  889. config: jest.fn(
  890. async (key: string, value: string, globalConfig?: boolean) => {
  891. const configPath = globalConfig
  892. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  893. : localGitConfigPath
  894. await fs.promises.appendFile(configPath, `\n${key} ${value}`)
  895. }
  896. ),
  897. configExists: jest.fn(
  898. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  899. const configPath = globalConfig
  900. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  901. : localGitConfigPath
  902. const content = await fs.promises.readFile(configPath)
  903. const lines = content
  904. .toString()
  905. .split('\n')
  906. .filter(x => x)
  907. return lines.some(x => x.startsWith(key))
  908. }
  909. ),
  910. env: {},
  911. fetch: jest.fn(),
  912. getDefaultBranch: jest.fn(),
  913. getWorkingDirectory: jest.fn(() => workspace),
  914. init: jest.fn(),
  915. isDetached: jest.fn(),
  916. lfsFetch: jest.fn(),
  917. lfsInstall: jest.fn(),
  918. log1: jest.fn(),
  919. remoteAdd: jest.fn(),
  920. removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
  921. revParse: jest.fn(),
  922. setEnvironmentVariable: jest.fn((name: string, value: string) => {
  923. git.env[name] = value
  924. }),
  925. shaExists: jest.fn(),
  926. submoduleForeach: jest.fn(async () => {
  927. return ''
  928. }),
  929. submoduleSync: jest.fn(),
  930. submoduleStatus: jest.fn(async () => {
  931. return true
  932. }),
  933. submoduleUpdate: jest.fn(),
  934. tagExists: jest.fn(),
  935. tryClean: jest.fn(),
  936. tryConfigUnset: jest.fn(
  937. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  938. const configPath = globalConfig
  939. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  940. : localGitConfigPath
  941. let content = await fs.promises.readFile(configPath)
  942. let lines = content
  943. .toString()
  944. .split('\n')
  945. .filter(x => x)
  946. .filter(x => !x.startsWith(key))
  947. await fs.promises.writeFile(configPath, lines.join('\n'))
  948. return true
  949. }
  950. ),
  951. tryDisableAutomaticGarbageCollection: jest.fn(),
  952. tryGetFetchUrl: jest.fn(),
  953. getSubmoduleConfigPaths: jest.fn(async () => {
  954. return []
  955. }),
  956. tryConfigUnsetValue: jest.fn(async () => {
  957. return true
  958. }),
  959. tryGetConfigValues: jest.fn(async () => {
  960. return []
  961. }),
  962. tryGetConfigKeys: jest.fn(async () => {
  963. return []
  964. }),
  965. tryReset: jest.fn(),
  966. version: jest.fn()
  967. }
  968. settings = {
  969. authToken: 'some auth token',
  970. clean: true,
  971. commit: '',
  972. filter: undefined,
  973. sparseCheckout: [],
  974. sparseCheckoutConeMode: true,
  975. fetchDepth: 1,
  976. fetchTags: false,
  977. showProgress: true,
  978. lfs: false,
  979. submodules: false,
  980. nestedSubmodules: false,
  981. persistCredentials: true,
  982. ref: 'refs/heads/main',
  983. repositoryName: 'my-repo',
  984. repositoryOwner: 'my-org',
  985. repositoryPath: '',
  986. sshKey: sshPath ? 'some ssh private key' : '',
  987. sshKnownHosts: '',
  988. sshStrict: true,
  989. sshUser: '',
  990. workflowOrganizationId: 123456,
  991. setSafeDirectory: true,
  992. githubServerUrl: githubServerUrl,
  993. allowUnsafePrCheckout: false
  994. }
  995. }
  996. async function getActualSshKeyPath(): Promise<string> {
  997. let actualTempFiles = (await fs.promises.readdir(runnerTemp))
  998. .sort()
  999. .map(x => path.join(runnerTemp, x))
  1000. if (actualTempFiles.length === 0) {
  1001. return ''
  1002. }
  1003. expect(actualTempFiles).toHaveLength(2)
  1004. expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy()
  1005. return actualTempFiles[0]
  1006. }
  1007. async function getActualSshKnownHostsPath(): Promise<string> {
  1008. let actualTempFiles = (await fs.promises.readdir(runnerTemp))
  1009. .sort()
  1010. .map(x => path.join(runnerTemp, x))
  1011. if (actualTempFiles.length === 0) {
  1012. return ''
  1013. }
  1014. expect(actualTempFiles).toHaveLength(2)
  1015. expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy()
  1016. expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy()
  1017. return actualTempFiles[1]
  1018. }