Ctrl K
ComunidadGuías…

internal/protect/store_test.go

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

Archivo original ↗Preguntar a KiBot
package protect

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

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

const remote = "https://10.0.0.5:6443"

func storePath(t *testing.T) string {
	t.Helper()
	return filepath.Join(t.TempDir(), "spinoza", "protected.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 TestAClusterNobodyDecidedOnIsUnknown(t *testing.T) {
	store := openStore(t, storePath(t))

	if store.Verdict(remote) != api.ProtectionUnknown {
		t.Fatalf("verdict = %q, want unknown so the app can ask", store.Verdict(remote))
	}
}

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

	for _, server := range []string{"https://127.0.0.1:6443", "https://localhost:6443", "https://[::1]:6443"} {
		if store.Verdict(server) != api.ProtectionOpen {
			t.Fatalf("verdict(%s) = %q, want open; kind and docker-desktop are not worth a prompt", server, store.Verdict(server))
		}
	}
}

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

	if store.Verdict("https://10.10.0.1:6443") != api.ProtectionUnknown {
		t.Fatal("a cluster on a private address was assumed local; a vpn-reachable production cluster looks exactly like this")
	}
}

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

	err := store.Set(remote, true)
	if err != nil {
		t.Fatalf("set: %v", err)
	}

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatalf("verdict = %q", store.Verdict(remote))
	}
	if openStore(t, path).Verdict(remote) != api.ProtectionProtected {
		t.Fatal("the answer did not survive a restart")
	}
}

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

	err := store.Set(remote, false)
	if err != nil {
		t.Fatalf("set: %v", err)
	}

	if openStore(t, path).Verdict(remote) != api.ProtectionOpen {
		t.Fatal("saying no was forgotten, so the prompt would come back every time")
	}
}

func TestUnprotectingAClusterAgain(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Set(remote, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if err := store.Set(remote, false); err != nil {
		t.Fatalf("unset: %v", err)
	}

	if openStore(t, path).Verdict(remote) != api.ProtectionOpen {
		t.Fatal("the cluster stayed protected after it was opened up")
	}
}

func TestEachClusterIsAnsweredSeparately(t *testing.T) {
	store := openStore(t, storePath(t))
	if err := store.Set(remote, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if store.Verdict("https://10.0.0.6:6443") != api.ProtectionUnknown {
		t.Fatal("protecting one cluster answered for another")
	}
}

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

	if store.Verdict("") != api.ProtectionUnknown {
		t.Fatalf("verdict = %q", store.Verdict(""))
	}
	if store.Set("", true) == nil {
		t.Fatal("protecting nothing reported success")
	}
}

func TestTheListIsWrittenForTheOwnerOnly(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Set(remote, true); err != nil {
		t.Fatalf("set: %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", info.Mode().Perm())
	}
}

func TestTheListIsReadableJSON(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Set(remote, true); err != nil {
		t.Fatalf("set: %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 !saved.Clusters[remote] {
		t.Fatalf("saved = %v", saved.Clusters)
	}
}

func TestABrokenListSurfacesAndProtectsNothing(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 protected-cluster list")
	}
	if store.Verdict(remote) != api.ProtectionUnknown {
		t.Fatalf("verdict = %q", store.Verdict(remote))
	}
}

func TestAnUnreadableListSurfaces(t *testing.T) {
	path := filepath.Join(t.TempDir(), "protected.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 TestAnAnswerThatCannotBeSavedIsNotKept(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", "protected.json"), clusters: map[string]bool{}}

	err := store.Set(remote, true)

	if err == nil {
		t.Fatal("an answer that could not be written reported success")
	}
	if store.Verdict(remote) != api.ProtectionUnknown {
		t.Fatal("the answer was kept in memory after it failed to save, so a restart would forget it silently")
	}
}

