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.
tailscale/client/local/syspolicy_test.go

382 lines
8.5 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_syspolicy
package local
import (
"encoding/json"
"testing"
"tailscale.com/util/syspolicy/setting"
)
// TestGetEffectivePolicy_ScopeMarshaling tests policy scope marshaling
func TestGetEffectivePolicy_ScopeMarshaling(t *testing.T) {
tests := []struct {
name string
scope mockPolicyScope
wantBytes string
}{
{
name: "device_scope",
scope: mockPolicyScope{text: "device"},
wantBytes: "device",
},
{
name: "user_scope",
scope: mockPolicyScope{text: "user"},
wantBytes: "user",
},
{
name: "empty_scope",
scope: mockPolicyScope{text: ""},
wantBytes: "",
},
{
name: "custom_scope",
scope: mockPolicyScope{text: "custom-scope-123"},
wantBytes: "custom-scope-123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.scope.MarshalText()
if err != nil {
t.Fatalf("MarshalText error: %v", err)
}
if string(data) != tt.wantBytes {
t.Errorf("marshaled = %q, want %q", string(data), tt.wantBytes)
}
})
}
}
// mockPolicyScope implements setting.PolicyScope for testing
type mockPolicyScope struct {
text string
err error
}
func (m mockPolicyScope) MarshalText() ([]byte, error) {
if m.err != nil {
return nil, m.err
}
return []byte(m.text), nil
}
// TestGetEffectivePolicy_ScopeMarshalError tests error handling
func TestGetEffectivePolicy_ScopeMarshalError(t *testing.T) {
scope := mockPolicyScope{
text: "",
err: &mockError{msg: "marshal failed"},
}
_, err := scope.MarshalText()
if err == nil {
t.Error("expected marshal error, got nil")
}
if err.Error() != "marshal failed" {
t.Errorf("error message = %q, want %q", err.Error(), "marshal failed")
}
}
type mockError struct {
msg string
}
func (e *mockError) Error() string {
return e.msg
}
// TestReloadEffectivePolicy_URLConstruction tests URL path construction
func TestReloadEffectivePolicy_URLConstruction(t *testing.T) {
tests := []struct {
name string
scope mockPolicyScope
wantPath string
}{
{
name: "device_scope_path",
scope: mockPolicyScope{text: "device"},
wantPath: "/localapi/v0/policy/device",
},
{
name: "user_scope_path",
scope: mockPolicyScope{text: "user"},
wantPath: "/localapi/v0/policy/user",
},
{
name: "custom_scope_path",
scope: mockPolicyScope{text: "custom"},
wantPath: "/localapi/v0/policy/custom",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scopeID, err := tt.scope.MarshalText()
if err != nil {
t.Fatalf("MarshalText error: %v", err)
}
path := "/localapi/v0/policy/" + string(scopeID)
if path != tt.wantPath {
t.Errorf("path = %q, want %q", path, tt.wantPath)
}
})
}
}
// TestPolicySnapshot_JSONEncoding tests Snapshot JSON handling
func TestPolicySnapshot_JSONEncoding(t *testing.T) {
tests := []struct {
name string
snapshot *setting.Snapshot
wantErr bool
}{
{
name: "empty_snapshot",
snapshot: &setting.Snapshot{},
wantErr: false,
},
{
name: "nil_snapshot",
snapshot: nil,
wantErr: false, // JSON can encode nil
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.snapshot)
if tt.wantErr && err == nil {
t.Error("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
if !tt.wantErr && len(data) == 0 {
t.Error("encoded data should not be empty")
}
// Verify it can be decoded
if !tt.wantErr {
var decoded setting.Snapshot
if err := json.Unmarshal(data, &decoded); err != nil {
t.Errorf("decode error: %v", err)
}
}
})
}
}
// TestPolicyScope_SpecialCharacters tests scope IDs with special characters
func TestPolicyScope_SpecialCharacters(t *testing.T) {
tests := []struct {
name string
scope mockPolicyScope
valid bool
}{
{
name: "alphanumeric",
scope: mockPolicyScope{text: "scope123"},
valid: true,
},
{
name: "with_hyphen",
scope: mockPolicyScope{text: "scope-123"},
valid: true,
},
{
name: "with_underscore",
scope: mockPolicyScope{text: "scope_123"},
valid: true,
},
{
name: "with_dot",
scope: mockPolicyScope{text: "scope.123"},
valid: true,
},
{
name: "with_slash",
scope: mockPolicyScope{text: "scope/123"},
valid: true, // Marshaling succeeds, but may need URL encoding
},
{
name: "with_space",
scope: mockPolicyScope{text: "scope 123"},
valid: true, // Marshaling succeeds, but may need URL encoding
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.scope.MarshalText()
if err != nil {
if tt.valid {
t.Errorf("unexpected error for valid scope: %v", err)
}
return
}
if !tt.valid {
t.Error("expected error for invalid scope")
}
// Verify round-trip
if string(data) != tt.scope.text {
t.Errorf("round-trip failed: got %q, want %q", string(data), tt.scope.text)
}
})
}
}
// TestPolicyScope_EdgeCases tests edge cases in scope handling
func TestPolicyScope_EdgeCases(t *testing.T) {
tests := []struct {
name string
scope mockPolicyScope
}{
{
name: "very_long_scope",
scope: mockPolicyScope{text: string(make([]byte, 1000))},
},
{
name: "unicode_scope",
scope: mockPolicyScope{text: "scope-日本語-中文"},
},
{
name: "only_numbers",
scope: mockPolicyScope{text: "12345"},
},
{
name: "single_character",
scope: mockPolicyScope{text: "a"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.scope.MarshalText()
if err != nil {
t.Errorf("MarshalText error: %v", err)
return
}
if len(data) == 0 {
t.Error("marshaled data should not be empty")
}
// Verify it matches input
if string(data) != tt.scope.text {
t.Error("marshaled data doesn't match input")
}
})
}
}
// TestGetEffectivePolicy_HTTPMethod tests that GET is used
func TestGetEffectivePolicy_HTTPMethod(t *testing.T) {
// GetEffectivePolicy uses lc.get200() which should use GET method
// This is a structural test to verify the API design
scope := mockPolicyScope{text: "device"}
scopeID, err := scope.MarshalText()
if err != nil {
t.Fatalf("MarshalText error: %v", err)
}
expectedPath := "/localapi/v0/policy/" + string(scopeID)
if expectedPath != "/localapi/v0/policy/device" {
t.Errorf("path = %q, want /localapi/v0/policy/device", expectedPath)
}
}
// TestReloadEffectivePolicy_HTTPMethod tests that POST is used
func TestReloadEffectivePolicy_HTTPMethod(t *testing.T) {
// ReloadEffectivePolicy uses lc.send() with POST method
// This is a structural test to verify the API design
scope := mockPolicyScope{text: "user"}
scopeID, err := scope.MarshalText()
if err != nil {
t.Fatalf("MarshalText error: %v", err)
}
expectedPath := "/localapi/v0/policy/" + string(scopeID)
if expectedPath != "/localapi/v0/policy/user" {
t.Errorf("path = %q, want /localapi/v0/policy/user", expectedPath)
}
// ReloadEffectivePolicy should send http.NoBody with POST
// (structural test - actual HTTP testing requires full client setup)
}
// TestPolicySnapshot_Decoding tests decoding various snapshot formats
func TestPolicySnapshot_Decoding(t *testing.T) {
tests := []struct {
name string
json string
wantErr bool
}{
{
name: "empty_object",
json: `{}`,
wantErr: false,
},
{
name: "null",
json: `null`,
wantErr: false,
},
{
name: "invalid_json",
json: `{invalid}`,
wantErr: true,
},
{
name: "array_instead_of_object",
json: `[]`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var snapshot setting.Snapshot
err := json.Unmarshal([]byte(tt.json), &snapshot)
if tt.wantErr && err == nil {
t.Error("expected decode error, got nil")
}
if !tt.wantErr && err != nil {
t.Errorf("unexpected decode error: %v", err)
}
})
}
}
// TestPolicyScopeEquality tests scope comparison
func TestPolicyScopeEquality(t *testing.T) {
scope1 := mockPolicyScope{text: "device"}
scope2 := mockPolicyScope{text: "device"}
scope3 := mockPolicyScope{text: "user"}
data1, _ := scope1.MarshalText()
data2, _ := scope2.MarshalText()
data3, _ := scope3.MarshalText()
if string(data1) != string(data2) {
t.Error("identical scopes should marshal to same value")
}
if string(data1) == string(data3) {
t.Error("different scopes should marshal to different values")
}
}