package api // Group B — WG admin API auth + validation (Scenario C). Non-hollow: asserts store effects and // fake-syncer call counts, not just statuses. import ( "bytes" "context" "encoding/base64" "encoding/json" "net/http" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // fakeWGSyncer counts SyncNow/Trigger calls; err scripts the SyncNow result. type fakeWGSyncer struct { syncCalls int triggerCalls int err error } func (f *fakeWGSyncer) SyncNow(ctx context.Context) error { f.syncCalls++; return f.err } func (f *fakeWGSyncer) Trigger() { f.triggerCalls++ } // testPK returns a VALID WG-shaped pubkey (44 std-base64 chars, 32 bytes) unique per fill byte. func testPK(fill byte) string { return base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{fill}, 32)) } func putTestEndpoint(t *testing.T, h *Handler) { t.Helper() body := `{"dns_name":"ep0.example","wg_port":443,"server_pubkey":"` + testPK(9) + `",` + `"tunnel_subnet":"10.77.0.0/24","pbs_tunnel_ip":"10.77.0.1"}` rr := do(h, http.MethodPut, "/admin/wg/endpoint", globalKey, body) if rr.Code != http.StatusOK { t.Fatalf("PUT endpoint = %d: %s", rr.Code, rr.Body.String()) } } func TestWGPeers_PerHostKeyForbidden(t *testing.T) { h, st, _ := newTestHandler(t) st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"}) putTestEndpoint(t, h) fake := &fakeWGSyncer{} h.SetWGSyncer(fake) for _, m := range []string{http.MethodPost, http.MethodDelete, http.MethodGet} { rr := do(h, m, "/admin/wg/peers", "HKEY", `{"pubkey":"`+testPK(1)+`"}`) if rr.Code != http.StatusForbidden { t.Errorf("%s with per-host key = %d, want 403", m, rr.Code) } } rr := do(h, http.MethodPut, "/admin/wg/endpoint", "HKEY", `{}`) if rr.Code != http.StatusForbidden { t.Errorf("PUT endpoint with per-host key = %d, want 403", rr.Code) } peers, _ := st.ListWGPeers() if len(peers) != 0 { t.Errorf("rows created despite 403: %d", len(peers)) } if fake.syncCalls != 0 { t.Errorf("sync ran despite 403: %d calls", fake.syncCalls) } } func TestWGPeers_AddHappyPath(t *testing.T) { h, st, _ := newTestHandler(t) putTestEndpoint(t, h) fake := &fakeWGSyncer{} h.SetWGSyncer(fake) rr := do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`","note":"test"}`) if rr.Code != http.StatusOK { t.Fatalf("POST = %d: %s", rr.Code, rr.Body.String()) } var resp struct { Pubkey string `json:"pubkey"` AssignedIP string `json:"assigned_ip"` Existed bool `json:"existed"` Sync string `json:"sync"` } json.Unmarshal(rr.Body.Bytes(), &resp) if resp.AssignedIP != "10.77.0.2/32" || resp.Existed || resp.Sync != "ok" { t.Errorf("resp = %+v, want .2/32 existed=false sync=ok", resp) } if fake.syncCalls != 1 { t.Errorf("sync calls = %d, want 1", fake.syncCalls) } peers, _ := st.ListWGPeers() if len(peers) != 1 || peers[0].AssignedIP != "10.77.0.2" || peers[0].Note != "test" { t.Errorf("stored peers = %+v", peers) } } func TestWGPeers_SyncDeferredOnPushFailure(t *testing.T) { h, _, _ := newTestHandler(t) putTestEndpoint(t, h) fake := &fakeWGSyncer{err: context.DeadlineExceeded} h.SetWGSyncer(fake) rr := do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`"}`) if rr.Code != http.StatusOK { t.Fatalf("POST = %d (DB write is source of truth; push failure must not fail the request)", rr.Code) } var resp struct { Sync string `json:"sync"` } json.Unmarshal(rr.Body.Bytes(), &resp) if len(resp.Sync) < 8 || resp.Sync[:8] != "deferred" { t.Errorf("sync = %q, want deferred:...", resp.Sync) } if fake.triggerCalls != 1 { t.Errorf("Trigger calls = %d, want 1 (reconciler retry requested)", fake.triggerCalls) } } func TestWGPeers_SyncDisabledWhenUnwired(t *testing.T) { h, _, _ := newTestHandler(t) putTestEndpoint(t, h) // no SetWGSyncer — nil seam rr := do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`"}`) if rr.Code != http.StatusOK { t.Fatalf("POST = %d", rr.Code) } var resp struct { Sync string `json:"sync"` } json.Unmarshal(rr.Body.Bytes(), &resp) if resp.Sync != "disabled" { t.Errorf("sync = %q, want disabled", resp.Sync) } } func TestWGPeers_BadPubkeyRejected(t *testing.T) { h, st, _ := newTestHandler(t) putTestEndpoint(t, h) bad := []string{ "not-base64", base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 16)), // 24 chars "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", // 44 chars, not base64 base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 33)), // 44 chars but 33 bytes } for _, pk := range bad { rr := do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+pk+`"}`) if rr.Code != http.StatusBadRequest { t.Errorf("pubkey %q = %d, want 400", pk, rr.Code) } } peers, _ := st.ListWGPeers() if len(peers) != 0 { t.Errorf("allocation happened for bad pubkey: %+v", peers) } } func TestWGPeers_NoEndpointIs409(t *testing.T) { h, _, _ := newTestHandler(t) rr := do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`"}`) if rr.Code != http.StatusConflict { t.Errorf("POST without endpoint = %d, want 409", rr.Code) } } func TestWGEndpoint_PBSOutsideSubnetRejected(t *testing.T) { h, _, _ := newTestHandler(t) body := `{"dns_name":"ep0.example","wg_port":443,"server_pubkey":"` + testPK(9) + `",` + `"tunnel_subnet":"10.77.0.0/24","pbs_tunnel_ip":"10.88.0.1"}` rr := do(h, http.MethodPut, "/admin/wg/endpoint", globalKey, body) if rr.Code != http.StatusBadRequest { t.Errorf("PUT with pbs outside subnet = %d, want 400", rr.Code) } rr = do(h, http.MethodGet, "/admin/wg/endpoint", globalKey, "") if rr.Code != http.StatusNotFound { t.Errorf("GET after rejected PUT = %d, want 404 (nothing stored)", rr.Code) } } func TestWGPeers_DeleteUnknown404NoSync(t *testing.T) { h, _, _ := newTestHandler(t) putTestEndpoint(t, h) fake := &fakeWGSyncer{} h.SetWGSyncer(fake) rr := do(h, http.MethodDelete, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(7)+`"}`) if rr.Code != http.StatusNotFound { t.Errorf("DELETE unknown = %d, want 404", rr.Code) } if fake.syncCalls != 0 { t.Errorf("sync ran on 404 delete: %d", fake.syncCalls) } } func TestWGPeers_DeleteKnownRemovesAndSyncs(t *testing.T) { h, st, _ := newTestHandler(t) putTestEndpoint(t, h) fake := &fakeWGSyncer{} h.SetWGSyncer(fake) do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`"}`) do(h, http.MethodPost, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(2)+`"}`) rr := do(h, http.MethodDelete, "/admin/wg/peers", globalKey, `{"pubkey":"`+testPK(1)+`"}`) if rr.Code != http.StatusOK { t.Fatalf("DELETE = %d: %s", rr.Code, rr.Body.String()) } peers, _ := st.ListWGPeers() if len(peers) != 1 || peers[0].Pubkey != testPK(2) { t.Errorf("peers after delete = %+v, want only p2", peers) } if fake.syncCalls != 3 { // 2 adds + 1 delete t.Errorf("sync calls = %d, want 3", fake.syncCalls) } }