mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
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:
@@ -44,3 +44,6 @@ dist/
|
||||
build/
|
||||
|
||||
tmp/
|
||||
|
||||
### AI Tooling ###
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/WeatherForecast",
|
||||
"expected": 200,
|
||||
"internalPort": 8080
|
||||
"internalPort": 8080,
|
||||
"expectedOutput": "temperatureC"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
[
|
||||
{
|
||||
// No expectedOutput because content is rendered client-side
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 4321
|
||||
"internalPort": 4321,
|
||||
"expectedOutput": "Astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
"internalPort": 80,
|
||||
"expectedOutput": "Astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": "Get started by editing"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -3,7 +3,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 3000
|
||||
"internalPort": 3000,
|
||||
"expectedOutput": "Nuxt"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
"internalPort": 80,
|
||||
"expectedOutput": "Vite + React"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
[
|
||||
{
|
||||
// No expectedOutput because content is rendered client-side
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
"internalPort": 80,
|
||||
"expectedOutput": "Laravel"
|
||||
},
|
||||
"envs": {
|
||||
"APP_KEY": "base64:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa="
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 8000
|
||||
"internalPort": 8000,
|
||||
"expectedOutput": "Welcome to FastAPI"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 8000
|
||||
"internalPort": 8000,
|
||||
"expectedOutput": "Game of Life"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 8000
|
||||
"internalPort": 8000,
|
||||
"expectedOutput": "Hello from Python Flask!"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 3000
|
||||
"internalPort": 3000,
|
||||
"expectedOutput": "Hello from Rails"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 4567
|
||||
"internalPort": 4567,
|
||||
"expectedOutput": "Choo Choo! Welcome to your Sinatra server"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"httpCheck": {
|
||||
"internalPort": 80,
|
||||
"path": "/",
|
||||
"expected": 200
|
||||
"expected": 200,
|
||||
"expectedOutput": "Railpack Static File"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"httpCheck": {
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
"internalPort": 80,
|
||||
"expectedOutput": "Railpack Static 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 {
|
||||
|
||||
Reference in New Issue
Block a user