gateway, provider tests: the tests stay off the network (testenv.Offline in their TestMains) — after 555f5706 (#1016) routing reads a plan key's quota windows, and a test whose provider has a vendor's real base URL and a made-up key would ask the vendor in the background. Routing's own read (keyAllowance, allowances) was already stubbed in routing_test.go's init, and a survey with a dial guard found no plan-key dial, but it found others: every plugin host's first start fetched models.dev's catalog (65 dials in the gateway's plugin tests), and provider tests asked api.github.com (TestSubscriptionUsageEachAccount, TestSubscriptionUsageServesStale), chatgpt.com (TestCopilotQuotaWithoutEditorsSignIn), server.codeium.com (TestDevinSignedInOnlyInMagpie) and decide.test (TestModelTest) with made-up accounts; each passed whether the vendor answered or not. internal/testenv's Offline wraps http.DefaultTransport's DialContext (so http.DefaultClient and every transport cloned from it later): localhost, *.localhost, loopback and unspecified addresses are dialled, anything else is refused before it is looked up, said once per address on stderr ("testenv: a test asked models.dev:443, refused: tests stay off the network"). internal/gateway's TestMain and internal/provider's (kiro_main_test.go, in RunIn's setup) call it before the tests run. TestLocal (the address table) and TestOffline (a loopback httptest server answers through DefaultClient; models.dev is refused through it and through a Clone made after the call) pass -race -count=3. go test -race -tags nogui -timeout 30m ./internal/testenv ./internal/provider ./internal/gateway passes under a temp HOME (gateway 516 s). The gateway and provider test binaries were then run under sandbox-exec with every non-loopback network-outbound denied, with the kernel's Sandbox reports streamed: both PASS, Offline refused only the six addresses above, and the kernel reported no denial, so no transport outside DefaultTransport reaches the network either. go vet, gofmt -l, linux and windows (nogui) builds pass. Tests only.
This commit is contained in:
1 parent
f311a61859
commit
a8d798e2cb
4 files changed
+115
-1
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/agentenv"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -62,5 +63,9 @@ func isolatedTests(m *testing.M) (int, error) {
|
||||
// no route is written to disk behind a test's back; the history's own
|
||||
// tests call saveRoute themselves
|
||||
keepRoutes = false
|
||||
// no test asks a vendor: a provider with a real base URL and a made-up
|
||||
// key would, in the background (a plan key's windows, #1016), and a
|
||||
// plugin host's first start fetched models.dev
|
||||
testenv.Offline()
|
||||
return m.Run(), nil
|
||||
}
|
||||
@@ -12,9 +12,12 @@ import (
|
||||
// them: Accounts() reads them, and asks Kiro who the account is. It also
|
||||
// gives the package a home of its own (testenv), so what a test leaves
|
||||
// behind it — a CLI's answer kept by refresh after the test is over —
|
||||
// never lands in the runner's magpie, nor in APPDATA on Windows.
|
||||
// never lands in the runner's magpie, nor in APPDATA on Windows. And it
|
||||
// keeps them off the network (testenv.Offline): a test of a sign-in or a
|
||||
// key at a vendor's real address asks only what it serves on loopback.
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testenv.RunIn(m, func(home string) {
|
||||
testenv.Offline()
|
||||
kiroCLIDB = func() string { return filepath.Join(home, "kiro-test", "kiro-cli", "data.sqlite3") }
|
||||
kiroIDEDir = func() string { return filepath.Join(home, "kiro-test", "sso") }
|
||||
askKiroIdentity = func(string, string) (string, string) { return "", "" }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Offline keeps the package's tests off the network: http.DefaultTransport
|
||||
// (and so http.DefaultClient, and every transport cloned from it after the
|
||||
// call) dials this computer only — localhost, a name under .localhost, a
|
||||
// loopback or unspecified address — and refuses anything else before it is
|
||||
// looked up, saying so on stderr once per address. A test that wants a
|
||||
// vendor serves it on loopback and points the request there (an
|
||||
// httptest.Server, a Transport rewriting the host). Without it the tests
|
||||
// of internal/gateway and internal/provider asked the real models.dev (its
|
||||
// catalog, fetched as each plugin host started), api.github.com,
|
||||
// chatgpt.com and Devin's server.codeium.com with made-up accounts, and a
|
||||
// plan key with a vendor's real base URL would have its windows read for
|
||||
// routing (#1016). A package calls it from its TestMain, before m.Run.
|
||||
func Offline() {
|
||||
t, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
dial := t.DialContext
|
||||
if dial == nil {
|
||||
dial = (&net.Dialer{}).DialContext
|
||||
}
|
||||
var mu sync.Mutex
|
||||
said := map[string]bool{}
|
||||
t.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if local(addr) {
|
||||
return dial(ctx, network, addr)
|
||||
}
|
||||
mu.Lock()
|
||||
if !said[addr] {
|
||||
said[addr] = true
|
||||
fmt.Fprintf(os.Stderr, "testenv: a test asked %s, refused: tests stay off the network\n", addr)
|
||||
}
|
||||
mu.Unlock()
|
||||
return nil, fmt.Errorf("testenv: %s is not this computer; tests stay off the network", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// local says addr, a host:port, is this computer, without looking it up.
|
||||
func local(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
host = strings.ToLower(strings.TrimSuffix(host, "."))
|
||||
if host == "localhost" || strings.HasSuffix(host, ".localhost") {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(strings.Trim(host, "[]"))
|
||||
return ip != nil && (ip.IsLoopback() || ip.IsUnspecified())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
for addr, want := range map[string]bool{
|
||||
"127.0.0.1:80": true, "[::1]:443": true, "localhost:3425": true, "LOCALHOST.:1": true,
|
||||
"magpie.localhost:80": true, "0.0.0.0:80": true, "[::]:80": true, "127.8.0.1:9": true,
|
||||
"models.dev:443": false, "api.kimi.com:443": false, "10.0.0.132:22": false,
|
||||
"192.168.1.2:80": false, "localhost.example.com:80": false, "decide.test:443": false,
|
||||
} {
|
||||
if got := local(addr); got != want {
|
||||
t.Errorf("local(%q) = %v, want %v", addr, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Offline lets a loopback server answer through http.DefaultClient and
|
||||
// refuses a vendor's host without dialing it.
|
||||
func TestOffline(t *testing.T) {
|
||||
tr := http.DefaultTransport.(*http.Transport)
|
||||
was := tr.DialContext
|
||||
t.Cleanup(func() { tr.DialContext = was })
|
||||
Offline()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("here")) }))
|
||||
defer srv.Close()
|
||||
resp, err := http.Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("loopback refused: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
clone := tr.Clone() // a transport made from it after the call
|
||||
for _, c := range []*http.Client{http.DefaultClient, {Transport: clone}} {
|
||||
_, err := c.Get("https://models.dev/api.json")
|
||||
if err == nil || !strings.Contains(err.Error(), "tests stay off the network") {
|
||||
t.Errorf("models.dev: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user