package main import ( "bytes" "context" "encoding/json" "flag" "io" "log" "net" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "scratchbox/internal/config" httpapi "scratchbox/internal/http" "scratchbox/internal/storage" ) func repoRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")) } func TestMainProcessSuccess(t *testing.T) { t.Parallel() tmp := t.TempDir() cfgPath := filepath.Join(tmp, "config.yaml") cfg := ` server: listen_addr: "127.0.0.1:0" limits: max_upload_size: "1MB" default_ttl: "1h" storage: data_dir: "` + filepath.Join(tmp, "data") + `" cleanup_interval: "10s" security: allowed_ips: [] trust_proxy_headers: false rate_limit: enabled: true requests_per_minute: 10 burst: 1 ` if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } cmd := exec.Command(os.Args[0], "-test.run=TestMainHelperProcess") cmd.Dir = repoRoot(t) cmd.Env = append(os.Environ(), "GO_WANT_MAIN_HELPER=1", "MAIN_HELPER_MODE=success", "MAIN_HELPER_CONFIG="+cfgPath, ) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("helper process failed: %v, output=%s", err, string(out)) } } func TestMainProcessBadConfigFails(t *testing.T) { t.Parallel() cmd := exec.Command(os.Args[0], "-test.run=TestMainHelperProcess") cmd.Dir = repoRoot(t) cmd.Env = append(os.Environ(), "GO_WANT_MAIN_HELPER=1", "MAIN_HELPER_MODE=bad-config", ) if err := cmd.Run(); err == nil { t.Fatalf("expected helper process failure for bad config") } } func TestMainProcessStorageInitFails(t *testing.T) { t.Parallel() tmp := t.TempDir() cfgPath := filepath.Join(tmp, "config.yaml") dataFile := filepath.Join(tmp, "data-file") if err := os.WriteFile(dataFile, []byte("x"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } cfg := ` server: listen_addr: "127.0.0.1:0" limits: max_upload_size: "1MB" default_ttl: "1h" storage: data_dir: "` + dataFile + `" cleanup_interval: "10s" security: allowed_ips: [] trust_proxy_headers: false rate_limit: enabled: true requests_per_minute: 10 burst: 1 ` if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } cmd := exec.Command(os.Args[0], "-test.run=TestMainHelperProcess") cmd.Dir = repoRoot(t) cmd.Env = append(os.Environ(), "GO_WANT_MAIN_HELPER=1", "MAIN_HELPER_MODE=storage-fail", "MAIN_HELPER_CONFIG="+cfgPath, ) if err := cmd.Run(); err == nil { t.Fatalf("expected helper process failure for storage init") } } func TestMainProcessHTTPInitFails(t *testing.T) { t.Parallel() tmp := t.TempDir() cfgPath := filepath.Join(tmp, "config.yaml") cfg := ` server: listen_addr: "127.0.0.1:0" limits: max_upload_size: "1MB" default_ttl: "1h" storage: data_dir: "` + filepath.Join(tmp, "data") + `" cleanup_interval: "10s" security: allowed_ips: [] trust_proxy_headers: false rate_limit: enabled: true requests_per_minute: 10 burst: 1 ` if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } cmd := exec.Command(os.Args[0], "-test.run=TestMainHelperProcess") cmd.Dir = repoRoot(t) cmd.Env = append(os.Environ(), "GO_WANT_MAIN_HELPER=1", "MAIN_HELPER_MODE=http-fail", "MAIN_HELPER_CONFIG="+cfgPath, ) if err := cmd.Run(); err == nil { t.Fatalf("expected helper process failure for http init") } } func TestMainHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_MAIN_HELPER") != "1" { return } flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) mode := os.Getenv("MAIN_HELPER_MODE") switch mode { case "success": cfgPath := os.Getenv("MAIN_HELPER_CONFIG") if cfgPath == "" { os.Exit(2) } go func() { time.Sleep(120 * time.Millisecond) proc, err := os.FindProcess(os.Getpid()) if err == nil { _ = proc.Signal(os.Interrupt) } }() os.Args = []string{"pastebin", "-config", cfgPath} main() os.Exit(0) case "bad-config": os.Args = []string{"pastebin", "-config", "/no/such/config.yaml"} main() os.Exit(0) case "storage-fail": cfgPath := os.Getenv("MAIN_HELPER_CONFIG") os.Args = []string{"pastebin", "-config", cfgPath} main() os.Exit(0) case "http-fail": cfgPath := os.Getenv("MAIN_HELPER_CONFIG") if err := os.Chdir(t.TempDir()); err != nil { os.Exit(2) } os.Args = []string{"pastebin", "-config", cfgPath} main() os.Exit(0) default: os.Exit(2) } } func TestLiveServerEndToEndHTTP(t *testing.T) { tmp := t.TempDir() cfgPath := filepath.Join(tmp, "config.yaml") cfgRaw := ` server: listen_addr: "127.0.0.1:0" limits: max_upload_size: "1MB" default_ttl: "1h" storage: data_dir: "` + filepath.Join(tmp, "data") + `" cleanup_interval: "10s" security: allowed_ips: [] trust_proxy_headers: false rate_limit: enabled: true requests_per_minute: 30 burst: 10 ` if err := os.WriteFile(cfgPath, []byte(cfgRaw), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } cfg, err := config.Load(cfgPath) if err != nil { t.Fatalf("config.Load() error = %v", err) } logger := log.New(io.Discard, "", 0) store, err := storage.NewFilesystemStore(cfg.Storage.DataDir) if err != nil { t.Fatalf("NewFilesystemStore() error = %v", err) } wd, err := os.Getwd() if err != nil { t.Fatalf("Getwd() error = %v", err) } if err := os.Chdir(repoRoot(t)); err != nil { t.Fatalf("Chdir() error = %v", err) } defer func() { _ = os.Chdir(wd) }() srv, err := httpapi.NewServer(cfg, store, logger) if err != nil { t.Fatalf("httpapi.NewServer() error = %v", err) } httpServer := &http.Server{ Addr: cfg.Server.ListenAddr, Handler: srv.Routes(), } ln, err := net.Listen("tcp", cfg.Server.ListenAddr) if err != nil { t.Fatalf("net.Listen() error = %v", err) } defer ln.Close() go func() { _ = httpServer.Serve(ln) }() t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = httpServer.Shutdown(ctx) }) baseURL := "http://" + ln.Addr().String() client := &http.Client{Timeout: 3 * time.Second} createReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/scratch", bytes.NewBufferString("hello from live server")) if err != nil { t.Fatalf("http.NewRequest(create) error = %v", err) } createReq.Header.Set("Content-Type", "text/plain; charset=utf-8") createResp, err := client.Do(createReq) if err != nil { t.Fatalf("client.Do(create) error = %v", err) } defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { b, _ := io.ReadAll(createResp.Body) t.Fatalf("create status = %d, want %d, body=%q", createResp.StatusCode, http.StatusCreated, string(b)) } var payload map[string]any if err := json.NewDecoder(createResp.Body).Decode(&payload); err != nil { t.Fatalf("decode create payload error = %v", err) } id, _ := payload["id"].(string) if id == "" { t.Fatalf("create payload missing id") } rawResp, err := client.Get(baseURL + "/raw/" + id) if err != nil { t.Fatalf("client.Get(raw) error = %v", err) } defer rawResp.Body.Close() if rawResp.StatusCode != http.StatusOK { t.Fatalf("raw status = %d, want %d", rawResp.StatusCode, http.StatusOK) } rawBody, err := io.ReadAll(rawResp.Body) if err != nil { t.Fatalf("ReadAll(raw) error = %v", err) } if got := string(rawBody); got != "hello from live server" { t.Fatalf("raw body = %q, want %q", got, "hello from live server") } viewResp, err := client.Get(baseURL + "/s/" + id) if err != nil { t.Fatalf("client.Get(view) error = %v", err) } defer viewResp.Body.Close() if viewResp.StatusCode != http.StatusOK { t.Fatalf("view status = %d, want %d", viewResp.StatusCode, http.StatusOK) } viewBody, err := io.ReadAll(viewResp.Body) if err != nil { t.Fatalf("ReadAll(view) error = %v", err) } if !strings.Contains(string(viewBody), "Scratch "+id) { t.Fatalf("view body missing scratch id") } }