API client: - NewClient now accepts tokenVersion (0 = auto-detect from token prefix) - tokenVersion stored on Client, used for 403 error hints - All callers pass cfg.NetBox.TokenVersion Tests added: - netbox: TokenVersion, NewClient auto-detect, explicit version, 403 v1 hint, 403 v2 no-hint, Authorization header verification - config: token_version preserved/auto-detected, defaults, missing file, invalid YAML, Path() - setup: save roundtrip, file permissions (0600), empty fields omitted, dir creation, full save→load roundtrip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+103
-10
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -58,7 +59,7 @@ func TestSearch_ReturnsBothDevicesAndVMs(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
results, err := c.Search(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
@@ -87,7 +88,7 @@ func TestSearch_MapsKindCorrectly(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
results, _ := c.Search(context.Background(), "")
|
||||
|
||||
for _, r := range results {
|
||||
@@ -113,7 +114,7 @@ func TestSearch_StripsPrefixFromPrimaryIP(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
results, _ := c.Search(context.Background(), "host")
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected at least one result")
|
||||
@@ -138,7 +139,7 @@ func TestSearch_TagsAreMapped(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
results, _ := c.Search(context.Background(), "")
|
||||
if len(results[0].Tags) != 2 {
|
||||
t.Errorf("tags: got %v, want [prod mgmt]", results[0].Tags)
|
||||
@@ -159,7 +160,7 @@ func TestSearch_PartialFailure_ReturnsAvailableResults(t *testing.T) {
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
results, err := c.Search(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("partial failure should not return error, got: %v", err)
|
||||
@@ -177,7 +178,7 @@ func TestSearch_BothFail_ReturnsError(t *testing.T) {
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
_, err := c.Search(context.Background(), "")
|
||||
if err == nil {
|
||||
t.Error("both endpoints failing should return an error")
|
||||
@@ -190,7 +191,7 @@ func TestGetIPs_Device(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
ips, err := c.GetIPs(context.Background(), HostEntry{ID: 1, Kind: "device"})
|
||||
if err != nil {
|
||||
t.Fatalf("GetIPs: %v", err)
|
||||
@@ -209,7 +210,7 @@ func TestGetIPs_VM(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
ips, err := c.GetIPs(context.Background(), HostEntry{ID: 2, Kind: "vm"})
|
||||
if err != nil {
|
||||
t.Fatalf("GetIPs: %v", err)
|
||||
@@ -220,7 +221,7 @@ func TestGetIPs_VM(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetIPs_UnknownKind(t *testing.T) {
|
||||
c := NewClient("http://localhost", "token")
|
||||
c := NewClient("http://localhost", "token", 0)
|
||||
_, err := c.GetIPs(context.Background(), HostEntry{ID: 1, Kind: "unknown"})
|
||||
if err == nil {
|
||||
t.Error("unknown kind should return an error")
|
||||
@@ -233,7 +234,7 @@ func TestGetIPsWithFilter(t *testing.T) {
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "token")
|
||||
c := NewClient(srv.URL, "token", 0)
|
||||
ips, err := c.GetIPsWithFilter(context.Background(), "device_id=1&interface_name=mgmt0")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIPsWithFilter: %v", err)
|
||||
@@ -243,6 +244,98 @@ func TestGetIPsWithFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
token string
|
||||
want int
|
||||
}{
|
||||
{"nbt_abc123", 2},
|
||||
{"nbt_", 2},
|
||||
{"abc123def456", 1},
|
||||
{"", 1},
|
||||
{"Token abc", 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := TokenVersion(tt.token); got != tt.want {
|
||||
t.Errorf("TokenVersion(%q) = %d, want %d", tt.token, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient_AutoDetectsVersion(t *testing.T) {
|
||||
c := NewClient("http://localhost", "nbt_secret", 0)
|
||||
if c.tokenVersion != 2 {
|
||||
t.Errorf("tokenVersion: got %d, want 2", c.tokenVersion)
|
||||
}
|
||||
|
||||
c2 := NewClient("http://localhost", "legacytoken", 0)
|
||||
if c2.tokenVersion != 1 {
|
||||
t.Errorf("tokenVersion: got %d, want 1", c2.tokenVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient_RespectsExplicitVersion(t *testing.T) {
|
||||
// Explicit version overrides auto-detection.
|
||||
c := NewClient("http://localhost", "legacytoken", 2)
|
||||
if c.tokenVersion != 2 {
|
||||
t.Errorf("tokenVersion: got %d, want 2", c.tokenVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func Test403_V1Token_HintsUpgrade(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "legacytoken", 1)
|
||||
_, err := c.Search(context.Background(), "host")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on 403")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "v1 token") {
|
||||
t.Errorf("expected v1 hint in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test403_V2Token_NoV1Hint(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "nbt_secret", 2)
|
||||
_, err := c.Search(context.Background(), "host")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on 403")
|
||||
}
|
||||
if strings.Contains(err.Error(), "v1 token") {
|
||||
t.Errorf("v1 hint should not appear for v2 token, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "check token permissions") {
|
||||
t.Errorf("expected permissions hint in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet_SendsAuthorizationHeader(t *testing.T) {
|
||||
var gotAuth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
b, _ := json.Marshal(deviceListResponse())
|
||||
w.Write(b)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "nbt_mytoken", 2)
|
||||
c.Search(context.Background(), "") //nolint:errcheck
|
||||
|
||||
want := "Token nbt_mytoken"
|
||||
if gotAuth != want {
|
||||
t.Errorf("Authorization header: got %q, want %q", gotAuth, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
Reference in New Issue
Block a user