network/e2e/deployments/replica_test.go
2026-01-30 06:30:04 +02:00

358 lines
11 KiB
Go

//go:build e2e
package deployments_test
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/DeBrosOfficial/network/e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestStaticReplica_CreatedOnDeploy verifies that deploying a static app
// creates replica records on a second node.
func TestStaticReplica_CreatedOnDeploy(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("replica-static-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/apps/react-app")
var deploymentID string
defer func() {
if !env.SkipCleanup && deploymentID != "" {
e2e.DeleteDeployment(t, env, deploymentID)
}
}()
t.Run("Deploy static app", func(t *testing.T) {
deploymentID = e2e.CreateTestDeployment(t, env, deploymentName, tarballPath)
require.NotEmpty(t, deploymentID)
t.Logf("Created deployment: %s (ID: %s)", deploymentName, deploymentID)
})
t.Run("Wait for replica setup", func(t *testing.T) {
// Static replicas should set up quickly (IPFS content)
time.Sleep(10 * time.Second)
})
t.Run("Deployment has replica records", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
// Check that replicas field exists and has entries
replicas, ok := deployment["replicas"].([]interface{})
if !ok {
// Replicas might be in a nested structure or separate endpoint
t.Logf("Deployment response: %+v", deployment)
// Try querying replicas via the deployment details
homeNodeID, _ := deployment["home_node_id"].(string)
require.NotEmpty(t, homeNodeID, "Deployment should have a home_node_id")
t.Logf("Home node: %s", homeNodeID)
// If replicas aren't in the response, that's still okay — we verify
// via DNS and cross-node serving below
t.Log("Replica records not in deployment response; will verify via DNS/serving")
return
}
assert.GreaterOrEqual(t, len(replicas), 1, "Should have at least 1 replica")
t.Logf("Found %d replica records", len(replicas))
for i, r := range replicas {
if replica, ok := r.(map[string]interface{}); ok {
t.Logf(" Replica %d: node=%s status=%s", i, replica["node_id"], replica["status"])
}
}
})
t.Run("Static content served via gateway", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
nodeURL := extractNodeURL(t, deployment)
if nodeURL == "" {
t.Skip("No node URL in deployment")
}
domain := extractDomain(nodeURL)
resp := e2e.TestDeploymentWithHostHeader(t, env, domain, "/")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode,
"Static content should be served (got %d: %s)", resp.StatusCode, string(body))
t.Logf("Served via gateway: status=%d", resp.StatusCode)
})
}
// TestDynamicReplica_CreatedOnDeploy verifies that deploying a dynamic (Node.js) app
// creates a replica process on a second node.
func TestDynamicReplica_CreatedOnDeploy(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
deploymentName := fmt.Sprintf("replica-nodejs-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/apps/node-api")
var deploymentID string
defer func() {
if !env.SkipCleanup && deploymentID != "" {
e2e.DeleteDeployment(t, env, deploymentID)
}
}()
t.Run("Deploy Node.js backend", func(t *testing.T) {
deploymentID = createNodeJSDeployment(t, env, deploymentName, tarballPath)
require.NotEmpty(t, deploymentID)
t.Logf("Created deployment: %s (ID: %s)", deploymentName, deploymentID)
})
t.Run("Wait for deployment and replica", func(t *testing.T) {
healthy := e2e.WaitForHealthy(t, env, deploymentID, 90*time.Second)
assert.True(t, healthy, "Deployment should become healthy")
// Extra wait for async replica setup
time.Sleep(15 * time.Second)
})
t.Run("Dynamic app served from both nodes", func(t *testing.T) {
e2e.SkipIfLocal(t)
if len(env.Config.Servers) < 2 {
t.Skip("Requires at least 2 servers")
}
deployment := e2e.GetDeployment(t, env, deploymentID)
nodeURL := extractNodeURL(t, deployment)
if nodeURL == "" {
t.Skip("No node URL in deployment")
}
domain := extractDomain(nodeURL)
resp := e2e.TestDeploymentWithHostHeader(t, env, domain, "/health")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode,
"Dynamic app should be served via gateway (got %d: %s)", resp.StatusCode, string(body))
t.Logf("Served via gateway: status=%d body=%s", resp.StatusCode, string(body))
})
}
// TestReplica_UpdatePropagation verifies that updating a deployment propagates to replicas.
func TestReplica_UpdatePropagation(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
e2e.SkipIfLocal(t)
if len(env.Config.Servers) < 2 {
t.Skip("Requires at least 2 servers")
}
deploymentName := fmt.Sprintf("replica-update-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/apps/react-app")
var deploymentID string
defer func() {
if !env.SkipCleanup && deploymentID != "" {
e2e.DeleteDeployment(t, env, deploymentID)
}
}()
t.Run("Deploy v1", func(t *testing.T) {
deploymentID = e2e.CreateTestDeployment(t, env, deploymentName, tarballPath)
require.NotEmpty(t, deploymentID)
time.Sleep(10 * time.Second) // Wait for replica
})
var v1CID string
t.Run("Record v1 CID", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
v1CID, _ = deployment["content_cid"].(string)
require.NotEmpty(t, v1CID)
t.Logf("v1 CID: %s", v1CID)
})
t.Run("Update to v2", func(t *testing.T) {
updateStaticDeployment(t, env, deploymentName, tarballPath)
time.Sleep(10 * time.Second) // Wait for update + replica propagation
})
t.Run("All nodes serve updated version", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
v2CID, _ := deployment["content_cid"].(string)
// v2 CID might be same (same tarball) but version should increment
version, _ := deployment["version"].(float64)
assert.Equal(t, float64(2), version, "Should be version 2")
t.Logf("v2 CID: %s, version: %v", v2CID, version)
// Verify via gateway
dep := e2e.GetDeployment(t, env, deploymentID)
depCID, _ := dep["content_cid"].(string)
assert.Equal(t, v2CID, depCID, "CID should match after update")
})
}
// TestReplica_RollbackPropagation verifies rollback propagates to replica nodes.
func TestReplica_RollbackPropagation(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
e2e.SkipIfLocal(t)
if len(env.Config.Servers) < 2 {
t.Skip("Requires at least 2 servers")
}
deploymentName := fmt.Sprintf("replica-rollback-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/apps/react-app")
var deploymentID string
defer func() {
if !env.SkipCleanup && deploymentID != "" {
e2e.DeleteDeployment(t, env, deploymentID)
}
}()
t.Run("Deploy v1 and update to v2", func(t *testing.T) {
deploymentID = e2e.CreateTestDeployment(t, env, deploymentName, tarballPath)
require.NotEmpty(t, deploymentID)
time.Sleep(10 * time.Second)
updateStaticDeployment(t, env, deploymentName, tarballPath)
time.Sleep(10 * time.Second)
})
var v1CID string
t.Run("Get v1 CID from versions", func(t *testing.T) {
versions := listVersions(t, env, deploymentName)
if len(versions) > 0 {
v1CID, _ = versions[0]["content_cid"].(string)
}
if v1CID == "" {
// Fall back: v1 CID from current deployment
deployment := e2e.GetDeployment(t, env, deploymentID)
v1CID, _ = deployment["content_cid"].(string)
}
t.Logf("v1 CID for rollback comparison: %s", v1CID)
})
t.Run("Rollback to v1", func(t *testing.T) {
rollbackDeployment(t, env, deploymentName, 1)
time.Sleep(10 * time.Second) // Wait for rollback + replica propagation
})
t.Run("All nodes have rolled-back CID", func(t *testing.T) {
deployment := e2e.GetDeployment(t, env, deploymentID)
currentCID, _ := deployment["content_cid"].(string)
t.Logf("Post-rollback CID: %s", currentCID)
assert.Equal(t, v1CID, currentCID, "CID should match v1 after rollback")
})
}
// TestReplica_TeardownOnDelete verifies that deleting a deployment removes replicas.
func TestReplica_TeardownOnDelete(t *testing.T) {
env, err := e2e.LoadTestEnv()
require.NoError(t, err, "Failed to load test environment")
e2e.SkipIfLocal(t)
if len(env.Config.Servers) < 2 {
t.Skip("Requires at least 2 servers")
}
deploymentName := fmt.Sprintf("replica-delete-%d", time.Now().Unix())
tarballPath := filepath.Join("../../testdata/apps/react-app")
deploymentID := e2e.CreateTestDeployment(t, env, deploymentName, tarballPath)
require.NotEmpty(t, deploymentID)
time.Sleep(10 * time.Second) // Wait for replica
// Get the domain before deletion
deployment := e2e.GetDeployment(t, env, deploymentID)
nodeURL := extractNodeURL(t, deployment)
domain := ""
if nodeURL != "" {
domain = extractDomain(nodeURL)
}
t.Run("Delete deployment", func(t *testing.T) {
e2e.DeleteDeployment(t, env, deploymentID)
time.Sleep(10 * time.Second) // Wait for teardown propagation
})
t.Run("Deployment no longer served on any node", func(t *testing.T) {
if domain == "" {
t.Skip("No domain to test")
}
req, err := http.NewRequest("GET", env.GatewayURL+"/", nil)
require.NoError(t, err)
req.Host = domain
resp, err := env.HTTPClient.Do(req)
if err != nil {
t.Logf("Connection failed (expected after deletion)")
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusOK {
assert.NotContains(t, string(body), "<div id=\"root\">",
"Deleted deployment should not be served")
}
t.Logf("status=%d (expected non-200)", resp.StatusCode)
})
}
// updateStaticDeployment updates an existing static deployment.
func updateStaticDeployment(t *testing.T, env *e2e.E2ETestEnv, name, tarballPath string) {
t.Helper()
var fileData []byte
info, err := os.Stat(tarballPath)
require.NoError(t, err)
if info.IsDir() {
fileData, err = exec.Command("tar", "-czf", "-", "-C", tarballPath, ".").Output()
require.NoError(t, err)
} else {
file, err := os.Open(tarballPath)
require.NoError(t, err)
defer file.Close()
fileData, _ = io.ReadAll(file)
}
body := &bytes.Buffer{}
boundary := "----WebKitFormBoundary7MA4YWxkTrZu0gW"
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"name\"\r\n\r\n")
body.WriteString(name + "\r\n")
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"tarball\"; filename=\"app.tar.gz\"\r\n")
body.WriteString("Content-Type: application/gzip\r\n\r\n")
body.Write(fileData)
body.WriteString("\r\n--" + boundary + "--\r\n")
req, err := http.NewRequest("POST", env.GatewayURL+"/v1/deployments/static/update", body)
require.NoError(t, err)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
req.Header.Set("Authorization", "Bearer "+env.APIKey)
resp, err := env.HTTPClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("Update failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
}