Rewrite the service registration model per the architecture plan:
Model changes:
- Domain is the unique key (removed Name field)
- Removed ExtraDomains — each domain is its own registration
- Removed SourceNode, BackendStatic, ReachOff
- Added Subdomains bool for wildcard matching control
- Files stored as <domain_with_underscores>.yaml
- API routes: /services/{domain:.+} (regex for dots in path)
- Added DeregisterBySource for cleanup before re-registration
Reconciliation changes:
- No ExtraDomains iteration — each service IS one domain
- reach:internal → sets InternalDomain (generates local=/)
- reach:public → sets Domain (no local=/)
- tcp-passthrough → DNS points to backend IP
- http → DNS points to Central IP
All 22 tests pass (9 manager + 8 handler + 5 config/cert).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
228 lines
6.2 KiB
Go
228 lines
6.2 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
func TestServicesRegister(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"domain": "my-api.payne.io",
|
|
"source": "wild-works",
|
|
"backend": map[string]any{
|
|
"address": "192.168.8.60:9001",
|
|
"type": "http",
|
|
},
|
|
"reach": "internal",
|
|
})
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
svc := resp["service"].(map[string]any)
|
|
if svc["domain"] != "my-api.payne.io" {
|
|
t.Errorf("expected domain my-api.payne.io, got %v", svc["domain"])
|
|
}
|
|
if svc["tls"] != "terminate" {
|
|
t.Errorf("expected tls=terminate default for http, got %v", svc["tls"])
|
|
}
|
|
if svc["source"] != "wild-works" {
|
|
t.Errorf("expected source wild-works, got %v", svc["source"])
|
|
}
|
|
}
|
|
|
|
func TestServicesRegister_TCPPassthrough(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"domain": "cloud.payne.io",
|
|
"source": "wild-cloud",
|
|
"backend": map[string]any{
|
|
"address": "192.168.8.240:443",
|
|
"type": "tcp-passthrough",
|
|
},
|
|
"subdomains": true,
|
|
"reach": "public",
|
|
})
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
svc := resp["service"].(map[string]any)
|
|
if svc["tls"] != "passthrough" {
|
|
t.Errorf("expected tls=passthrough for tcp-passthrough, got %v", svc["tls"])
|
|
}
|
|
if svc["subdomains"] != true {
|
|
t.Errorf("expected subdomains=true, got %v", svc["subdomains"])
|
|
}
|
|
}
|
|
|
|
func TestServicesListAll_Empty(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/services", nil)
|
|
w := httptest.NewRecorder()
|
|
api.ServicesListAll(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
svcs := resp["services"].([]any)
|
|
if len(svcs) != 0 {
|
|
t.Errorf("expected empty, got %d", len(svcs))
|
|
}
|
|
}
|
|
|
|
func TestServicesGet(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
// Register
|
|
body, _ := json.Marshal(map[string]any{
|
|
"domain": "get-test.example.com",
|
|
"source": "test",
|
|
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
|
|
"reach": "internal",
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
|
|
// Get by domain
|
|
req = httptest.NewRequest("GET", "/api/v1/services/get-test.example.com", nil)
|
|
req = mux.SetURLVars(req, map[string]string{"domain": "get-test.example.com"})
|
|
w = httptest.NewRecorder()
|
|
api.ServicesGet(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var svc map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &svc)
|
|
if svc["domain"] != "get-test.example.com" {
|
|
t.Errorf("expected domain get-test.example.com, got %v", svc["domain"])
|
|
}
|
|
}
|
|
|
|
func TestServicesGet_NotFound(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/services/nonexistent.com", nil)
|
|
req = mux.SetURLVars(req, map[string]string{"domain": "nonexistent.com"})
|
|
w := httptest.NewRecorder()
|
|
api.ServicesGet(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestServicesUpdate(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
// Register
|
|
body, _ := json.Marshal(map[string]any{
|
|
"domain": "update-test.example.com",
|
|
"source": "test",
|
|
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
|
|
"reach": "internal",
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
|
|
// Update reach to public
|
|
update, _ := json.Marshal(map[string]any{"reach": "public"})
|
|
req = httptest.NewRequest("PATCH", "/api/v1/services/update-test.example.com", bytes.NewReader(update))
|
|
req = mux.SetURLVars(req, map[string]string{"domain": "update-test.example.com"})
|
|
w = httptest.NewRecorder()
|
|
api.ServicesUpdate(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
svc := resp["service"].(map[string]any)
|
|
if svc["reach"] != "public" {
|
|
t.Errorf("expected reach=public, got %v", svc["reach"])
|
|
}
|
|
}
|
|
|
|
func TestServicesDeregister(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
// Register
|
|
body, _ := json.Marshal(map[string]any{
|
|
"domain": "delete-me.example.com",
|
|
"source": "test",
|
|
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
|
|
"reach": "internal",
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
|
|
// Delete
|
|
req = httptest.NewRequest("DELETE", "/api/v1/services/delete-me.example.com", nil)
|
|
req = mux.SetURLVars(req, map[string]string{"domain": "delete-me.example.com"})
|
|
w = httptest.NewRecorder()
|
|
api.ServicesDeregister(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
// Verify gone
|
|
req = httptest.NewRequest("GET", "/api/v1/services/delete-me.example.com", nil)
|
|
req = mux.SetURLVars(req, map[string]string{"domain": "delete-me.example.com"})
|
|
w = httptest.NewRecorder()
|
|
api.ServicesGet(w, req)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404 after delete, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestServicesRegister_Validation(t *testing.T) {
|
|
api, _ := setupTestAPI(t)
|
|
|
|
// Missing domain
|
|
body, _ := json.Marshal(map[string]any{
|
|
"backend": map[string]any{"address": "127.0.0.1:80", "type": "http"},
|
|
"reach": "internal",
|
|
})
|
|
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
api.ServicesRegister(w, req)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for missing domain, got %d", w.Code)
|
|
}
|
|
}
|