Artifact 06c9077d363652de8737d58f5ef847d65819d2b31ca997833064f2a07a758d2d:
- File
repo_pull_test.go
— part of check-in
[47dc6363c0]
at
2026-10-12 19:01:46
on branch main
— fix(sync): frame HTTP requests and replies as fossil's http.c does (#310) (#342)
The server refused application/x-fossil-debug with HTTP 415, where fossil dispatches it to sync like the other x-fossil types. The client read any 2xx as a sync reply and decoded a reply as compressed only when typed exactly application/x-fossil, so a reply with no Content-Type was read as plain cards and a proxy's HTML page was parsed as cards.
Replies now go through xfer.DecodeReply, which applies section 4's reply rule by case-insensitive prefix: no type is the compressed container, an x-fossil-debug or x-fossil-uncompressed prefix is card text, another x-fossil prefix is compressed, and anything else is refused. Only status 200 is a reply. (user: [email protected], size: 3071) [annotate] [blame] [check-ins using]
package libfossil_test import ( "context" "io" "net/http" "net/http/httptest" "path/filepath" "testing" "github.com/danmestas/go-libfossil" ) // serverRepo provisions a fossil repo populated with one commit and an // httptest server hosting its xfer endpoint. Returns the URL to dial. func serverRepo(t *testing.T) (*libfossil.Repo, string) { t.Helper() dir := t.TempDir() repoPath := filepath.Join(dir, "server.fossil") repo, err := libfossil.Create(repoPath, libfossil.CreateOpts{}) if err != nil { t.Fatalf("libfossil.Create: %v", err) } t.Cleanup(func() { _ = repo.Close() }) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), 500) return } resp, err := repo.HandleSync(r.Context(), body) if err != nil { http.Error(w, err.Error(), 500) return } // HandleSync's reply is the compressed container. ยง4 has a client // refuse a reply typed as anything else, and net/http would sniff // this body as application/octet-stream. w.Header().Set("Content-Type", "application/x-fossil") _, _ = w.Write(resp) })) t.Cleanup(srv.Close) return repo, srv.URL } func TestRepoPull_Roundtrip(t *testing.T) { _, url := serverRepo(t) clientPath := filepath.Join(t.TempDir(), "client.fossil") client, err := libfossil.Create(clientPath, libfossil.CreateOpts{}) if err != nil { t.Fatalf("libfossil.Create client: %v", err) } defer client.Close() res, err := client.Pull(context.Background(), url, libfossil.PullOpts{}) if err != nil { t.Fatalf("Pull: %v", err) } if res == nil { t.Fatalf("Pull returned nil result") } } func TestRepoPull_NilCtxPanics(t *testing.T) { clientPath := filepath.Join(t.TempDir(), "client.fossil") client, err := libfossil.Create(clientPath, libfossil.CreateOpts{}) if err != nil { t.Fatalf("Create: %v", err) } defer client.Close() defer func() { if r := recover(); r == nil { t.Fatal("expected panic on nil ctx, got none") } }() //nolint:staticcheck // intentionally nil to exercise the assert _, _ = client.Pull(nil, "http://x", libfossil.PullOpts{}) } func TestRepoPull_EmptyURLPanics(t *testing.T) { clientPath := filepath.Join(t.TempDir(), "client.fossil") client, err := libfossil.Create(clientPath, libfossil.CreateOpts{}) if err != nil { t.Fatalf("Create: %v", err) } defer client.Close() defer func() { if r := recover(); r == nil { t.Fatal("expected panic on empty url, got none") } }() _, _ = client.Pull(context.Background(), "", libfossil.PullOpts{}) } func TestRepoPull_TransportError(t *testing.T) { clientPath := filepath.Join(t.TempDir(), "client.fossil") client, err := libfossil.Create(clientPath, libfossil.CreateOpts{}) if err != nil { t.Fatalf("Create: %v", err) } defer client.Close() // Unreachable URL; expect wrapped error, no panic. _, err = client.Pull(context.Background(), "http://127.0.0.1:1/missing", libfossil.PullOpts{}) if err == nil { t.Fatal("expected transport error, got nil") } }