Files
2026-05-16 20:07:57 +01:00

147 lines
4.6 KiB
Go

package api
import (
"context"
"fmt"
"net/http/httptest"
"strings"
"testing"
agentctl "github.com/kandev/kandev/internal/agent/runtime/agentctl"
"github.com/kandev/kandev/internal/agentctl/server/config"
"github.com/kandev/kandev/internal/agentctl/server/instance"
"github.com/kandev/kandev/internal/common/logger"
)
// TestHandshakeIntegration_FullFlow exercises the complete client↔server
// handshake flow: nonce-based auth → token retrieval → authenticated requests.
// No Docker, no agent binaries — runs in CI as a regular test.
func TestHandshakeIntegration_FullFlow(t *testing.T) {
// 1. Set up a ControlServer with a bootstrap nonce (simulating Docker/Sprites mode)
cfg := &config.Config{
AuthToken: "test-generated-token-xyz",
BootstrapNonce: "integration-test-nonce-abc123",
}
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"})
cs := NewControlServer(cfg, &instance.Manager{}, log)
server := httptest.NewServer(cs.Router())
defer server.Close()
// Parse host:port from test server URL
host, port := parseHostPort(t, server.URL)
// 2. Create a ControlClient with NO auth token (simulating fresh backend)
client := agentctl.NewControlClient(host, port, log)
ctx := context.Background()
// 3. Health check should work without auth
if err := client.Health(ctx); err != nil {
t.Fatalf("health check should work without auth: %v", err)
}
// 4. Authenticated endpoint should be REJECTED (no token yet)
_, err := client.ListInstances(ctx)
if err == nil {
t.Fatal("expected error for unauthenticated request, got nil")
}
// 5. Handshake with correct nonce — should return token
token, err := client.Handshake(ctx, "integration-test-nonce-abc123")
if err != nil {
t.Fatalf("handshake failed: %v", err)
}
if token == "" {
t.Fatal("handshake returned empty token")
}
// 6. After handshake, authenticated requests should WORK
// (ListInstances may return an endpoint/client error, but it should
// NOT be a 401 — it should get past auth.)
_, err = client.ListInstances(ctx)
if err != nil {
// If the error mentions "unauthorized" or "auth", auth is still failing
if containsAuthError(err.Error()) {
t.Fatalf("request after handshake still rejected by auth: %v", err)
}
// Any other error (e.g., nil instMgr panic recovery) is fine —
// it means auth passed and we hit the handler
}
// 7. Second handshake with same nonce should FAIL (burned)
_, err = client.Handshake(ctx, "integration-test-nonce-abc123")
if err == nil {
t.Fatal("second handshake should fail (nonce burned)")
}
// 8. Handshake with wrong nonce should also fail
_, err = client.Handshake(ctx, "wrong-nonce")
if err == nil {
t.Fatal("handshake with wrong nonce should fail")
}
}
// TestHandshakeIntegration_StandaloneMode verifies that when AuthToken is set
// directly (standalone mode, no nonce), normal Bearer auth works.
func TestHandshakeIntegration_StandaloneMode(t *testing.T) {
cfg := &config.Config{
AuthToken: "standalone-secret-token",
// No BootstrapNonce — standalone mode
}
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"})
cs := NewControlServer(cfg, &instance.Manager{}, log)
server := httptest.NewServer(cs.Router())
defer server.Close()
host, port := parseHostPort(t, server.URL)
ctx := context.Background()
// Without auth token: rejected
noAuthClient := agentctl.NewControlClient(host, port, log)
_, err := noAuthClient.ListInstances(ctx)
if err == nil {
t.Fatal("expected rejection without auth token")
}
// With correct auth token: passes auth
authClient := agentctl.NewControlClient(host, port, log,
agentctl.WithControlAuthToken("standalone-secret-token"))
_, err = authClient.ListInstances(ctx)
if err != nil && containsAuthError(err.Error()) {
t.Fatalf("request with correct token rejected: %v", err)
}
// Handshake should fail (no nonce configured)
_, err = noAuthClient.Handshake(ctx, "any-nonce")
if err == nil {
t.Fatal("handshake should fail when no nonce is configured")
}
}
func parseHostPort(t *testing.T, url string) (string, int) {
t.Helper()
var host string
var port int
// httptest URLs look like "http://127.0.0.1:PORT"
n, err := fmt.Sscanf(url, "http://%s", &host)
if err != nil || n != 1 {
t.Fatalf("failed to parse test server URL %q", url)
}
// Split host:port
for i := len(host) - 1; i >= 0; i-- {
if host[i] == ':' {
_, _ = fmt.Sscanf(host[i+1:], "%d", &port)
host = host[:i]
break
}
}
return host, port
}
func containsAuthError(s string) bool {
lower := strings.ToLower(s)
return strings.Contains(lower, "unauthorized") || strings.Contains(lower, "status 401")
}