Все проверки выполнены успешно
CI / test (push) Successful in 2m32s
Docker / Build and publish worker image (push) Successful in 18m17s
- 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.
186 строки
6.3 KiB
Go
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)
|
|
}
|