Files
orca/src/main/github/github-enterprise-repository.test.ts
T
Neil 19d082a164 fix(github): resolve owner/repo through SSH Host aliases (#10284) (#10361)
* fix(github): resolve owner/repo through SSH Host aliases (#10284)

Expand OpenSSH Host → HostName via ssh -G before classifying github.com
identity so PR merge works when origin is git@alias:owner/repo.git.
Transport URLs stay unchanged so IdentityFile selection is preserved.
Do not long-negative-cache indeterminate ssh -G failures.

* fix(github): harden SSH alias resolution
2026-07-25 23:34:17 -07:00

483 lines
16 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { commandExecFileAsyncMock, ghExecFileAsyncMock, gitExecFileAsyncMock, resolveWithSshGMock } =
vi.hoisted(() => ({
commandExecFileAsyncMock: vi.fn(),
ghExecFileAsyncMock: vi.fn(),
gitExecFileAsyncMock: vi.fn(),
resolveWithSshGMock: vi.fn()
}))
// Mock only the exec boundary so the real remote-identity parsing, runtime
// option resolution, and `gh auth status` parsing run against controlled output.
vi.mock('../git/runner', () => ({
commandExecFileAsync: commandExecFileAsyncMock,
ghExecFileAsync: ghExecFileAsyncMock,
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('../ssh/ssh-g-config-resolution', () => ({
resolveWithSshG: resolveWithSshGMock
}))
import {
_resetGitHubHostAuthCache,
getEnterpriseGitHubRepoSlug,
isGitHubHostAuthenticated,
isGitHubHostAuthenticatedForGlobalCli
} from './github-enterprise-repository'
import { _resetSshHostnameResolutionCache } from './github-ssh-host-alias-resolution'
function mockOriginRemote(url: string): void {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'remote' && args[1] === 'get-url') {
return { stdout: `${url}\n`, stderr: '' }
}
return { stdout: '', stderr: '' }
})
}
function sshConfig(hostname: string, port = 22) {
return {
hostname,
port,
identityFile: [],
identitiesOnly: false,
forwardAgent: false,
proxyUseFdpass: false,
controlMaster: 'no',
controlPersist: 'no'
}
}
// gh auth status inventory entries represent hosts with configured credentials.
function mockHostAuthenticated(host = 'github.acme-corp.com'): void {
mockAuthenticatedHosts([host])
}
function mockAuthenticatedHosts(hosts: string[]): void {
ghExecFileAsyncMock.mockResolvedValue({
stdout: hosts
.map((host) => `${host}\n ✓ Logged in to ${host} account kelora (keyring)`)
.join('\n'),
stderr: ''
})
}
// gh exits non-zero and reports no matching host when not logged in.
function mockHostNotAuthenticated(): void {
ghExecFileAsyncMock.mockRejectedValue(
Object.assign(new Error('exit 1'), {
stdout: '',
stderr: 'You are not logged into any GitHub hosts. To log in, run: gh auth login'
})
)
}
describe('getEnterpriseGitHubRepoSlug', () => {
beforeEach(() => {
commandExecFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
gitExecFileAsyncMock.mockReset()
resolveWithSshGMock.mockReset()
resolveWithSshGMock.mockResolvedValue(null)
_resetGitHubHostAuthCache()
_resetSshHostnameResolutionCache()
})
it('resolves a GHES remote whose host the user is gh-authenticated to (#8312)', async () => {
mockOriginRemote('https://github.acme-corp.com/team/orca.git')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
// Why: inventory configured gh hosts without targeting the untrusted remote;
// passing it as --hostname could expose an ambient enterprise token.
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], { cwd: '/repo' })
})
it('resolves a GHES SCP-style SSH remote', async () => {
mockOriginRemote('git@github.acme-corp.com:team/orca.git')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
})
it('expands an SSH Host alias to the authenticated GHES HostName (#10284)', async () => {
mockOriginRemote('git@ghe-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('github.acme-corp.com'))
mockHostAuthenticated('github.acme-corp.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
expect(resolveWithSshGMock).toHaveBeenCalledWith('ghe-work')
})
it('keeps a failed GHES alias probe indeterminate and recovers on retry', async () => {
vi.useFakeTimers()
mockOriginRemote('git@ghe-work:team/orca.git')
resolveWithSshGMock
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(sshConfig('github.acme-corp.com'))
mockHostNotAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeUndefined()
await vi.advanceTimersByTimeAsync(5_001)
ghExecFileAsyncMock.mockReset()
mockHostAuthenticated('github.acme-corp.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
})
it('returns null for a Host alias that resolves to github.com (dotcom path owns it)', async () => {
mockOriginRemote('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('ssh.github.com', 443))
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('expands aliases in the repository WSL runtime', async () => {
mockOriginRemote('git@github-work:team/orca.git')
commandExecFileAsyncMock.mockResolvedValueOnce({
stdout: 'hostname github.com\nport 22\n',
stderr: ''
})
await expect(
getEnterpriseGitHubRepoSlug('/repo', null, {
localGitExecOptions: { wslDistro: 'Ubuntu' }
})
).resolves.toBeNull()
expect(commandExecFileAsyncMock).toHaveBeenCalledWith('ssh', ['-G', '--', 'github-work'], {
cwd: '/repo',
timeout: 5_000,
wslDistro: 'Ubuntu'
})
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('uses the unique ported auth host for a hostname-only SSH remote', async () => {
mockOriginRemote('git@ghe.acme.com:team/orca.git')
mockHostAuthenticated('ghe.acme.com:8443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
})
it.each([
[['ghe.acme.com', 'ghe.acme.com:9443', 'ghe.acme.com:8443']],
[['ghe.acme.com:8443', 'ghe.acme.com:9443', 'ghe.acme.com']]
])(
'requires exact host:port authentication regardless of auth inventory order: %j',
async (authenticatedHosts) => {
mockOriginRemote('https://ghe.acme.com:8443/team/orca.git')
mockAuthenticatedHosts(authenticatedHosts)
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
}
)
it.each([
'https://ghe.acme.com:8443/team/orca.git',
'https://ghe.acme.com:80/team/orca.git',
'http://ghe.acme.com:443/team/orca.git'
])('rejects portless authentication for a non-default web endpoint: %s', async (remoteUrl) => {
mockOriginRemote(remoteUrl)
mockHostAuthenticated('ghe.acme.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it.each(['http://ghe.acme.com:80/team/orca.git', 'https://ghe.acme.com:443/team/orca.git'])(
'matches a protocol-default port to the portless auth host: %s',
async (remoteUrl) => {
mockOriginRemote(remoteUrl)
mockHostAuthenticated('ghe.acme.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com'
})
}
)
it('preserves exact authentication for an HTTP endpoint on port 443', async () => {
mockOriginRemote('http://ghe.acme.com:443/team/orca.git')
mockHostAuthenticated('ghe.acme.com:443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:443'
})
})
it('preserves exact authentication for an HTTPS endpoint on port 80', async () => {
mockOriginRemote('https://ghe.acme.com:80/team/orca.git')
mockHostAuthenticated('ghe.acme.com:80')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:80'
})
})
it('probes gh in the repository WSL runtime, not the host/default distro', async () => {
mockOriginRemote('https://github.acme-corp.com/team/orca.git')
mockHostAuthenticated()
await getEnterpriseGitHubRepoSlug('/repo', null, {
localGitExecOptions: { wslDistro: 'Ubuntu' }
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {
cwd: '/repo',
wslDistro: 'Ubuntu'
})
})
it('leaves github.com to getOwnerRepo without probing gh auth', async () => {
mockOriginRemote('https://github.com/team/orca.git')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('declines a custom host the user is not gh-authenticated to (leaves it for Gitea)', async () => {
mockOriginRemote('https://gitea.example.com/team/orca.git')
mockHostNotAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it('returns null for an unparseable remote', async () => {
mockOriginRemote('not-a-remote-url')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('returns null when the origin remote lookup fails', async () => {
gitExecFileAsyncMock.mockRejectedValue(new Error('no such remote'))
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it('preserves an indeterminate auth inventory failure so a later probe can recover', async () => {
mockOriginRemote('git@ghe.acme.com:team/orca.git')
ghExecFileAsyncMock.mockRejectedValueOnce(
Object.assign(new Error('not installed'), { stdout: '', stderr: '' })
)
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeUndefined()
mockHostAuthenticated('ghe.acme.com:8443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
})
})
describe('isGitHubHostAuthenticated', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
gitExecFileAsyncMock.mockReset()
_resetGitHubHostAuthCache()
})
it('runs gh in the SSH-local runtime (no cwd) for connection-backed repos', async () => {
mockHostAuthenticated()
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/remote/repo', 'ssh-1')
).resolves.toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {})
})
it('caches per runtime+host so detection polling does not re-spawn gh', async () => {
mockHostAuthenticated()
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('coalesces concurrent probes for the same runtime and host', async () => {
let finishProbe: (() => void) | undefined
ghExecFileAsyncMock.mockImplementation(
() =>
new Promise((resolve) => {
finishProbe = () =>
resolve({
stdout:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
})
})
)
const first = isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
const second = isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
finishProbe?.()
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
})
it('does not share cache state across WSL distros', async () => {
mockHostAuthenticated()
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Ubuntu' })
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Debian' })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('derives cache scope from an implicit WSL UNC repository path', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
ghExecFileAsyncMock.mockImplementation(async (_args: string[], options: { cwd?: string }) => ({
stdout: options.cwd?.includes('Ubuntu')
? 'github.com\n ✓ Logged in to github.com account other (keyring)'
: 'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
}))
try {
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/native/repo')).resolves.toBe(
true
)
await expect(
isGitHubHostAuthenticated(
'github.acme-corp.com',
String.raw`\\wsl.localhost\Ubuntu\home\me\repo`
)
).resolves.toBe(false)
await expect(
isGitHubHostAuthenticated(
'github.acme-corp.com',
String.raw`\\wsl.localhost\Debian\home\me\repo`
)
).resolves.toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('shares the native gh auth probe across SSH connections', async () => {
mockHostAuthenticated()
await isGitHubHostAuthenticated('github.acme-corp.com', '/remote/a', 'ssh-1')
await isGitHubHostAuthenticated('github.acme-corp.com', '/remote/b', 'ssh-2')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('does not target an unconfigured remote host with ambient credentials', async () => {
mockHostAuthenticated('github.acme-corp.com')
await expect(isGitHubHostAuthenticated('evil.example.test', '/repo')).resolves.toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], { cwd: '/repo' })
expect(ghExecFileAsyncMock.mock.calls.flat()).not.toContain('evil.example.test')
})
it.each([
[['ghe.acme.com:8443', 'ghe.acme.com:9443']],
[['ghe.acme.com:9443', 'ghe.acme.com:8443']]
])(
'does not guess between multiple ported auth hosts for one SSH hostname: %j',
async (authenticatedHosts) => {
mockAuthenticatedHosts(authenticatedHosts)
await expect(isGitHubHostAuthenticated('ghe.acme.com', '/repo')).resolves.toBe(false)
}
)
it.each([
[
['ghe.acme.com', false],
['ghe.acme.com:8443', true]
],
[
['ghe.acme.com:8443', true],
['ghe.acme.com', false]
]
] as const)(
'keeps exact-port and ambiguous hostname auth cached independently: %j',
async (...requests) => {
mockAuthenticatedHosts(['ghe.acme.com:8443', 'ghe.acme.com:9443'])
for (const [host, expected] of requests) {
await expect(isGitHubHostAuthenticated(host, '/repo')).resolves.toBe(expected)
}
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
}
)
it('treats default web ports as the same auth host', async () => {
mockHostAuthenticated('ghe.acme.com:443')
await expect(isGitHubHostAuthenticated('ghe.acme.com', '/repo')).resolves.toBe(true)
})
it('validates a global project host by inventory without targeting it', async () => {
mockHostAuthenticated('github.acme-corp.com')
await expect(isGitHubHostAuthenticatedForGlobalCli('evil.example.test')).resolves.toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {})
expect(ghExecFileAsyncMock.mock.calls.flat()).not.toContain('evil.example.test')
})
it('treats a listed host as authenticated even when gh exits non-zero', async () => {
ghExecFileAsyncMock.mockRejectedValue(
Object.assign(new Error('exit 1'), {
stdout: '',
stderr:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)\n X github.com: token expired'
})
)
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).resolves.toBe(true)
})
it('does not cache a hard gh failure so a later probe can recover', async () => {
ghExecFileAsyncMock.mockRejectedValueOnce(
Object.assign(new Error('not installed'), { stdout: '', stderr: '' })
)
expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(false)
mockHostAuthenticated()
expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(true)
})
})