b18f6aee1b
Additive migration; AddWGPeer = one tx, idempotent on pubkey, lowest-free-host allocation skipping network/pbs_tunnel_ip/broadcast, UNIQUE(assigned_ip) race backstop with one internal retry; typed ErrWGEndpointUnset/ErrWGSubnetExhausted. Group-A tests + red-proof (allocator-ignores-rows mutation fails 3 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package store
|
|
|
|
// Group A — the /32 allocator (S1, doc 06 §3.2). Non-hollow: every test asserts allocation
|
|
// EFFECTS (exact IPs, row counts, typed errors), not just nil-errors.
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
)
|
|
|
|
func setTestEndpoint(t *testing.T, s *Store, subnet, pbsIP string) {
|
|
t.Helper()
|
|
err := s.SetWGEndpoint(&WGEndpoint{
|
|
DNSName: "ep0.example", WGPort: 443, ServerPubkey: "SPK",
|
|
TunnelSubnet: subnet, PBSTunnelIP: pbsIP,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetWGEndpoint: %v", err)
|
|
}
|
|
}
|
|
|
|
func peerCount(t *testing.T, s *Store) int {
|
|
t.Helper()
|
|
peers, err := s.ListWGPeers()
|
|
if err != nil {
|
|
t.Fatalf("ListWGPeers: %v", err)
|
|
}
|
|
return len(peers)
|
|
}
|
|
|
|
func TestWGEndpoint_UpsertAndGet(t *testing.T) {
|
|
s := newTestStore(t)
|
|
if _, err := s.GetWGEndpoint(); err != sql.ErrNoRows {
|
|
t.Fatalf("unset endpoint: err = %v, want sql.ErrNoRows", err)
|
|
}
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
e, err := s.GetWGEndpoint()
|
|
if err != nil || e.EndpointID != "ep0" || e.TunnelSubnet != "10.77.0.0/24" || e.WGPort != 443 {
|
|
t.Fatalf("GetWGEndpoint = %+v, %v", e, err)
|
|
}
|
|
// Upsert overwrites, same row.
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
e2, err := s.GetWGEndpoint()
|
|
if err != nil || e2.EndpointID != "ep0" {
|
|
t.Fatalf("after upsert: %+v, %v", e2, err)
|
|
}
|
|
}
|
|
|
|
func TestAddWGPeer_SequentialAllocationSkipsNetPBSAndBroadcast(t *testing.T) {
|
|
s := newTestStore(t)
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
|
|
// .0 network and .1 endpoint are skipped → first three peers get .2 .3 .4 exactly.
|
|
want := []string{"10.77.0.2", "10.77.0.3", "10.77.0.4"}
|
|
for i, pk := range []string{"p1", "p2", "p3"} {
|
|
ip, existed, err := s.AddWGPeer(pk, "", "")
|
|
if err != nil || existed {
|
|
t.Fatalf("add %s: ip=%q existed=%v err=%v", pk, ip, existed, err)
|
|
}
|
|
if ip != want[i] {
|
|
t.Errorf("peer %s ip = %q, want %q", pk, ip, want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddWGPeer_FreedIPReused(t *testing.T) {
|
|
s := newTestStore(t)
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
for _, pk := range []string{"p1", "p2", "p3"} {
|
|
s.AddWGPeer(pk, "", "")
|
|
}
|
|
if err := s.RemoveWGPeer("p2"); err != nil {
|
|
t.Fatalf("RemoveWGPeer(p2): %v", err)
|
|
}
|
|
ip, existed, err := s.AddWGPeer("p4", "", "")
|
|
if err != nil || existed {
|
|
t.Fatalf("add p4: %v existed=%v", err, existed)
|
|
}
|
|
if ip != "10.77.0.3" {
|
|
t.Errorf("p4 ip = %q, want the freed lowest 10.77.0.3", ip)
|
|
}
|
|
}
|
|
|
|
func TestAddWGPeer_IdempotentOnExistingPubkey(t *testing.T) {
|
|
s := newTestStore(t)
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
ip1, _, err := s.AddWGPeer("p1", "", "")
|
|
if err != nil {
|
|
t.Fatalf("first add: %v", err)
|
|
}
|
|
before := peerCount(t, s)
|
|
ip2, existed, err := s.AddWGPeer("p1", "", "")
|
|
if err != nil || !existed {
|
|
t.Fatalf("re-add: err=%v existed=%v, want idempotent hit", err, existed)
|
|
}
|
|
if ip2 != ip1 {
|
|
t.Errorf("re-add ip = %q, want unchanged %q", ip2, ip1)
|
|
}
|
|
if after := peerCount(t, s); after != before {
|
|
t.Errorf("row count changed on idempotent re-add: %d -> %d", before, after)
|
|
}
|
|
}
|
|
|
|
func TestAddWGPeer_NoEndpoint(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_, _, err := s.AddWGPeer("p1", "", "")
|
|
if err != ErrWGEndpointUnset {
|
|
t.Fatalf("err = %v, want ErrWGEndpointUnset", err)
|
|
}
|
|
if n := peerCount(t, s); n != 0 {
|
|
t.Errorf("peer rows created without endpoint: %d", n)
|
|
}
|
|
}
|
|
|
|
func TestAddWGPeer_SubnetExhausted(t *testing.T) {
|
|
s := newTestStore(t)
|
|
// /30: .0 network, .1 endpoint, .2 the only host, .3 broadcast.
|
|
setTestEndpoint(t, s, "10.77.0.0/30", "10.77.0.1")
|
|
ip, _, err := s.AddWGPeer("p1", "", "")
|
|
if err != nil || ip != "10.77.0.2" {
|
|
t.Fatalf("p1: ip=%q err=%v, want the single free host .2", ip, err)
|
|
}
|
|
_, _, err = s.AddWGPeer("p2", "", "")
|
|
if err != ErrWGSubnetExhausted {
|
|
t.Fatalf("p2 err = %v, want ErrWGSubnetExhausted", err)
|
|
}
|
|
if n := peerCount(t, s); n != 1 {
|
|
t.Errorf("partial row on exhaustion: count = %d, want 1", n)
|
|
}
|
|
}
|
|
|
|
func TestRemoveWGPeer_UnknownIsNoRows(t *testing.T) {
|
|
s := newTestStore(t)
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
if err := s.RemoveWGPeer("ghost"); err != sql.ErrNoRows {
|
|
t.Fatalf("err = %v, want sql.ErrNoRows", err)
|
|
}
|
|
}
|
|
|
|
func TestListWGPeers_DeterministicOrder(t *testing.T) {
|
|
s := newTestStore(t)
|
|
setTestEndpoint(t, s, "10.77.0.0/24", "10.77.0.1")
|
|
s.AddWGPeer("pA", "h1", "note-a")
|
|
s.AddWGPeer("pB", "", "")
|
|
peers, err := s.ListWGPeers()
|
|
if err != nil || len(peers) != 2 {
|
|
t.Fatalf("ListWGPeers: %v (%d)", err, len(peers))
|
|
}
|
|
if peers[0].Pubkey != "pA" || peers[0].AssignedIP != "10.77.0.2" || peers[0].HostID != "h1" {
|
|
t.Errorf("peers[0] = %+v", peers[0])
|
|
}
|
|
if peers[1].AssignedIP != "10.77.0.3" {
|
|
t.Errorf("peers[1] = %+v", peers[1])
|
|
}
|
|
}
|