git-auth-helper.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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 path from 'path'
  6. import {IGitCommandManager} from '../lib/git-command-manager'
  7. import {IGitSourceSettings} from '../lib/git-source-settings'
  8. const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
  9. const originalRunnerTemp = process.env['RUNNER_TEMP']
  10. const originalHome = process.env['HOME']
  11. let workspace: string
  12. let localGitConfigPath: string
  13. let globalGitConfigPath: string
  14. let runnerTemp: string
  15. let tempHomedir: string
  16. let git: IGitCommandManager & {env: {[key: string]: string}}
  17. let settings: IGitSourceSettings
  18. describe('git-auth-helper tests', () => {
  19. beforeAll(async () => {
  20. // Clear test workspace
  21. await io.rmRF(testWorkspace)
  22. })
  23. beforeEach(() => {
  24. // Mock setSecret
  25. jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
  26. // Mock error/warning/info/debug
  27. jest.spyOn(core, 'error').mockImplementation(jest.fn())
  28. jest.spyOn(core, 'warning').mockImplementation(jest.fn())
  29. jest.spyOn(core, 'info').mockImplementation(jest.fn())
  30. jest.spyOn(core, 'debug').mockImplementation(jest.fn())
  31. })
  32. afterEach(() => {
  33. // Unregister mocks
  34. jest.restoreAllMocks()
  35. // Restore HOME
  36. if (originalHome) {
  37. process.env['HOME'] = originalHome
  38. } else {
  39. delete process.env['HOME']
  40. }
  41. })
  42. afterAll(() => {
  43. // Restore RUNNER_TEMP
  44. delete process.env['RUNNER_TEMP']
  45. if (originalRunnerTemp) {
  46. process.env['RUNNER_TEMP'] = originalRunnerTemp
  47. }
  48. })
  49. const configureAuth_configuresAuthHeader =
  50. 'configureAuth configures auth header'
  51. it(configureAuth_configuresAuthHeader, async () => {
  52. // Arrange
  53. await setup(configureAuth_configuresAuthHeader)
  54. expect(settings.authToken).toBeTruthy() // sanity check
  55. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  56. // Act
  57. await authHelper.configureAuth()
  58. // Assert config
  59. const configContent = (
  60. await fs.promises.readFile(localGitConfigPath)
  61. ).toString()
  62. const basicCredential = Buffer.from(
  63. `x-access-token:${settings.authToken}`,
  64. 'utf8'
  65. ).toString('base64')
  66. expect(
  67. configContent.indexOf(
  68. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  69. )
  70. ).toBeGreaterThanOrEqual(0)
  71. })
  72. const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
  73. 'configureAuth configures auth header even when persist credentials false'
  74. it(
  75. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
  76. async () => {
  77. // Arrange
  78. await setup(
  79. configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
  80. )
  81. expect(settings.authToken).toBeTruthy() // sanity check
  82. settings.persistCredentials = false
  83. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  84. // Act
  85. await authHelper.configureAuth()
  86. // Assert config
  87. const configContent = (
  88. await fs.promises.readFile(localGitConfigPath)
  89. ).toString()
  90. expect(
  91. configContent.indexOf(
  92. `http.https://github.com/.extraheader AUTHORIZATION`
  93. )
  94. ).toBeGreaterThanOrEqual(0)
  95. }
  96. )
  97. const configureAuth_registersBasicCredentialAsSecret =
  98. 'configureAuth registers basic credential as secret'
  99. it(configureAuth_registersBasicCredentialAsSecret, async () => {
  100. // Arrange
  101. await setup(configureAuth_registersBasicCredentialAsSecret)
  102. expect(settings.authToken).toBeTruthy() // sanity check
  103. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  104. // Act
  105. await authHelper.configureAuth()
  106. // Assert secret
  107. const setSecretSpy = core.setSecret as jest.Mock<any, any>
  108. expect(setSecretSpy).toHaveBeenCalledTimes(1)
  109. const expectedSecret = Buffer.from(
  110. `x-access-token:${settings.authToken}`,
  111. 'utf8'
  112. ).toString('base64')
  113. expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
  114. })
  115. const configureGlobalAuth_copiesGlobalGitConfig =
  116. 'configureGlobalAuth copies global git config'
  117. it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
  118. // Arrange
  119. await setup(configureGlobalAuth_copiesGlobalGitConfig)
  120. await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
  121. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  122. // Act
  123. await authHelper.configureAuth()
  124. await authHelper.configureGlobalAuth()
  125. // Assert original global config not altered
  126. let configContent = (
  127. await fs.promises.readFile(globalGitConfigPath)
  128. ).toString()
  129. expect(configContent).toBe('value-from-global-config')
  130. // Assert temporary global config
  131. expect(git.env['HOME']).toBeTruthy()
  132. const basicCredential = Buffer.from(
  133. `x-access-token:${settings.authToken}`,
  134. 'utf8'
  135. ).toString('base64')
  136. configContent = (
  137. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  138. ).toString()
  139. expect(
  140. configContent.indexOf('value-from-global-config')
  141. ).toBeGreaterThanOrEqual(0)
  142. expect(
  143. configContent.indexOf(
  144. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  145. )
  146. ).toBeGreaterThanOrEqual(0)
  147. })
  148. const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
  149. 'configureGlobalAuth creates new git config when global does not exist'
  150. it(
  151. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
  152. async () => {
  153. // Arrange
  154. await setup(
  155. configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
  156. )
  157. await io.rmRF(globalGitConfigPath)
  158. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  159. // Act
  160. await authHelper.configureAuth()
  161. await authHelper.configureGlobalAuth()
  162. // Assert original global config not recreated
  163. try {
  164. await fs.promises.stat(globalGitConfigPath)
  165. throw new Error(
  166. `Did not expect file to exist: '${globalGitConfigPath}'`
  167. )
  168. } catch (err) {
  169. if (err.code !== 'ENOENT') {
  170. throw err
  171. }
  172. }
  173. // Assert temporary global config
  174. expect(git.env['HOME']).toBeTruthy()
  175. const basicCredential = Buffer.from(
  176. `x-access-token:${settings.authToken}`,
  177. 'utf8'
  178. ).toString('base64')
  179. const configContent = (
  180. await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
  181. ).toString()
  182. expect(
  183. configContent.indexOf(
  184. `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
  185. )
  186. ).toBeGreaterThanOrEqual(0)
  187. }
  188. )
  189. const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse =
  190. 'configureSubmoduleAuth does not configure token when persist credentials false'
  191. it(
  192. configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse,
  193. async () => {
  194. // Arrange
  195. await setup(
  196. configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse
  197. )
  198. settings.persistCredentials = false
  199. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  200. await authHelper.configureAuth()
  201. ;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
  202. // Act
  203. await authHelper.configureSubmoduleAuth()
  204. // Assert
  205. expect(git.submoduleForeach).not.toHaveBeenCalled()
  206. }
  207. )
  208. const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue =
  209. 'configureSubmoduleAuth configures token when persist credentials true'
  210. it(
  211. configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue,
  212. async () => {
  213. // Arrange
  214. await setup(
  215. configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue
  216. )
  217. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  218. await authHelper.configureAuth()
  219. ;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
  220. // Act
  221. await authHelper.configureSubmoduleAuth()
  222. // Assert
  223. expect(git.submoduleForeach).toHaveBeenCalledTimes(1)
  224. }
  225. )
  226. const removeAuth_removesToken = 'removeAuth removes token'
  227. it(removeAuth_removesToken, async () => {
  228. // Arrange
  229. await setup(removeAuth_removesToken)
  230. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  231. await authHelper.configureAuth()
  232. let gitConfigContent = (
  233. await fs.promises.readFile(localGitConfigPath)
  234. ).toString()
  235. expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
  236. // Act
  237. await authHelper.removeAuth()
  238. // Assert git config
  239. gitConfigContent = (
  240. await fs.promises.readFile(localGitConfigPath)
  241. ).toString()
  242. expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
  243. })
  244. const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
  245. it(removeGlobalAuth_removesOverride, async () => {
  246. // Arrange
  247. await setup(removeGlobalAuth_removesOverride)
  248. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  249. await authHelper.configureAuth()
  250. await authHelper.configureGlobalAuth()
  251. const homeOverride = git.env['HOME'] // Sanity check
  252. expect(homeOverride).toBeTruthy()
  253. await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
  254. // Act
  255. await authHelper.removeGlobalAuth()
  256. // Assert
  257. expect(git.env['HOME']).toBeUndefined()
  258. try {
  259. await fs.promises.stat(homeOverride)
  260. throw new Error(`Should have been deleted '${homeOverride}'`)
  261. } catch (err) {
  262. if (err.code !== 'ENOENT') {
  263. throw err
  264. }
  265. }
  266. })
  267. })
  268. async function setup(testName: string): Promise<void> {
  269. testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
  270. // Directories
  271. workspace = path.join(testWorkspace, testName, 'workspace')
  272. runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
  273. tempHomedir = path.join(testWorkspace, testName, 'home-dir')
  274. await fs.promises.mkdir(workspace, {recursive: true})
  275. await fs.promises.mkdir(runnerTemp, {recursive: true})
  276. await fs.promises.mkdir(tempHomedir, {recursive: true})
  277. process.env['RUNNER_TEMP'] = runnerTemp
  278. process.env['HOME'] = tempHomedir
  279. // Create git config
  280. globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
  281. await fs.promises.writeFile(globalGitConfigPath, '')
  282. localGitConfigPath = path.join(workspace, '.git', 'config')
  283. await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
  284. await fs.promises.writeFile(localGitConfigPath, '')
  285. git = {
  286. branchDelete: jest.fn(),
  287. branchExists: jest.fn(),
  288. branchList: jest.fn(),
  289. checkout: jest.fn(),
  290. checkoutDetach: jest.fn(),
  291. config: jest.fn(
  292. async (key: string, value: string, globalConfig?: boolean) => {
  293. const configPath = globalConfig
  294. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  295. : localGitConfigPath
  296. await fs.promises.appendFile(configPath, `\n${key} ${value}`)
  297. }
  298. ),
  299. configExists: jest.fn(
  300. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  301. const configPath = globalConfig
  302. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  303. : localGitConfigPath
  304. const content = await fs.promises.readFile(configPath)
  305. const lines = content
  306. .toString()
  307. .split('\n')
  308. .filter(x => x)
  309. return lines.some(x => x.startsWith(key))
  310. }
  311. ),
  312. env: {},
  313. fetch: jest.fn(),
  314. getWorkingDirectory: jest.fn(() => workspace),
  315. init: jest.fn(),
  316. isDetached: jest.fn(),
  317. lfsFetch: jest.fn(),
  318. lfsInstall: jest.fn(),
  319. log1: jest.fn(),
  320. remoteAdd: jest.fn(),
  321. removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
  322. setEnvironmentVariable: jest.fn((name: string, value: string) => {
  323. git.env[name] = value
  324. }),
  325. submoduleForeach: jest.fn(async () => {
  326. return ''
  327. }),
  328. submoduleSync: jest.fn(),
  329. submoduleUpdate: jest.fn(),
  330. tagExists: jest.fn(),
  331. tryClean: jest.fn(),
  332. tryConfigUnset: jest.fn(
  333. async (key: string, globalConfig?: boolean): Promise<boolean> => {
  334. const configPath = globalConfig
  335. ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
  336. : localGitConfigPath
  337. let content = await fs.promises.readFile(configPath)
  338. let lines = content
  339. .toString()
  340. .split('\n')
  341. .filter(x => x)
  342. .filter(x => !x.startsWith(key))
  343. await fs.promises.writeFile(configPath, lines.join('\n'))
  344. return true
  345. }
  346. ),
  347. tryDisableAutomaticGarbageCollection: jest.fn(),
  348. tryGetFetchUrl: jest.fn(),
  349. tryReset: jest.fn()
  350. }
  351. settings = {
  352. authToken: 'some auth token',
  353. clean: true,
  354. commit: '',
  355. fetchDepth: 1,
  356. lfs: false,
  357. submodules: false,
  358. nestedSubmodules: false,
  359. persistCredentials: true,
  360. ref: 'refs/heads/master',
  361. repositoryName: 'my-repo',
  362. repositoryOwner: 'my-org',
  363. repositoryPath: ''
  364. }
  365. }