Ctrl K
ComunidadGuías…

internal/kubeconfig/store_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package kubeconfig

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/sophotechlabs/spinoza/internal/filetx"
)

func storePath(t *testing.T) string {
	t.Helper()
	return filepath.Join(t.TempDir(), "spinoza", "kubeconfigs.json")
}

func openStore(t *testing.T, path string) *Store {
	t.Helper()
	store, err := Open(t.Context(), path)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	return store
}

func holdTransaction(t *testing.T, path string) func() {
	t.Helper()
	entered := make(chan struct{})
	release := make(chan struct{})
	done := make(chan error, 1)
	go func() {
		done <- filetx.Exclusive(t.Context(), path, func() error {
			close(entered)
			<-release
			return nil
		})
	}()
	select {
	case <-entered:
	case err := <-done:
		t.Fatalf("hold transaction: %v", err)
	}
	return func() {
		close(release)
		if err := <-done; err != nil {
			t.Fatalf("release transaction: %v", err)
		}
	}
}

func TestAnAbsentListStartsEmpty(t *testing.T) {
	store := openStore(t, storePath(t))

	if len(store.Paths()) != 0 {
		t.Fatalf("paths = %v, want none before anything is added", store.Paths())
	}
}

func TestAnAddedKubeconfigSurvivesARestart(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)

	err := store.Add("/tmp/one.yaml")
	if err != nil {
		t.Fatalf("add: %v", err)
	}

	reopened := openStore(t, path)
	if !slices.Equal(reopened.Paths(), []string{"/tmp/one.yaml"}) {
		t.Fatalf("paths = %v, want the added kubeconfig read back", reopened.Paths())
	}
}

func TestTheListKeepsTheOrderKubeconfigsWereAdded(t *testing.T) {
	store := openStore(t, storePath(t))

	for _, path := range []string{"/tmp/one.yaml", "/tmp/two.yaml", "/tmp/three.yaml"} {
		if err := store.Add(path); err != nil {
			t.Fatalf("add %s: %v", path, err)
		}
	}

	want := []string{"/tmp/one.yaml", "/tmp/two.yaml", "/tmp/three.yaml"}
	if !slices.Equal(store.Paths(), want) {
		t.Fatalf("paths = %v, want %v", store.Paths(), want)
	}
}

func TestConcurrentAddsAreAllPersisted(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	const additions = 32
	var group sync.WaitGroup

	for index := range additions {
		entry := fmt.Sprintf("/tmp/cluster-%02d.yaml", index)
		group.Go(func() {
			if err := store.Add(entry); err != nil {
				t.Errorf("add %s: %v", entry, err)
			}
		})
	}
	group.Wait()

	want := make([]string, additions)
	for index := range additions {
		want[index] = fmt.Sprintf("/tmp/cluster-%02d.yaml", index)
	}
	slices.Sort(want)
	held := store.Paths()
	slices.Sort(held)
	if !slices.Equal(held, want) {
		t.Fatalf("paths = %v, want every concurrent addition", held)
	}
	persisted := openStore(t, path).Paths()
	slices.Sort(persisted)
	if !slices.Equal(persisted, want) {
		t.Fatalf("persisted paths = %v, want every concurrent addition", persisted)
	}
}

func TestSimultaneousProcessesDoNotLoseKubeconfigs(t *testing.T) {
	path := storePath(t)
	first := openStore(t, path)
	second := openStore(t, path)
	unlock := holdTransaction(t, path)
	done := make(chan error, 2)
	go func() {
		done <- first.Add("/tmp/one.yaml")
	}()
	go func() {
		done <- second.Add("/tmp/two.yaml")
	}()
	select {
	case err := <-done:
		unlock()
		t.Fatalf("a write escaped the transaction lock: %v", err)
	case <-time.After(50 * time.Millisecond):
	}
	unlock()
	for range 2 {
		if err := <-done; err != nil {
			t.Fatalf("add: %v", err)
		}
	}

	paths := openStore(t, path).Paths()
	slices.Sort(paths)
	if !slices.Equal(paths, []string{"/tmp/one.yaml", "/tmp/two.yaml"}) {
		t.Fatalf("paths = %v, want both processes' kubeconfigs", paths)
	}
}

func TestPathsPickUpAnotherProcessesAddition(t *testing.T) {
	path := storePath(t)
	first := openStore(t, path)
	second := openStore(t, path)
	if err := second.Add("/tmp/other.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	if !slices.Equal(first.Paths(), []string{"/tmp/other.yaml"}) {
		t.Fatalf("paths = %v, want the other process's addition", first.Paths())
	}
}

