Files
creator-hub/cmd/control-plane/main_test.go
T

212 lines
6.6 KiB
Go

package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/gofiber/fiber/v3"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func TestAPIProxyPreservesMethodPathAndStatus(t *testing.T) {
requests := make(chan *http.Request, 1)
gateway := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
requests <- request
response.WriteHeader(http.StatusServiceUnavailable)
}))
defer gateway.Close()
upstream, err := url.Parse(gateway.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/browsers/account-a/start?force=true", nil)
response, err := newHandler(upstream, t.TempDir()).Test(request)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
proxied := <-requests
if proxied.Method != http.MethodPost || proxied.URL.Path != "/v1/browsers/account-a/start" || proxied.URL.RawQuery != "force=true" {
t.Fatalf("unexpected proxy request %s %s", proxied.Method, proxied.URL.String())
}
if response.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("expected upstream status 503, got %d", response.StatusCode)
}
}
func TestLoadConfigRejectsInvalidGateway(t *testing.T) {
t.Setenv("DOCKER_GATEWAY_URL", "file:///var/run/docker.sock")
if _, err := loadConfig(); err == nil {
t.Fatal("expected invalid gateway URL to be rejected")
}
}
func TestLoadConfigRejectsInvalidDatabase(t *testing.T) {
t.Setenv("DATABASE_URL", "file:///tmp/creatorhub.db")
if _, err := loadConfig(); err == nil {
t.Fatal("expected non-Postgres database URL to be rejected")
}
}
func TestExecuteContextStopsOnSIGTERM(t *testing.T) {
if os.Getenv("CREATORHUB_SIGTERM_HELPER") == "1" {
command := &cobra.Command{Use: "shutdown-test", RunE: func(command *cobra.Command, _ []string) error {
_, _ = os.Stdout.WriteString("ready\n")
<-command.Context().Done()
_, _ = os.Stdout.WriteString("stopped\n")
return nil
}}
if err := execute(command); err != nil {
t.Fatal(err)
}
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
process := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestExecuteContextStopsOnSIGTERM$")
process.Env = append(os.Environ(), "CREATORHUB_SIGTERM_HELPER=1")
stdout, err := process.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := process.Start(); err != nil {
t.Fatal(err)
}
scanner := bufio.NewScanner(stdout)
if !scanner.Scan() || scanner.Text() != "ready" {
t.Fatalf("helper did not become ready: %q err=%v", scanner.Text(), scanner.Err())
}
if err := process.Process.Signal(syscall.SIGTERM); err != nil {
t.Fatal(err)
}
if !scanner.Scan() || scanner.Text() != "stopped" {
t.Fatalf("helper did not finish cleanup: %q err=%v", scanner.Text(), scanner.Err())
}
if err := process.Wait(); err != nil || ctx.Err() != nil {
t.Fatalf("process did not exit gracefully after SIGTERM: wait=%v context=%v", err, ctx.Err())
}
}
func TestStartupLogRedactsGatewayURL(t *testing.T) {
gateway, err := url.Parse("https://internal-user:internal-pass@docker-gateway:8443/v1?access_token=internal-token")
if err != nil {
t.Fatal(err)
}
logger := logrus.StandardLogger()
previousOutput, previousFormatter, previousLevel := logger.Out, logger.Formatter, logger.Level
t.Cleanup(func() {
logrus.SetOutput(previousOutput)
logrus.SetFormatter(previousFormatter)
logrus.SetLevel(previousLevel)
})
var output bytes.Buffer
logrus.SetOutput(&output)
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetLevel(logrus.InfoLevel)
logStartup(config{listenAddr: ":8080", gateway: gateway})
var entry map[string]any
if err := json.Unmarshal(output.Bytes(), &entry); err != nil {
t.Fatal(err)
}
if entry["docker_gateway"] != "https://docker-gateway:8443" {
t.Fatalf("unexpected redacted gateway: %#v", entry["docker_gateway"])
}
for _, sensitive := range []string{"internal-user", "internal-pass", "access_token", "internal-token"} {
if strings.Contains(output.String(), sensitive) {
t.Fatalf("startup log leaked %q: %s", sensitive, output.String())
}
}
}
func TestPhaseARequestRejectsUnknownFields(t *testing.T) {
app := fiber.New()
app.Post("/", func(c fiber.Ctx) error {
var input struct {
ID string `json:"id"`
}
if err := decodePhaseA(c, &input); err != nil {
return phaseAError(c, err)
}
return c.SendStatus(http.StatusNoContent)
})
response, err := app.Test(httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"id":"account-a","password":"must-not-be-accepted"}`)))
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusBadRequest {
t.Fatalf("expected unknown sensitive field to be rejected, got %d", response.StatusCode)
}
}
func TestPhaseAErrorRedactsInternalDetails(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return phaseAError(c, errors.New("database exposed a secret value"))
})
response, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil))
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
if response.StatusCode != http.StatusInternalServerError || strings.Contains(string(body), "secret") || !strings.Contains(string(body), "phase A operation failed") {
t.Fatalf("internal error was not redacted: status=%d body=%s", response.StatusCode, body)
}
}
func TestSPAServesFileAndIndexFallback(t *testing.T) {
directory := t.TempDir()
files := map[string]string{
"index.html": "index",
"app.js": "asset",
"hello world.js": "space",
"应用.js": "unicode",
}
for name, body := range files {
if err := os.WriteFile(filepath.Join(directory, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(directory, "..", "outside-secret.txt"), []byte("secret"), 0o600); err != nil {
t.Fatal(err)
}
app := newHandler(&url.URL{Scheme: "http", Host: "127.0.0.1"}, directory)
for path, want := range map[string]string{
"/app.js": "asset",
"/hello%20world.js": "space",
"/%E5%BA%94%E7%94%A8.js": "unicode",
"/%2e%2e%2foutside-secret.txt": "index",
"/dashboard": "index",
} {
response, err := app.Test(httptest.NewRequest(http.MethodGet, path, nil))
if err != nil {
t.Fatal(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil || response.StatusCode != http.StatusOK || string(body) != want {
t.Fatalf("GET %s: status=%d body=%q err=%v", path, response.StatusCode, body, readErr)
}
}
}