174 lines
5.7 KiB
Go
174 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
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 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 TestPhaseAAccountRequestRejectsSecretsAndUnknownFields(t *testing.T) {
|
|
for name, body := range map[string]string{
|
|
"raw password": `{"id":"account-a","platform":"mock","platform_account_key":"a","authorization_kind":"owned","credential_reference":{"id":"credential-a","provider":"os_keyring","key":"creatorhub/account-a"},"password":"secret"}`,
|
|
"nested secret": `{"id":"account-a","platform":"mock","platform_account_key":"a","authorization_kind":"owned","credential_reference":{"id":"credential-a","provider":"os_keyring","key":"creatorhub/account-a","secret":"secret"}}`,
|
|
"unknown field": `{"id":"account-a","platform":"mock","platform_account_key":"a","authorization_kind":"owned","credential_reference":{"id":"credential-a","provider":"os_keyring","key":"creatorhub/account-a"},"extra":true}`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
app := fiber.New()
|
|
app.Post("/", func(c fiber.Ctx) error {
|
|
var input accountRequest
|
|
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(body)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected sensitive or unknown 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(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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStartupLogIncludesListenAddress(t *testing.T) {
|
|
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"})
|
|
var entry map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entry["listen_addr"] != ":8080" || entry["service"] != "control-plane" {
|
|
t.Fatalf("unexpected startup log: %#v", entry)
|
|
}
|
|
}
|