func TestConcurrentDuplicateAddsStoreOneEntry(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	const callers = 16
	results := make(chan error, callers)
	var group sync.WaitGroup

	for range callers {
		group.Go(func() {
			results <- store.Add("/tmp/one.yaml")
		})
	}
	group.Wait()
	close(results)

	added := 0
	for err := range results {
		if err == nil {
			added++
		}
	}
	if added != 1 {
		t.Fatalf("successful adds = %d, want one", added)
	}
	if !slices.Equal(store.Paths(), []string{"/tmp/one.yaml"}) {
		t.Fatalf("paths = %v, want one entry", store.Paths())
	}
	if !slices.Equal(openStore(t, path).Paths(), []string{"/tmp/one.yaml"}) {
		t.Fatal("the single entry was not persisted")
	}
}

func TestTheSameKubeconfigIsNotAddedTwice(t *testing.T) {
	store := openStore(t, storePath(t))
	if err := store.Add("/tmp/one.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	err := store.Add("/tmp/one.yaml")

	if err == nil {
		t.Fatal("the same kubeconfig was added twice")
	}
	if len(store.Paths()) != 1 {
		t.Fatalf("paths = %v, want the one entry", store.Paths())
	}
}

func TestRemovingDropsTheKubeconfigFromDisk(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Add("/tmp/one.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}
	if err := store.Add("/tmp/two.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	err := store.Remove("/tmp/one.yaml")
	if err != nil {
		t.Fatalf("remove: %v", err)
	}

	if !slices.Equal(openStore(t, path).Paths(), []string{"/tmp/two.yaml"}) {
		t.Fatalf("paths = %v, want only the kept kubeconfig", openStore(t, path).Paths())
	}
}

func TestRemovingSomethingThatWasNeverAddedSaysSo(t *testing.T) {
	store := openStore(t, storePath(t))

	err := store.Remove("/tmp/one.yaml")

	if err == nil {
		t.Fatal("removing an unknown kubeconfig reported success")
	}
}

func TestTheListIsWrittenForTheOwnerOnly(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)

	if err := store.Add("/tmp/one.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Fatalf("mode = %v, want 0600; the file names files holding cluster credentials", info.Mode().Perm())
	}
}

func TestTheListIsReadableJSON(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Add("/tmp/one.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}

	var saved state
	if unmarshalErr := json.Unmarshal(body, &saved); unmarshalErr != nil {
		t.Fatalf("unmarshal %s: %v", body, unmarshalErr)
	}
	if !slices.Equal(saved.Kubeconfigs, []string{"/tmp/one.yaml"}) {
		t.Fatalf("saved = %v", saved.Kubeconfigs)
	}
}

func TestPathsHandsOutACopy(t *testing.T) {
	store := openStore(t, storePath(t))
	if err := store.Add("/tmp/one.yaml"); err != nil {
		t.Fatalf("add: %v", err)
	}

	store.Paths()[0] = "/tmp/rewritten.yaml"

	if store.Paths()[0] != "/tmp/one.yaml" {
		t.Fatalf("paths = %v, want the store's own list left alone", store.Paths())
	}
}

func TestABrokenListSurfacesAndStartsEmpty(t *testing.T) {
	path := storePath(t)
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
		t.Fatalf("write: %v", err)
	}

	store, err := Open(t.Context(), path)

	if err == nil {
		t.Fatal("an unreadable list was reported as an empty one")
	}
	if store == nil {
		t.Fatal("spinoza refused to start over a broken kubeconfig list")
	}
	if len(store.Paths()) != 0 {
		t.Fatalf("paths = %v", store.Paths())
	}
}

func TestAddDoesNotReplaceAMalformedListFromStaleMemory(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Add("/tmp/held.yaml"); err != nil {
		t.Fatalf("initial add: %v", err)
	}
	broken := []byte("{not json")
	if err := os.WriteFile(path, broken, 0o600); err != nil {
		t.Fatalf("break list: %v", err)
	}

	err := store.Add("/tmp/new.yaml")

	if err == nil {
		t.Fatal("an add over a malformed list reported success")
	}
	body, readErr := os.ReadFile(path)
	if readErr != nil {
		t.Fatalf("read broken list: %v", readErr)
	}
	if !bytes.Equal(body, broken) {
		t.Fatalf("malformed list was replaced with %q", body)
	}
	if !slices.Equal(store.Paths(), []string{"/tmp/held.yaml"}) {
		t.Fatalf("paths = %v, want the last readable list", store.Paths())
	}
}

func TestAListLargerThanTheReadLimitIsNotWritten(t *testing.T) {
	store := openStore(t, storePath(t))

	err := store.write([]string{strings.Repeat("x", maxFileBytes)})

	if err == nil {
		t.Fatal("an oversized kubeconfig list was written")
	}
}

func TestAnUnreadableListSurfaces(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "kubeconfigs.json")
	if err := os.Mkdir(path, 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}

	_, err := Open(t.Context(), path)

	if err == nil {
		t.Fatal("a directory in place of the list read as an empty list")
	}
}

func TestWithNoPathTheListLivesForThisRunOnly(t *testing.T) {
	store := openStore(t, "")

	err := store.Add("/tmp/one.yaml")
	if err != nil {
		t.Fatalf("add: %v", err)
	}

	if !slices.Equal(store.Paths(), []string{"/tmp/one.yaml"}) {
		t.Fatalf("paths = %v, want the kubeconfig held in memory", store.Paths())
	}
}

func TestAListThatCannotBeWrittenLeavesTheStoreAlone(t *testing.T) {
	blocked := filepath.Join(t.TempDir(), "file")
	if err := os.WriteFile(blocked, []byte("not a directory"), 0o600); err != nil {
		t.Fatalf("write: %v", err)
	}
	store := &Store{ctx: t.Context(), path: filepath.Join(blocked, "spinoza", "kubeconfigs.json")}

	err := store.Add("/tmp/one.yaml")

	if err == nil {
		t.Fatal("a list that could not be written reported success")
	}
	if len(store.Paths()) != 0 {
		t.Fatalf("paths = %v, want nothing remembered when it could not be saved", store.Paths())
	}
}

func TestAListThatCannotBeReplacedLeavesTheStoreAlone(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "kubeconfigs.json")
	if err := os.Mkdir(path, 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	store := &Store{ctx: t.Context(), path: path}

	err := store.Add("/tmp/one.yaml")

	if err == nil {
		t.Fatal("a list that could not replace its file reported success")
	}
	leftovers, globErr := filepath.Glob(filepath.Join(dir, "kubeconfigs-*.json"))
	if globErr != nil {
		t.Fatalf("glob: %v", globErr)
	}
	if len(leftovers) != 0 {
		t.Fatalf("leftovers = %v, want the half-written file cleaned up", leftovers)
	}
}

func TestDefaultPathSitsUnderTheUserConfigDirectory(t *testing.T) {
	path, err := DefaultPath()
	if err != nil {
		t.Fatalf("DefaultPath: %v", err)
	}

	if !strings.HasSuffix(path, filepath.Join("spinoza", "kubeconfigs.json")) {
		t.Fatalf("path = %q, want it under a spinoza directory", path)
	}
}

func lockedDir(t *testing.T) string {
	t.Helper()
	dir := filepath.Join(t.TempDir(), "locked")
	if err := os.MkdirAll(dir, 0o500); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	t.Cleanup(func() {
		_ = os.Chmod(dir, 0o700)
	})
	return dir
}

func TestAListThatCannotBeReadIsReported(t *testing.T) {
	path := filepath.Join(t.TempDir(), "kubeconfigs.json")
	if err := os.Mkdir(path, 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}

	store, err := Open(t.Context(), path)

	if err == nil {
		t.Fatal("a list that could not be read opened without complaint")
	}
	if store == nil {
		t.Fatal("a store that could not be read must still be usable")
	}
}

func TestAListThatIsNotJsonIsReported(t *testing.T) {
	path := filepath.Join(t.TempDir(), "kubeconfigs.json")
	if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
		t.Fatalf("write: %v", err)
	}

	store, err := Open(t.Context(), path)

	if err == nil {
		t.Fatal("a list that is not json opened without complaint")
	}
	if len(store.Paths()) != 0 {
		t.Fatalf("paths = %v, want nothing kept from a file that could not be read", store.Paths())
	}
}

