2
0

ref-helper.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import * as assert from 'assert'
  2. import * as core from '@actions/core'
  3. import * as github from '@actions/github'
  4. import * as refHelper from '../lib/ref-helper'
  5. import {IGitCommandManager} from '../lib/git-command-manager'
  6. const commit = '1234567890123456789012345678901234567890'
  7. const sha256Commit =
  8. '1234567890123456789012345678901234567890123456789012345678901234'
  9. let git: IGitCommandManager
  10. describe('ref-helper tests', () => {
  11. beforeEach(() => {
  12. git = {} as unknown as IGitCommandManager
  13. })
  14. it('getCheckoutInfo requires git', async () => {
  15. const git = null as unknown as IGitCommandManager
  16. try {
  17. await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit)
  18. throw new Error('Should not reach here')
  19. } catch (err) {
  20. expect((err as any)?.message).toBe('Arg git cannot be empty')
  21. }
  22. })
  23. it('getCheckoutInfo requires ref or commit', async () => {
  24. try {
  25. await refHelper.getCheckoutInfo(git, '', '')
  26. throw new Error('Should not reach here')
  27. } catch (err) {
  28. expect((err as any)?.message).toBe(
  29. 'Args ref and commit cannot both be empty'
  30. )
  31. }
  32. })
  33. it('getCheckoutInfo sha only', async () => {
  34. const checkoutInfo = await refHelper.getCheckoutInfo(git, '', commit)
  35. expect(checkoutInfo.ref).toBe(commit)
  36. expect(checkoutInfo.startPoint).toBeFalsy()
  37. })
  38. it('getCheckoutInfo sha-256 only', async () => {
  39. const checkoutInfo = await refHelper.getCheckoutInfo(git, '', sha256Commit)
  40. expect(checkoutInfo.ref).toBe(sha256Commit)
  41. expect(checkoutInfo.startPoint).toBeFalsy()
  42. })
  43. it('getCheckoutInfo refs/heads/', async () => {
  44. const checkoutInfo = await refHelper.getCheckoutInfo(
  45. git,
  46. 'refs/heads/my/branch',
  47. commit
  48. )
  49. expect(checkoutInfo.ref).toBe('my/branch')
  50. expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
  51. })
  52. it('getCheckoutInfo refs/pull/', async () => {
  53. const checkoutInfo = await refHelper.getCheckoutInfo(
  54. git,
  55. 'refs/pull/123/merge',
  56. commit
  57. )
  58. expect(checkoutInfo.ref).toBe('refs/remotes/pull/123/merge')
  59. expect(checkoutInfo.startPoint).toBeFalsy()
  60. })
  61. it('getCheckoutInfo refs/tags/', async () => {
  62. const checkoutInfo = await refHelper.getCheckoutInfo(
  63. git,
  64. 'refs/tags/my-tag',
  65. commit
  66. )
  67. expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
  68. expect(checkoutInfo.startPoint).toBeFalsy()
  69. })
  70. it('getCheckoutInfo refs/', async () => {
  71. const checkoutInfo = await refHelper.getCheckoutInfo(
  72. git,
  73. 'refs/gh/queue/main/pr-123',
  74. commit
  75. )
  76. expect(checkoutInfo.ref).toBe(commit)
  77. expect(checkoutInfo.startPoint).toBeFalsy()
  78. })
  79. it('getCheckoutInfo refs/ without commit', async () => {
  80. const checkoutInfo = await refHelper.getCheckoutInfo(
  81. git,
  82. 'refs/non-standard-ref',
  83. ''
  84. )
  85. expect(checkoutInfo.ref).toBe('refs/non-standard-ref')
  86. expect(checkoutInfo.startPoint).toBeFalsy()
  87. })
  88. it('getCheckoutInfo unqualified branch only', async () => {
  89. git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
  90. return true
  91. })
  92. const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my/branch', '')
  93. expect(checkoutInfo.ref).toBe('my/branch')
  94. expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
  95. })
  96. it('getCheckoutInfo unqualified tag only', async () => {
  97. git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
  98. return false
  99. })
  100. git.tagExists = jest.fn(async (pattern: string) => {
  101. return true
  102. })
  103. const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my-tag', '')
  104. expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
  105. expect(checkoutInfo.startPoint).toBeFalsy()
  106. })
  107. it('getCheckoutInfo unqualified ref only, not a branch or tag', async () => {
  108. git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
  109. return false
  110. })
  111. git.tagExists = jest.fn(async (pattern: string) => {
  112. return false
  113. })
  114. try {
  115. await refHelper.getCheckoutInfo(git, 'my-ref', '')
  116. throw new Error('Should not reach here')
  117. } catch (err) {
  118. expect((err as any)?.message).toBe(
  119. "A branch or tag with the name 'my-ref' could not be found"
  120. )
  121. }
  122. })
  123. it('getRefSpec requires ref or commit', async () => {
  124. assert.throws(
  125. () => refHelper.getRefSpec('', ''),
  126. /Args ref and commit cannot both be empty/
  127. )
  128. })
  129. it('getRefSpec sha + refs/heads/', async () => {
  130. const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit)
  131. expect(refSpec.length).toBe(1)
  132. expect(refSpec[0]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
  133. })
  134. it('getRefSpec sha + refs/pull/', async () => {
  135. const refSpec = refHelper.getRefSpec('refs/pull/123/merge', commit)
  136. expect(refSpec.length).toBe(1)
  137. expect(refSpec[0]).toBe(`+${commit}:refs/remotes/pull/123/merge`)
  138. })
  139. it('getRefSpec sha + refs/tags/', async () => {
  140. const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
  141. expect(refSpec.length).toBe(1)
  142. expect(refSpec[0]).toBe(`+refs/tags/my-tag:refs/tags/my-tag`)
  143. })
  144. it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
  145. // When fetchTags is true, only include tags wildcard (specific tag is redundant)
  146. const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit, true)
  147. expect(refSpec.length).toBe(1)
  148. expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
  149. })
  150. it('getRefSpec sha + refs/heads/ with fetchTags', async () => {
  151. // When fetchTags is true, include both the branch refspec and tags wildcard
  152. const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit, true)
  153. expect(refSpec.length).toBe(2)
  154. expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
  155. expect(refSpec[1]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
  156. })
  157. it('getRefSpec sha only', async () => {
  158. const refSpec = refHelper.getRefSpec('', commit)
  159. expect(refSpec.length).toBe(1)
  160. expect(refSpec[0]).toBe(commit)
  161. })
  162. it('getRefSpec unqualified ref only', async () => {
  163. const refSpec = refHelper.getRefSpec('my-ref', '')
  164. expect(refSpec.length).toBe(2)
  165. expect(refSpec[0]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
  166. expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
  167. })
  168. it('getRefSpec unqualified ref only with fetchTags', async () => {
  169. // When fetchTags is true, skip specific tag pattern since wildcard covers all
  170. const refSpec = refHelper.getRefSpec('my-ref', '', true)
  171. expect(refSpec.length).toBe(2)
  172. expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
  173. expect(refSpec[1]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
  174. })
  175. it('getRefSpec refs/heads/ only', async () => {
  176. const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
  177. expect(refSpec.length).toBe(1)
  178. expect(refSpec[0]).toBe(
  179. '+refs/heads/my/branch:refs/remotes/origin/my/branch'
  180. )
  181. })
  182. it('getRefSpec refs/pull/ only', async () => {
  183. const refSpec = refHelper.getRefSpec('refs/pull/123/merge', '')
  184. expect(refSpec.length).toBe(1)
  185. expect(refSpec[0]).toBe('+refs/pull/123/merge:refs/remotes/pull/123/merge')
  186. })
  187. it('getRefSpec refs/tags/ only', async () => {
  188. const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '')
  189. expect(refSpec.length).toBe(1)
  190. expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
  191. })
  192. it('getRefSpec refs/tags/ only with fetchTags', async () => {
  193. // When fetchTags is true, only include tags wildcard (specific tag is redundant)
  194. const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '', true)
  195. expect(refSpec.length).toBe(1)
  196. expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
  197. })
  198. it('getRefSpec refs/heads/ only with fetchTags', async () => {
  199. // When fetchTags is true, include both the branch refspec and tags wildcard
  200. const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '', true)
  201. expect(refSpec.length).toBe(2)
  202. expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
  203. expect(refSpec[1]).toBe(
  204. '+refs/heads/my/branch:refs/remotes/origin/my/branch'
  205. )
  206. })
  207. describe('checkCommitInfo', () => {
  208. const repositoryOwner = 'some-owner'
  209. const repositoryName = 'some-repo'
  210. const ref = 'refs/pull/123/merge'
  211. const sha1Head = '1111111111222222222233333333334444444444'
  212. const sha1Base = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd'
  213. const sha256Head =
  214. '1111111111222222222233333333334444444444555555555566666666667777'
  215. const sha256Base =
  216. 'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff0000'
  217. let debugSpy: jest.SpyInstance
  218. let getOctokitSpy: jest.SpyInstance
  219. let repoGetSpy: jest.Mock
  220. let originalEventName: string
  221. let originalPayload: unknown
  222. let originalRef: string
  223. let originalSha: string
  224. function setPullRequestContext(
  225. expectedHeadSha: string,
  226. expectedBaseSha: string,
  227. mergeCommit: string
  228. ): void {
  229. ;(github.context as any).eventName = 'pull_request'
  230. github.context.ref = ref
  231. github.context.sha = mergeCommit
  232. ;(github.context as any).payload = {
  233. action: 'synchronize',
  234. after: expectedHeadSha,
  235. number: 123,
  236. pull_request: {
  237. base: {
  238. sha: expectedBaseSha
  239. }
  240. },
  241. repository: {
  242. private: false
  243. }
  244. }
  245. }
  246. beforeEach(() => {
  247. originalEventName = github.context.eventName
  248. originalPayload = github.context.payload
  249. originalRef = github.context.ref
  250. originalSha = github.context.sha
  251. jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
  252. owner: repositoryOwner,
  253. repo: repositoryName
  254. })
  255. debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
  256. repoGetSpy = jest.fn(async () => ({}))
  257. getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
  258. rest: {
  259. repos: {
  260. get: repoGetSpy
  261. }
  262. }
  263. } as any)
  264. })
  265. afterEach(() => {
  266. ;(github.context as any).eventName = originalEventName
  267. ;(github.context as any).payload = originalPayload
  268. github.context.ref = originalRef
  269. github.context.sha = originalSha
  270. jest.restoreAllMocks()
  271. })
  272. it('returns early for SHA-1 merge commit', async () => {
  273. setPullRequestContext(sha1Head, sha1Base, commit)
  274. await refHelper.checkCommitInfo(
  275. 'token',
  276. `Merge ${sha1Head} into ${sha1Base}`,
  277. repositoryOwner,
  278. repositoryName,
  279. ref,
  280. commit
  281. )
  282. expect(getOctokitSpy).not.toHaveBeenCalled()
  283. expect(repoGetSpy).not.toHaveBeenCalled()
  284. })
  285. it('matches SHA-256 merge commit info', async () => {
  286. const actualHeadSha =
  287. '9999999999888888888877777777776666666666555555555544444444443333'
  288. setPullRequestContext(sha256Head, sha256Base, sha256Commit)
  289. await refHelper.checkCommitInfo(
  290. 'token',
  291. `Merge ${actualHeadSha} into ${sha256Base}`,
  292. repositoryOwner,
  293. repositoryName,
  294. ref,
  295. sha256Commit
  296. )
  297. expect(getOctokitSpy).toHaveBeenCalledWith(
  298. 'token',
  299. expect.objectContaining({
  300. userAgent: expect.stringContaining(
  301. `expected_head_sha=${sha256Head};actual_head_sha=${actualHeadSha}`
  302. )
  303. })
  304. )
  305. expect(repoGetSpy).toHaveBeenCalledWith({
  306. owner: repositoryOwner,
  307. repo: repositoryName
  308. })
  309. expect(debugSpy).toHaveBeenCalledWith(
  310. `Expected head sha ${sha256Head}; actual head sha ${actualHeadSha}`
  311. )
  312. expect(debugSpy).not.toHaveBeenCalledWith('Unexpected message format')
  313. })
  314. it('does not match 50-char hex as a valid merge', async () => {
  315. const invalidHeadSha =
  316. '99999999998888888888777777777766666666665555555555'
  317. setPullRequestContext(sha1Head, sha1Base, commit)
  318. await refHelper.checkCommitInfo(
  319. 'token',
  320. `Merge ${invalidHeadSha} into ${sha1Base}`,
  321. repositoryOwner,
  322. repositoryName,
  323. ref,
  324. commit
  325. )
  326. expect(getOctokitSpy).not.toHaveBeenCalled()
  327. expect(repoGetSpy).not.toHaveBeenCalled()
  328. expect(debugSpy).toHaveBeenCalledWith('Unexpected message format')
  329. })
  330. })
  331. })