clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / tinygit / cgi_test.go
cgi_test.go 3.7K
1package tinygit23import (4 "bytes"5 "compress/gzip"6 "context"7 "errors"8 "io"9 "net/http"10 "net/http/httptest"11 "os"12 "path/filepath"13 "strconv"14 "strings"15 "testing"16 "time"17)1819func TestCGIFailureAfterHeadersPreservesPartialPack(t *testing.T) {20 dir := t.TempDir()21 script := "#!/bin/sh\nprintf 'Content-Type: application/octet-stream\\r\\n\\r\\npartial-pack'\nprintf 'backend failed' >&2\nexit 1\n"22 if err := os.WriteFile(filepath.Join(dir, "git"), []byte(script), 0700); err != nil {23 t.Fatal(err)24 }25 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))26 g := &GitRelay{root: dir}27 w := httptest.NewRecorder()28 err := g.cgi(httptest.NewRequest(http.MethodGet, "/", nil), w, Repository{Owner: "owner", Identifier: "test"}, "/git-upload-pack")29 var streamed *cgiStreamError30 if !errors.As(err, &streamed) || !strings.Contains(err.Error(), "backend failed") {31 t.Fatalf("backend failure did not retain committed-response state: %v", err)32 }33 if got := w.Body.String(); got != "partial-pack" {34 t.Fatalf("error corrupted pack response: %q", got)35 }36}3738func TestCGIGzipInputDecodesAndRemovesSpool(t *testing.T) {39 want := strings.Repeat("object negotiation\n", 10000)40 var compressed bytes.Buffer41 zw := gzip.NewWriter(&compressed)42 if _, err := io.WriteString(zw, want); err != nil {43 t.Fatal(err)44 }45 if err := zw.Close(); err != nil {46 t.Fatal(err)47 }48 req := httptest.NewRequest(http.MethodPost, "/", &compressed)49 req.Header.Set("Content-Encoding", "gzip")50 body, length, cleanup, err := cgiInput(req)51 if err != nil {52 t.Fatal(err)53 }54 defer cleanup()55 got, err := io.ReadAll(body)56 if err != nil || string(got) != want || length != strconv.Itoa(len(want)) {57 t.Fatalf("decoded request: length=%s bytes=%d err=%v", length, len(got), err)58 }59 file, ok := body.(*os.File)60 if !ok {61 t.Fatal("compressed request did not use a disk spool")62 }63 cleanup()64 if _, err := os.Stat(file.Name()); !errors.Is(err, os.ErrNotExist) {65 t.Fatalf("temporary request remains after cleanup: %v", err)66 }67}6869// A backend that waits for more input after its first output must not require70// the whole pack to fit in memory before the client can receive any of it.71func TestCGIStreamsBeforeBackendExits(t *testing.T) {72 dir := t.TempDir()73 script := "#!/bin/sh\nprintf 'Content-Type: application/octet-stream\\r\\n\\r\\nfirst'\nread release\nprintf 'last'\n"74 if err := os.WriteFile(filepath.Join(dir, "git"), []byte(script), 0700); err != nil {75 t.Fatal(err)76 }77 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))78 input, release := io.Pipe()79 defer input.Close()80 defer release.Close()81 g := &GitRelay{root: dir}82 finished := make(chan error, 1)83 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {84 r.Body = input85 finished <- g.cgi(r, w, Repository{Owner: "owner", Identifier: "test"}, "/git-upload-pack")86 }))87 defer server.Close()88 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)89 defer cancel()90 req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)91 if err != nil {92 t.Fatal(err)93 }94 response, err := server.Client().Do(req)95 if err != nil {96 _ = release.Close()97 t.Fatalf("backend output was not streamed: %v", err)98 }99 defer response.Body.Close()100 first := make([]byte, 5)101 if _, err := io.ReadFull(response.Body, first); err != nil || string(first) != "first" {102 _ = release.Close()103 t.Fatalf("first backend chunk = %q, %v", first, err)104 }105 if _, err := io.WriteString(release, "continue\n"); err != nil {106 t.Fatal(err)107 }108 _ = release.Close()109 rest, err := io.ReadAll(response.Body)110 if err != nil || string(rest) != "last" {111 t.Fatalf("remaining backend output = %q, %v", rest, err)112 }113 if err := <-finished; err != nil {114 t.Fatal(err)115 }116}