Files
scratchbox/cmd/pastebin/main_test.go
T

334 lines
7.9 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"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
}
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")
}
}