Skip to content

Commit 630e442

Browse files
committed
test: Add unit tests for backup restoration and validation logic
1 parent fe6020c commit 630e442

1 file changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
package router
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
"time"
12+
13+
"github.com/apex/log"
14+
"github.com/gin-gonic/gin"
15+
16+
"github.com/realmctl/wings/config"
17+
"github.com/realmctl/wings/environment"
18+
"github.com/realmctl/wings/events"
19+
"github.com/realmctl/wings/internal/models"
20+
"github.com/realmctl/wings/remote"
21+
wserver "github.com/realmctl/wings/server"
22+
)
23+
24+
func init() {
25+
config.Set(&config.Configuration{AuthenticationToken: "test-token"})
26+
}
27+
28+
type backupTestRemoteClient struct {
29+
restoreStatus chan string
30+
}
31+
32+
func (c backupTestRemoteClient) GetBackupRemoteUploadURLs(context.Context, string, int64) (remote.BackupRemoteUploadResponse, error) {
33+
return remote.BackupRemoteUploadResponse{}, nil
34+
}
35+
36+
func (c backupTestRemoteClient) GetInstallationScript(context.Context, string) (remote.InstallationScript, error) {
37+
return remote.InstallationScript{}, nil
38+
}
39+
40+
func (c backupTestRemoteClient) GetServerConfiguration(context.Context, string) (remote.ServerConfigurationResponse, error) {
41+
return remote.ServerConfigurationResponse{}, nil
42+
}
43+
44+
func (c backupTestRemoteClient) GetServers(context.Context, int) ([]remote.RawServerData, error) {
45+
return nil, nil
46+
}
47+
48+
func (c backupTestRemoteClient) ResetServersState(context.Context) error {
49+
return nil
50+
}
51+
52+
func (c backupTestRemoteClient) SetArchiveStatus(context.Context, string, bool) error {
53+
return nil
54+
}
55+
56+
func (c backupTestRemoteClient) SetBackupStatus(context.Context, string, remote.BackupRequest) error {
57+
return nil
58+
}
59+
60+
func (c backupTestRemoteClient) SendRestorationStatus(_ context.Context, backup string, _ bool) error {
61+
if c.restoreStatus != nil {
62+
select {
63+
case c.restoreStatus <- backup:
64+
default:
65+
}
66+
}
67+
return nil
68+
}
69+
70+
func (c backupTestRemoteClient) SetInstallationStatus(context.Context, string, remote.InstallStatusRequest) error {
71+
return nil
72+
}
73+
74+
func (c backupTestRemoteClient) SetTransferStatus(context.Context, string, bool) error {
75+
return nil
76+
}
77+
78+
func (c backupTestRemoteClient) ValidateSftpCredentials(context.Context, remote.SftpAuthRequest) (remote.SftpAuthResponse, error) {
79+
return remote.SftpAuthResponse{}, nil
80+
}
81+
82+
func (c backupTestRemoteClient) SendActivityLogs(context.Context, []models.Activity) error {
83+
return nil
84+
}
85+
86+
type backupTestEnvironment struct{}
87+
88+
func (backupTestEnvironment) Type() string { return "test" }
89+
90+
func (backupTestEnvironment) Config() *environment.Configuration {
91+
return &environment.Configuration{}
92+
}
93+
94+
func (backupTestEnvironment) Events() *events.Bus { return events.NewBus() }
95+
96+
func (backupTestEnvironment) Exists() (bool, error) { return true, nil }
97+
98+
func (backupTestEnvironment) IsRunning(context.Context) (bool, error) { return false, nil }
99+
100+
func (backupTestEnvironment) InSituUpdate() error { return nil }
101+
102+
func (backupTestEnvironment) OnBeforeStart(context.Context) error { return nil }
103+
104+
func (backupTestEnvironment) Start(context.Context) error { return nil }
105+
106+
func (backupTestEnvironment) Stop(context.Context) error { return nil }
107+
108+
func (backupTestEnvironment) WaitForStop(context.Context, time.Duration, bool) error {
109+
return nil
110+
}
111+
112+
func (backupTestEnvironment) Terminate(context.Context, string) error { return nil }
113+
114+
func (backupTestEnvironment) Destroy() error { return nil }
115+
116+
func (backupTestEnvironment) ExitState() (uint32, bool, error) { return 0, false, nil }
117+
118+
func (backupTestEnvironment) Create() error { return nil }
119+
120+
func (backupTestEnvironment) Attach(context.Context) error { return nil }
121+
122+
func (backupTestEnvironment) SendCommand(string) error { return nil }
123+
124+
func (backupTestEnvironment) Readlog(int) ([]string, error) { return nil, nil }
125+
126+
func (backupTestEnvironment) State() string { return environment.ProcessOfflineState }
127+
128+
func (backupTestEnvironment) SetState(string) {}
129+
130+
func (backupTestEnvironment) Uptime(context.Context) (int64, error) { return 0, nil }
131+
132+
func (backupTestEnvironment) SetLogCallback(func([]byte)) {}
133+
134+
func newBackupRestoreContext(t *testing.T, client backupTestRemoteClient, backupID string, body string) (*gin.Context, *httptest.ResponseRecorder, *wserver.Server) {
135+
t.Helper()
136+
137+
gin.SetMode(gin.TestMode)
138+
139+
w := httptest.NewRecorder()
140+
c, _ := gin.CreateTestContext(w)
141+
c.Request = httptest.NewRequest(http.MethodPost, "/api/servers/server/backup/"+backupID+"/restore", strings.NewReader(body))
142+
c.Request.Header.Set("Content-Type", "application/json")
143+
c.Params = gin.Params{
144+
{Key: "server", Value: "server"},
145+
{Key: "backup", Value: backupID},
146+
}
147+
148+
s, err := wserver.New(client)
149+
if err != nil {
150+
t.Fatal(err)
151+
}
152+
s.Config().Uuid = "server"
153+
s.Environment = backupTestEnvironment{}
154+
155+
c.Set("server", s)
156+
c.Set("api_client", client)
157+
c.Set("logger", log.WithField("test", t.Name()))
158+
159+
return c, w, s
160+
}
161+
162+
func TestPostServerRestoreBackupRejectsLoopbackDownloadURL(t *testing.T) {
163+
hit := make(chan struct{}, 1)
164+
internal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165+
hit <- struct{}{}
166+
w.Header().Set("Content-Type", "application")
167+
_, _ = w.Write([]byte("not a gzip archive"))
168+
}))
169+
defer internal.Close()
170+
downloadURL := strings.Replace(internal.URL, "127.0.0.1", "localhost", 1)
171+
if downloadURL == internal.URL {
172+
t.Fatalf("expected test server URL to use 127.0.0.1, got %s", internal.URL)
173+
}
174+
175+
client := backupTestRemoteClient{restoreStatus: make(chan string, 1)}
176+
backupID := "11111111-1111-1111-1111-111111111111"
177+
c, w, s := newBackupRestoreContext(t, client, backupID, fmt.Sprintf(`{"adapter":"s3","download_url":%q}`, downloadURL))
178+
defer s.CtxCancel()
179+
180+
postServerRestoreBackup(c)
181+
182+
if c.Writer.Status() != http.StatusBadRequest {
183+
t.Fatalf("expected restore request to be rejected, got status %d body %s", c.Writer.Status(), w.Body.String())
184+
}
185+
186+
select {
187+
case <-hit:
188+
t.Fatal("expected loopback server not to receive restore download request")
189+
case <-time.After(100 * time.Millisecond):
190+
}
191+
}
192+
193+
func TestPostServerRestoreBackupRejectsNonUuidBackupID(t *testing.T) {
194+
client := backupTestRemoteClient{restoreStatus: make(chan string, 1)}
195+
c, w, s := newBackupRestoreContext(t, client, "../target/archive", `{"adapter":"s3","download_url":"https://example.com/archive.tar.gz"}`)
196+
defer s.CtxCancel()
197+
198+
postServerRestoreBackup(c)
199+
200+
if c.Writer.Status() != http.StatusBadRequest {
201+
t.Fatalf("expected non-UUID backup id to be rejected, got status %d body %s", c.Writer.Status(), w.Body.String())
202+
}
203+
}
204+
205+
func TestPostServerRestoreBackupRejectsBadDownloadStatus(t *testing.T) {
206+
setBackupRestoreAllowlist(t, []string{"127.0.0.1"})
207+
208+
remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
209+
w.Header().Set("Content-Type", "application/x-gzip")
210+
http.Error(w, "missing", http.StatusNotFound)
211+
}))
212+
defer remote.Close()
213+
214+
client := backupTestRemoteClient{restoreStatus: make(chan string, 1)}
215+
backupID := "11111111-1111-1111-1111-111111111111"
216+
c, w, s := newBackupRestoreContext(t, client, backupID, fmt.Sprintf(`{"adapter":"s3","download_url":%q}`, remote.URL))
217+
defer s.CtxCancel()
218+
219+
postServerRestoreBackup(c)
220+
221+
if c.Writer.Status() != http.StatusBadRequest {
222+
t.Fatalf("expected restore request to be rejected, got status %d body %s", c.Writer.Status(), w.Body.String())
223+
}
224+
}
225+
226+
func TestBackupRestoreContentTypeValidation(t *testing.T) {
227+
tests := map[string]bool{
228+
"application/gzip": true,
229+
"application/gzip; charset=binary": true,
230+
"application/x-gzip": true,
231+
"application/x-gzip; charset=binary": true,
232+
"application": false,
233+
"gzip": false,
234+
"text/plain": false,
235+
"": false,
236+
}
237+
238+
for contentType, expected := range tests {
239+
if got := isSupportedBackupRestoreContentType(contentType); got != expected {
240+
t.Fatalf("expected content type %q support to be %v, got %v", contentType, expected, got)
241+
}
242+
}
243+
}
244+
245+
func TestParseBackupUuid(t *testing.T) {
246+
tests := map[string]struct {
247+
expected string
248+
valid bool
249+
}{
250+
"11111111-1111-1111-1111-111111111111": {expected: "11111111-1111-1111-1111-111111111111", valid: true},
251+
"11111111-1111-1111-1111-AAAAAAAAAAAA": {expected: "11111111-1111-1111-1111-aaaaaaaaaaaa", valid: true},
252+
"11111111111111111111111111111111": {valid: false},
253+
"../target/archive": {valid: false},
254+
}
255+
256+
for value, test := range tests {
257+
w := httptest.NewRecorder()
258+
c, _ := gin.CreateTestContext(w)
259+
got, ok := parseBackupUuid(c, value)
260+
if ok != test.valid {
261+
t.Fatalf("expected validity for %q to be %v, got %v", value, test.valid, ok)
262+
}
263+
if got != test.expected {
264+
t.Fatalf("expected normalized backup UUID %q, got %q", test.expected, got)
265+
}
266+
}
267+
}
268+
269+
func TestBackupRestoreBlockedIPValidation(t *testing.T) {
270+
setBackupRestoreAllowlist(t, nil)
271+
272+
tests := map[string]bool{
273+
"127.0.0.1": true,
274+
"10.0.0.1": true,
275+
"169.254.1.1": true,
276+
"100.64.0.1": true,
277+
"198.18.0.1": true,
278+
"::1": true,
279+
"fe80::1": true,
280+
"8.8.8.8": false,
281+
"2606:4700::11": false,
282+
}
283+
284+
for raw, expected := range tests {
285+
if got := isBlockedBackupRestoreIP("", net.ParseIP(raw)); got != expected {
286+
t.Fatalf("expected blocked state for %q to be %v, got %v", raw, expected, got)
287+
}
288+
}
289+
}
290+
291+
func TestBackupRestoreDestinationAllowlist(t *testing.T) {
292+
setBackupRestoreAllowlist(t, []string{
293+
"minio.internal",
294+
"10.0.0.10",
295+
"192.168.50.0/24",
296+
})
297+
298+
tests := []struct {
299+
name string
300+
host string
301+
ip string
302+
blocked bool
303+
}{
304+
{name: "hostname", host: "minio.internal", ip: "10.0.0.20", blocked: false},
305+
{name: "ip", host: "10.0.0.10", ip: "10.0.0.10", blocked: false},
306+
{name: "cidr", host: "192.168.50.10", ip: "192.168.50.10", blocked: false},
307+
{name: "not listed", host: "10.0.0.11", ip: "10.0.0.11", blocked: true},
308+
}
309+
310+
for _, test := range tests {
311+
t.Run(test.name, func(t *testing.T) {
312+
if got := isBlockedBackupRestoreIP(test.host, net.ParseIP(test.ip)); got != test.blocked {
313+
t.Fatalf("expected blocked state for %q/%q to be %v, got %v", test.host, test.ip, test.blocked, got)
314+
}
315+
})
316+
}
317+
}
318+
319+
func TestBackupRestoreHTTPClientDoesNotLimitResponseBodyRead(t *testing.T) {
320+
client := backupRestoreHttpClient()
321+
if client.Timeout != 0 {
322+
t.Fatalf("expected restore client not to set total request timeout, got %s", client.Timeout)
323+
}
324+
}
325+
326+
func setBackupRestoreAllowlist(t *testing.T, entries []string) {
327+
t.Helper()
328+
329+
previous := config.Get()
330+
t.Cleanup(func() {
331+
config.Set(previous)
332+
})
333+
334+
next := *previous
335+
next.System.Backups.RestoreHostAllowlist = entries
336+
config.Set(&next)
337+
}

0 commit comments

Comments
 (0)