func TestAnAnswerThatCannotReplaceTheFileIsNotKept(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "protected.json")
	if err := os.Mkdir(path, 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	store := &Store{ctx: t.Context(), path: path, clusters: map[string]bool{}}

	err := store.Set(remote, true)

	if err == nil {
		t.Fatal("an answer that could not replace its file reported success")
	}
	leftovers, globErr := filepath.Glob(filepath.Join(dir, "protected-*.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 TestWithNoPathTheAnswersLastForThisRunOnly(t *testing.T) {
	store := openStore(t, "")

	if err := store.Set(remote, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatalf("verdict = %q", store.Verdict(remote))
	}
}

func TestAServerThatIsNotAURLIsNotLocal(t *testing.T) {
	if Local("://nonsense") {
		t.Fatal("an unparseable server was treated as local")
	}
	if Local("https://kubernetes.default.svc") {
		t.Fatal("a named host that is not localhost was treated as local")
	}
}

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

	want := filepath.Join("spinoza", "protected.json")
	if filepath.Join(filepath.Base(filepath.Dir(path)), filepath.Base(path)) != want {
		t.Fatalf("path = %q, want it under a spinoza directory", path)
	}
}

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

	for i := range 5 {
		if err := store.Set("https://10.0.0."+strconv.Itoa(i)+":6443", i%2 == 0); err != nil {
			t.Fatalf("set: %v", err)
		}
	}

	reopened := openStore(t, path)
	if reopened.Verdict("https://10.0.0.0:6443") != api.ProtectionProtected {
		t.Fatal("the first answer was lost")
	}
	if reopened.Verdict("https://10.0.0.1:6443") != api.ProtectionOpen {
		t.Fatal("a later answer overwrote an earlier one")
	}
}

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

	for index := range clusters {
		server := "https://10.0.1." + strconv.Itoa(index) + ":6443"
		protected := index%2 == 0
		group.Go(func() {
			if err := store.Set(server, protected); err != nil {
				t.Errorf("set %s: %v", server, err)
			}
		})
	}
	group.Wait()

	reopened := openStore(t, path)
	for index := range clusters {
		server := "https://10.0.1." + strconv.Itoa(index) + ":6443"
		want := api.ProtectionOpen
		if index%2 == 0 {
			want = api.ProtectionProtected
		}
		if got := reopened.Verdict(server); got != want {
			t.Fatalf("%s verdict = %q, want %q", server, got, want)
		}
	}
}

func TestSimultaneousProcessesDoNotLoseProtectionDecisions(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.Set("https://10.0.2.1:6443", true)
	}()
	go func() {
		done <- second.Set("https://10.0.2.2:6443", false)
	}()
	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("set: %v", err)
		}
	}

	reopened := openStore(t, path)
	if reopened.Verdict("https://10.0.2.1:6443") != api.ProtectionProtected {
		t.Fatal("the first process's protection decision was lost")
	}
	if reopened.Verdict("https://10.0.2.2:6443") != api.ProtectionOpen {
		t.Fatal("the second process's protection decision was lost")
	}
}

func TestAVerdictPicksUpAnotherProcessesDecision(t *testing.T) {
	path := storePath(t)
	first := openStore(t, path)
	second := openStore(t, path)
	if err := second.Set(remote, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if first.Verdict(remote) != api.ProtectionProtected {
		t.Fatal("the first process retained its stale protection verdict")
	}
}

func readOnlyDir(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 TestAFileThatCannotBeReadIsReported(t *testing.T) {
	path := filepath.Join(t.TempDir(), "protected.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 file that could not be read opened without complaint")
	}
}

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

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

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

func TestSetDoesNotReplaceMalformedDecisionsFromStaleMemory(t *testing.T) {
	path := storePath(t)
	store := openStore(t, path)
	if err := store.Set(remote, true); err != nil {
		t.Fatalf("initial set: %v", err)
	}
	broken := []byte("{not json")
	if err := os.WriteFile(path, broken, 0o600); err != nil {
		t.Fatalf("break decisions: %v", err)
	}

	err := store.Set("https://10.0.0.6:6443", true)

	if err == nil {
		t.Fatal("a write over malformed decisions reported success")
	}
	body, readErr := os.ReadFile(path)
	if readErr != nil {
		t.Fatalf("read broken decisions: %v", readErr)
	}
	if !bytes.Equal(body, broken) {
		t.Fatalf("malformed decisions were replaced with %q", body)
	}
	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatal("the last readable decision was discarded")
	}
}

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

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

	if err == nil {
		t.Fatal("oversized protection decisions were written")
	}
}

func TestAnAnswerThatCannotBeSavedIsReported(t *testing.T) {
	store := openStore(t, filepath.Join(readOnlyDir(t), "nested", "protected.json"))

	err := store.Set(remote, true)

	if err == nil {
		t.Fatal("an answer that could not be written was reported as saved")
	}
	if store.Verdict(remote) != api.ProtectionUnknown {
		t.Fatal("an answer that was not written was remembered anyway")
	}
}

