Files
worker/internal/webapp/handlers_cluster_test.go
Gleb Tv e987f24903
Все проверки выполнены успешно
CI / test (push) Successful in 2m32s
Docker / Build and publish worker image (push) Successful in 18m17s
fix(worker): harden control-plane lifecycle
- reconnect safely after token rotation and retry leased results
- reject malformed tasks and remove production cluster debug mutation
- validate environment files and require immutable container images

BREAKING CHANGE: Docker install, deploy, and Compose now require an
immutable repository@sha256 image reference.
2026-07-19 23:11:43 +03:00

186 строки
6.3 KiB
Go

package webapp
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// stubCluster is a minimal ClusterView implementation used by the
// handler tests. It returns canned values so the JSON shape can be
// pinned without standing up a real raft group.
type stubCluster struct {
stats ClusterStats
clusterIDOut string
addrOut string
}
func (s *stubCluster) Stats() ClusterStats { return s.stats }
func (s *stubCluster) ClusterID() string { return s.clusterIDOut }
func (s *stubCluster) LocalAddr() string { return s.addrOut }
// withClusterServer returns a test server whose ClusterView is the
// supplied stub. The first-run password path is also exercised so
// the session cookie is available for the cluster-endpoint probes.
// Returns the *httptest.Server, the underlying *Server, and the
// authenticated http.Client (cookie jar already populated).
func withClusterServer(t *testing.T, c ClusterView) (*httptest.Server, *Server, *http.Client) {
t.Helper()
srv := newTestServer(t, &stubRunner{id: "w-1"})
srv.SetCluster(c)
ts := newHTTPTestServer(t, srv)
client, _ := loginAsFirstRun(t, ts.URL, srv)
clearRequiresChange(t, srv)
return ts, srv, client
}
// TestClusterStatus_NotConfigured verifies the 503 path when no
// cluster subsystem is attached to the webapp.
func TestClusterStatus_NotConfigured(t *testing.T) {
srv := newTestServer(t, &stubRunner{id: "w-1"})
ts := newHTTPTestServer(t, srv)
c, _ := loginAsFirstRun(t, ts.URL, srv)
clearRequiresChange(t, srv)
resp, err := c.Get(ts.URL + "/web/api/cluster/status")
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode,
"cluster status must 503 when no cluster is attached")
}
// TestClusterStatus_HappyPath verifies the JSON shape of the
// /web/api/cluster/status response when a stub cluster is attached.
func TestClusterStatus_HappyPath(t *testing.T) {
stub := &stubCluster{
stats: ClusterStats{
NodeID: "worker1",
LocalAddr: "127.0.0.1:17401",
State: "Leader",
Leader: "worker1",
Term: 17,
AppliedIndex: 42,
LastIndex: 42,
NumPeers: 2,
Voters: []string{"worker1", "worker2"},
FSMChecks: 1,
FSMMembers: 2,
FSMConfigVersion: 7,
FSMOutboxLen: 3,
FSMPartition: "steady",
},
clusterIDOut: "worker1",
addrOut: "127.0.0.1:17401",
}
ts, _, c := withClusterServer(t, stub)
resp, err := c.Get(ts.URL + "/web/api/cluster/status")
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type"))
body, _ := io.ReadAll(resp.Body)
var got clusterStatusResponse
require.NoError(t, json.Unmarshal(body, &got))
assert.Equal(t, "worker1", got.SelfID)
assert.Equal(t, "Leader", got.Role)
assert.EqualValues(t, 17, got.Term)
assert.Equal(t, "worker1", got.LeaderID)
assert.Equal(t, []string{"worker1", "worker2"}, got.Voters)
assert.EqualValues(t, 42, got.AppliedIndex)
assert.EqualValues(t, 42, got.CommitIndex)
assert.Equal(t, 1, got.FSMChecks)
assert.Equal(t, 2, got.FSMMembers)
assert.EqualValues(t, 7, got.FSMConfigVersion)
assert.Equal(t, 3, got.FSMOutboxLen)
assert.Equal(t, "steady", got.FSMPartition)
assert.Equal(t, "worker1", got.ClusterID)
assert.Equal(t, "127.0.0.1:17401", got.LocalAddr)
}
// TestClusterStatus_FSMFieldsZeroByDefault pins the FSM-side fields
// to the zero value when the stub cluster does not set them. Guards
// against a future refactor accidentally widening the wire format
// with a non-zero default for a fresh cluster.
func TestClusterStatus_FSMFieldsZeroByDefault(t *testing.T) {
stub := &stubCluster{
stats: ClusterStats{
NodeID: "worker1", State: "Follower", Leader: "worker2",
Voters: []string{"worker1", "worker2"},
},
clusterIDOut: "worker1",
addrOut: "127.0.0.1:17401",
}
ts, _, c := withClusterServer(t, stub)
resp, err := c.Get(ts.URL + "/web/api/cluster/status")
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
body, _ := io.ReadAll(resp.Body)
var got clusterStatusResponse
require.NoError(t, json.Unmarshal(body, &got))
assert.EqualValues(t, 0, got.FSMConfigVersion, "fresh cluster must report config_version 0")
assert.Equal(t, 0, got.FSMOutboxLen, "fresh cluster must report outbox_len 0")
assert.Equal(t, "", got.FSMPartition, "fresh cluster must report partition empty/zero")
}
// TestClusterStatus_RequiresSession ensures the cluster admin
// endpoint is gated by the session middleware.
func TestClusterStatus_RequiresSession(t *testing.T) {
stub := &stubCluster{}
srv := newTestServer(t, &stubRunner{id: "w-1"})
srv.SetCluster(stub)
ts := newHTTPTestServer(t, srv)
// No session cookie — should redirect to login.
client := httpClient()
resp, err := client.Get(ts.URL + "/web/api/cluster/status")
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusFound, resp.StatusCode,
"cluster status must redirect to login without session")
assert.Equal(t, "/web/login", resp.Header.Get("Location"))
}
// TestClusterApplyTestConfig_NotExposed verifies production requests cannot
// append a hardcoded config through the former debug endpoint.
func TestClusterApplyTestConfig_NotExposed(t *testing.T) {
t.Setenv("WORKER_CLUSTER_DEBUG_APPLY", "true")
ts, _, c := withClusterServer(t, &stubCluster{})
resp, err := c.Post(ts.URL+"/web/api/cluster/apply-test-config", "", nil)
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}
// TestSetClusterDetaches verifies SetCluster(nil) returns the server
// to the no-cluster-attached state (503 from the endpoints).
func TestSetClusterDetaches(t *testing.T) {
srv := newTestServer(t, &stubRunner{id: "w-1"})
stub := &stubCluster{}
srv.SetCluster(stub)
require.NotNil(t, srv.Cluster())
srv.SetCluster(nil)
require.Nil(t, srv.Cluster())
ts := newHTTPTestServer(t, srv)
c, _ := loginAsFirstRun(t, ts.URL, srv)
clearRequiresChange(t, srv)
resp, err := c.Get(ts.URL + "/web/api/cluster/status")
require.NoError(t, err)
defer resp.Body.Close() //nolint:errcheck
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
}