Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/__test__/git-auth-helper.test.ts
diff options
context:
space:
mode:
authoreric sciple <ericsciple@users.noreply.github.com>2025-11-03 13:08:38 -0600
committerGitHub <noreply@github.com>2025-11-03 13:08:38 -0600
commitff20ae9cd9d5fc6ce99d108d89ea013d4c12f489 (patch)
treed75c06690a2192c6fdeacc5da6222a0683ab81d6 /__test__/git-auth-helper.test.ts
parent33f3caae0058b205c722b2415877b66f0b33d1d9 (diff)
Persist creds to a separate file (#2286)
Diffstat (limited to '__test__/git-auth-helper.test.ts')
-rw-r--r--__test__/git-auth-helper.test.ts409
1 files changed, 380 insertions, 29 deletions
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index 7633704..ad3566a 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
- // Assert config
- const configContent = (
+ // Assert config - check that .git/config contains includeIf entries
+ const localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Assert credentials config file contains the actual credentials
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader(
- configureAuth_AcceptsGitHubServerUrl,
+ configureAuth_AcceptsGitHubServerUrlSetToGHEC,
'https://github.com'
)
})
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
- // Assert config
- const configContent = (
- await fs.promises.readFile(localGitConfigPath)
+ // Assert config - check credentials config file (not local .git/config)
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION`
)
).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
expectedSshCommand
)
- // Asserty git config
+ // Assert git config
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
.toString()
.split('\n')
.filter(x => x)
- expect(gitConfigLines).toHaveLength(1)
- expect(gitConfigLines[0]).toMatch(/^http\./)
+ // Should have includeIf entries pointing to credentials file
+ expect(gitConfigLines.length).toBeGreaterThan(0)
+ expect(
+ gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
+ ).toBeTruthy()
})
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
+ // Global config should have include.path pointing to credentials file
+ expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+ // Check credentials in the separate config file
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBeGreaterThan(0)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
+ // Global config should have include.path pointing to credentials file
+ expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+ // Check credentials in the separate config file
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBeGreaterThan(0)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
- expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
+ // Should configure insteadOf (2 calls for two values)
+ expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
- expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
- expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
+ expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
/url.*insteadOf.*git@github.com:/
)
- expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
+ expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*org-123456@github.com:/
)
}
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
- expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
+ // Should configure sshCommand (1 call)
+ expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
- expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
- expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
+ expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
}
)
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
- let gitConfigContent = (
+
+ // Verify includeIf entries exist in local config
+ let localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
- expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify both host and container includeIf entries are present
+ const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+ expect(
+ localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify credentials file exists
+ let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+ // Verify credentials file contains the auth token
+ let credentialsContent = (
+ await fs.promises.readFile(credentialsFilePath)
+ ).toString()
+ const basicCredential = Buffer.from(
+ `x-access-token:${settings.authToken}`,
+ 'utf8'
+ ).toString('base64')
+ expect(
+ credentialsContent.indexOf(
+ `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
+ )
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify the includeIf entries point to the credentials file
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsFilePath)
+ )
+ expect(
+ localConfigContent.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ localConfigContent.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
// Act
await authHelper.removeAuth()
- // Assert git config
- gitConfigContent = (
+ // Assert all includeIf entries removed from local git config
+ localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
- expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
+ expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(
+ localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+ ).toBeLessThan(0)
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+ ).toBeLessThan(0)
+ expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert credentials config file deleted
+ credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(0)
+
+ // Verify credentials file no longer exists on disk
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
+ })
+
+ const removeAuth_removesTokenFromSubmodules =
+ 'removeAuth removes token from submodules'
+ it(removeAuth_removesTokenFromSubmodules, async () => {
+ // Arrange
+ await setup(removeAuth_removesTokenFromSubmodules)
+
+ // Create fake submodule config paths
+ const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+ const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
+ const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+ const submodule2ConfigPath = path.join(submodule2Dir, 'config')
+
+ await fs.promises.mkdir(submodule1Dir, {recursive: true})
+ await fs.promises.mkdir(submodule2Dir, {recursive: true})
+ await fs.promises.writeFile(submodule1ConfigPath, '')
+ await fs.promises.writeFile(submodule2ConfigPath, '')
+
+ // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
+ const mockGetSubmoduleConfigPaths =
+ git.getSubmoduleConfigPaths as jest.Mock<any, any>
+ mockGetSubmoduleConfigPaths.mockResolvedValue([
+ submodule1ConfigPath,
+ submodule2ConfigPath
+ ])
+
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ await authHelper.configureAuth()
+ await authHelper.configureSubmoduleAuth()
+
+ // Verify credentials file exists
+ let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+ // Verify submodule 1 config has includeIf entries
+ let submodule1Content = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+ expect(
+ submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule1Content.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify submodule 2 config has includeIf entries
+ let submodule2Content = (
+ await fs.promises.readFile(submodule2ConfigPath)
+ ).toString()
+ const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
+ expect(
+ submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule2Content.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify both host and container paths are in each submodule config
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsFilePath)
+ )
+ expect(
+ submodule1Content.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule2Content.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Act - ensure mock persists for removeAuth
+ mockGetSubmoduleConfigPaths.mockResolvedValue([
+ submodule1ConfigPath,
+ submodule2ConfigPath
+ ])
+ await authHelper.removeAuth()
+
+ // Assert submodule 1 includeIf entries removed
+ submodule1Content = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert submodule 2 includeIf entries removed
+ submodule2Content = (
+ await fs.promises.readFile(submodule2ConfigPath)
+ ).toString()
+ expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert credentials config file deleted
+ credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(0)
+
+ // Verify credentials file no longer exists on disk
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
})
const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
}
}
})
+
+ const testCredentialsConfigPath_matchesCredentialsConfigPaths =
+ 'testCredentialsConfigPath matches credentials config paths'
+ it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
+ // Arrange
+ await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+
+ // Get a real credentials config path
+ const credentialsConfigPath = await (
+ authHelper as any
+ ).getCredentialsConfigPath()
+
+ // Act & Assert
+ expect(
+ (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
+ ).toBe(true)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
+ )
+ ).toBe(true)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+ )
+ ).toBe(true)
+
+ // Test invalid paths
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/other-config.config'
+ )
+ ).toBe(false)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-invalid.config'
+ )
+ ).toBe(false)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-.config'
+ )
+ ).toBe(false)
+ expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
+ })
})
async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir
+ process.env['GITHUB_WORKSPACE'] = workspace
// Create git config
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
- async (key: string, value: string, globalConfig?: boolean) => {
- const configPath = globalConfig
- ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
- : localGitConfigPath
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ add?: boolean,
+ configFile?: string
+ ) => {
+ const configPath =
+ configFile ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ // Ensure directory exists
+ await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
}
),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
env: {},
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
+ getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(),
isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
return true
}
),
+ tryConfigUnsetValue: jest.fn(
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<boolean> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ let content = await fs.promises.readFile(targetConfigPath)
+ let lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x)
+ .filter(x => !(x.startsWith(key) && x.includes(value)))
+ await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
+ return true
+ }
+ ),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
+ tryGetConfigValues: jest.fn(
+ async (
+ key: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<string[]> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ const content = await fs.promises.readFile(targetConfigPath)
+ const lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x && x.startsWith(key))
+ .map(x => x.substring(key.length).trim())
+ return lines
+ }
+ ),
+ tryGetConfigKeys: jest.fn(
+ async (
+ pattern: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<string[]> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ const content = await fs.promises.readFile(targetConfigPath)
+ const lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x)
+ const keys = lines
+ .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
+ .map(x => x.split(' ')[0])
+ return [...new Set(keys)] // Remove duplicates
+ }
+ ),
tryReset: jest.fn(),
version: jest.fn()
}
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
async function getActualSshKeyPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+ .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
async function getActualSshKnownHostsPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+ .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {