846 lines
24 KiB
Go
846 lines
24 KiB
Go
package zooid
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"fiatjaf.com/nostr"
|
|
)
|
|
|
|
func TestAPIHandler_Authentication(t *testing.T) {
|
|
// Create a temporary config directory
|
|
configDir := t.TempDir()
|
|
|
|
// Create a test keypair for authentication
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
|
|
// Create API handler with whitelist containing our test pubkey
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
t.Run("missing authorization header", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/relay/test", strings.NewReader("{}"))
|
|
req.Host = "api.example.com"
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid authorization format", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/relay/test", strings.NewReader("{}"))
|
|
req.Host = "api.example.com"
|
|
req.Header.Set("Authorization", "Bearer token123")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid base64", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/relay/test", strings.NewReader("{}"))
|
|
req.Host = "api.example.com"
|
|
req.Header.Set("Authorization", "Nostr not-valid-base64!!!")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid event kind", func(t *testing.T) {
|
|
event := nostr.Event{
|
|
Kind: 1, // Wrong kind
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"u", "http://api.example.com/relay/test"},
|
|
{"method", "POST"},
|
|
},
|
|
}
|
|
event.Sign(secretKey)
|
|
|
|
req := createAuthRequest(http.MethodPost, "http://api.example.com/relay/test", event, "{}")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid signature", func(t *testing.T) {
|
|
event := nostr.Event{
|
|
Kind: nostr.KindHTTPAuth,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"u", "http://api.example.com/relay/test"},
|
|
{"method", "POST"},
|
|
},
|
|
}
|
|
// Don't sign the event - invalid signature
|
|
|
|
req := createAuthRequest(http.MethodPost, "http://api.example.com/relay/test", event, "{}")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing u tag", func(t *testing.T) {
|
|
event := nostr.Event{
|
|
Kind: nostr.KindHTTPAuth,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"method", "POST"},
|
|
},
|
|
}
|
|
event.Sign(secretKey)
|
|
|
|
req := createAuthRequest(http.MethodPost, "http://api.example.com/relay/test", event, "{}")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing method tag", func(t *testing.T) {
|
|
event := nostr.Event{
|
|
Kind: nostr.KindHTTPAuth,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"u", "http://api.example.com/relay/test"},
|
|
},
|
|
}
|
|
event.Sign(secretKey)
|
|
|
|
req := createAuthRequest(http.MethodPost, "http://api.example.com/relay/test", event, "{}")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("pubkey not in whitelist", func(t *testing.T) {
|
|
// Create a different keypair not in whitelist
|
|
otherSecret := nostr.Generate()
|
|
|
|
event := nostr.Event{
|
|
Kind: nostr.KindHTTPAuth,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"u", "http://api.example.com/relay/test"},
|
|
{"method", "POST"},
|
|
},
|
|
}
|
|
event.Sign(otherSecret)
|
|
|
|
req := createAuthRequest(http.MethodPost, "http://api.example.com/relay/test", event, "{}")
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected status %d, got %d", http.StatusForbidden, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_CreateRelay(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
validConfig := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "testrelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Test Relay",
|
|
"pubkey": pubkey.Hex(),
|
|
"description": "A test relay",
|
|
},
|
|
}
|
|
|
|
t.Run("create relay successfully", func(t *testing.T) {
|
|
body, _ := json.Marshal(validConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/newrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify file was created
|
|
configPath := filepath.Join(configDir, "newrelay.toml")
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
t.Error("config file was not created")
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate id returns conflict", func(t *testing.T) {
|
|
body, _ := json.Marshal(validConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/newrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status %d, got %d", http.StatusConflict, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate schema returns conflict", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "other.example.com",
|
|
"schema": "testrelay", // Same schema as existing
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/other", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status %d, got %d", http.StatusConflict, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate host returns conflict", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "relay.example.com", // Same host as existing
|
|
"schema": "otherschema",
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/other2", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status %d, got %d", http.StatusConflict, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing required fields", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
// missing schema and secret
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/badrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid secret key", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "badrelay",
|
|
"secret": "not-a-valid-hex-key",
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/badrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/badrelay", secretKey, []byte("not json"))
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_UpdateRelay(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
// Create initial relay
|
|
initialConfig := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "testrelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Test Relay",
|
|
"pubkey": pubkey.Hex(),
|
|
},
|
|
}
|
|
body, _ := json.Marshal(initialConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
api.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create initial relay: %d - %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
t.Run("update existing relay", func(t *testing.T) {
|
|
updatedConfig := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "testrelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Updated Relay Name",
|
|
"pubkey": pubkey.Hex(),
|
|
"description": "Updated description",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(updatedConfig)
|
|
req := createAuthenticatedRequest(http.MethodPut, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("update non-existent relay returns not found", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "nonexistent.example.com",
|
|
"schema": "nonexistent",
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPut, "http://api.example.com/relay/nonexistent", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("update with duplicate schema", func(t *testing.T) {
|
|
// Create another relay first
|
|
otherConfig := map[string]interface{}{
|
|
"host": "other.example.com",
|
|
"schema": "otherrelay",
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ := json.Marshal(otherConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/otherrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
api.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create other relay: %d", w.Code)
|
|
}
|
|
|
|
// Try to update first relay with second relay's schema
|
|
updateConfig := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "otherrelay", // Duplicate
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ = json.Marshal(updateConfig)
|
|
req = createAuthenticatedRequest(http.MethodPut, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w = httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status %d, got %d", http.StatusConflict, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_PatchRelay(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
// Create initial relay with full config
|
|
initialConfig := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "testrelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Original Name",
|
|
"icon": "https://example.com/original.png",
|
|
"pubkey": pubkey.Hex(),
|
|
"description": "Original description",
|
|
},
|
|
"policy": map[string]interface{}{
|
|
"public_join": false,
|
|
"strip_signatures": false,
|
|
},
|
|
"groups": map[string]interface{}{
|
|
"enabled": true,
|
|
},
|
|
}
|
|
body, _ := json.Marshal(initialConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
api.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create initial relay: %d - %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
t.Run("patch existing relay - update single field", func(t *testing.T) {
|
|
patch := map[string]interface{}{
|
|
"info": map[string]interface{}{
|
|
"name": "Patched Name",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(patch)
|
|
req := createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("patch existing relay - update nested fields", func(t *testing.T) {
|
|
patch := map[string]interface{}{
|
|
"info": map[string]interface{}{
|
|
"description": "Updated description",
|
|
"icon": "https://example.com/new-icon.png",
|
|
},
|
|
"policy": map[string]interface{}{
|
|
"public_join": true,
|
|
},
|
|
}
|
|
body, _ := json.Marshal(patch)
|
|
req := createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("patch non-existent relay returns not found", func(t *testing.T) {
|
|
patch := map[string]interface{}{
|
|
"info": map[string]interface{}{
|
|
"name": "New Name",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(patch)
|
|
req := createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/nonexistent", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("patch with duplicate schema returns conflict", func(t *testing.T) {
|
|
// Create another relay first
|
|
otherConfig := map[string]interface{}{
|
|
"host": "other.example.com",
|
|
"schema": "anotherrelay",
|
|
"secret": secretKey.Hex(),
|
|
}
|
|
body, _ := json.Marshal(otherConfig)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/anotherrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
api.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create other relay: %d", w.Code)
|
|
}
|
|
|
|
// Try to patch first relay with second relay's schema
|
|
patch := map[string]interface{}{
|
|
"schema": "anotherrelay", // Duplicate
|
|
}
|
|
body, _ = json.Marshal(patch)
|
|
req = createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w = httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status %d, got %d", http.StatusConflict, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("patch with invalid secret key", func(t *testing.T) {
|
|
patch := map[string]interface{}{
|
|
"secret": "not-a-valid-hex-key",
|
|
}
|
|
body, _ := json.Marshal(patch)
|
|
req := createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("patch removing required field fails", func(t *testing.T) {
|
|
patch := map[string]interface{}{
|
|
"host": nil,
|
|
}
|
|
body, _ := json.Marshal(patch)
|
|
req := createAuthenticatedRequest(http.MethodPatch, "http://api.example.com/relay/testrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_DeleteRelay(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
// Create a relay to delete
|
|
config := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "deleterelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Delete Me",
|
|
"pubkey": pubkey.Hex(),
|
|
},
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/deleterelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
api.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create relay: %d", w.Code)
|
|
}
|
|
|
|
t.Run("delete existing relay", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodDelete, "http://api.example.com/relay/deleterelay", secretKey, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
// Verify file was deleted
|
|
configPath := filepath.Join(configDir, "deleterelay.toml")
|
|
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
|
t.Error("config file was not deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("delete non-existent relay returns not found", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodDelete, "http://api.example.com/relay/nonexistent", secretKey, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_MethodNotAllowed(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
t.Run("GET method not allowed", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodGet, "http://api.example.com/relay/test", secretKey, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_InvalidPath(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
t.Run("invalid path returns not found", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/invalid/path", secretKey, []byte("{}"))
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("missing relay id", func(t *testing.T) {
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/", secretKey, []byte("{}"))
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIHandler_ConfigValidation(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
|
|
secretKey := nostr.Generate()
|
|
pubkey := secretKey.Public()
|
|
whitelist := pubkey.Hex()
|
|
api := NewAPIHandler(whitelist, configDir)
|
|
|
|
t.Run("invalid info.pubkey", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "relay.example.com",
|
|
"schema": "badpubkey",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Test",
|
|
"pubkey": "not-a-valid-pubkey",
|
|
},
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/badpubkey", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("valid full config", func(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "full.example.com",
|
|
"schema": "fullrelay",
|
|
"secret": secretKey.Hex(),
|
|
"info": map[string]interface{}{
|
|
"name": "Full Test Relay",
|
|
"icon": "https://example.com/icon.png",
|
|
"pubkey": pubkey.Hex(),
|
|
"description": "A full test relay",
|
|
},
|
|
"policy": map[string]interface{}{
|
|
"public_join": true,
|
|
"strip_signatures": false,
|
|
},
|
|
"groups": map[string]interface{}{
|
|
"enabled": true,
|
|
},
|
|
"push": map[string]interface{}{
|
|
"enabled": true,
|
|
},
|
|
"management": map[string]interface{}{
|
|
"enabled": true,
|
|
},
|
|
"blossom": map[string]interface{}{
|
|
"enabled": true,
|
|
},
|
|
"roles": map[string]interface{}{
|
|
"member": map[string]interface{}{
|
|
"can_invite": true,
|
|
"can_manage": false,
|
|
},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(config)
|
|
req := createAuthenticatedRequest(http.MethodPost, "http://api.example.com/relay/fullrelay", secretKey, body)
|
|
w := httptest.NewRecorder()
|
|
|
|
api.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify the TOML file was created with correct content
|
|
configPath := filepath.Join(configDir, "fullrelay.toml")
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read config file: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "host = \"full.example.com\"") {
|
|
t.Error("TOML missing host")
|
|
}
|
|
if !strings.Contains(contentStr, "schema = \"fullrelay\"") {
|
|
t.Error("TOML missing schema")
|
|
}
|
|
if !strings.Contains(contentStr, "name = \"Full Test Relay\"") {
|
|
t.Error("TOML missing info.name")
|
|
}
|
|
if !strings.Contains(contentStr, "enabled = true") {
|
|
t.Error("TOML missing enabled flags")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func createAuthRequest(method, url string, event nostr.Event, body string) *http.Request {
|
|
var bodyReader *bytes.Reader
|
|
if body != "" {
|
|
bodyReader = bytes.NewReader([]byte(body))
|
|
} else {
|
|
bodyReader = bytes.NewReader([]byte{})
|
|
}
|
|
|
|
req := httptest.NewRequest(method, url, bodyReader)
|
|
req.Host = "api.example.com"
|
|
|
|
jevt, _ := json.Marshal(event)
|
|
authHeader := "Nostr " + base64.StdEncoding.EncodeToString(jevt)
|
|
req.Header.Set("Authorization", authHeader)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
return req
|
|
}
|
|
|
|
func createAuthenticatedRequest(method, url string, secretKey nostr.SecretKey, body []byte) *http.Request {
|
|
var bodyReader *bytes.Reader
|
|
if body != nil {
|
|
bodyReader = bytes.NewReader(body)
|
|
} else {
|
|
bodyReader = bytes.NewReader([]byte{})
|
|
}
|
|
|
|
req := httptest.NewRequest(method, url, bodyReader)
|
|
req.Host = "api.example.com"
|
|
|
|
// Create NIP-98 auth event
|
|
event := nostr.Event{
|
|
Kind: nostr.KindHTTPAuth,
|
|
CreatedAt: nostr.Now(),
|
|
Tags: nostr.Tags{
|
|
{"u", url},
|
|
{"method", method},
|
|
},
|
|
}
|
|
event.Sign(secretKey)
|
|
|
|
jevt, _ := json.Marshal(event)
|
|
authHeader := "Nostr " + base64.StdEncoding.EncodeToString(jevt)
|
|
req.Header.Set("Authorization", authHeader)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
return req
|
|
}
|
|
|
|
func TestNewAPIHandler(t *testing.T) {
|
|
t.Run("empty whitelist", func(t *testing.T) {
|
|
api := NewAPIHandler("", "/tmp")
|
|
if len(api.whitelist) != 0 {
|
|
t.Error("expected empty whitelist")
|
|
}
|
|
})
|
|
|
|
t.Run("single pubkey", func(t *testing.T) {
|
|
pubkey := nostr.Generate().Public().Hex()
|
|
api := NewAPIHandler(pubkey, "/tmp")
|
|
if len(api.whitelist) != 1 {
|
|
t.Error("expected 1 entry in whitelist")
|
|
}
|
|
if !api.whitelist[pubkey] {
|
|
t.Error("pubkey not in whitelist")
|
|
}
|
|
})
|
|
|
|
t.Run("multiple pubkeys", func(t *testing.T) {
|
|
pubkey1 := nostr.Generate().Public().Hex()
|
|
pubkey2 := nostr.Generate().Public().Hex()
|
|
whitelist := fmt.Sprintf("%s, %s", pubkey1, pubkey2)
|
|
api := NewAPIHandler(whitelist, "/tmp")
|
|
if len(api.whitelist) != 2 {
|
|
t.Error("expected 2 entries in whitelist")
|
|
}
|
|
if !api.whitelist[pubkey1] || !api.whitelist[pubkey2] {
|
|
t.Error("pubkeys not in whitelist")
|
|
}
|
|
})
|
|
|
|
t.Run("whitespace trimming", func(t *testing.T) {
|
|
pubkey := nostr.Generate().Public().Hex()
|
|
whitelist := " " + pubkey + " "
|
|
api := NewAPIHandler(whitelist, "/tmp")
|
|
if len(api.whitelist) != 1 {
|
|
t.Error("expected 1 entry in whitelist after trimming")
|
|
}
|
|
})
|
|
}
|