Files
tailscale/ipn/store/stores_test.go
kari-ts 7355116c05 ipn/store: make WriteState(id, nil) delete key instead of adding nil entry (#19920)
All StateStore implementations store a nil value in the cache map when WriteState is called with a nil byte slice instead of deleting the key. This causes ReadState to return (nil, nil) instead of (nil, ErrStateNotExist), since the key is still present in the map.

This breaks reset-auth in Windows, Linux, and Android, and the node can't log back in without manually editing the state file. (macOS uses a different state store)
DeleteProfile, DeleteAllProfilesForUser, setUnattendedModeAsConfigured are impacted but don't seem to break because the deleted keys are not reread.

This deletes the key from the cache instead.

Fixes tailscale/corp#42477

Signed-off-by: kari-ts <kari@tailscale.com>
2026-05-29 11:22:14 -07:00

216 lines
4.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package store
import (
"maps"
"path/filepath"
"testing"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tstest"
"tailscale.com/types/logger"
)
func TestNewStore(t *testing.T) {
oldKnownStores := maps.Clone(knownStores)
t.Cleanup(func() {
knownStores = oldKnownStores
})
knownStores = map[string]Provider{}
type store1 struct {
ipn.StateStore
path string
}
type store2 struct {
ipn.StateStore
path string
}
Register("arn:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
return &store1{new(mem.Store), path}, nil
})
Register("kube:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
return &store2{new(mem.Store), path}, nil
})
Register("mem:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
return new(mem.Store), nil
})
path := "mem:abcd"
if s, err := New(t.Logf, path); err != nil {
t.Fatalf("%q: %v", path, err)
} else if _, ok := s.(*mem.Store); !ok {
t.Fatalf("%q: got: %T, want: %T", path, s, new(mem.Store))
}
path = "arn:foo"
if s, err := New(t.Logf, path); err != nil {
t.Fatalf("%q: %v", path, err)
} else if _, ok := s.(*store1); !ok {
t.Fatalf("%q: got: %T, want: %T", path, s, new(store1))
}
path = "kube:abcd"
if s, err := New(t.Logf, path); err != nil {
t.Fatalf("%q: %v", path, err)
} else if _, ok := s.(*store2); !ok {
t.Fatalf("%q: got: %T, want: %T", path, s, new(store2))
}
path = filepath.Join(t.TempDir(), "state")
if s, err := New(t.Logf, path); err != nil {
t.Fatalf("%q: %v", path, err)
} else if _, ok := s.(*FileStore); !ok {
t.Fatalf("%q: got: %T, want: %T", path, s, new(FileStore))
}
}
func testStoreSemantics(t *testing.T, store ipn.StateStore) {
t.Helper()
tests := []struct {
// if true, data is data to write. If false, data is expected
// output of read.
write bool
id ipn.StateKey
data string
// If write=false, true if we expect a not-exist error.
notExists bool
}{
{
id: "foo",
notExists: true,
},
{
write: true,
id: "foo",
data: "bar",
},
{
id: "foo",
data: "bar",
},
{
id: "baz",
notExists: true,
},
{
write: true,
id: "baz",
data: "quux",
},
{
id: "foo",
data: "bar",
},
{
id: "baz",
data: "quux",
},
}
for _, test := range tests {
if test.write {
if err := store.WriteState(test.id, []byte(test.data)); err != nil {
t.Errorf("writing %q to %q: %v", test.data, test.id, err)
}
} else {
bs, err := store.ReadState(test.id)
if err != nil {
if test.notExists && err == ipn.ErrStateNotExist {
continue
}
t.Errorf("reading %q: %v", test.id, err)
continue
}
if string(bs) != test.data {
t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
}
}
}
}
func testStoreDeleteSemantics(t *testing.T, store ipn.StateStore) {
t.Helper()
// Write a key, verify it exists.
if err := store.WriteState("delme", []byte("val")); err != nil {
t.Fatalf("WriteState: %v", err)
}
if bs, err := store.ReadState("delme"); err != nil {
t.Fatalf("ReadState after write: %v", err)
} else if string(bs) != "val" {
t.Fatalf("ReadState after write: got %q, want %q", bs, "val")
}
// Delete by writing nil.
if err := store.WriteState("delme", nil); err != nil {
t.Fatalf("WriteState(nil): %v", err)
}
// Read should return ErrStateNotExist.
if _, err := store.ReadState("delme"); err != ipn.ErrStateNotExist {
t.Fatalf("ReadState after delete: got err %v, want ErrStateNotExist", err)
}
// Delete of a non-existent key should not error.
if err := store.WriteState("never-existed", nil); err != nil {
t.Fatalf("WriteState(nil) on non-existent key: %v", err)
}
}
func TestMemoryStore(t *testing.T) {
tstest.PanicOnLog()
store := new(mem.Store)
testStoreSemantics(t, store)
testStoreDeleteSemantics(t, store)
}
func TestFileStore(t *testing.T) {
tstest.PanicOnLog()
dir := t.TempDir()
path := filepath.Join(dir, "test-file-store.conf")
store, err := NewFileStore(nil, path)
if err != nil {
t.Fatalf("creating file store failed: %v", err)
}
testStoreSemantics(t, store)
testStoreDeleteSemantics(t, store)
// Build a brand new file store and check that both IDs written
// above are still there.
store, err = NewFileStore(nil, path)
if err != nil {
t.Fatalf("creating second file store failed: %v", err)
}
expected := map[ipn.StateKey]string{
"foo": "bar",
"baz": "quux",
}
for key, want := range expected {
bs, err := store.ReadState(key)
if err != nil {
t.Errorf("reading %q (2nd store): %v", key, err)
continue
}
if string(bs) != want {
t.Errorf("reading %q (2nd store): got %q, want %q", key, bs, want)
}
}
// Verify deleted key is still gone after reload.
if _, err := store.ReadState("delme"); err != ipn.ErrStateNotExist {
t.Fatalf("reading deleted key from reloaded store: got err %v, want ErrStateNotExist", err)
}
}