From b26eed7dffbac99c2c3ed1da69c724917d82690e Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Sun, 1 Sep 2024 23:01:44 -0500 Subject: [PATCH] Now with working tests --- .github/workflows/ci.yaml | 2 + Makefile | 2 +- cmd/server/main_test.go | 48 +-------- server/server_test.go | 33 +++++++ server/views_test.go | 43 ++++++++ {testing => testfunc}/files/ansi-color | 0 {testing => testfunc}/files/code.py | 0 {testing => testfunc}/files/markdown.md | 0 {testing => testfunc}/files/python.diff | 0 testfunc/integration.go | 126 ++++++++++++++++++++++++ testing/__init__.py | 54 ---------- 11 files changed, 210 insertions(+), 98 deletions(-) create mode 100644 server/server_test.go create mode 100644 server/views_test.go rename {testing => testfunc}/files/ansi-color (100%) rename {testing => testfunc}/files/code.py (100%) rename {testing => testfunc}/files/markdown.md (100%) rename {testing => testfunc}/files/python.diff (100%) create mode 100644 testfunc/integration.go delete mode 100644 testing/__init__.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 417a148..bdc70bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,5 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install dependencies + run: make minimal - name: Run tests run: make test diff --git a/Makefile b/Makefile index 032d1cc..a4b7a25 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: minimal -minimal: bin/server bin/fpb bin/fput assets settings.py install-hooks +minimal: bin/server bin/fpb bin/fput assets .PHONY: bin/server bin/server: diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 9031f50..c463fb7 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -3,53 +3,15 @@ package main import ( "bytes" "context" - "fmt" - "net/http" "strconv" "strings" "testing" "time" -) - -const port = 17491 -func waitForReady(ctx context.Context, timeout time.Duration) error { - client := http.Client{} - startTime := time.Now() - for { - req, err := http.NewRequestWithContext( - ctx, - http.MethodGet, - fmt.Sprintf("http://localhost:%d/healthz", port), - nil, - ) - if err != nil { - return fmt.Errorf("creating request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Error making request: %s\n", err.Error()) - continue - } - if resp.StatusCode == http.StatusOK { - fmt.Println("/healthz is ready!") - resp.Body.Close() - return nil - } - resp.Body.Close() + "github.com/chriskuehl/fluffy/testfunc" +) - select { - case <-ctx.Done(): - return ctx.Err() - default: - if time.Since(startTime) >= timeout { - return fmt.Errorf("timeout reached while waiting for endpoint") - } - time.Sleep(250 * time.Millisecond) - } - } -} +const port = 14921 func TestIntegration(t *testing.T) { ctx := context.Background() @@ -63,8 +25,8 @@ func TestIntegration(t *testing.T) { } close(done) }() - if err := waitForReady(ctx, 5*time.Second); err != nil { - t.Errorf("unexpected error: %v", err) + if err := testfunc.WaitForReady(ctx, 5*time.Second, port); err != nil { + t.Fatalf("unexpected error: %v", err) } cancel() <-done diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..337c0ac --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,33 @@ +package server_test + +import ( + "fmt" + "io" + "net/http" + "testing" + + "github.com/chriskuehl/fluffy/testfunc" +) + +func TestHealthz(t *testing.T) { + ts := testfunc.RunningServer(t, testfunc.NewConfig()) + defer ts.Cleanup() + + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/healthz", ts.Port)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, http.StatusOK) + } + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := "ok\n" + body := string(bodyBytes) + if body != want { + t.Errorf("unexpected body: got %q, want %q", body, want) + } +} diff --git a/server/views_test.go b/server/views_test.go new file mode 100644 index 0000000..2cf5082 --- /dev/null +++ b/server/views_test.go @@ -0,0 +1,43 @@ +package server_test + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/chriskuehl/fluffy/testfunc" +) + +func TestIndex(t *testing.T) { + ts := testfunc.RunningServer(t, testfunc.NewConfig()) + defer ts.Cleanup() + + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", ts.Port)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: got %d, want %d", resp.StatusCode, http.StatusOK) + } + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + body := string(bodyBytes) + + wantContent := []string{ + `Report Abuse`, + } + for _, want := range wantContent { + if !strings.Contains(body, want) { + t.Errorf("unexpected body: wanted to find %q but it is not present\nBody:\n%s", want, body) + } + } +} diff --git a/testing/files/ansi-color b/testfunc/files/ansi-color similarity index 100% rename from testing/files/ansi-color rename to testfunc/files/ansi-color diff --git a/testing/files/code.py b/testfunc/files/code.py similarity index 100% rename from testing/files/code.py rename to testfunc/files/code.py diff --git a/testing/files/markdown.md b/testfunc/files/markdown.md similarity index 100% rename from testing/files/markdown.md rename to testfunc/files/markdown.md diff --git a/testing/files/python.diff b/testfunc/files/python.diff similarity index 100% rename from testing/files/python.diff rename to testfunc/files/python.diff diff --git a/testfunc/integration.go b/testfunc/integration.go new file mode 100644 index 0000000..30c5ddf --- /dev/null +++ b/testfunc/integration.go @@ -0,0 +1,126 @@ +package testfunc + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "sync" + "testing" + "time" + + "github.com/chriskuehl/fluffy/server" + "github.com/chriskuehl/fluffy/server/logging" +) + +func NewConfig() *server.Config { + c := server.NewConfig() + c.Version = "(test)" + return c +} + +type TestServer struct { + Cleanup func() + Logs *bytes.Buffer + Port int +} + +func RunningServer(t *testing.T, config *server.Config) TestServer { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + var buf bytes.Buffer + port, done, err := run(t, ctx, &buf, config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := WaitForReady(ctx, 5*time.Second, port); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return TestServer{ + Cleanup: sync.OnceFunc(func() { + cancel() + <-done + }), + Logs: &buf, + Port: port, + } +} + +type serverState struct { + port int + err error +} + +func run(t *testing.T, ctx context.Context, w io.Writer, config *server.Config) (int, chan struct{}, error) { + done := make(chan struct{}) + logger := logging.NewSlogLogger(slog.New(slog.NewTextHandler(w, nil))) + + handler, err := server.NewServer(logger, config) + if err != nil { + return 0, nil, fmt.Errorf("creating server: %w", err) + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, nil, fmt.Errorf("listening: %w", err) + } + httpServer := &http.Server{Handler: handler} + go func() { + if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed { + t.Errorf("listening and serving: %v", err) + } + }() + go func() { + <-ctx.Done() + shutdownCtx := context.Background() + shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) + defer cancel() + if err := httpServer.Shutdown(shutdownCtx); err != nil { + t.Errorf("shutting down http server: %v", err) + } + close(done) + }() + + return listener.Addr().(*net.TCPAddr).Port, done, nil +} + +func WaitForReady(ctx context.Context, timeout time.Duration, port int) error { + client := http.Client{} + startTime := time.Now() + for { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf("http://localhost:%d/healthz", port), + nil, + ) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error making request: %s\n", err.Error()) + continue + } + if resp.StatusCode == http.StatusOK { + fmt.Println("/healthz is ready!") + resp.Body.Close() + return nil + } + resp.Body.Close() + + select { + case <-ctx.Done(): + return ctx.Err() + default: + if time.Since(startTime) >= timeout { + return fmt.Errorf("timeout reached while waiting for endpoint") + } + time.Sleep(250 * time.Millisecond) + } + } +} diff --git a/testing/__init__.py b/testing/__init__.py deleted file mode 100644 index 3f5c813..0000000 --- a/testing/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -import re - -import requests - - -PLAINTEXT_TESTCASES = ( - '', - '\t\t\t', - ' ', - 'hello world', - 'éóñəå ⊂(◉‿◉)つ(ノ≥∇≤)ノ', - 'hello\nworld\n', -) - -BINARY_TESTCASES = ( - b'hello world\00', - b'\x43\x92\xd9\x0f\xaf\x32\x2c\x00\x12\x23', - b'\x11\x22\x33\x44\x55', -) - -FILE_CONTENT_TESTCASES = tuple( - content.encode('utf8') - for content in PLAINTEXT_TESTCASES -) + BINARY_TESTCASES - - -def urls_from_details(details): - """Return list of URLs to objects from details page source.""" - return re.findall( - r'\s+Raw Text', - paste_html, - ) - return url - - -def assert_url_matches_content(url, content): - req = requests.get(url) - assert req.content == content