Skip to content

Commit 062ff53

Browse files
committed
test(gateway): add recovery admin auth/method/throttle coverage
1 parent 6fb487b commit 062ff53

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package gateway
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
11+
"github.com/LumeraProtocol/supernode/v2/p2p"
12+
cascadeService "github.com/LumeraProtocol/supernode/v2/supernode/cascade"
13+
)
14+
15+
type stubCascadeFactory struct{}
16+
17+
type stubCascadeTask struct{}
18+
19+
type stubP2PClient struct{}
20+
21+
var _ p2p.Client = stubP2PClient{}
22+
23+
func (stubCascadeFactory) NewCascadeRegistrationTask() cascadeService.CascadeTask { return stubCascadeTask{} }
24+
25+
func (stubCascadeTask) Register(context.Context, *cascadeService.RegisterRequest, func(*cascadeService.RegisterResponse) error) error {
26+
return nil
27+
}
28+
29+
func (stubCascadeTask) Download(context.Context, *cascadeService.DownloadRequest, func(*cascadeService.DownloadResponse) error) error {
30+
return nil
31+
}
32+
33+
func (stubCascadeTask) CleanupDownload(context.Context, string) error { return nil }
34+
35+
func (stubP2PClient) Retrieve(context.Context, string, ...bool) ([]byte, error) { return nil, nil }
36+
func (stubP2PClient) BatchRetrieve(context.Context, []string, int, string, ...bool) (map[string][]byte, error) {
37+
return nil, nil
38+
}
39+
func (stubP2PClient) BatchRetrieveStream(context.Context, []string, int32, string, func(string, []byte) error, ...bool) (int32, error) {
40+
return 0, nil
41+
}
42+
func (stubP2PClient) Store(context.Context, []byte, int) (string, error) { return "", nil }
43+
func (stubP2PClient) StoreBatch(context.Context, [][]byte, int, string) error { return nil }
44+
func (stubP2PClient) Delete(context.Context, string) error { return nil }
45+
func (stubP2PClient) Stats(context.Context) (*p2p.StatsSnapshot, error) { return nil, nil }
46+
func (stubP2PClient) NClosestNodes(context.Context, int, string, ...string) []string { return nil }
47+
func (stubP2PClient) NClosestNodesWithIncludingNodeList(context.Context, int, string, []string, []string) []string {
48+
return nil
49+
}
50+
func (stubP2PClient) LocalStore(context.Context, string, []byte) (string, error) { return "", nil }
51+
func (stubP2PClient) DisableKey(context.Context, string) error { return nil }
52+
func (stubP2PClient) EnableKey(context.Context, string) error { return nil }
53+
func (stubP2PClient) GetLocalKeys(context.Context, *time.Time, time.Time) ([]string, error) { return nil, nil }
54+
55+
func mustJSONBody(t *testing.T, rr *httptest.ResponseRecorder) map[string]any {
56+
t.Helper()
57+
var body map[string]any
58+
if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil {
59+
t.Fatalf("decode body: %v", err)
60+
}
61+
return body
62+
}
63+
64+
func TestRecoveryAdminWrapAuthTokenBehavior(t *testing.T) {
65+
orig := RecoveryAdminToken
66+
defer func() { RecoveryAdminToken = orig }()
67+
68+
ra := &recoveryAdmin{enabled: true}
69+
hit := false
70+
h := ra.wrap(func(http.ResponseWriter, *http.Request) { hit = true })
71+
72+
t.Run("unset token returns 503", func(t *testing.T) {
73+
RecoveryAdminToken = ""
74+
hit = false
75+
rr := httptest.NewRecorder()
76+
req := httptest.NewRequest(http.MethodGet, "/", nil)
77+
78+
h(rr, req)
79+
80+
if rr.Code != http.StatusServiceUnavailable {
81+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusServiceUnavailable)
82+
}
83+
if hit {
84+
t.Fatal("wrapped handler should not run")
85+
}
86+
})
87+
88+
t.Run("wrong token returns 401", func(t *testing.T) {
89+
RecoveryAdminToken = "secret"
90+
hit = false
91+
rr := httptest.NewRecorder()
92+
req := httptest.NewRequest(http.MethodGet, "/", nil)
93+
req.Header.Set(recoveryHeaderToken, "wrong")
94+
95+
h(rr, req)
96+
97+
if rr.Code != http.StatusUnauthorized {
98+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusUnauthorized)
99+
}
100+
if hit {
101+
t.Fatal("wrapped handler should not run")
102+
}
103+
})
104+
105+
t.Run("correct token runs handler", func(t *testing.T) {
106+
RecoveryAdminToken = "secret"
107+
hit = false
108+
rr := httptest.NewRecorder()
109+
req := httptest.NewRequest(http.MethodGet, "/", nil)
110+
req.Header.Set(recoveryHeaderToken, "secret")
111+
112+
h(rr, req)
113+
114+
if rr.Code != http.StatusOK {
115+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusOK)
116+
}
117+
if !hit {
118+
t.Fatal("wrapped handler should run")
119+
}
120+
})
121+
}
122+
123+
func TestRecoveryAdminMethodEnforcement(t *testing.T) {
124+
ra := &recoveryAdmin{}
125+
126+
t.Run("handleReseed enforces POST", func(t *testing.T) {
127+
rr := httptest.NewRecorder()
128+
req := httptest.NewRequest(http.MethodGet, "/api/v1/recovery/reseed?action_id=a1", nil)
129+
130+
ra.handleReseed(rr, req)
131+
132+
if rr.Code != http.StatusMethodNotAllowed {
133+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusMethodNotAllowed)
134+
}
135+
})
136+
137+
t.Run("handleStatus enforces GET", func(t *testing.T) {
138+
rr := httptest.NewRecorder()
139+
req := httptest.NewRequest(http.MethodPost, "/api/v1/recovery/actions/a1/status", nil)
140+
141+
ra.handleStatus(rr, req, "a1")
142+
143+
if rr.Code != http.StatusMethodNotAllowed {
144+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusMethodNotAllowed)
145+
}
146+
})
147+
}
148+
149+
func TestRecoveryAdminSemaphoreThrottling(t *testing.T) {
150+
ra := &recoveryAdmin{
151+
cascadeFactory: stubCascadeFactory{},
152+
p2pClient: stubP2PClient{},
153+
reseedSem: make(chan struct{}, 1),
154+
statusSem: make(chan struct{}, 1),
155+
}
156+
157+
t.Run("reseed returns 429 while another run in progress", func(t *testing.T) {
158+
ra.reseedSem <- struct{}{}
159+
defer func() { <-ra.reseedSem }()
160+
161+
rr := httptest.NewRecorder()
162+
req := httptest.NewRequest(http.MethodPost, "/api/v1/recovery/reseed?action_id=a1", nil)
163+
164+
ra.handleReseed(rr, req)
165+
166+
if rr.Code != http.StatusTooManyRequests {
167+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusTooManyRequests)
168+
}
169+
})
170+
171+
t.Run("status returns 429 while another probe in progress", func(t *testing.T) {
172+
ra.statusSem <- struct{}{}
173+
defer func() { <-ra.statusSem }()
174+
175+
rr := httptest.NewRecorder()
176+
req := httptest.NewRequest(http.MethodGet, "/api/v1/recovery/actions/a1/status", nil)
177+
178+
ra.handleStatus(rr, req, "a1")
179+
180+
if rr.Code != http.StatusTooManyRequests {
181+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusTooManyRequests)
182+
}
183+
})
184+
}
185+
186+
func TestRecoveryAdminBasicResponseShapeAndStatusCodes(t *testing.T) {
187+
ra := &recoveryAdmin{}
188+
189+
t.Run("handleStatus missing deps => 503 with ok=false", func(t *testing.T) {
190+
rr := httptest.NewRecorder()
191+
req := httptest.NewRequest(http.MethodGet, "/api/v1/recovery/actions/a1/status", nil)
192+
193+
ra.handleStatus(rr, req, "a1")
194+
195+
if rr.Code != http.StatusServiceUnavailable {
196+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusServiceUnavailable)
197+
}
198+
body := mustJSONBody(t, rr)
199+
if body["ok"] != false {
200+
t.Fatalf("ok = %v, want false", body["ok"])
201+
}
202+
if _, ok := body["error"]; !ok {
203+
t.Fatal("expected error field")
204+
}
205+
})
206+
207+
t.Run("handleReseed missing deps => 503 with ok=false", func(t *testing.T) {
208+
rr := httptest.NewRecorder()
209+
req := httptest.NewRequest(http.MethodPost, "/api/v1/recovery/reseed?action_id=a1", nil)
210+
211+
ra.handleReseed(rr, req)
212+
213+
if rr.Code != http.StatusServiceUnavailable {
214+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusServiceUnavailable)
215+
}
216+
body := mustJSONBody(t, rr)
217+
if body["ok"] != false {
218+
t.Fatalf("ok = %v, want false", body["ok"])
219+
}
220+
if _, ok := body["error"]; !ok {
221+
t.Fatal("expected error field")
222+
}
223+
})
224+
}

0 commit comments

Comments
 (0)