summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/config/config_test.go208
-rw-r--r--internal/config/testdata/full.conf7
-rw-r--r--internal/config/testdata/malformed.conf2
-rw-r--r--internal/config/testdata/partial.conf2
-rw-r--r--internal/config/testdata/system.conf7
-rw-r--r--internal/server/handlers/handlers_test.go99
6 files changed, 325 insertions, 0 deletions
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..36b2e97
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,208 @@
+package config
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func noSystemConfig(t *testing.T) {
+ t.Helper()
+ old := systemConfigPath
+ systemConfigPath = filepath.Join(t.TempDir(), "absent.conf")
+ t.Cleanup(func() { systemConfigPath = old })
+}
+
+func writeConf(t *testing.T, body string) string {
+ t.Helper()
+ path := filepath.Join(t.TempDir(), "openstore.conf")
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
+ t.Fatalf("write conf: %v", err)
+ }
+ return path
+}
+
+func TestLoadMissingFileUsesDefaults(t *testing.T) {
+ noSystemConfig(t)
+
+ cfg, err := Load(filepath.Join(t.TempDir(), "nonexistent.conf"))
+ if err != nil {
+ t.Fatalf("load with no config file failed: %v", err)
+ }
+
+ if cfg.Serve.Addr != ":3000" {
+ t.Errorf("addr: got %q, want \":3000\"", cfg.Serve.Addr)
+ }
+ if cfg.Instance.Name != "Openstore" {
+ t.Errorf("name: got %q, want \"Openstore\"", cfg.Instance.Name)
+ }
+}
+
+func TestLoadFullFileOverridesDefaults(t *testing.T) {
+ noSystemConfig(t)
+
+ cfg, err := Load("testdata/full.conf")
+ if err != nil {
+ t.Fatalf("load: %v", err)
+ }
+
+ tests := []struct {
+ field string
+ got string
+ want string
+ }{
+ {"addr", cfg.Serve.Addr, "127.0.0.1:8080"},
+ {"name", cfg.Instance.Name, "Full Store"},
+ {"contact-email", cfg.Instance.ContactEmail, "full@store.com"},
+ {"base-url", cfg.Instance.BaseURL, "https://full.store.com"},
+ }
+
+ for _, tt := range tests {
+ if tt.got != tt.want {
+ t.Errorf("%s: got %q, want %q", tt.field, tt.got, tt.want)
+ }
+ }
+}
+
+func TestLoadPartialFileKeepsDefaults(t *testing.T) {
+ noSystemConfig(t)
+
+ cfg, err := Load("testdata/partial.conf")
+ if err != nil {
+ t.Fatalf("load: %v", err)
+ }
+
+ if cfg.Instance.Name != "Only Name Set" {
+ t.Errorf("name: got %q, want \"Only Name Set\"", cfg.Instance.Name)
+ }
+ if cfg.Serve.Addr != ":3000" {
+ t.Errorf("unset addr should keep default, got %q", cfg.Serve.Addr)
+ }
+ if cfg.Instance.BaseURL != "https://www.example.com" {
+ t.Errorf("unset base-url should keep default, got %q", cfg.Instance.BaseURL)
+ }
+}
+
+func TestLoadUserConfigWinsOverSystem(t *testing.T) {
+ old := systemConfigPath
+ systemConfigPath = "testdata/system.conf"
+ t.Cleanup(func() { systemConfigPath = old })
+
+ cfg, err := Load("testdata/partial.conf")
+ if err != nil {
+ t.Fatalf("load: %v", err)
+ }
+
+ if cfg.Instance.Name != "Only Name Set" {
+ t.Errorf("user config should win, got name %q", cfg.Instance.Name)
+ }
+ if cfg.Instance.ContactEmail != "system@store.com" {
+ t.Errorf("key absent from user config should fall back to system, got %q",
+ cfg.Instance.ContactEmail)
+ }
+ if cfg.Serve.Addr != "127.0.0.1:9999" {
+ t.Errorf("section absent from user config should fall back to system, got %q",
+ cfg.Serve.Addr)
+ }
+}
+
+func TestLoadMalformedFile(t *testing.T) {
+ noSystemConfig(t)
+
+ if _, err := Load("testdata/malformed.conf"); err == nil {
+ t.Error("malformed config file loaded without error")
+ }
+}
+
+func TestLoadRejectsBadBaseURL(t *testing.T) {
+ noSystemConfig(t)
+
+ tests := map[string]string{
+ "not a url": "garbage",
+ "no scheme": "example.com/path",
+ "scheme only": "https://",
+ "path only": "/listings",
+ "bad url syntax": "://x",
+ }
+
+ for name, badURL := range tests {
+ t.Run(name, func(t *testing.T) {
+ path := writeConf(t, "[instance]\nbase-url = "+badURL+"\n")
+
+ _, err := Load(path)
+ if err == nil {
+ t.Fatalf("bad base-url %q accepted", badURL)
+ }
+ if !strings.Contains(err.Error(), "base-url") {
+ t.Errorf("error should mention base-url, got: %v", err)
+ }
+ })
+ }
+}
+
+func TestLoadEmptyValueKeepsDefault(t *testing.T) {
+ noSystemConfig(t)
+
+ path := writeConf(t, "[instance]\nname = \nbase-url = \n")
+
+ cfg, err := Load(path)
+ if err != nil {
+ t.Fatalf("load: %v", err)
+ }
+
+ if cfg.Instance.Name != "Openstore" {
+ t.Errorf("empty value should leave default, got name %q", cfg.Instance.Name)
+ }
+ if cfg.Instance.BaseURL != "https://www.example.com" {
+ t.Errorf("empty value should leave default, got base-url %q", cfg.Instance.BaseURL)
+ }
+}
+
+func TestLoadAcceptsGoodBaseURL(t *testing.T) {
+ noSystemConfig(t)
+
+ urls := []string{
+ "https://example.com",
+ "http://example.com",
+ "https://example.com/subpath",
+ "https://sub.example.com:8443",
+ }
+
+ for _, u := range urls {
+ t.Run(u, func(t *testing.T) {
+ path := writeConf(t, "[instance]\nbase-url = "+u+"\n")
+
+ cfg, err := Load(path)
+ if err != nil {
+ t.Fatalf("good base-url %q rejected: %v", u, err)
+ }
+ if cfg.Instance.BaseURL != u {
+ t.Errorf("base-url: got %q, want %q", cfg.Instance.BaseURL, u)
+ }
+ })
+ }
+}
+
+func TestDefaultPath(t *testing.T) {
+ t.Setenv("XDG_CONFIG_HOME", "/tmp/xdg-test")
+
+ path, err := DefaultPath()
+ if err != nil {
+ t.Fatalf("DefaultPath: %v", err)
+ }
+
+ want := filepath.Join("/tmp/xdg-test", "openstore", "openstore.conf")
+ if path != want {
+ t.Errorf("got %q, want %q", path, want)
+ }
+}
+
+func TestDefaultPathWithoutConfigDir(t *testing.T) {
+ t.Setenv("XDG_CONFIG_HOME", "")
+ t.Setenv("HOME", "")
+
+ if _, err := DefaultPath(); err == nil {
+ t.Error("DefaultPath returned no error with no config dir available")
+ }
+}
diff --git a/internal/config/testdata/full.conf b/internal/config/testdata/full.conf
new file mode 100644
index 0000000..1f703f0
--- /dev/null
+++ b/internal/config/testdata/full.conf
@@ -0,0 +1,7 @@
+[serve]
+addr = 127.0.0.1:8080
+
+[instance]
+name = Full Store
+contact-email = full@store.com
+base-url = https://full.store.com
diff --git a/internal/config/testdata/malformed.conf b/internal/config/testdata/malformed.conf
new file mode 100644
index 0000000..327aace
--- /dev/null
+++ b/internal/config/testdata/malformed.conf
@@ -0,0 +1,2 @@
+[instance
+name = Unclosed Section
diff --git a/internal/config/testdata/partial.conf b/internal/config/testdata/partial.conf
new file mode 100644
index 0000000..e84544d
--- /dev/null
+++ b/internal/config/testdata/partial.conf
@@ -0,0 +1,2 @@
+[instance]
+name = Only Name Set
diff --git a/internal/config/testdata/system.conf b/internal/config/testdata/system.conf
new file mode 100644
index 0000000..f2533a8
--- /dev/null
+++ b/internal/config/testdata/system.conf
@@ -0,0 +1,7 @@
+[serve]
+addr = 127.0.0.1:9999
+
+[instance]
+name = System Store
+contact-email = system@store.com
+base-url = https://system.store.com
diff --git a/internal/server/handlers/handlers_test.go b/internal/server/handlers/handlers_test.go
new file mode 100644
index 0000000..b2ae43e
--- /dev/null
+++ b/internal/server/handlers/handlers_test.go
@@ -0,0 +1,99 @@
+package handlers
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "codeberg.org/mihna123/openstore/internal/config"
+)
+
+func newTestConfig() *config.Config {
+ return &config.Config{
+ Serve: config.Serve{Addr: ":3000"},
+ Instance: config.Instance{
+ Name: "TestStore",
+ ContactEmail: "test@store.com",
+ BaseURL: "https://test.store.com",
+ },
+ }
+}
+
+func get(t *testing.T, mux *http.ServeMux, method, path string) *httptest.ResponseRecorder {
+ t.Helper()
+ rec := httptest.NewRecorder()
+ mux.ServeHTTP(rec, httptest.NewRequest(method, path, nil))
+ return rec
+}
+
+func TestRoutes(t *testing.T) {
+ mux := Routes(newTestConfig())
+
+ tests := []struct {
+ name string
+ method string
+ path string
+ want int
+ }{
+ {"home", http.MethodGet, "/", http.StatusOK},
+ {"unknown path", http.MethodGet, "/nonsense", http.StatusNotFound},
+ {"wrong method on home", http.MethodPost, "/", http.StatusMethodNotAllowed},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rec := get(t, mux, tt.method, tt.path)
+ if rec.Code != tt.want {
+ t.Errorf("%s %s: got status %d, want %d", tt.method, tt.path, rec.Code, tt.want)
+ }
+ })
+ }
+}
+
+func TestHomeRendersInstanceName(t *testing.T) {
+ cfg := newTestConfig()
+ cfg.Instance.Name = "Prodavnica123"
+ mux := Routes(cfg)
+
+ rec := get(t, mux, http.MethodGet, "/")
+ if rec.Code != http.StatusOK {
+ t.Fatalf("got status %d, want 200", rec.Code)
+ }
+
+ body := rec.Body.String()
+ if strings.Count(body, "Prodavnica123") != 2 {
+ t.Errorf("instance name should appear in title and heading, got:\n%s", body)
+ }
+
+ if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
+ t.Errorf("got Content-Type %q, want text/html", ct)
+ }
+}
+
+func TestHomeEscapesInstanceName(t *testing.T) {
+ cfg := newTestConfig()
+ cfg.Instance.Name = `<script>alert("xss")</script>`
+ mux := Routes(cfg)
+
+ body := get(t, mux, http.MethodGet, "/").Body.String()
+
+ if strings.Contains(body, "<script>") {
+ t.Errorf("instance name rendered unescaped, got:\n%s", body)
+ }
+ if !strings.Contains(body, "&lt;script&gt;") {
+ t.Errorf("expected escaped instance name in body, got:\n%s", body)
+ }
+}
+
+func TestHomeUsesConfiguredNameNotDefault(t *testing.T) {
+ cfg := newTestConfig()
+ cfg.Instance.Name = "ConfiguredName"
+ mux := Routes(cfg)
+
+ body := get(t, mux, http.MethodGet, "/").Body.String()
+
+ if strings.Contains(body, "Openstore") {
+ t.Errorf("handler rendered a hardcoded name instead of the config, got:\n%s", body)
+ }
+}