func TestAKubeconfigThatCannotBeSavedIsNotRemembered(t *testing.T) {
	store, err := Open(t.Context(), filepath.Join(lockedDir(t), "nested", "kubeconfigs.json"))
	if err != nil {
		t.Fatalf("open: %v", err)
	}

	addErr := store.Add("/home/arch/.kube/other")

	if addErr == nil {
		t.Fatal("a list that could not be written was reported as saved")
	}
	if len(store.Paths()) != 0 {
		t.Fatalf("paths = %v, want the list unchanged", store.Paths())
	}
}

func TestARemovalThatCannotBeSavedLeavesTheListAlone(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "kubeconfigs.json")
	store, err := Open(t.Context(), path)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	if addErr := store.Add("/home/arch/.kube/other"); addErr != nil {
		t.Fatalf("add: %v", addErr)
	}
	if chmodErr := os.Chmod(dir, 0o500); chmodErr != nil {
		t.Fatalf("chmod: %v", chmodErr)
	}
	t.Cleanup(func() {
		_ = os.Chmod(dir, 0o700)
	})

	removeErr := store.Remove("/home/arch/.kube/other")

	if removeErr == nil {
		t.Fatal("a removal that could not be written was reported as saved")
	}
	if len(store.Paths()) != 1 {
		t.Fatalf("paths = %v, want the kubeconfig still on the list", store.Paths())
	}
}