test(acceptance): add AS-7 auth rejection integration test
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Quality gate #7: Verify nodes without valid tokens are rejected with HTTP 401. - Created as7_auth_reject_test.go following AS1-AS6 pattern - Tests WebSocket connection without X-Spaxel-Token header - Verifies HTTP 401 Unauthorized response - Validates simulator exits non-zero with invalid token - Confirms no zombie nodes in fleet after rejection - Registered AS7_AuthRejectIntegration in test runner Closes: bf-2d9fj
This commit is contained in:
parent
bd64d602bc
commit
c1954a365a
2 changed files with 155 additions and 0 deletions
154
mothership/test/acceptance/as7_auth_reject_test.go
Normal file
154
mothership/test/acceptance/as7_auth_reject_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// AS-7: Auth rejection test
|
||||
// Quality gate #7: node without a valid token must get HTTP 401 and be rejected.
|
||||
package acceptance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// AS7_AuthRejectIntegration verifies that nodes without valid tokens are rejected with HTTP 401.
|
||||
// Steps:
|
||||
// 1. Start mothership with PIN configured
|
||||
// 2. Attempt WebSocket connection without X-Spaxel-Token header
|
||||
// 3. Verify HTTP 401 response
|
||||
// 4. Verify simulator exits non-zero
|
||||
// 5. Verify no zombie node in fleet
|
||||
// 6. Verify mothership logs the rejection
|
||||
func AS7_AuthRejectIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping AS-7 test in short mode")
|
||||
}
|
||||
|
||||
// Set environment for AS-7 test (uses port 18087)
|
||||
t.Setenv("SPAXEL_PORT", "18087")
|
||||
t.Setenv("SPAXEL_MOTHERSHIP_URL", "http://localhost:18087")
|
||||
|
||||
ctx := context.Background()
|
||||
mothershipURL := getMothershipURL()
|
||||
|
||||
// Create empty data directory
|
||||
tempDir, err := os.MkdirTemp("", "spaxel-as7-*")
|
||||
if err != nil {
|
||||
t.Fatalf("AS-7 FAIL: Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
dataDir := filepath.Join(tempDir, "data")
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
t.Fatalf("AS-7 FAIL: Failed to create data dir: %v", err)
|
||||
}
|
||||
|
||||
// Step 1: Start mothership
|
||||
cmd := startMothershipWithDataDir(t, dataDir)
|
||||
defer stopMothership(cmd)
|
||||
|
||||
if !waitForMothership(ctx, mothershipURL) {
|
||||
t.Fatal("AS-7 FAIL: Mothership did not become ready")
|
||||
}
|
||||
|
||||
// Configure PIN
|
||||
setPIN(t, mothershipURL, "777777")
|
||||
|
||||
// Step 2: Attempt WebSocket connection without token header
|
||||
t.Log("AS-7: Attempting WebSocket connection without token...")
|
||||
|
||||
// Convert HTTP URL to WebSocket URL
|
||||
wsURL, err := url.Parse(mothershipURL)
|
||||
if err != nil {
|
||||
t.Fatalf("AS-7 FAIL: Failed to parse mothership URL: %v", err)
|
||||
}
|
||||
wsScheme := "ws"
|
||||
if wsURL.Scheme == "https" {
|
||||
wsScheme = "wss"
|
||||
}
|
||||
nodeWSURL := fmt.Sprintf("%s://%s/ws/node", wsScheme, wsURL.Host)
|
||||
|
||||
// Try to connect without token header - expect 401
|
||||
t.Logf("AS-7: Connecting to %s without token", nodeWSURL)
|
||||
wsDialer := &websocket.Dialer{
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
conn, resp, err := wsDialer.Dial(nodeWSURL, nil)
|
||||
|
||||
// Step 3: Verify HTTP 401 response
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
|
||||
t.Log("AS-7: WebSocket connection rejected with HTTP 401 (expected)")
|
||||
} else if resp != nil {
|
||||
t.Errorf("AS-7 FAIL: Expected HTTP 401, got %d", resp.StatusCode)
|
||||
} else {
|
||||
t.Errorf("AS-7 FAIL: Connection failed with no HTTP response: %v", err)
|
||||
}
|
||||
} else {
|
||||
conn.Close()
|
||||
t.Error("AS-7 FAIL: WebSocket connection succeeded without token (should have been rejected)")
|
||||
}
|
||||
|
||||
// Step 4: Verify simulator exits non-zero when attempting connection with invalid token
|
||||
t.Log("AS-7: Testing simulator rejection with invalid token...")
|
||||
simPath := os.Getenv("SPAXEL_SIM_PATH")
|
||||
if simPath == "" {
|
||||
simPath = "/tmp/spaxel-sim-as7"
|
||||
buildCmd := exec.Command("go", "build", "-o", simPath, "../cmd/sim")
|
||||
buildCmd.Dir = filepath.Join("..", "..")
|
||||
if output, err := buildCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("AS-7 FAIL: Failed to build simulator: %v: %s", err, string(output))
|
||||
}
|
||||
defer os.Remove(simPath)
|
||||
}
|
||||
|
||||
simCtx, simCancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer simCancel()
|
||||
|
||||
invalidToken := "invalid-token-0000000000000000000000000000000000000000000000000000000000000000"
|
||||
simArgs := []string{
|
||||
"--mothership", fmt.Sprintf("ws://localhost:18087/ws/node"),
|
||||
"--token", invalidToken,
|
||||
"--nodes", "1",
|
||||
"--duration", "5",
|
||||
}
|
||||
|
||||
simCmd := exec.CommandContext(simCtx, simPath, simArgs...)
|
||||
simCmd.Stdout = nil // Suppress output
|
||||
simCmd.Stderr = nil
|
||||
|
||||
if err := simCmd.Start(); err != nil {
|
||||
t.Fatalf("AS-7 FAIL: Failed to start simulator: %v", err)
|
||||
}
|
||||
|
||||
// Wait for simulator to exit
|
||||
simErr := simCmd.Wait()
|
||||
|
||||
if simErr == nil {
|
||||
t.Error("AS-7 FAIL: Simulator exited with success (expected non-zero exit with invalid token)")
|
||||
} else {
|
||||
t.Logf("AS-7: Simulator exited with error (expected): %v", simErr)
|
||||
}
|
||||
|
||||
// Step 5: Verify no zombie node in fleet
|
||||
time.Sleep(2 * time.Second) // Give mothership time to process
|
||||
nodes := getNodesIntegration(t, mothershipURL)
|
||||
if len(nodes) > 0 {
|
||||
t.Errorf("AS-7 FAIL: Expected 0 nodes with invalid token, got %d nodes", len(nodes))
|
||||
for _, node := range nodes {
|
||||
t.Logf("AS-7: Unexpected node: MAC=%s Name=%s", node["mac"], node["name"])
|
||||
}
|
||||
} else {
|
||||
t.Log("AS-7: No zombie nodes in fleet (expected)")
|
||||
}
|
||||
|
||||
// Step 6: Verify mothership logged the rejection
|
||||
// We can't easily check logs from the running mothership process,
|
||||
// but we've verified the key behaviors: 401 response, simulator error, no nodes added
|
||||
t.Log("AS-7: Auth rejection test PASSED")
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ func TestMain(m *testing.M) {
|
|||
{"AS4_BLEIdentity", AS4_BLEIdentityIntegration},
|
||||
{"AS5_OTAUpdate", AS5_OTAUpdateIntegration},
|
||||
{"AS6_Replay", AS6_ReplayIntegration},
|
||||
{"AS7_AuthReject", AS7_AuthRejectIntegration},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue