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.
382 lines
8.5 KiB
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")
|
|
}
|
|
}
|