package push import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" ) // fakeServer stands in for a remote podkit server with ingest enabled. type fakeServer struct { mu sync.Mutex existing map[string]int64 received map[string]string srv *httptest.Server } func newFakeServer(t *testing.T, existing map[string]int64) *fakeServer { t.Helper() f := &fakeServer{existing: existing, received: map[string]string{}} if f.existing == nil { f.existing = map[string]int64{} } mux := http.NewServeMux() mux.HandleFunc("/manifest", func(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() _ = json.NewEncoder(w).Encode(map[string]any{"files": f.existing}) }) mux.HandleFunc("/ingest", func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) f.mu.Lock() defer f.mu.Unlock() f.received[r.URL.Query().Get("name")] = string(body) w.WriteHeader(http.StatusOK) }) f.srv = httptest.NewServer(mux) t.Cleanup(f.srv.Close) return f } func libraryDir(t *testing.T, files map[string]string) string { t.Helper() dir := t.TempDir() for name, body := range files { if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } return dir } func TestPushUploadsAudio(t *testing.T) { dir := libraryDir(t, map[string]string{ "01 - April.mp3": "aaa", "02 - May.mp3": "bbbb", }) f := newFakeServer(t, nil) results, err := Run(context.Background(), Options{Dir: dir, Server: f.srv.URL, Token: "t"}) if err != nil { t.Fatal(err) } uploaded, skipped, failed := Summarise(results) if uploaded != 2 || skipped != 0 || failed != 0 { t.Fatalf("uploaded=%d skipped=%d failed=%d", uploaded, skipped, failed) } if f.received["01 - April.mp3"] != "aaa" || f.received["02 - May.mp3"] != "bbbb" { t.Errorf("server received %v", f.received) } } // Re-running a push must not re-send what is already there; a lecture series is // gigabytes and the second run should be nearly free. func TestPushSkipsFilesTheServerHas(t *testing.T) { dir := libraryDir(t, map[string]string{ "01 - April.mp3": "aaa", "02 - May.mp3": "bbbb", }) f := newFakeServer(t, map[string]int64{"01 - April.mp3": 3}) results, err := Run(context.Background(), Options{Dir: dir, Server: f.srv.URL}) if err != nil { t.Fatal(err) } uploaded, skipped, _ := Summarise(results) if uploaded != 1 || skipped != 1 { t.Fatalf("uploaded=%d skipped=%d", uploaded, skipped) } if _, sent := f.received["01 - April.mp3"]; sent { t.Error("a file the server already had was re-uploaded") } } // A same-named file of a different size is a different file — most likely a // truncated earlier upload — and must be re-sent rather than assumed good. func TestPushResendsOnSizeMismatch(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) f := newFakeServer(t, map[string]int64{"01 - April.mp3": 99}) results, err := Run(context.Background(), Options{Dir: dir, Server: f.srv.URL}) if err != nil { t.Fatal(err) } uploaded, skipped, _ := Summarise(results) if uploaded != 1 || skipped != 0 { t.Fatalf("uploaded=%d skipped=%d", uploaded, skipped) } } func TestPushForceIgnoresManifest(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) f := newFakeServer(t, map[string]int64{"01 - April.mp3": 3}) results, err := Run(context.Background(), Options{Dir: dir, Server: f.srv.URL, Force: true}) if err != nil { t.Fatal(err) } if uploaded, _, _ := Summarise(results); uploaded != 1 { t.Fatalf("uploaded=%d, want 1 under --force", uploaded) } } func TestPushIgnoresNonAudio(t *testing.T) { dir := libraryDir(t, map[string]string{ "01 - April.mp3": "aaa", "feed.xml": "", "cover.jpg": "img", "notes.txt": "hello", }) f := newFakeServer(t, nil) if _, err := Run(context.Background(), Options{Dir: dir, Server: f.srv.URL}); err != nil { t.Fatal(err) } if len(f.received) != 1 { t.Errorf("server received %v, want only the mp3", f.received) } } func TestPushSendsToken(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) var gotAuth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/manifest" { gotAuth = r.Header.Get("Authorization") _ = json.NewEncoder(w).Encode(map[string]any{"files": map[string]int64{}}) return } w.WriteHeader(http.StatusOK) })) defer srv.Close() if _, err := Run(context.Background(), Options{Dir: dir, Server: srv.URL, Token: "sekrit"}); err != nil { t.Fatal(err) } if gotAuth != "Bearer sekrit" { t.Errorf("Authorization header was %q", gotAuth) } } // A 404 from the manifest means either a bad token or a server without // --ingest. The error has to say so, since a bare "404" is unactionable. func TestPushExplainsManifest404(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) })) defer srv.Close() _, err := Run(context.Background(), Options{Dir: dir, Server: srv.URL}) if err == nil { t.Fatal("expected an error") } for _, want := range []string{"token", "--ingest"} { if !contains(err.Error(), want) { t.Errorf("error should mention %q; got %q", want, err) } } } func TestPushReportsUploadFailure(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/manifest" { _ = json.NewEncoder(w).Encode(map[string]any{"files": map[string]int64{}}) return } http.Error(w, "disk full", http.StatusInsufficientStorage) })) defer srv.Close() results, err := Run(context.Background(), Options{Dir: dir, Server: srv.URL}) if err != nil { t.Fatal(err) } if _, _, failed := Summarise(results); failed != 1 { t.Fatalf("failed=%d, want 1", failed) } if !contains(results[0].Err.Error(), "disk full") { t.Errorf("server's reason was lost: %v", results[0].Err) } } func TestPushRejectsBadServerURL(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) for _, s := range []string{"", "not-a-url", "/relative/path"} { if _, err := Run(context.Background(), Options{Dir: dir, Server: s}); err == nil { t.Errorf("server %q was accepted", s) } } } func TestPushRejectsEmptyLibrary(t *testing.T) { f := newFakeServer(t, nil) if _, err := Run(context.Background(), Options{Dir: t.TempDir(), Server: f.srv.URL}); err == nil { t.Error("expected an error for a directory with no audio") } } // The base URL may include a path, since the server will usually sit behind a // reverse proxy at something like /podkit. func TestPushHonoursServerPathPrefix(t *testing.T) { dir := libraryDir(t, map[string]string{"01 - April.mp3": "aaa"}) var paths []string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { paths = append(paths, r.URL.Path) if r.URL.Path == "/podkit/manifest" { _ = json.NewEncoder(w).Encode(map[string]any{"files": map[string]int64{}}) return } w.WriteHeader(http.StatusOK) })) defer srv.Close() if _, err := Run(context.Background(), Options{Dir: dir, Server: srv.URL + "/podkit"}); err != nil { t.Fatal(err) } if len(paths) != 2 || paths[0] != "/podkit/manifest" || paths[1] != "/podkit/ingest" { t.Errorf("requested %v", paths) } } func contains(h, n string) bool { for i := 0; i+len(n) <= len(h); i++ { if h[i:i+len(n)] == n { return true } } return false }