ref-helper.test.ts 12 KB

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