orama/pkg/cli/sandbox/hetzner_test.go

304 lines
9.0 KiB
Go

package sandbox
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestValidateToken_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-token" {
t.Errorf("unexpected auth header: %s", r.Header.Get("Authorization"))
}
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]interface{}{"servers": []interface{}{}})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
if err := client.ValidateToken(); err != nil {
t.Errorf("ValidateToken() error = %v, want nil", err)
}
}
func TestValidateToken_InvalidToken(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]string{
"code": "unauthorized",
"message": "unable to authenticate",
},
})
}))
defer srv.Close()
client := newTestClient(srv, "bad-token")
if err := client.ValidateToken(); err == nil {
t.Error("ValidateToken() expected error for invalid token")
}
}
func TestCreateServer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/servers" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var req CreateServerRequest
json.NewDecoder(r.Body).Decode(&req)
if req.Name != "sbx-test-1" {
t.Errorf("unexpected server name: %s", req.Name)
}
if req.ServerType != "cx22" {
t.Errorf("unexpected server type: %s", req.ServerType)
}
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]interface{}{
"server": map[string]interface{}{
"id": 12345,
"name": req.Name,
"status": "initializing",
"public_net": map[string]interface{}{
"ipv4": map[string]string{"ip": "1.2.3.4"},
},
"labels": req.Labels,
"server_type": map[string]string{"name": "cx22"},
},
})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
server, err := client.CreateServer(CreateServerRequest{
Name: "sbx-test-1",
ServerType: "cx22",
Image: "ubuntu-24.04",
Location: "fsn1",
SSHKeys: []int64{1},
Labels: map[string]string{"orama-sandbox": "test"},
})
if err != nil {
t.Fatalf("CreateServer() error = %v", err)
}
if server.ID != 12345 {
t.Errorf("server ID = %d, want 12345", server.ID)
}
if server.Name != "sbx-test-1" {
t.Errorf("server name = %s, want sbx-test-1", server.Name)
}
if server.PublicNet.IPv4.IP != "1.2.3.4" {
t.Errorf("server IP = %s, want 1.2.3.4", server.PublicNet.IPv4.IP)
}
}
func TestDeleteServer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" || r.URL.Path != "/v1/servers/12345" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
w.WriteHeader(200)
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
if err := client.DeleteServer(12345); err != nil {
t.Errorf("DeleteServer() error = %v", err)
}
}
func TestListServersByLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("label_selector") != "orama-sandbox=test" {
t.Errorf("unexpected label_selector: %s", r.URL.Query().Get("label_selector"))
}
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]interface{}{
"servers": []map[string]interface{}{
{"id": 1, "name": "sbx-test-1", "status": "running", "public_net": map[string]interface{}{"ipv4": map[string]string{"ip": "1.1.1.1"}}, "server_type": map[string]string{"name": "cx22"}},
{"id": 2, "name": "sbx-test-2", "status": "running", "public_net": map[string]interface{}{"ipv4": map[string]string{"ip": "2.2.2.2"}}, "server_type": map[string]string{"name": "cx22"}},
},
})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
servers, err := client.ListServersByLabel("orama-sandbox=test")
if err != nil {
t.Fatalf("ListServersByLabel() error = %v", err)
}
if len(servers) != 2 {
t.Errorf("got %d servers, want 2", len(servers))
}
}
func TestWaitForServer_AlreadyRunning(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]interface{}{
"server": map[string]interface{}{
"id": 1,
"name": "test",
"status": "running",
"public_net": map[string]interface{}{
"ipv4": map[string]string{"ip": "1.1.1.1"},
},
"server_type": map[string]string{"name": "cx22"},
},
})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
server, err := client.WaitForServer(1, 5*time.Second)
if err != nil {
t.Fatalf("WaitForServer() error = %v", err)
}
if server.Status != "running" {
t.Errorf("server status = %s, want running", server.Status)
}
}
func TestAssignFloatingIP(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/floating_ips/100/actions/assign" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var body map[string]int64
json.NewDecoder(r.Body).Decode(&body)
if body["server"] != 200 {
t.Errorf("unexpected server ID: %d", body["server"])
}
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]interface{}{"action": map[string]interface{}{"id": 1, "status": "running"}})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
if err := client.AssignFloatingIP(100, 200); err != nil {
t.Errorf("AssignFloatingIP() error = %v", err)
}
}
func TestUploadSSHKey(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/ssh_keys" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]interface{}{
"ssh_key": map[string]interface{}{
"id": 42,
"name": "orama-sandbox",
"fingerprint": "aa:bb:cc:dd",
"public_key": "ssh-ed25519 AAAA...",
},
})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
key, err := client.UploadSSHKey("orama-sandbox", "ssh-ed25519 AAAA...")
if err != nil {
t.Fatalf("UploadSSHKey() error = %v", err)
}
if key.ID != 42 {
t.Errorf("key ID = %d, want 42", key.ID)
}
}
func TestCreateFirewall(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/firewalls" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]interface{}{
"firewall": map[string]interface{}{
"id": 99,
"name": "orama-sandbox",
},
})
}))
defer srv.Close()
client := newTestClient(srv, "test-token")
fw, err := client.CreateFirewall("orama-sandbox", SandboxFirewallRules(), map[string]string{"orama-sandbox": "infra"})
if err != nil {
t.Fatalf("CreateFirewall() error = %v", err)
}
if fw.ID != 99 {
t.Errorf("firewall ID = %d, want 99", fw.ID)
}
}
func TestSandboxFirewallRules(t *testing.T) {
rules := SandboxFirewallRules()
if len(rules) != 6 {
t.Errorf("got %d rules, want 6", len(rules))
}
expectedPorts := map[string]bool{"22": false, "53": false, "80": false, "443": false, "51820": false}
for _, r := range rules {
expectedPorts[r.Port] = true
if r.Direction != "in" {
t.Errorf("rule %s direction = %s, want in", r.Port, r.Direction)
}
}
for port, seen := range expectedPorts {
if !seen {
t.Errorf("missing firewall rule for port %s", port)
}
}
}
func TestParseHetznerError(t *testing.T) {
body := `{"error":{"code":"uniqueness_error","message":"server name already used"}}`
err := parseHetznerError([]byte(body), 409)
if err == nil {
t.Fatal("expected error")
}
expected := "hetzner API error (HTTP 409): uniqueness_error — server name already used"
if err.Error() != expected {
t.Errorf("error = %q, want %q", err.Error(), expected)
}
}
// newTestClient creates a HetznerClient pointing at a test server.
func newTestClient(ts *httptest.Server, token string) *HetznerClient {
client := NewHetznerClient(token)
// Override the base URL by using a custom transport
client.httpClient = ts.Client()
// We need to override the base URL — wrap the transport
origTransport := client.httpClient.Transport
client.httpClient.Transport = &testTransport{
base: origTransport,
testURL: ts.URL,
}
return client
}
// testTransport rewrites requests to point at the test server.
type testTransport struct {
base http.RoundTripper
testURL string
}
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the URL to point at the test server
req.URL.Scheme = "http"
req.URL.Host = t.testURL[len("http://"):]
if t.base != nil {
return t.base.RoundTrip(req)
}
return http.DefaultTransport.RoundTrip(req)
}