package instconf_test import ( "errors" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-ecore/instconf" ) // parse builds a File the way a daemon gets one, so the tests exercise the // parser's own trimming rather than a map the test wrote by hand. func parse(t *testing.T, text string) ini.File { t.Helper() conf, err := ini.Load(strings.NewReader(text)) require.NoError(t, err) return conf } func TestCanonicalOriginTrimsTrailingSlashesAndSpace(t *testing.T) { for name, tc := range map[string]struct{ in, want string }{ "empty stays empty": {"", ""}, "blank stays empty": {" ", ""}, "no slash untouched": {"https://x.example.org", "https://x.example.org"}, "one trailing slash": {"https://x.example.org/", "https://x.example.org"}, "several slashes": {"https://x.example.org///", "https://x.example.org"}, "trailing space": {"https://x.example.org ", "https://x.example.org"}, "leading space": {" https://x.example.org", "https://x.example.org"}, "space then slashes": {" https://x.example.org// ", "https://x.example.org"}, "port kept": {"https://x.example.org:8443/", "https://x.example.org:8443"}, "path prefix kept": {"https://example.org/git/", "https://example.org/git"}, "scheme-less untouched": {"x.example.org/", "x.example.org"}, } { t.Run(name, func(t *testing.T) { assert.Equal(t, tc.want, instconf.CanonicalOrigin(tc.in)) }) } } // The donors disagreed here: one canonicalized with strings.TrimSuffix, which // leaves "https://x/" from "https://x//", and one with strings.TrimRight, which // does not. This pins the strict reading, because the whole point is that the // string compares equal to a browser's Origin header. func TestCanonicalOriginBeatsTrimSuffix(t *testing.T) { const doubled = "https://x.example.org//" assert.Equal(t, "https://x.example.org/", strings.TrimSuffix(doubled, "/"), "sanity: TrimSuffix is the behaviour being overruled") assert.Equal(t, "https://x.example.org", instconf.CanonicalOrigin(doubled)) } func TestExternalOriginReadsOriginOnly(t *testing.T) { conf := parse(t, ` [git.sr.ht] origin=https://git.example.org/ internal-origin=http://git.internal:5001 `) assert.Equal(t, "https://git.example.org", instconf.ExternalOrigin(conf, "git.sr.ht"), "internal-origin must never reach a browser") } func TestExternalOriginMissing(t *testing.T) { conf := parse(t, ` [git.sr.ht] internal-origin=http://git.internal:5001 [meta.sr.ht] origin= `) assert.Empty(t, instconf.ExternalOrigin(conf, "git.sr.ht"), "key absent") assert.Empty(t, instconf.ExternalOrigin(conf, "meta.sr.ht"), "key present but blank") assert.Empty(t, instconf.ExternalOrigin(conf, "nope.sr.ht"), "section absent") assert.Empty(t, instconf.ExternalOrigin(nil, "git.sr.ht"), "no config at all") } func TestInternalOriginPrefersInternal(t *testing.T) { conf := parse(t, ` [git.sr.ht] origin=https://git.example.org internal-origin=http://git.internal:5001/ `) assert.Equal(t, "http://git.internal:5001", instconf.InternalOrigin(conf, "git.sr.ht")) } func TestInternalOriginFallsBackToOrigin(t *testing.T) { conf := parse(t, ` [git.sr.ht] origin=https://git.example.org/ [meta.sr.ht] origin=https://meta.example.org internal-origin= `) assert.Equal(t, "https://git.example.org", instconf.InternalOrigin(conf, "git.sr.ht"), "internal-origin absent") assert.Equal(t, "https://meta.example.org", instconf.InternalOrigin(conf, "meta.sr.ht"), "internal-origin present but blank must not shadow origin") assert.Empty(t, instconf.InternalOrigin(conf, "nope.sr.ht")) } func TestInternalAPIOriginLadder(t *testing.T) { full := ` [git.sr.ht] api-internal-origin=http://git.internal:5101/ internal-origin=http://git.internal:5001 api-origin=https://api.git.example.org origin=https://git.example.org ` for name, tc := range map[string]struct { drop []string want string }{ "api-internal-origin wins": {nil, "http://git.internal:5101"}, "then internal-origin": { []string{"api-internal-origin"}, "http://git.internal:5001"}, "then api-origin": { []string{"api-internal-origin", "internal-origin"}, "https://api.git.example.org"}, "then origin": { []string{"api-internal-origin", "internal-origin", "api-origin"}, "https://git.example.org"}, } { t.Run(name, func(t *testing.T) { text := full for _, k := range tc.drop { text = dropKey(t, text, k) } got, ok := instconf.InternalAPIOrigin(parse(t, text), "git.sr.ht") require.True(t, ok) assert.Equal(t, tc.want, got) }) } } func TestInternalAPIOriginNotConfigured(t *testing.T) { conf := parse(t, ` [git.sr.ht] repos=/var/lib/git [meta.sr.ht] origin= `) got, ok := instconf.InternalAPIOrigin(conf, "git.sr.ht") assert.False(t, ok, "no candidate key at all") assert.Empty(t, got) got, ok = instconf.InternalAPIOrigin(conf, "meta.sr.ht") assert.False(t, ok, "a blank origin is not a configured API origin") assert.Empty(t, got) got, ok = instconf.InternalAPIOrigin(conf, "nope.sr.ht") assert.False(t, ok, "section absent") assert.Empty(t, got) } func TestAPIOriginKeysMatchesTheLadder(t *testing.T) { want := []string{"api-internal-origin", "internal-origin", "api-origin", "origin"} assert.Equal(t, want, instconf.APIOriginKeys()) keys := instconf.APIOriginKeys() keys[0] = "clobbered" assert.Equal(t, want, instconf.APIOriginKeys(), "the exported ladder must not be writable") } func TestOriginHostAndAuthority(t *testing.T) { for name, tc := range map[string]struct{ in, host, authority string }{ "plain": {"https://git.example.org", "git.example.org", "git.example.org"}, "trailing slash": {"https://git.example.org/", "git.example.org", "git.example.org"}, "several slashes": {"https://git.example.org//", "git.example.org", "git.example.org"}, "surrounding space": {" https://git.example.org ", "git.example.org", "git.example.org"}, "with port": {"https://git.example.org:8443", "git.example.org", "git.example.org:8443"}, "path prefix": {"https://example.org/git", "example.org", "example.org"}, "ipv6 with port": {"http://[::1]:8080", "::1", "[::1]:8080"}, "scheme relative": {"//git.example.org", "git.example.org", "git.example.org"}, "case as configured": {"https://Git.Example.ORG", "Git.Example.ORG", "Git.Example.ORG"}, // Everything below has no host to report. "" is the answer in every // case; a caller on a security path must not read it as "allow". "empty": {"", "", ""}, "blank": {" ", "", ""}, "scheme-less": {"git.example.org", "", ""}, "bare word": {"not-a-url", "", ""}, "no scheme": {"://git.example.org", "", ""}, "space inside": {"https://git example.org", "", ""}, "bad escape": {"https://example.org/%zz", "", ""}, "unclosed ipv6": {"http://[::1", "", ""}, } { t.Run(name, func(t *testing.T) { assert.Equal(t, tc.host, instconf.OriginHost(tc.in), "OriginHost") assert.Equal(t, tc.authority, instconf.OriginAuthority(tc.in), "OriginAuthority") }) } } // The donor that answered "localhost" for an unparseable origin made every // malformed config agree with a local client. This package refuses to guess. func TestOriginHostNeverInventsLocalhost(t *testing.T) { for _, bad := range []string{"", " ", "not-a-url", "://x", "http://[::1"} { assert.Empty(t, instconf.OriginHost(bad), "origin %q", bad) assert.Empty(t, instconf.OriginAuthority(bad), "origin %q", bad) } } func TestRequireSatisfied(t *testing.T) { conf := parse(t, ` [sr.ht] network-key=abc [webhooks] private-key=def [git.sr.ht] internal-origin=http://git.internal:5001 [diff.sr.ht] origin=https://diff.example.org `) err := instconf.Require(conf, instconf.Need("sr.ht", "network-key"), instconf.Need("webhooks", "private-key"), instconf.Need("diff.sr.ht", "origin"), instconf.NeedAny("git.sr.ht", instconf.APIOriginKeys()...), ) assert.NoError(t, err) assert.NoError(t, instconf.Require(conf), "no requirements is not a failure") } func TestRequireReportsEveryMissingKeyAtOnce(t *testing.T) { conf := parse(t, ` [sr.ht] network-key=abc [meta.sr.ht] origin= [git.sr.ht] repos=/var/lib/git `) err := instconf.Require(conf, instconf.Need("sr.ht", "network-key"), // present instconf.Need("webhooks", "private-key"), // section absent instconf.Need("meta.sr.ht", "origin"), // present but blank instconf.Need("diff.sr.ht", "origin"), // key absent instconf.NeedAny("git.sr.ht", instconf.APIOriginKeys()...), // ladder empty ) require.Error(t, err) require.ErrorIs(t, err, instconf.ErrIncompleteConfig) var missing *instconf.MissingKeysError require.ErrorAs(t, err, &missing) assert.Equal(t, []string{ "[webhooks] private-key", "[meta.sr.ht] origin", "[diff.sr.ht] origin", "[git.sr.ht] one of api-internal-origin, internal-origin, api-origin, origin", }, missing.Strings(), "every gap in one pass, in the order asked") msg := err.Error() assert.Contains(t, msg, "incomplete configuration") assert.Contains(t, msg, "[diff.sr.ht] origin") assert.NotContains(t, msg, "network-key", "a satisfied key must not be reported") } func TestRequireNamelessKeyIsUnsatisfiable(t *testing.T) { conf := parse(t, "[sr.ht]\nnetwork-key=abc\n") err := instconf.Require(conf, instconf.NeedAny("sr.ht")) require.Error(t, err) assert.Contains(t, err.Error(), "[sr.ht] ") } // dropKey removes an "key=..." line from an ini fixture. func dropKey(t *testing.T, text, key string) string { t.Helper() var kept []string var dropped bool for _, line := range strings.Split(text, "\n") { if strings.HasPrefix(line, key+"=") { dropped = true continue } kept = append(kept, line) } require.True(t, dropped, "fixture has no %q line", key) return strings.Join(kept, "\n") } // The sentinel is what a daemon matches on to tell "not configured yet" apart // from the failures that come after startup, so check it with errors.Is // directly rather than only through testify. func TestErrIncompleteConfigIsTheSentinel(t *testing.T) { err := instconf.Require(nil, instconf.Need("sr.ht", "network-key")) require.Error(t, err) assert.True(t, errors.Is(err, instconf.ErrIncompleteConfig)) }