Files
bsncubed 23dcfb393f Scaffold ipswap: tray-driven Windows IP preset switcher
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>
2026-08-20 13:29:35 +10:00

64 lines
1.4 KiB
Go

package updater
import "testing"
func TestParseVersion(t *testing.T) {
cases := []struct {
in string
want Version
}{
{"v1.2.0", Version{1, 2, 0, ""}},
{"1.2.0", Version{1, 2, 0, ""}},
{"1.2", Version{1, 2, 0, ""}},
{"1", Version{1, 0, 0, ""}},
{"v1.2.0-rc1", Version{1, 2, 0, "rc1"}},
{"1.2.0+build7", Version{1, 2, 0, ""}},
{" v0.1.0 ", Version{0, 1, 0, ""}},
}
for _, tc := range cases {
got, err := ParseVersion(tc.in)
if err != nil {
t.Errorf("ParseVersion(%q) errored: %v", tc.in, err)
continue
}
if got != tc.want {
t.Errorf("ParseVersion(%q) = %+v, want %+v", tc.in, got, tc.want)
}
}
for _, in := range []string{"", "v", "abc", "1.2.3.4", "1.x", "-1.0.0"} {
if got, err := ParseVersion(in); err == nil {
t.Errorf("ParseVersion(%q) = %+v, want error", in, got)
}
}
}
func TestIsNewer(t *testing.T) {
yes := [][2]string{
{"v1.2.1", "v1.2.0"},
{"v1.3.0", "v1.2.9"},
{"v2.0.0", "v1.99.99"},
{"v1.2.0", "v1.2.0-rc1"},
}
for _, tc := range yes {
if !IsNewer(tc[0], tc[1]) {
t.Errorf("IsNewer(%q, %q) = false, want true", tc[0], tc[1])
}
}
no := [][2]string{
{"v1.2.0", "v1.2.0"},
{"v1.2.0", "v1.2.1"},
{"v1.2.0-rc1", "v1.2.0"},
// An unreadable tag must never trigger an update offer.
{"garbage", "v1.2.0"},
{"v1.2.0", "garbage"},
{"", "v1.0.0"},
}
for _, tc := range no {
if IsNewer(tc[0], tc[1]) {
t.Errorf("IsNewer(%q, %q) = true, want false", tc[0], tc[1])
}
}
}