feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// newUnixDockerServer levanta un httptest-style server en un socket Unix temporal.
|
||||
// Retorna el server (ya iniciado) y el path al socket.
|
||||
func newUnixDockerServer(t *testing.T, handler http.Handler) (socketPath string) {
|
||||
t.Helper()
|
||||
|
||||
socketPath = t.TempDir() + "/docker_exec_test.sock"
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("listen unix %s: %v", socketPath, err)
|
||||
}
|
||||
|
||||
srv := &http.Server{Handler: handler}
|
||||
go srv.Serve(ln) //nolint:errcheck
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
ln.Close()
|
||||
})
|
||||
return socketPath
|
||||
}
|
||||
|
||||
// buildDockerExecHandler construye un http.Handler que simula el flujo completo del
|
||||
// Docker Engine API para exec: create → start (stream multiplexado) → inspect.
|
||||
func buildDockerExecHandler(t *testing.T, exitCode int, stdoutPayload, stderrPayload string, delayStart time.Duration) http.Handler {
|
||||
t.Helper()
|
||||
|
||||
const fakeExecID = "deadbeefcafe"
|
||||
|
||||
// Construir stream multiplexado de Docker (Tty=false).
|
||||
// Frame: [type(1)] [0 0 0(3)] [size big-endian uint32(4)] [payload]
|
||||
buildFrame := func(streamType byte, payload string) []byte {
|
||||
if payload == "" {
|
||||
return nil
|
||||
}
|
||||
b := make([]byte, 8+len(payload))
|
||||
b[0] = streamType
|
||||
binary.BigEndian.PutUint32(b[4:8], uint32(len(payload)))
|
||||
copy(b[8:], payload)
|
||||
return b
|
||||
}
|
||||
|
||||
var streamBody []byte
|
||||
streamBody = append(streamBody, buildFrame(1, stdoutPayload)...)
|
||||
streamBody = append(streamBody, buildFrame(2, stderrPayload)...)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// POST /containers/{id}/exec — devuelve ExecID.
|
||||
mux.HandleFunc("/containers/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, "/exec") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"Id": fakeExecID}) //nolint:errcheck
|
||||
})
|
||||
|
||||
// POST /exec/{id}/start — transmite stream multiplexado.
|
||||
// GET /exec/{id}/json — devuelve ExitCode.
|
||||
mux.HandleFunc("/exec/", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/start") && r.Method == http.MethodPost:
|
||||
if delayStart > 0 {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
http.Error(w, "cancelled", http.StatusGatewayTimeout)
|
||||
return
|
||||
case <-time.After(delayStart):
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if len(streamBody) > 0 {
|
||||
w.Write(streamBody) //nolint:errcheck
|
||||
}
|
||||
|
||||
case strings.HasSuffix(r.URL.Path, "/json") && r.Method == http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]int{"ExitCode": exitCode}) //nolint:errcheck
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func TestDockerContainerExec(t *testing.T) {
|
||||
const containerID = "abc123container"
|
||||
|
||||
t.Run("binario en whitelist exitcode 0 stdout stderr capturados", func(t *testing.T) {
|
||||
handler := buildDockerExecHandler(t, 0, "hello stdout\n", "hello stderr\n", 0)
|
||||
socketPath := newUnixDockerServer(t, handler)
|
||||
|
||||
result, err := DockerContainerExec(DockerExecOpts{
|
||||
ContainerID: containerID,
|
||||
Cmd: []string{"ls", "-la"},
|
||||
BinariesAllowed: []string{"ls", "cat", "echo"},
|
||||
DockerHost: "unix://" + socketPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
t.Errorf("ExitCode: got %d, want 0", result.ExitCode)
|
||||
}
|
||||
if result.Stdout != "hello stdout\n" {
|
||||
t.Errorf("Stdout: got %q, want %q", result.Stdout, "hello stdout\n")
|
||||
}
|
||||
if result.Stderr != "hello stderr\n" {
|
||||
t.Errorf("Stderr: got %q, want %q", result.Stderr, "hello stderr\n")
|
||||
}
|
||||
if result.Duration < 0 {
|
||||
t.Errorf("Duration should be >= 0, got %d", result.Duration)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("binario NO en whitelist error sin contactar engine", func(t *testing.T) {
|
||||
// No levantamos server: si se contacta el engine, falla con connection refused.
|
||||
// La validacion debe fallar ANTES de intentar conectar.
|
||||
_, err := DockerContainerExec(DockerExecOpts{
|
||||
ContainerID: containerID,
|
||||
Cmd: []string{"bash", "-c", "rm -rf /"},
|
||||
BinariesAllowed: []string{"ls", "cat"},
|
||||
DockerHost: "unix:///tmp/nonexistent-docker-for-test.sock",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for binary not in whitelist, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not in whitelist") {
|
||||
t.Errorf("error should mention whitelist, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitelist vacia rechaza todo", func(t *testing.T) {
|
||||
_, err := DockerContainerExec(DockerExecOpts{
|
||||
ContainerID: containerID,
|
||||
Cmd: []string{"ls"},
|
||||
BinariesAllowed: []string{},
|
||||
DockerHost: "unix:///tmp/nonexistent-docker-for-test.sock",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty whitelist, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no binaries whitelisted") {
|
||||
t.Errorf("error should mention empty whitelist, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeout simulado", func(t *testing.T) {
|
||||
// El handler demora 2s en /start, el timeout de la funcion es 1s.
|
||||
handler := buildDockerExecHandler(t, 0, "should not arrive", "", 2*time.Second)
|
||||
socketPath := newUnixDockerServer(t, handler)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := DockerContainerExec(DockerExecOpts{
|
||||
ContainerID: containerID,
|
||||
Cmd: []string{"sleep"},
|
||||
BinariesAllowed: []string{"sleep"},
|
||||
DockerHost: "unix://" + socketPath,
|
||||
TimeoutSeconds: 1,
|
||||
})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
// Cualquier error de contexto/red es valido (context deadline exceeded, connection reset, etc.)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("test itself timed out — DockerContainerExec did not respect TimeoutSeconds")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user