penguin/homekey-go

homekey-go

internal/app/settings_test.go

raw ยท 3036 bytes

package app

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
)

func TestSettingsNormalizeAndConflict(t *testing.T) {
	dir := t.TempDir()
	seed := filepath.Join(dir, "options.json")
	path := filepath.Join(dir, "config.json")
	os.WriteFile(seed, []byte(`{"interface":" ","locks":[{"id":"front ","name":"Front ","port":51826,"finish":"silver","readers":["TestReader01 "]}],"readers":[{"id":" TestReader01 ","address":"70:af:09:16:42:5a ","adapter":" hci0 "}]}`), 0600)
	s, err := OpenSettings(path, true)
	if err != nil {
		t.Fatal(err)
	}
	if _, err = os.Stat(path); !os.IsNotExist(err) {
		t.Fatal("status imported settings to disk")
	}
	raw, err := os.ReadFile(seed)
	if err != nil {
		t.Fatal(err)
	}
	if err = os.WriteFile(path, raw, 0600); err != nil {
		t.Fatal(err)
	}
	s, err = OpenSettings(path)
	if err != nil {
		t.Fatal(err)
	}
	c, rev := s.Snapshot()
	if c.Readers[0].Address != "70:AF:09:16:42:5A" || c.Readers[0].Adapter != "hci0" || c.Locks[0].Readers[0] != "TestReader01" {
		t.Fatalf("whitespace not normalized: %+v", c)
	}
	if err = s.Save(c, "stale"); !errors.Is(err, ErrConflict) {
		t.Fatal("stale revision accepted")
	}
	c.Readers[0].Name = "Porch"
	if err = s.Save(c, rev); err != nil {
		t.Fatal(err)
	}
	if err = s.Save(c, rev); !errors.Is(err, ErrReloading) {
		t.Fatal("second save accepted during reload")
	}
	os.WriteFile(seed, []byte(`{"locks":[],"readers":[]}`), 0600)
	reopened, err := OpenSettings(path)
	if err != nil {
		t.Fatal(err)
	}
	c, _ = reopened.Snapshot()
	if len(c.Readers) != 1 || c.Readers[0].Name != "Porch" {
		t.Fatal("Supervisor seed overwrote saved settings")
	}
	info, _ := os.Stat(path)
	if info.Mode().Perm() != 0600 {
		t.Fatal("unsafe config permissions")
	}
}
func TestConfigurationHTTPValidation(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.json")
	writeConfig(path, Config{Locks: []LockConfig{}, Readers: []ReaderConfig{}})
	s, _ := OpenSettings(path)
	calls := 0
	s.Reload = func() { calls++ }
	handler := Handler(nil, nil, false, s)
	c, rev := s.Snapshot()
	send := func(header, revision string, c Config) *httptest.ResponseRecorder {
		body, _ := json.Marshal(map[string]any{"config": c, "revision": revision})
		r := httptest.NewRequest("POST", "/api/config", bytes.NewReader(body))
		r.Header.Set("X-Homekey-Action", header)
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, r)
		return w
	}
	if w := send("", rev, c); w.Code != 403 {
		t.Fatal("CSRF action header not enforced")
	}
	if w := send("configuration", "stale", c); w.Code != 409 {
		t.Fatal("stale settings not rejected")
	}
	c.Locks = append(c.Locks, LockConfig{ID: "front", Name: "Front", Port: 51826, Finish: "silver", Readers: []string{"missing"}})
	if w := send("configuration", rev, c); w.Code != 400 || calls != 0 {
		t.Fatal("invalid settings restarted service")
	}
	c.Locks[0].Readers = []string{}
	if w := send("configuration", rev, c); w.Code != 202 || calls != 1 {
		t.Fatalf("save failed %d %s", w.Code, w.Body.String())
	}
}