func TestAStoreWithNowhereToWriteKeepsTheAnswerInMemory(t *testing.T) {
	store, err := Open(t.Context(), "")
	if err != nil {
		t.Fatalf("open: %v", err)
	}

	if setErr := store.Set(remote, true); setErr != nil {
		t.Fatalf("set: %v", setErr)
	}

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatalf("verdict = %q", store.Verdict(remote))
	}
}

func writeRaw(t *testing.T, path string, clusters map[string]bool) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	body, err := json.Marshal(map[string]any{"clusters": clusters})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if writeErr := os.WriteFile(path, body, 0o600); writeErr != nil {
		t.Fatalf("write: %v", writeErr)
	}
}

func readRaw(t *testing.T, path string) map[string]bool {
	t.Helper()
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	var saved struct {
		Clusters map[string]bool `json:"clusters"`
	}
	if unmarshalErr := json.Unmarshal(body, &saved); unmarshalErr != nil {
		t.Fatalf("unmarshal: %v", unmarshalErr)
	}
	return saved.Clusters
}

func TestAnAnswerWrittenBeforeTheKeyWasNormalisedIsStillFound(t *testing.T) {
	path := storePath(t)
	writeRaw(t, path, map[string]bool{"https://10.0.0.5:6443/": true})

	store := openStore(t, path)

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatalf("verdict = %q, want protected; an older file must not silently lose its answer", store.Verdict(remote))
	}
}

func TestTheSameClusterSpeltTwoWaysIsOneAnswer(t *testing.T) {
	store := openStore(t, storePath(t))
	if err := store.Set("HTTPS://10.0.0.5:6443/", true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatal("a trailing slash and a capital letter made a second cluster out of one")
	}
}

func TestWritingAnAnswerLeavesOnlyTheNormalisedKey(t *testing.T) {
	path := storePath(t)
	stale := "https://10.0.0.5:6443/"
	writeRaw(t, path, map[string]bool{stale: true})
	store := openStore(t, path)

	if err := store.Set(stale, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	held := readRaw(t, path)
	if _, found := held[stale]; found {
		t.Fatalf("the old key survived the write: %v", held)
	}
	if !held[remote] {
		t.Fatalf("the normalised key was not written: %v", held)
	}
}

func TestAClusterBehindARancherPathIsItsOwnAnswer(t *testing.T) {
	store := openStore(t, storePath(t))
	one := "https://rancher.example.com/k8s/clusters/c-m-aaaaaaaa"
	two := "https://rancher.example.com/k8s/clusters/c-m-bbbbbbbb"
	if err := store.Set(one, true); err != nil {
		t.Fatalf("set: %v", err)
	}

	if store.Verdict(two) != api.ProtectionUnknown {
		t.Fatal("protecting one Rancher cluster answered for another on the same host")
	}
}

func TestTwoSpellingsOfOneClusterResolveToProtected(t *testing.T) {
	path := storePath(t)
	writeRaw(t, path, map[string]bool{
		"https://10.0.0.5:6443/": true,
		"HTTPS://10.0.0.5:6443":  false,
	})

	store := openStore(t, path)

	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatalf("verdict = %q, want protected; a conflict must never quietly open a cluster", store.Verdict(remote))
	}
}

func TestAnEmptyKeyInTheFileIsIgnored(t *testing.T) {
	path := storePath(t)
	writeRaw(t, path, map[string]bool{"": true, remote: true})

	store := openStore(t, path)

	if store.Verdict("") != api.ProtectionUnknown {
		t.Fatalf("verdict = %q, want unknown", store.Verdict(""))
	}
	if store.Verdict(remote) != api.ProtectionProtected {
		t.Fatal("a junk key stopped the real ones being read")
	}
}

func TestProtectedWinsWhateverOrderTheKeysAreRead(t *testing.T) {
	protectedFirst := map[string]bool{}
	adopt(protectedFirst, map[string]bool{"https://10.0.0.5:6443/": true})
	adopt(protectedFirst, map[string]bool{"HTTPS://10.0.0.5:6443": false})

	openFirst := map[string]bool{}
	adopt(openFirst, map[string]bool{"HTTPS://10.0.0.5:6443": false})
	adopt(openFirst, map[string]bool{"https://10.0.0.5:6443/": true})

	if !protectedFirst[remote] {
		t.Fatal("reading the protected spelling first left the cluster open")
	}
	if !openFirst[remote] {
		t.Fatal("reading the open spelling first left the cluster open")
	}
}