package web // Group C (S2) + Group D (v0.47.0 multi-endpoint management) — the /offsite page. // v0.47.0 amends the S2 render pins DELIBERATELY for the multi-card layout + Endpoint // column; the pubkey-title and empty-/hosts/-href assertions are kept verbatim. import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) func renderOffsite(t *testing.T, s *Server) string { t.Helper() rr := httptest.NewRecorder() s.handleOffsite(rr, httptest.NewRequest(http.MethodGet, "/offsite", nil)) if rr.Code != http.StatusOK { t.Fatalf("offsite render = %d", rr.Code) } return rr.Body.String() } func postOffsiteEndpoint(t *testing.T, s *Server, form url.Values) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, "/offsite/endpoints", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() s.handleOffsiteEndpointSave(rr, req) return rr } // validEndpointForm returns a fully-valid add form; tests mutate single fields. func validEndpointForm(id string) url.Values { return url.Values{ "endpoint_id": {id}, "dns_name": {id + ".felhom.eu"}, "wg_port": {"443"}, "server_pubkey": {"CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk="}, "tunnel_subnet": {"10.78.0.0/24"}, "pbs_tunnel_ip": {"10.78.0.1"}, } } func TestOffsite_NoEndpoint(t *testing.T) { s, _ := newTestServer(t) html := renderOffsite(t, s) if !strings.Contains(html, "Not configured") { t.Errorf("no-endpoint state missing: %s", html[:200]) } if !strings.Contains(html, "No WireGuard peers registered") { t.Errorf("empty-peers state missing") } // The add-endpoint form is offered even before the first endpoint exists. if !strings.Contains(html, `action="/offsite/endpoints"`) { t.Errorf("add-endpoint form missing") } } // E1 — two endpoint cards render (ids visible), per-endpoint peer counts derive from // subnet membership, and the peer table's Endpoint column shows the containing endpoint. func TestOffsite_EndpointAndPeers(t *testing.T) { s, st := newTestServer(t) if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443, ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1", }); err != nil { t.Fatal(err) } if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 51820, ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=", TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1", }); err != nil { t.Fatal(err) } st.UpsertHost(&store.Host{HostID: "hv1", CustomerID: "c1", APIKey: "k"}) if _, _, err := st.RegisterWGPeerForHost("hv1", "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="); err != nil { t.Fatal(err) } if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "unbound-test"); err != nil { t.Fatal(err) } html := renderOffsite(t, s) for _, want := range []string{ `data-endpoint-id="ep0"`, // ep0 card `data-endpoint-id="ep1"`, // ep1 card "ep0.felhom.eu:443", // ep0 address "ep1.felhom.eu:51820", // ep1 address "10.77.0.0/24", // ep0 subnet "10.77.0.1:8007", // ep0 PBS tunnel addr "10.77.0.2/32", // bound peer ip "10.77.0.3/32", // unbound peer ip `href="/hosts/hv1"`, // bound peer links to its host "unbound-test", // note column } { if !strings.Contains(html, want) { t.Errorf("offsite page missing %q", want) } } // Per-endpoint peer counts: both allocated peers live in ep0's subnet; ep1 has none. ep0Card := html[strings.Index(html, `data-endpoint-id="ep0"`):strings.Index(html, `data-endpoint-id="ep1"`)] ep1Card := html[strings.Index(html, `data-endpoint-id="ep1"`):] if !strings.Contains(ep0Card, "2") { t.Error("ep0 card missing peer count 2") } if !strings.Contains(ep1Card, "0") { t.Error("ep1 card missing peer count 0") } // The peer table's Endpoint column shows ep0 for both peers. if got := strings.Count(html, "ep0"); got != 2 { t.Errorf("Endpoint column shows ep0 %d times, want 2", got) } // Unbound peer renders an em-dash host cell, not a broken link. if strings.Contains(html, `href="/hosts/"`) { t.Error("unbound peer rendered an empty host link") } // Full pubkey rides the title attribute (truncated display). if !strings.Contains(html, `title="AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="`) { t.Error("full pubkey missing from title attr") } } // E5 — a peer whose IP falls in NO endpoint's subnet renders Endpoint "—". func TestOffsite_OrphanPeerEndpointColumn(t *testing.T) { s, st := newTestServer(t) if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443, ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1", }); err != nil { t.Fatal(err) } if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "orphan"); err != nil { t.Fatal(err) // allocates 10.77.0.2 } // Replace ep0 with an endpoint on a DIFFERENT subnet → the peer's IP matches nothing. if err := st.DeleteWGEndpoint("ep0"); err != nil { t.Fatal(err) } if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 443, ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=", TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1", }); err != nil { t.Fatal(err) } html := renderOffsite(t, s) if !strings.Contains(html, "10.77.0.2/32") { t.Fatal("orphan peer missing from the table") } if strings.Contains(html, "ep1") { t.Error("orphan peer wrongly attributed to ep1 — must render em dash") } } // E2 — a valid POST upserts via SetWGEndpoint and redirects back to the page. func TestOffsiteEndpointSave_Add(t *testing.T) { s, st := newTestServer(t) rr := postOffsiteEndpoint(t, s, validEndpointForm("ep1")) if rr.Code != http.StatusSeeOther { t.Fatalf("add = %d (%s), want 303", rr.Code, rr.Body.String()) } eps, err := st.ListWGEndpoints() if err != nil || len(eps) != 1 { t.Fatalf("endpoints after add = %v / %v, want 1", eps, err) } if eps[0].EndpointID != "ep1" || eps[0].DNSName != "ep1.felhom.eu" || eps[0].WGPort != 443 || eps[0].TunnelSubnet != "10.78.0.0/24" || eps[0].PBSTunnelIP != "10.78.0.1" { t.Errorf("stored endpoint = %+v", eps[0]) } // The new card renders. if !strings.Contains(renderOffsite(t, s), `data-endpoint-id="ep1"`) { t.Error("new endpoint card missing from the page") } } // E6 — every invalid form field → 400 and NOTHING stored. func TestOffsiteEndpointSave_Validation(t *testing.T) { s, st := newTestServer(t) cases := []struct { name string field string value string }{ {"bad cidr", "tunnel_subnet", "10.78.0.0/240"}, {"not a cidr", "tunnel_subnet", "banana"}, {"pbs ip outside subnet", "pbs_tunnel_ip", "10.99.0.1"}, {"pbs ip garbage", "pbs_tunnel_ip", "not-an-ip"}, {"port zero", "wg_port", "0"}, {"port too big", "wg_port", "70000"}, {"port garbage", "wg_port", "abc"}, {"empty pubkey", "server_pubkey", ""}, {"bad endpoint id", "endpoint_id", "EP 1!"}, {"empty endpoint id", "endpoint_id", ""}, {"empty dns", "dns_name", ""}, } for _, c := range cases { form := validEndpointForm("ep1") form.Set(c.field, c.value) rr := postOffsiteEndpoint(t, s, form) if rr.Code != http.StatusBadRequest { t.Errorf("%s: status = %d, want 400", c.name, rr.Code) } if eps, _ := st.ListWGEndpoints(); len(eps) != 0 { t.Fatalf("%s: endpoint STORED despite invalid input: %+v", c.name, eps) } } } // E3 — editing ep0's tunnel_subnet while peers are allocated inside the CURRENT subnet is // refused with 409 and the stored subnet is UNCHANGED. // RED-PROOF 4: removing the subnet-change guard makes this FAIL (subnet rewritten). func TestOffsiteEndpointSave_SubnetChangeGuard(t *testing.T) { s, st := newTestServer(t) if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443, ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1", }); err != nil { t.Fatal(err) } if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "pin"); err != nil { t.Fatal(err) // 10.77.0.2 — inside the current subnet } form := validEndpointForm("ep0") form.Set("tunnel_subnet", "10.99.0.0/24") form.Set("pbs_tunnel_ip", "10.99.0.1") rr := postOffsiteEndpoint(t, s, form) if rr.Code != http.StatusConflict { t.Fatalf("subnet change with peers = %d, want 409", rr.Code) } ep, err := st.GetWGEndpoint() if err != nil { t.Fatal(err) } if ep.TunnelSubnet != "10.77.0.0/24" { t.Errorf("stored subnet = %s — the refused edit CHANGED it", ep.TunnelSubnet) } // Non-subnet edits stay allowed while peers exist (e.g. a port move). form = validEndpointForm("ep0") form.Set("tunnel_subnet", "10.77.0.0/24") form.Set("pbs_tunnel_ip", "10.77.0.1") form.Set("wg_port", "51820") if rr := postOffsiteEndpoint(t, s, form); rr.Code != http.StatusSeeOther { t.Errorf("same-subnet edit = %d, want 303", rr.Code) } if ep, _ := st.GetWGEndpoint(); ep.WGPort != 51820 { t.Errorf("port edit not stored: %+v", ep) } } // E4 — deleting an endpoint with peers allocated in its subnet is refused with 409 and the // row survives; a peer-free endpoint deletes cleanly; an unknown one 404s. // RED-PROOF 3: removing the peers-in-subnet guard makes this FAIL (endpoint deleted). func TestOffsiteEndpointDelete_Guard(t *testing.T) { s, st := newTestServer(t) if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep0", DNSName: "ep0.felhom.eu", WGPort: 443, ServerPubkey: "CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=", TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1", }); err != nil { t.Fatal(err) } if err := st.SetWGEndpoint(&store.WGEndpoint{ EndpointID: "ep1", DNSName: "ep1.felhom.eu", WGPort: 443, ServerPubkey: "CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=", TunnelSubnet: "10.78.0.0/24", PBSTunnelIP: "10.78.0.1", }); err != nil { t.Fatal(err) } if _, _, err := st.AddWGPeer("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", "", "pin"); err != nil { t.Fatal(err) // 10.77.0.2 — pins ep0 } del := func(id string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/offsite/endpoints/"+id+"/delete", nil) rr := httptest.NewRecorder() s.handleOffsiteEndpointDelete(rr, req, id) return rr } if rr := del("ep0"); rr.Code != http.StatusConflict { t.Fatalf("delete with peers = %d, want 409", rr.Code) } if eps, _ := st.ListWGEndpoints(); len(eps) != 2 { t.Fatalf("endpoint count after refused delete = %d, want 2 (ep0 must survive)", len(eps)) } if rr := del("ep1"); rr.Code != http.StatusSeeOther { t.Errorf("peer-free delete = %d, want 303", rr.Code) } if eps, _ := st.ListWGEndpoints(); len(eps) != 1 || eps[0].EndpointID != "ep0" { t.Errorf("endpoints after ep1 delete = %+v, want [ep0]", eps) } if rr := del("ghost"); rr.Code != http.StatusNotFound { t.Errorf("unknown endpoint delete = %d, want 404", rr.Code) } }