Procházet zdrojové kódy

Some tests from GPT

Piotr Czajkowski před 1 týdnem
rodič
revize
b65e5ea1ce
3 změnil soubory, kde provedl 326 přidání a 0 odebrání
  1. 18 0
      hash_test.go
  2. 124 0
      server_test.go
  3. 184 0
      storage_test.go

+ 18 - 0
hash_test.go

@@ -0,0 +1,18 @@
+package main
+
+import "testing"
+
+func TestGetHash(t *testing.T) {
+	for _, tc := range []struct{ link, want string }{
+		{"", "00000000"},
+		{"https://example.com", "96248650"},
+		{"https://example.com/69897/4885590609", "2be9aa73"},
+		{"https://example.com/105844/11202952336", "2be9aa73"},
+	} {
+		t.Run(tc.link, func(t *testing.T) {
+			if got := getHash(tc.link); got != tc.want {
+				t.Fatalf("getHash(%q) = %q, want %q", tc.link, got, tc.want)
+			}
+		})
+	}
+}

+ 124 - 0
server_test.go

@@ -0,0 +1,124 @@
+package main
+
+import (
+	"html"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+)
+
+func resetServer(t *testing.T) {
+	t.Helper()
+	resetLinks(t)
+	previousQueue, previousDomain := toSave, *domain
+	toSave = make(chan string, 10)
+	*domain = ""
+	t.Cleanup(func() {
+		toSave, *domain = previousQueue, previousDomain
+	})
+}
+
+func TestShorten(t *testing.T) {
+	link := "https://example.com/path"
+	for _, tc := range []struct{ name, target, domain, prefix string }{
+		{"query", "/s/?link=" + url.QueryEscape(link), "", "short.test/"},
+		{"path", "/s/" + link, "", "short.test/"},
+		{"configured domain", "/s/?link=" + url.QueryEscape(link), "https://short.test", "https://short.test/"},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			resetServer(t)
+			*domain = tc.domain
+			r := httptest.NewRequest(http.MethodGet, tc.target, nil)
+			r.Host = "short.test"
+			w := httptest.NewRecorder()
+			shorten(w, r)
+			if w.Code != http.StatusOK {
+				t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+			}
+			want := `value="` + tc.prefix + getHash(link) + `"`
+			if !strings.Contains(w.Body.String(), want) || getLink(getHash(link)) != link {
+				t.Fatalf("shortened link missing or not stored: %s", w.Body.String())
+			}
+			if len(toSave) != 1 {
+				t.Fatal("shorten did not queue exactly one record")
+			}
+		})
+	}
+}
+
+func TestShortenRejectsInvalidURL(t *testing.T) {
+	resetServer(t)
+	w := httptest.NewRecorder()
+	shorten(w, httptest.NewRequest(http.MethodGet, "/s/?link="+url.QueryEscape("http://%zz"), nil))
+	if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "Error parsing link:") {
+		t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+	}
+	if len(toSave) != 0 {
+		t.Fatal("invalid URL was queued")
+	}
+}
+
+func TestDecode(t *testing.T) {
+	for _, target := range []string{"/d/abc", "/d/?link=abc", "/d/?link=" + url.QueryEscape("https://short.test/abc")} {
+		t.Run(target, func(t *testing.T) {
+			resetServer(t)
+			link := "https://example.com/?a=1&b=2"
+			links.Store("abc", link)
+			w := httptest.NewRecorder()
+			decode(w, httptest.NewRequest(http.MethodGet, target, nil))
+			if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), `value="`+html.EscapeString(link)+`"`) {
+				t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+			}
+		})
+	}
+}
+
+func TestDecodeMissing(t *testing.T) {
+	for _, target := range []string{"/d/", "/d/missing", "/d/?link=https://short.test/missing"} {
+		t.Run(target, func(t *testing.T) {
+			resetServer(t)
+			w := httptest.NewRecorder()
+			decode(w, httptest.NewRequest(http.MethodGet, target, nil))
+			if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "Not found!") {
+				t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+			}
+		})
+	}
+}
+
+func TestRedirectOrServe(t *testing.T) {
+	t.Run("known ID redirects", func(t *testing.T) {
+		resetServer(t)
+		link := "https://example.com/path?q=one&other=two"
+		links.Store("abc", link)
+		w := httptest.NewRecorder()
+		redirectOrServe(w, httptest.NewRequest(http.MethodGet, "/abc", nil))
+		if w.Code != http.StatusMovedPermanently || w.Header().Get("Location") != link {
+			t.Fatalf("status = %d, Location = %q", w.Code, w.Header().Get("Location"))
+		}
+	})
+	for _, target := range []string{"/", "/missing"} {
+		t.Run(target, func(t *testing.T) {
+			resetServer(t)
+			w := httptest.NewRecorder()
+			redirectOrServe(w, httptest.NewRequest(http.MethodGet, target, nil))
+			if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Link to shorten:") {
+				t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+			}
+		})
+	}
+}
+
+func TestReturnErrorEscapesHTML(t *testing.T) {
+	message := `<script>alert("oops")</script>`
+	w := httptest.NewRecorder()
+	returnErorr(w, message)
+	if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), html.EscapeString(message)) {
+		t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
+	}
+	if strings.Contains(w.Body.String(), message) {
+		t.Fatal("error message was rendered as executable HTML")
+	}
+}

+ 184 - 0
storage_test.go

