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/ipn/backend_test.go

327 lines
7.6 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"testing"
"tailscale.com/health"
"tailscale.com/types/empty"
"tailscale.com/types/key"
)
func TestNotifyString(t *testing.T) {
for _, tt := range []struct {
name string
value Notify
expected string
}{
{
name: "notify-empty",
value: Notify{},
expected: "Notify{}",
},
{
name: "notify-with-login-finished",
value: Notify{LoginFinished: &empty.Message{}},
expected: "Notify{LoginFinished}",
},
{
name: "notify-with-multiple-fields",
value: Notify{LoginFinished: &empty.Message{}, Health: &health.State{}},
expected: "Notify{LoginFinished Health{...}}",
},
} {
t.Run(tt.name, func(t *testing.T) {
actual := tt.value.String()
if actual != tt.expected {
t.Fatalf("expected=%q, actual=%q", tt.expected, actual)
}
})
}
}
// ===== State Tests =====
func TestState_String(t *testing.T) {
tests := []struct {
state State
expected string
}{
{NoState, "NoState"},
{InUseOtherUser, "InUseOtherUser"},
{NeedsLogin, "NeedsLogin"},
{NeedsMachineAuth, "NeedsMachineAuth"},
{Stopped, "Stopped"},
{Starting, "Starting"},
{Running, "Running"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
got := tt.state.String()
if got != tt.expected {
t.Errorf("State(%d).String() = %q, want %q", tt.state, got, tt.expected)
}
})
}
}
func TestState_Values(t *testing.T) {
// Test that all state values are distinct
states := []State{NoState, InUseOtherUser, NeedsLogin, NeedsMachineAuth, Stopped, Starting, Running}
seen := make(map[State]bool)
for _, s := range states {
if seen[s] {
t.Errorf("duplicate state value: %v", s)
}
seen[s] = true
}
}
func TestState_Transitions(t *testing.T) {
// Test common state transitions make sense
tests := []struct {
name string
from State
to State
valid bool
}{
{"stopped_to_starting", Stopped, Starting, true},
{"starting_to_running", Starting, Running, true},
{"running_to_stopped", Running, Stopped, true},
{"needs_login_to_starting", NeedsLogin, Starting, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Just verify states are different (basic sanity)
if tt.from == tt.to {
t.Errorf("transition from %v to %v: states are the same", tt.from, tt.to)
}
})
}
}
// ===== EngineStatus Tests =====
func TestEngineStatus(t *testing.T) {
es := EngineStatus{
RBytes: 1000,
WBytes: 2000,
NumLive: 5,
LiveDERPs: 2,
LivePeers: make(map[key.NodePublic]ipnstate.PeerStatusLite),
}
if es.RBytes != 1000 {
t.Errorf("RBytes = %d, want 1000", es.RBytes)
}
if es.WBytes != 2000 {
t.Errorf("WBytes = %d, want 2000", es.WBytes)
}
if es.NumLive != 5 {
t.Errorf("NumLive = %d, want 5", es.NumLive)
}
if es.LiveDERPs != 2 {
t.Errorf("LiveDERPs = %d, want 2", es.LiveDERPs)
}
}
func TestEngineStatus_ZeroValues(t *testing.T) {
var es EngineStatus
if es.RBytes != 0 {
t.Errorf("zero EngineStatus.RBytes = %d, want 0", es.RBytes)
}
if es.WBytes != 0 {
t.Errorf("zero EngineStatus.WBytes = %d, want 0", es.WBytes)
}
if es.NumLive != 0 {
t.Errorf("zero EngineStatus.NumLive = %d, want 0", es.NumLive)
}
}
// ===== NotifyWatchOpt Tests =====
func TestNotifyWatchOpt_Constants(t *testing.T) {
// Verify all constants are distinct powers of 2 (can be OR'd together)
opts := []NotifyWatchOpt{
NotifyWatchEngineUpdates,
NotifyInitialState,
NotifyInitialPrefs,
NotifyInitialNetMap,
NotifyNoPrivateKeys,
NotifyInitialDriveShares,
NotifyInitialOutgoingFiles,
NotifyInitialHealthState,
NotifyRateLimit,
NotifyHealthActions,
NotifyInitialSuggestedExitNode,
}
seen := make(map[NotifyWatchOpt]bool)
for _, opt := range opts {
if seen[opt] {
t.Errorf("duplicate NotifyWatchOpt value: %d", opt)
}
seen[opt] = true
// Verify it's a power of 2 (single bit set)
if opt != 0 && (opt&(opt-1)) != 0 {
t.Errorf("NotifyWatchOpt %d is not a power of 2", opt)
}
}
}
func TestNotifyWatchOpt_Combinations(t *testing.T) {
// Test combining multiple options
combined := NotifyWatchEngineUpdates | NotifyInitialState | NotifyInitialPrefs
// Check that all bits are set
if combined&NotifyWatchEngineUpdates == 0 {
t.Error("combined should include NotifyWatchEngineUpdates")
}
if combined&NotifyInitialState == 0 {
t.Error("combined should include NotifyInitialState")
}
if combined&NotifyInitialPrefs == 0 {
t.Error("combined should include NotifyInitialPrefs")
}
// Check that other bits are not set
if combined&NotifyInitialNetMap != 0 {
t.Error("combined should not include NotifyInitialNetMap")
}
}
func TestNotifyWatchOpt_BitwiseOperations(t *testing.T) {
var opts NotifyWatchOpt
// Start with nothing
if opts != 0 {
t.Errorf("initial opts = %d, want 0", opts)
}
// Add NotifyWatchEngineUpdates
opts |= NotifyWatchEngineUpdates
if opts&NotifyWatchEngineUpdates == 0 {
t.Error("should have NotifyWatchEngineUpdates set")
}
// Add NotifyInitialState
opts |= NotifyInitialState
if opts&NotifyInitialState == 0 {
t.Error("should have NotifyInitialState set")
}
// Both should still be set
if opts&NotifyWatchEngineUpdates == 0 {
t.Error("should still have NotifyWatchEngineUpdates set")
}
}
// ===== GoogleIDTokenType Tests =====
func TestGoogleIDTokenType(t *testing.T) {
expected := "ts_android_google_login"
if GoogleIDTokenType != expected {
t.Errorf("GoogleIDTokenType = %q, want %q", GoogleIDTokenType, expected)
}
}
// ===== Notify Field Tests =====
func TestNotify_WithVersion(t *testing.T) {
n := Notify{Version: "1.2.3"}
s := n.String()
if s != "Notify{Version=\"1.2.3\"}" {
t.Errorf("Notify with version: got %q", s)
}
}
func TestNotify_WithState(t *testing.T) {
state := Running
n := Notify{State: &state}
s := n.String()
if s == "Notify{}" {
t.Error("Notify with State should not be empty string")
}
}
func TestNotify_WithErr(t *testing.T) {
errMsg := "test error"
n := Notify{ErrMessage: &errMsg}
s := n.String()
if s == "Notify{}" {
t.Error("Notify with ErrMessage should not be empty string")
}
}
func TestNotify_MultipleFields(t *testing.T) {
state := Running
errMsg := "error"
n := Notify{
Version: "1.0.0",
State: &state,
ErrMessage: &errMsg,
LoginFinished: &empty.Message{},
}
s := n.String()
// Should contain multiple indicators
if s == "Notify{}" {
t.Error("Notify with multiple fields should have non-empty string")
}
}
// ===== Edge Cases =====
func TestState_InvalidValue(t *testing.T) {
// Test that an invalid state value doesn't panic
defer func() {
if r := recover(); r != nil {
t.Errorf("State.String() panicked with invalid value: %v", r)
}
}()
var s State = 999
_ = s.String() // Should not panic
}
func TestNotifyWatchOpt_Zero(t *testing.T) {
var opt NotifyWatchOpt
if opt != 0 {
t.Errorf("zero NotifyWatchOpt = %d, want 0", opt)
}
}
func TestNotifyWatchOpt_AllBits(t *testing.T) {
// Combine all options
all := NotifyWatchEngineUpdates |
NotifyInitialState |
NotifyInitialPrefs |
NotifyInitialNetMap |
NotifyNoPrivateKeys |
NotifyInitialDriveShares |
NotifyInitialOutgoingFiles |
NotifyInitialHealthState |
NotifyRateLimit |
NotifyHealthActions |
NotifyInitialSuggestedExitNode
// Should have multiple bits set
if all == 0 {
t.Error("combining all NotifyWatchOpt should be non-zero")
}
// Check each individual bit is present
if all&NotifyWatchEngineUpdates == 0 {
t.Error("all should include NotifyWatchEngineUpdates")
}
if all&NotifyInitialSuggestedExitNode == 0 {
t.Error("all should include NotifyInitialSuggestedExitNode")
}
}