23dcfb393f
Implements the design in claude.md as a building skeleton: pure Go, no cgo, cross-compiles to a single Windows .exe from Linux. Architecture follows the spec's deliberate split. The fast path is native — tray icon, grouped submenus, a Win32 MessageBox showing a live before/after diff, then netsh. The slow path is an embedded web editor served on a random loopback port and opened in the default browser. Two decisions worth recording: Reads use GetAdaptersAddresses, writes use netsh. The spec left the enumeration mechanism open; parsing `netsh show config` breaks on a non-English Windows because the output is localised. DNS static-vs-DHCP origin is not exposed by that API, so it comes from one registry read. The netsh command plan is built in portable code. That puts the delete-every- existing-address step — the one that stops secondary addresses leaking across switches — under test without needing a Windows box. The editor requires the session token in a header for mutations, not just the cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie attached, but it cannot set a header. The updater refuses to install a release that publishes no SHA256. Not yet done: no group picker for export (the API supports it), no single-instance guard, and internal/server/web/app.css is reconstructed from the description in claude.md rather than the canonical apointless.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestAPIURL(t *testing.T) {
|
|
got, err := apiURL("https://gitea.apointless.space/bsncubed/ipswap")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "https://gitea.apointless.space/api/v1/repos/bsncubed/ipswap/releases/latest"
|
|
if got != want {
|
|
t.Errorf("apiURL = %q, want %q", got, want)
|
|
}
|
|
|
|
if _, err := apiURL("https://gitea.apointless.space/bsncubed/ipswap/"); err != nil {
|
|
t.Errorf("a trailing slash should be tolerated: %v", err)
|
|
}
|
|
for _, bad := range []string{"", "not a url", "https://host", "https://host/a/b/c"} {
|
|
if _, err := apiURL(bad); err == nil {
|
|
t.Errorf("apiURL(%q) should have failed", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckFindsNewerRelease(t *testing.T) {
|
|
rel := Release{
|
|
TagName: "v1.2.0",
|
|
Body: "ipswap.exe e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n",
|
|
Assets: []Asset{
|
|
{Name: "SHA256SUMS", BrowserDownloadURL: "http://x/sums"},
|
|
{Name: "ipswap.exe", BrowserDownloadURL: "http://x/ipswap.exe"},
|
|
},
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(rel)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := &Checker{Repo: srv.URL + "/owner/repo", Current: "v1.1.0"}
|
|
up, err := c.Check(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if up == nil {
|
|
t.Fatal("expected an update, got nil")
|
|
}
|
|
if up.Version != "1.2.0" {
|
|
t.Errorf("version = %q, want 1.2.0", up.Version)
|
|
}
|
|
if up.Asset.Name != "ipswap.exe" {
|
|
t.Errorf("picked asset %q, want ipswap.exe", up.Asset.Name)
|
|
}
|
|
if up.SHA256 != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
|
t.Errorf("checksum = %q", up.SHA256)
|
|
}
|
|
}
|
|
|
|
func TestCheckIgnoresOlderRelease(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(Release{TagName: "v1.0.0"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := &Checker{Repo: srv.URL + "/owner/repo", Current: "v1.2.0"}
|
|
up, err := c.Check(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if up != nil {
|
|
t.Errorf("expected no update, got %+v", up)
|
|
}
|
|
}
|
|
|
|
func TestChecksumFor(t *testing.T) {
|
|
sums := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ipswap.exe\n" +
|
|
"aaaabbbbccccddddeeeeffff00001111222233334444555566667777888899990 other.zip\n"
|
|
got := checksumFor(sums, "ipswap.exe")
|
|
if got != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
|
t.Errorf("checksumFor = %q", got)
|
|
}
|
|
if checksumFor(sums, "missing.exe") != "" {
|
|
t.Error("expected empty checksum for an absent file")
|
|
}
|
|
// sha256sum's binary-mode marker must not defeat the lookup.
|
|
if checksumFor("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 *ipswap.exe", "ipswap.exe") == "" {
|
|
t.Error("binary-mode '*' prefix should be tolerated")
|
|
}
|
|
}
|