mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1453 lines
36 KiB
Go
1453 lines
36 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_debug
|
|
|
|
package localapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/netmap"
|
|
)
|
|
|
|
// mockBackendForDebug implements the subset of LocalBackend methods needed for debug tests
|
|
type mockBackendForDebug struct {
|
|
ipnlocal.NoOpBackend
|
|
getEndpointChanges func(context.Context, netip.Addr) (any, error)
|
|
setComponentDebugLogging func(string, time.Time) error
|
|
debugRebind func() error
|
|
debugReSTUN func() error
|
|
debugNotify func(ipn.Notify)
|
|
debugRotateDiscoKey func() error
|
|
setDevStateStore func(key, value string) error
|
|
netMap *netmap.NetworkMap
|
|
controlKnobs *tailcfg.ControlKnobs
|
|
}
|
|
|
|
func (m *mockBackendForDebug) GetPeerEndpointChanges(ctx context.Context, ip netip.Addr) (any, error) {
|
|
if m.getEndpointChanges != nil {
|
|
return m.getEndpointChanges(ctx, ip)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) SetComponentDebugLogging(component string, until time.Time) error {
|
|
if m.setComponentDebugLogging != nil {
|
|
return m.setComponentDebugLogging(component, until)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) DebugRebind() error {
|
|
if m.debugRebind != nil {
|
|
return m.debugRebind()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) DebugReSTUN() error {
|
|
if m.debugReSTUN != nil {
|
|
return m.debugReSTUN()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) DebugNotify(n ipn.Notify) {
|
|
if m.debugNotify != nil {
|
|
m.debugNotify(n)
|
|
}
|
|
}
|
|
|
|
func (m *mockBackendForDebug) DebugRotateDiscoKey() error {
|
|
if m.debugRotateDiscoKey != nil {
|
|
return m.debugRotateDiscoKey()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) SetDevStateStore(key, value string) error {
|
|
if m.setDevStateStore != nil {
|
|
return m.setDevStateStore(key, value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockBackendForDebug) NetMap() *netmap.NetworkMap {
|
|
return m.netMap
|
|
}
|
|
|
|
func (m *mockBackendForDebug) ControlKnobs() *tailcfg.ControlKnobs {
|
|
if m.controlKnobs != nil {
|
|
return m.controlKnobs
|
|
}
|
|
return &tailcfg.ControlKnobs{}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_MissingIP tests missing IP parameter
|
|
func TestServeDebugPeerEndpointChanges_MissingIP(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "missing 'ip' parameter") {
|
|
t.Errorf("body = %q, want missing ip error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_InvalidIP tests invalid IP parameter
|
|
func TestServeDebugPeerEndpointChanges_InvalidIP(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=invalid", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "invalid IP") {
|
|
t.Errorf("body = %q, want invalid IP error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_Success tests successful endpoint changes retrieval
|
|
func TestServeDebugPeerEndpointChanges_Success(t *testing.T) {
|
|
testIP := netip.MustParseAddr("100.64.0.1")
|
|
mockChanges := map[string]interface{}{
|
|
"changes": []string{"endpoint1", "endpoint2"},
|
|
"count": 2,
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{
|
|
getEndpointChanges: func(ctx context.Context, ip netip.Addr) (any, error) {
|
|
if ip != testIP {
|
|
t.Errorf("ip = %v, want %v", ip, testIP)
|
|
}
|
|
return mockChanges, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=100.64.0.1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_PermissionDenied tests permission check
|
|
func TestServeDebugPeerEndpointChanges_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=100.64.0.1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_BackendError tests backend error handling
|
|
func TestServeDebugPeerEndpointChanges_BackendError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{
|
|
getEndpointChanges: func(ctx context.Context, ip netip.Addr) (any, error) {
|
|
return nil, fmt.Errorf("backend error")
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=100.64.0.1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_Success tests successful component logging
|
|
func TestServeComponentDebugLogging_Success(t *testing.T) {
|
|
componentSeen := ""
|
|
untilSeen := time.Time{}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setComponentDebugLogging: func(component string, until time.Time) error {
|
|
componentSeen = component
|
|
untilSeen = until
|
|
return nil
|
|
},
|
|
},
|
|
clock: tstest.Clock{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=magicsock&secs=60", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if componentSeen != "magicsock" {
|
|
t.Errorf("component = %q, want magicsock", componentSeen)
|
|
}
|
|
|
|
var result struct {
|
|
Error string
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
if result.Error != "" {
|
|
t.Errorf("error = %q, want empty", result.Error)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_PermissionDenied tests permission check
|
|
func TestServeComponentDebugLogging_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=test&secs=30", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_BackendError tests backend error handling
|
|
func TestServeComponentDebugLogging_BackendError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setComponentDebugLogging: func(component string, until time.Time) error {
|
|
return fmt.Errorf("logging error")
|
|
},
|
|
},
|
|
clock: tstest.Clock{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=test&secs=30", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var result struct {
|
|
Error string
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
if result.Error != "logging error" {
|
|
t.Errorf("error = %q, want 'logging error'", result.Error)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugRotateDiscoKey_Success tests successful disco key rotation
|
|
func TestServeDebugRotateDiscoKey_Success(t *testing.T) {
|
|
rotateCalled := false
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRotateDiscoKey: func() error {
|
|
rotateCalled = true
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugRotateDiscoKey(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if !rotateCalled {
|
|
t.Error("DebugRotateDiscoKey was not called")
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if body != "done\n" {
|
|
t.Errorf("body = %q, want 'done\\n'", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugRotateDiscoKey_PermissionDenied tests permission check
|
|
func TestServeDebugRotateDiscoKey_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugRotateDiscoKey(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugRotateDiscoKey_MethodNotAllowed tests POST requirement
|
|
func TestServeDebugRotateDiscoKey_MethodNotAllowed(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugRotateDiscoKey(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugRotateDiscoKey_BackendError tests backend error
|
|
func TestServeDebugRotateDiscoKey_BackendError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRotateDiscoKey: func() error {
|
|
return fmt.Errorf("rotation failed")
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugRotateDiscoKey(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusInternalServerError)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "rotation failed") {
|
|
t.Errorf("body = %q, want rotation error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDevSetStateStore_Success tests successful state store set
|
|
func TestServeDevSetStateStore_Success(t *testing.T) {
|
|
keySeen := ""
|
|
valueSeen := ""
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setDevStateStore: func(key, value string) error {
|
|
keySeen = key
|
|
valueSeen = value
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/dev-set-state-store?key=testkey&value=testvalue", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDevSetStateStore(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if keySeen != "testkey" {
|
|
t.Errorf("key = %q, want testkey", keySeen)
|
|
}
|
|
|
|
if valueSeen != "testvalue" {
|
|
t.Errorf("value = %q, want testvalue", valueSeen)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if body != "done\n" {
|
|
t.Errorf("body = %q, want 'done\\n'", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDevSetStateStore_PermissionDenied tests permission check
|
|
func TestServeDevSetStateStore_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/dev-set-state-store?key=test&value=test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDevSetStateStore(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDevSetStateStore_MethodNotAllowed tests POST requirement
|
|
func TestServeDevSetStateStore_MethodNotAllowed(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/dev-set-state-store", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDevSetStateStore(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// TestServeDevSetStateStore_BackendError tests backend error
|
|
func TestServeDevSetStateStore_BackendError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setDevStateStore: func(key, value string) error {
|
|
return fmt.Errorf("store error")
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/dev-set-state-store?key=test&value=test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDevSetStateStore(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterRules_Success tests successful packet filter rules retrieval
|
|
func TestServeDebugPacketFilterRules_Success(t *testing.T) {
|
|
testRules := []tailcfg.FilterRule{
|
|
{SrcIPs: []string{"100.64.0.0/10"}},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
netMap: &netmap.NetworkMap{
|
|
PacketFilterRules: testRules,
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-rules", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterRules(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterRules_NoNetmap tests nil netmap
|
|
func TestServeDebugPacketFilterRules_NoNetmap(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-rules", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterRules(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "no netmap") {
|
|
t.Errorf("body = %q, want no netmap error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterRules_PermissionDenied tests permission check
|
|
func TestServeDebugPacketFilterRules_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-rules", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterRules(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterMatches_Success tests successful packet filter matches retrieval
|
|
func TestServeDebugPacketFilterMatches_Success(t *testing.T) {
|
|
testFilter := []tailcfg.FilterRule{
|
|
{SrcIPs: []string{"*"}},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
netMap: &netmap.NetworkMap{
|
|
PacketFilter: testFilter,
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-matches", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterMatches(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterMatches_NoNetmap tests nil netmap
|
|
func TestServeDebugPacketFilterMatches_NoNetmap(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-matches", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterMatches(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterMatches_PermissionDenied tests permission check
|
|
func TestServeDebugPacketFilterMatches_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-matches", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterMatches(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugOptionalFeatures_Success tests optional features endpoint
|
|
func TestServeDebugOptionalFeatures_Success(t *testing.T) {
|
|
h := &Handler{
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-optional-features", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugOptionalFeatures(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
|
|
// Response should be valid JSON with Features field
|
|
var result struct {
|
|
Features []string
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_InvalidJSON tests invalid JSON body
|
|
func TestServeDebugLog_InvalidJSON(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader([]byte("invalid json")))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "invalid JSON") {
|
|
t.Errorf("body = %q, want invalid JSON error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_Success tests successful log upload
|
|
func TestServeDebugLog_Success(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
logReq := struct {
|
|
Lines []string
|
|
Prefix string
|
|
}{
|
|
Lines: []string{"test log line 1", "test log line 2"},
|
|
Prefix: "test-prefix",
|
|
}
|
|
|
|
body, _ := json.Marshal(logReq)
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_PermissionDenied tests permission check
|
|
func TestServeDebugLog_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_MethodNotAllowed tests POST requirement
|
|
func TestServeDebugLog_MethodNotAllowed(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-log", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_DefaultPrefix tests default prefix when not provided
|
|
func TestServeDebugLog_DefaultPrefix(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
logReq := struct {
|
|
Lines []string
|
|
Prefix string
|
|
}{
|
|
Lines: []string{"test line"},
|
|
// Prefix intentionally empty
|
|
}
|
|
|
|
body, _ := json.Marshal(logReq)
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_EmptyLines tests empty lines array
|
|
func TestServeDebugLog_EmptyLines(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
logReq := struct {
|
|
Lines []string
|
|
Prefix string
|
|
}{
|
|
Lines: []string{},
|
|
Prefix: "test",
|
|
}
|
|
|
|
body, _ := json.Marshal(logReq)
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_MissingAction tests missing action parameter
|
|
func TestServeDebug_MissingAction(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "missing parameter 'action'") {
|
|
t.Errorf("body = %q, want missing action error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_UnknownAction tests unknown action
|
|
func TestServeDebug_UnknownAction(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=unknown-action", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "unknown action") {
|
|
t.Errorf("body = %q, want unknown action error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_PermissionDenied tests permission check
|
|
func TestServeDebug_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=rebind", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_MethodNotAllowed tests POST requirement
|
|
func TestServeDebug_MethodNotAllowed(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_RebindAction tests rebind action
|
|
func TestServeDebug_RebindAction(t *testing.T) {
|
|
rebindCalled := false
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRebind: func() error {
|
|
rebindCalled = true
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=rebind", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if !rebindCalled {
|
|
t.Error("DebugRebind was not called")
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if body != "done\n" {
|
|
t.Errorf("body = %q, want 'done\\n'", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_RestunAction tests restun action
|
|
func TestServeDebug_RestunAction(t *testing.T) {
|
|
restunCalled := false
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugReSTUN: func() error {
|
|
restunCalled = true
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=restun", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if !restunCalled {
|
|
t.Error("DebugReSTUN was not called")
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_NotifyAction tests notify action with JSON body
|
|
func TestServeDebug_NotifyAction(t *testing.T) {
|
|
var notifySeen *ipn.Notify
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugNotify: func(n ipn.Notify) {
|
|
notifySeen = &n
|
|
},
|
|
},
|
|
}
|
|
|
|
notify := ipn.Notify{
|
|
State: ptr(ipn.Running),
|
|
}
|
|
body, _ := json.Marshal(notify)
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug", bytes.NewReader(body))
|
|
req.Header.Set("Debug-Action", "notify")
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if notifySeen == nil {
|
|
t.Fatal("DebugNotify was not called")
|
|
}
|
|
|
|
if notifySeen.State == nil || *notifySeen.State != ipn.Running {
|
|
t.Errorf("notify state = %v, want Running", notifySeen.State)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_NotifyActionInvalidJSON tests notify with invalid JSON
|
|
func TestServeDebug_NotifyActionInvalidJSON(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug", bytes.NewReader([]byte("invalid")))
|
|
req.Header.Set("Debug-Action", "notify")
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_RebindError tests rebind error handling
|
|
func TestServeDebug_RebindError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRebind: func() error {
|
|
return fmt.Errorf("rebind failed")
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=rebind", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "rebind failed") {
|
|
t.Errorf("body = %q, want rebind error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_RotateDiscoKeyAction tests rotate-disco-key action
|
|
func TestServeDebug_RotateDiscoKeyAction(t *testing.T) {
|
|
rotateCalled := false
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRotateDiscoKey: func() error {
|
|
rotateCalled = true
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if !rotateCalled {
|
|
t.Error("DebugRotateDiscoKey was not called")
|
|
}
|
|
}
|
|
|
|
// TestServeDebug_RotateDiscoKeyError tests rotate-disco-key error
|
|
func TestServeDebug_RotateDiscoKeyError(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRotateDiscoKey: func() error {
|
|
return fmt.Errorf("rotation error")
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug?action=rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebug(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
// ptr is a helper to create a pointer to a value
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
// TestDebugEventError_JSON tests debugEventError JSON encoding
|
|
func TestDebugEventError_JSON(t *testing.T) {
|
|
err := debugEventError{Error: "test error"}
|
|
|
|
data, jsonErr := json.Marshal(err)
|
|
if jsonErr != nil {
|
|
t.Fatalf("failed to marshal: %v", jsonErr)
|
|
}
|
|
|
|
var decoded debugEventError
|
|
if jsonErr := json.Unmarshal(data, &decoded); jsonErr != nil {
|
|
t.Fatalf("failed to unmarshal: %v", jsonErr)
|
|
}
|
|
|
|
if decoded.Error != "test error" {
|
|
t.Errorf("error = %q, want 'test error'", decoded.Error)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_IPv6 tests IPv6 address
|
|
func TestServeDebugPeerEndpointChanges_IPv6(t *testing.T) {
|
|
testIP := netip.MustParseAddr("fd7a:115c::1")
|
|
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{
|
|
getEndpointChanges: func(ctx context.Context, ip netip.Addr) (any, error) {
|
|
if ip != testIP {
|
|
t.Errorf("ip = %v, want %v", ip, testIP)
|
|
}
|
|
return map[string]string{"status": "ok"}, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=fd7a:115c::1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_ZeroSeconds tests zero seconds duration
|
|
func TestServeComponentDebugLogging_ZeroSeconds(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=test&secs=0", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_InvalidSeconds tests invalid seconds value
|
|
func TestServeComponentDebugLogging_InvalidSeconds(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
}
|
|
|
|
// Invalid secs value should default to 0
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=test&secs=invalid", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterRules_EmptyRules tests empty packet filter rules
|
|
func TestServeDebugPacketFilterRules_EmptyRules(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
netMap: &netmap.NetworkMap{
|
|
PacketFilterRules: []tailcfg.FilterRule{},
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-rules", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterRules(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
// Should return valid JSON even for empty rules
|
|
var rules []tailcfg.FilterRule
|
|
if err := json.NewDecoder(w.Body).Decode(&rules); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
if len(rules) != 0 {
|
|
t.Errorf("len(rules) = %d, want 0", len(rules))
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPacketFilterMatches_EmptyFilter tests empty packet filter
|
|
func TestServeDebugPacketFilterMatches_EmptyFilter(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
netMap: &netmap.NetworkMap{
|
|
PacketFilter: []tailcfg.FilterRule{},
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-packet-filter-matches", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPacketFilterMatches(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_LargeLogRequest tests large number of log lines
|
|
func TestServeDebugLog_LargeLogRequest(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: func(format string, args ...any) {}, // Discard logs
|
|
}
|
|
|
|
// Create 100 log lines
|
|
lines := make([]string, 100)
|
|
for i := range lines {
|
|
lines[i] = fmt.Sprintf("log line %d", i)
|
|
}
|
|
|
|
logReq := struct {
|
|
Lines []string
|
|
Prefix string
|
|
}{
|
|
Lines: lines,
|
|
Prefix: "large-test",
|
|
}
|
|
|
|
body, _ := json.Marshal(logReq)
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugOptionalFeatures_ResponseStructure tests response structure
|
|
func TestServeDebugOptionalFeatures_ResponseStructure(t *testing.T) {
|
|
h := &Handler{
|
|
b: &mockBackendForDebug{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-optional-features", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugOptionalFeatures(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
// Verify response can be decoded
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
// Should have Features field
|
|
if _, ok := response["Features"]; !ok {
|
|
t.Error("response missing 'Features' field")
|
|
}
|
|
}
|
|
|
|
// TestServeDevSetStateStore_EmptyValue tests empty value parameter
|
|
func TestServeDevSetStateStore_EmptyValue(t *testing.T) {
|
|
keySeen := ""
|
|
valueSeen := "not-empty"
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setDevStateStore: func(key, value string) error {
|
|
keySeen = key
|
|
valueSeen = value
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/dev-set-state-store?key=testkey&value=", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDevSetStateStore(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if keySeen != "testkey" {
|
|
t.Errorf("key = %q, want testkey", keySeen)
|
|
}
|
|
|
|
if valueSeen != "" {
|
|
t.Errorf("value = %q, want empty", valueSeen)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_ContextCancellation tests context cancellation
|
|
func TestServeDebugPeerEndpointChanges_ContextCancellation(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{
|
|
getEndpointChanges: func(ctx context.Context, ip netip.Addr) (any, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=100.64.0.1", nil)
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d (context cancelled)", w.Code, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugLog_MultilineMessages tests log lines with newlines
|
|
func TestServeDebugLog_MultilineMessages(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{},
|
|
clock: tstest.Clock{},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
logReq := struct {
|
|
Lines []string
|
|
Prefix string
|
|
}{
|
|
Lines: []string{
|
|
"line 1\nwith newline",
|
|
"line 2\twith tab",
|
|
"line 3 normal",
|
|
},
|
|
Prefix: "multiline-test",
|
|
}
|
|
|
|
body, _ := json.Marshal(logReq)
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-log", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugLog(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugRotateDiscoKey_MultipleRotations tests multiple sequential rotations
|
|
func TestServeDebugRotateDiscoKey_MultipleRotations(t *testing.T) {
|
|
rotateCount := 0
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
debugRotateDiscoKey: func() error {
|
|
rotateCount++
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
// Rotate 3 times
|
|
for i := 0; i < 3; i++ {
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-rotate-disco-key", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugRotateDiscoKey(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("rotation %d: status = %d, want %d", i, w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
if rotateCount != 3 {
|
|
t.Errorf("rotateCount = %d, want 3", rotateCount)
|
|
}
|
|
}
|
|
|
|
// TestServeComponentDebugLogging_EmptyComponent tests empty component name
|
|
func TestServeComponentDebugLogging_EmptyComponent(t *testing.T) {
|
|
componentSeen := "not-empty"
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDebug{
|
|
setComponentDebugLogging: func(component string, until time.Time) error {
|
|
componentSeen = component
|
|
return nil
|
|
},
|
|
},
|
|
clock: tstest.Clock{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/component-debug-logging?component=&secs=30", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveComponentDebugLogging(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if componentSeen != "" {
|
|
t.Errorf("component = %q, want empty", componentSeen)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugPeerEndpointChanges_NilResult tests nil result from backend
|
|
func TestServeDebugPeerEndpointChanges_NilResult(t *testing.T) {
|
|
h := &Handler{
|
|
PermitRead: true,
|
|
b: &mockBackendForDebug{
|
|
getEndpointChanges: func(ctx context.Context, ip netip.Addr) (any, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-peer-endpoint-changes?ip=100.64.0.1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugPeerEndpointChanges(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
// Should encode null in JSON
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "null") {
|
|
t.Errorf("body = %q, want null", body)
|
|
}
|
|
}
|