package scenario import ( "context" "fmt" "io" "path/filepath" "time" "github.com/moby/moby/api/types/container" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" ) var ( GotenbergDockerRepository string GotenbergVersion string GotenbergContainerPlatform string ) type noopLogger struct{} func (n *noopLogger) Printf(format string, v ...any) { // NOOP } // integrationAllowList is the default allow-list pattern injected into // every Gotenberg container started by the integration tests. The outbound // URL guard introduced for SSRF protection rejects URLs whose host // resolves to a non-public IP, which would block: // // - host.docker.internal (Docker host gateway, RFC1918) // - The static helper server running inside the test network // - file:// URIs created in /tmp by the API context // // Setting the allow-list to a permissive pattern flips the URL guard into // "allow-list match bypasses the IP check" mode for every URL the tests // touch. Operator-supplied deny-lists still apply, so deny-list scenarios // keep working. Test scenarios that exercise allow-list semantics // explicitly override this default in their environment table. // // Production operators wanting a similar bypass for trusted internal // destinations should set their own --*-allow-list with a tighter regex // (for example ^https?://internal\.svc(:|/|$)). const integrationAllowList = `.+` // applyDefaultEnv merges baseline environment variables that the // integration tests rely on into env, without overwriting values supplied // by the test scenario itself. Tests can clear a default by setting it to // the empty string in their scenario table. func applyDefaultEnv(env map[string]string) map[string]string { if env == nil { env = make(map[string]string) } defaults := map[string]string{ "CHROMIUM_ALLOW_LIST": integrationAllowList, "API_DOWNLOAD_FROM_ALLOW_LIST": integrationAllowList, "WEBHOOK_ALLOW_LIST": integrationAllowList, } for k, v := range defaults { if _, ok := env[k]; !ok { env[k] = v } } return env } func startGotenbergContainer(ctx context.Context, env map[string]string) (*testcontainers.DockerNetwork, testcontainers.Container, error) { ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() env = applyDefaultEnv(env) n, err := network.New(ctx) if err != nil { return nil, nil, fmt.Errorf("create Gotenberg container network: %w", err) } healthPath := "/health" if env["API_ROOT_PATH"] != "" { healthPath = fmt.Sprintf("%shealth", env["API_ROOT_PATH"]) } req := testcontainers.ContainerRequest{ Image: fmt.Sprintf("gotenberg/%s:%s", GotenbergDockerRepository, GotenbergVersion), ImagePlatform: GotenbergContainerPlatform, ExposedPorts: []string{"3000/tcp"}, HostConfigModifier: func(hostConfig *container.HostConfig) { hostConfig.ExtraHosts = []string{"host.docker.internal:host-gateway"} }, Networks: []string{n.Name}, WaitingFor: wait.ForHTTP(healthPath), Env: env, } c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, Logger: &noopLogger{}, }) if err != nil { err = fmt.Errorf("start new Gotenberg container: %w", err) } return n, c, err } func execCommandInIntegrationToolsContainer(ctx context.Context, cmd []string, path string) (string, error) { ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() req := testcontainers.ContainerRequest{ Image: "gotenberg/integration-tools:latest", ImagePlatform: GotenbergContainerPlatform, Files: []testcontainers.ContainerFile{ { HostFilePath: path, ContainerFilePath: filepath.Base(path), FileMode: 0o700, }, }, Cmd: []string{"tail", "-f", "/dev/null"}, // Keeps container running indefinitely. } c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, Logger: &noopLogger{}, }) if err != nil { return "", fmt.Errorf("start new Integration Tools container: %w", err) } defer func(c testcontainers.Container, ctx context.Context) { err := c.Terminate(ctx) if err != nil { fmt.Printf("terminate container: %v\n", err) } }(c, ctx) _, output, err := c.Exec(ctx, cmd, exec.Multiplexed()) if err != nil { return "", fmt.Errorf("exec %q: %w", cmd, err) } b, err := io.ReadAll(output) if err != nil { return "", fmt.Errorf("read output: %w", err) } return string(b), nil } func containerHttpEndpoint(ctx context.Context, container testcontainers.Container, port string) (string, error) { ip, err := container.Host(ctx) if err != nil { return "", fmt.Errorf("get container IP: %w", err) } mapped, err := container.MappedPort(ctx, port) if err != nil { return "", fmt.Errorf("get container port: %w", err) } return fmt.Sprintf("http://%s:%s", ip, mapped.Port()), nil } func containerLogEntries(ctx context.Context, container testcontainers.Container) (string, error) { logReader, err := container.Logs(ctx) if err != nil { return "", fmt.Errorf("get container log entries: %w", err) } defer logReader.Close() logsBytes, err := io.ReadAll(logReader) if err != nil { return "", fmt.Errorf("read container log entries: %w", err) } return string(logsBytes), nil }