Files
Sam Morrow 0ea1f775a7 fix: preserve authority for loopback GHES hosts
Address review: the loopback exception accepted http://localhost:3000 and
http://[::1], but newGHESHost built URLs from u.Hostname(), which drops the
port (silently retargeting the dev server to port 80) and strips IPv6 brackets
(producing an unusable URL such as http://::1/api/v3/).

Derive the base-host REST/GraphQL/upload/raw/authorization URLs from u.Host so
the port and IPv6 brackets are preserved. Subdomain-isolation URLs keep using
the bare hostname, since a label cannot be prepended to a host:port or an IP
literal. Add tests for the ::1 case and for port preservation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-14 13:53:02 +02:00

117 lines
3.0 KiB
Go

package utils //nolint:revive //TODO: figure out a better name for this package
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseAPIHost(t *testing.T) {
tests := []struct {
name string
input string
wantRestURL string
wantErr bool
errContains string
}{
{
name: "empty string defaults to dotcom",
input: "",
wantRestURL: "https://api.github.com/",
},
{
name: "github.com hostname",
input: "https://github.com",
wantRestURL: "https://api.github.com/",
},
{
name: "subdomain of github.com",
input: "https://foo.github.com",
wantRestURL: "https://api.github.com/",
},
{
name: "hostname ending in github.com but not a subdomain",
input: "https://mycompanygithub.com",
wantRestURL: "https://mycompanygithub.com/api/v3/",
},
{
name: "hostname ending in notgithub.com",
input: "https://notgithub.com",
wantRestURL: "https://notgithub.com/api/v3/",
},
{
name: "ghe.com hostname",
input: "https://ghe.com",
wantRestURL: "https://api.ghe.com/",
},
{
name: "subdomain of ghe.com",
input: "https://mycompany.ghe.com",
wantRestURL: "https://api.mycompany.ghe.com/",
},
{
name: "hostname ending in ghe.com but not a subdomain",
input: "https://myghe.com",
wantRestURL: "https://myghe.com/api/v3/",
},
{
name: "missing scheme",
input: "github.com",
wantErr: true,
},
{
name: "http GHES rejected to avoid cleartext credentials",
input: "http://ghes.example.com",
wantErr: true,
errContains: "host must use https",
},
{
name: "http loopback allowed for local development",
input: "http://localhost",
wantRestURL: "http://localhost/api/v3/",
},
{
name: "http 127.0.0.1 loopback allowed for local development",
input: "http://127.0.0.1",
wantRestURL: "http://127.0.0.1/api/v3/",
},
{
name: "http loopback preserves port for local development",
input: "http://localhost:3000",
wantRestURL: "http://localhost:3000/api/v3/",
},
{
name: "http ipv6 loopback preserves brackets",
input: "http://[::1]",
wantRestURL: "http://[::1]/api/v3/",
},
{
name: "http ipv6 loopback preserves brackets and port",
input: "http://[::1]:8080",
wantRestURL: "http://[::1]:8080/api/v3/",
},
{
name: "http remote host rejected",
input: "http://notgithub.com",
wantErr: true,
errContains: "host must use https",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
host, err := parseAPIHost(tc.input)
if tc.wantErr {
require.Error(t, err)
if tc.errContains != "" {
assert.Contains(t, err.Error(), tc.errContains)
}
return
}
require.NoError(t, err)
assert.Equal(t, tc.wantRestURL, host.restURL.String())
})
}
}