@@ -0,0 +1,184 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+)
+
+// Tests share application globals and must not run in parallel.
+func resetLinks(t *testing.T) {
+	t.Helper()
+	clear := func() {
+		links.Range(func(key, value interface{}) bool {
+			links.Delete(key)
+			return true
+		})
+	}
+	clear()
+	t.Cleanup(clear)
+}
+
+func TestAddLink(t *testing.T) {
+	resetLinks(t)
+	queue := make(chan string, 2)
+	link := "https://example.com/path?q=one&other=two"
+	ok, id := addLink(link, queue)
+	if !ok {
+		t.Fatalf("addLink failed: %s", id)
+	}
+	if got := getLink(id); got != link {
+		t.Fatalf("stored URL = %q, want %q", got, link)
+	}
+	select {
+	case got := <-queue:
+		if want := fmt.Sprintf(format, id, link); got != want {
+			t.Fatalf("queued record = %q, want %q", got, want)
+		}
+	default:
+		t.Fatal("new link was not queued for saving")
+	}
+	if ok, got := addLink(link, queue); !ok || got != id {
+		t.Fatalf("duplicate URL returned (%v, %q), want (true, %q)", ok, got, id)
+	}
+	if len(queue) != 0 {
+		t.Fatal("duplicate URL was queued again")
+	}
+}
+
+func TestAddLinkRejectsInvalidURL(t *testing.T) {
+	for _, link := range []string{"http://%zz", "http://[::1", "https://example.com/\n"} {
+		t.Run(link, func(t *testing.T) {
+			resetLinks(t)
+			queue := make(chan string, 1)
+			ok, message := addLink(link, queue)
+			if ok || !strings.Contains(message, "Error parsing link:") {
+				t.Fatalf("invalid URL returned (%v, %q)", ok, message)
+			}
+			if len(queue) != 0 || getLink(getHash(link)) != "" {
+				t.Fatal("invalid URL was stored or queued")
+			}
+		})
+	}
+}
+
+func TestAddLinkRejectsCollision(t *testing.T) {
+	resetLinks(t)
+	first := "https://example.com/69897/4885590609"
+	second := "https://example.com/105844/11202952336"
+	queue := make(chan string, 2)
+	ok, id := addLink(first, queue)
+	if !ok {
+		t.Fatalf("first URL rejected: %s", id)
+	}
+	if ok, message := addLink(second, queue); ok || message == "" {
+		t.Fatalf("collision returned (%v, %q), want rejection", ok, message)
+	}
+	if getLink(id) != first || len(queue) != 1 {
+		t.Fatal("collision replaced the original URL or queued another record")
+	}
+}
+
+func TestAddLinkConcurrentDuplicates(t *testing.T) {
+	resetLinks(t)
+	const workers = 32
+	queue := make(chan string, workers)
+	link := "https://example.com/concurrent"
+	var wg sync.WaitGroup
+	for i := 0; i < workers; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			if ok, id := addLink(link, queue); !ok || id != getHash(link) {
+				t.Errorf("addLink returned (%v, %q)", ok, id)
+			}
+		}()
+	}
+	wg.Wait()
+	if len(queue) != 1 || getLink(getHash(link)) != link {
+		t.Fatal("concurrent duplicates should store and queue exactly one mapping")
+	}
+}
+
+func TestGetLinkMissing(t *testing.T) {
+	resetLinks(t)
+	if got := getLink("missing"); got != "" {
+		t.Fatalf("missing URL = %q, want empty string", got)
+	}
+}
+
+func TestReadLinks(t *testing.T) {
+	resetLinks(t)
+	path := filepath.Join(t.TempDir(), "links.txt")
+	content := "one<>https://example.com/first\ntwo<>https://example.com/?q=<>\n"
+	if err := os.WriteFile(path, []byte(content), 0600); err != nil {
+		t.Fatal(err)
+	}
+	readLinks(path)
+	for id, want := range map[string]string{
+		"one": "https://example.com/first",
+		"two": "https://example.com/?q=<>",
+	} {
+		if got := getLink(id); got != want {
+			t.Fatalf("getLink(%q) = %q, want %q", id, got, want)
+		}
+	}
+}
+
+func TestReadLinksCreatesMissingFile(t *testing.T) {
+	resetLinks(t)
+	path := filepath.Join(t.TempDir(), "links.txt")
+	readLinks(path)
+	info, err := os.Stat(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if info.Size() != 0 {
+		t.Fatal("new storage file should be empty")
+	}
+}
+
+func TestSaveLinkAppendsAndReloads(t *testing.T) {
+	resetLinks(t)
+	path := filepath.Join(t.TempDir(), "links.txt")
+	initial := "old<>https://example.com/old\n"
+	if err := os.WriteFile(path, []byte(initial), 0600); err != nil {
+		t.Fatal(err)
+	}
+	queue := make(chan string, 2)
+	queue <- "new<>https://example.com/new\n"
+	queue <- "query<>https://example.com/?q=<>\n"
+	close(queue)
+	done := make(chan struct{})
+	go func() {
+		saveLink(path, queue)
+		close(done)
+	}()
+	select {
+	case <-done:
+	case <-time.After(5 * time.Second):
+		t.Fatal("saveLink did not stop after the queue closed")
+	}
+	content, err := os.ReadFile(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := initial + "new<>https://example.com/new\nquery<>https://example.com/?q=<>\n"
+	if string(content) != want {
+		t.Fatalf("saved content = %q, want %q", content, want)
+	}
+	readLinks(path)
+	for id, want := range map[string]string{
+		"old":   "https://example.com/old",
+		"new":   "https://example.com/new",
+		"query": "https://example.com/?q=<>",
+	} {
+		if got := getLink(id); got != want {
+			t.Fatalf("reloaded %q = %q, want %q", id, got, want)
+		}
+	}
+}