test: add http content assertion to integration tests (#428)

* build: ignore local claude settings

* test: add httpCheck expectedOutput assertions

* test: add expectedOutput to httpCheck to assert against http response content

* test: remove expectedOutput for FE rendered content
This commit is contained in:
Michael Bianco
2026-01-15 09:49:44 -07:00
committed by GitHub
parent 2c152cffdd
commit 25d9662825
27 changed files with 71 additions and 28 deletions
+3
View File
@@ -44,3 +44,6 @@ dist/
build/
tmp/
### AI Tooling ###
.claude/settings.local.json
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/WeatherForecast",
"expected": 200,
"internalPort": 8080
"internalPort": 8080,
"expectedOutput": "temperatureC"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 4000
"internalPort": 4000,
"expectedOutput": "OK"
},
"envs": {
"DATABASE_URL": "postgresql://friends_user:friends_password@postgres:5432/friends_db"
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 4000
"internalPort": 4000,
"expectedOutput": "Peace of mind from prototype to production"
},
"envs": {
"PHX_SERVER": "true",
+1
View File
@@ -1,5 +1,6 @@
[
{
// No expectedOutput because content is rendered client-side
"httpCheck": {
"path": "/",
"expected": 200,
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 4321
"internalPort": 4321,
"expectedOutput": "Astro"
}
}
]
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 80
"internalPort": 80,
"expectedOutput": "Astro"
}
}
]
+1
View File
@@ -1,5 +1,6 @@
[
{
// No expectedOutput because content is rendered client-side
"httpCheck": {
"path": "/",
"expected": 200,
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "Get started by editing"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "Nuxt"
}
}
]
+2 -1
View File
@@ -9,7 +9,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "New Remix App"
},
// TEMPORARY: Remove this once NPM_CONFIG_PRODUCTION is removed from node.go:241
// Currently causes: npm warn config production Use `--omit=dev` instead.
+2 -1
View File
@@ -8,7 +8,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "Welcome Home!!!"
},
// TanStack Start outputs to stderr during server startup
"stderrAllowed": true
@@ -1,5 +1,6 @@
[
{
// No expectedOutput because content is rendered client-side
"httpCheck": {
"path": "/",
"expected": 200,
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "React Router"
}
}
]
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 80
"internalPort": 80,
"expectedOutput": "Vite + React"
}
}
]
+1
View File
@@ -1,5 +1,6 @@
[
{
// No expectedOutput because content is rendered client-side
"httpCheck": {
"path": "/",
"expected": 200,
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 80
"internalPort": 80,
"expectedOutput": "Laravel"
},
"envs": {
"APP_KEY": "base64:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa="
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 8000
"internalPort": 8000,
"expectedOutput": "Django + PostgreSQL + Redis"
},
"envs": {
"DATABASE_URL": "postgresql://django_user:django_password@postgres:5432/django_db",
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 8000
"internalPort": 8000,
"expectedOutput": "Welcome to FastAPI"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 8000
"internalPort": 8000,
"expectedOutput": "Game of Life"
}
}
]
+2 -1
View File
@@ -10,7 +10,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 8000
"internalPort": 8000,
"expectedOutput": "Hello from Python Flask!"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "Hello from Rails"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 3000
"internalPort": 3000,
"expectedOutput": "OK"
},
"envs": {
"DATABASE_URL": "postgresql://rails_user:rails_password@postgres:5432/rails_db"
+2 -1
View File
@@ -9,7 +9,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 4567
"internalPort": 4567,
"expectedOutput": "Choo Choo! Welcome to your Sinatra server"
}
}
]
+2 -1
View File
@@ -3,7 +3,8 @@
"httpCheck": {
"internalPort": 80,
"path": "/",
"expected": 200
"expected": 200,
"expectedOutput": "Railpack Static File"
}
}
]
+2 -1
View File
@@ -6,7 +6,8 @@
"httpCheck": {
"path": "/",
"expected": 200,
"internalPort": 80
"internalPort": 80,
"expectedOutput": "Railpack Static File"
}
}
]
+22 -7
View File
@@ -2,6 +2,7 @@ package integration_tests
import (
"fmt"
"io"
"net"
"net/http"
"os"
@@ -100,9 +101,10 @@ func stopAndCleanupCompose(config *ComposeConfig, logger interface {
// HTTPCheck defines an HTTP endpoint check for a container
// Path defaults to /, Expected defaults to 200.
type HTTPCheck struct {
InternalPort int `json:"internalPort"`
Path string `json:"path"`
Expected int `json:"expected"`
InternalPort int `json:"internalPort"`
Path string `json:"path"`
Expected int `json:"expected"`
ExpectedOutput StringOrArray `json:"expectedOutput"`
}
// runContainerWithHTTPCheck starts a container from the given image,
@@ -156,10 +158,17 @@ func runContainerWithHTTPCheck(t *testing.T, imageName string, envs map[string]s
// Small grace period: some servers bind after initial log lines
time.Sleep(containerStartGracePeriod)
url := fmt.Sprintf("http://127.0.0.1:%d%s", hostPort, hc.Path)
if err := waitForHTTPStatus(url, hc.Expected); err != nil {
body, err := waitForHTTPStatus(url, hc.Expected)
if err != nil {
return fmt.Errorf("http check failed: %w", err)
}
for _, expected := range hc.ExpectedOutput {
if !strings.Contains(body, expected) {
return fmt.Errorf("response body did not contain expected string: %q\nBody:\n%s", expected, body)
}
}
t.Logf("HTTP check passed (%s => %d)", hc.Path, hc.Expected)
return nil
}
@@ -177,7 +186,7 @@ func pickFreePort() (int, error) {
return l.Addr().(*net.TCPAddr).Port, nil
}
func waitForHTTPStatus(url string, expected int) error {
func waitForHTTPStatus(url string, expected int) (string, error) {
deadline := time.Now().Add(httpCheckTimeout)
client := &http.Client{Timeout: httpCheckClientTimeout}
@@ -185,9 +194,15 @@ func waitForHTTPStatus(url string, expected int) error {
for time.Now().Before(deadline) {
resp, err := client.Get(url)
if err == nil {
body, readErr := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if readErr != nil {
lastErr = fmt.Errorf("failed to read response body: %w", readErr)
time.Sleep(httpCheckInterval)
continue
}
if resp.StatusCode == expected {
return nil
return string(body), nil
}
lastErr = fmt.Errorf("http status %d != %d", resp.StatusCode, expected)
} else {
@@ -202,7 +217,7 @@ func waitForHTTPStatus(url string, expected int) error {
lastErr = fmt.Errorf("timeout waiting for %s", url)
}
return lastErr
return "", lastErr
}
func fetchContainerLogs(name string) string {