Files
wehub-resource-sync 6d978fe483
Build / Frontend Build (push) Has been cancelled
Build / Backend Build (push) Has been cancelled
Build / Docker Build (push) Has been cancelled
Build / Kubernetes Deploy Smoke Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:24 +08:00

693 lines
24 KiB
Go

package services
import (
"errors"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"clawreef/internal/models"
"clawreef/internal/repository"
"github.com/gorilla/websocket"
)
func TestInstanceProxyServiceUsesRuntimeBindingForV2(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/instances/123/proxy/apps/openclaw" {
t.Fatalf("unexpected upstream path %s", r.URL.Path)
}
if got := r.Header.Get("X-Forwarded-Prefix"); got != "/api/v1/instances/123/proxy" {
t.Fatalf("X-Forwarded-Prefix = %q", got)
}
if got := r.Header.Get("Authorization"); got != "Bearer internal-openclaw-token" {
t.Fatalf("Authorization = %q", got)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/openclaw/user-45/instance-123"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[123] = &models.Instance{
ID: 123,
UserID: 45,
Type: "openclaw",
RuntimeType: "gateway",
Status: "running",
WorkspacePath: &workspacePath,
RuntimeGeneration: 3,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[123] = &models.InstanceRuntimeBinding{
InstanceID: 123,
RuntimePodID: 9,
GatewayPort: gatewayPort,
State: "running",
Generation: 3,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
9: {ID: 9, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 123, "openclaw", "/api/v1/instances/123/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
service.httpClient = upstream.Client()
service.openClawGatewayToken = "internal-openclaw-token"
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/123/proxy/apps/openclaw?token="+url.QueryEscape(token.Token), nil)
rec := httptest.NewRecorder()
if err := service.ProxyRequest(req.Context(), 123, token.Token, rec, req); err != nil {
t.Fatalf("ProxyRequest returned error: %v", err)
}
if rec.Code != http.StatusOK || rec.Body.String() != "ok" {
t.Fatalf("unexpected proxy response %d %q", rec.Code, rec.Body.String())
}
}
func TestInstanceProxyServiceInjectsInstanceTokenForHermesLite(t *testing.T) {
instanceToken := "igt_hermes_instance"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/chat" {
t.Fatalf("unexpected upstream path %s", r.URL.Path)
}
if r.URL.RawQuery != "" {
t.Fatalf("unexpected upstream query %q", r.URL.RawQuery)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+instanceToken {
t.Fatalf("Authorization = %q", got)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/hermes/user-45/instance-127"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[127] = &models.Instance{
ID: 127,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
Status: "running",
AccessToken: &instanceToken,
WorkspacePath: &workspacePath,
RuntimeGeneration: 5,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[127] = &models.InstanceRuntimeBinding{
InstanceID: 127,
RuntimePodID: 10,
GatewayPort: gatewayPort,
State: "running",
Generation: 5,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
10: {ID: 10, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 127, "hermes", "/api/v1/instances/127/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
service.httpClient = upstream.Client()
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/127/proxy/chat?token="+url.QueryEscape(token.Token), nil)
rec := httptest.NewRecorder()
if err := service.ProxyRequest(req.Context(), 127, token.Token, rec, req); err != nil {
t.Fatalf("ProxyRequest returned error: %v", err)
}
if rec.Code != http.StatusOK || rec.Body.String() != "ok" {
t.Fatalf("unexpected proxy response %d %q", rec.Code, rec.Body.String())
}
}
func TestInstanceProxyServiceUsesHermesLiteAccessURLForRootEntry(t *testing.T) {
instanceToken := "igt_hermes_instance"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/chat/" {
t.Fatalf("unexpected upstream path %s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+instanceToken {
t.Fatalf("Authorization = %q", got)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("<!doctype html><html><head><title>Hermes</title></head><body>ok</body></html>"))
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/hermes/user-45/instance-131"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[131] = &models.Instance{
ID: 131,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
Status: "running",
AccessToken: &instanceToken,
WorkspacePath: &workspacePath,
RuntimeGeneration: 5,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[131] = &models.InstanceRuntimeBinding{
InstanceID: 131,
RuntimePodID: 14,
GatewayPort: gatewayPort,
State: "running",
Generation: 5,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
14: {ID: 14, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 131, "hermes", "/api/v1/instances/131/proxy/chat/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
service.httpClient = upstream.Client()
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/131/proxy/?token="+url.QueryEscape(token.Token), nil)
rec := httptest.NewRecorder()
if err := service.ProxyRequest(req.Context(), 131, token.Token, rec, req); err != nil {
t.Fatalf("ProxyRequest returned error: %v", err)
}
if rec.Code != http.StatusOK {
t.Fatalf("unexpected proxy response %d %q", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), `<base href="/api/v1/instances/131/proxy/chat/">`) {
t.Fatalf("expected Hermes Lite HTML to include chat proxy base, got %q", rec.Body.String())
}
}
func TestInstanceProxyServicePreservesHermesRuntimeQueryToken(t *testing.T) {
instanceToken := "igt_hermes_instance"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/ws" {
t.Fatalf("unexpected upstream path %s", r.URL.Path)
}
if got := r.URL.Query().Get("token"); got != "hermes-session-token" {
t.Fatalf("upstream token query = %q", got)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+instanceToken {
t.Fatalf("Authorization = %q", got)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/hermes/user-45/instance-130"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[130] = &models.Instance{
ID: 130,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
Status: "running",
AccessToken: &instanceToken,
WorkspacePath: &workspacePath,
RuntimeGeneration: 5,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[130] = &models.InstanceRuntimeBinding{
InstanceID: 130,
RuntimePodID: 13,
GatewayPort: gatewayPort,
State: "running",
Generation: 5,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
13: {ID: 13, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 130, "hermes", "/api/v1/instances/130/proxy/chat/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
service.httpClient = upstream.Client()
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/130/proxy/api/ws?token=hermes-session-token", nil)
rec := httptest.NewRecorder()
if err := service.ProxyRequest(req.Context(), 130, token.Token, rec, req); err != nil {
t.Fatalf("ProxyRequest returned error: %v", err)
}
if rec.Code != http.StatusOK || rec.Body.String() != "ok" {
t.Fatalf("unexpected proxy response %d %q", rec.Code, rec.Body.String())
}
}
func TestInstanceProxyServiceRewritesHermesLiteHTMLBase(t *testing.T) {
instanceToken := "igt_hermes_instance"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
t.Fatalf("unexpected upstream path %s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+instanceToken {
t.Fatalf("Authorization = %q", got)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("<!doctype html><html><head><title>Hermes</title></head><body>ok</body></html>"))
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/hermes/user-45/instance-128"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[128] = &models.Instance{
ID: 128,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
Status: "running",
AccessToken: &instanceToken,
WorkspacePath: &workspacePath,
RuntimeGeneration: 5,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[128] = &models.InstanceRuntimeBinding{
InstanceID: 128,
RuntimePodID: 11,
GatewayPort: gatewayPort,
State: "running",
Generation: 5,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
11: {ID: 11, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 128, "hermes", "/api/v1/instances/128/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
service.httpClient = upstream.Client()
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/128/proxy/?token="+url.QueryEscape(token.Token), nil)
rec := httptest.NewRecorder()
if err := service.ProxyRequest(req.Context(), 128, token.Token, rec, req); err != nil {
t.Fatalf("ProxyRequest returned error: %v", err)
}
if rec.Code != http.StatusOK {
t.Fatalf("unexpected proxy response %d %q", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), `<base href="/api/v1/instances/128/proxy/">`) {
t.Fatalf("expected Hermes Lite HTML to include proxy base, got %q", rec.Body.String())
}
}
func TestInstanceProxyServiceProxiesHermesLiteWebSocket(t *testing.T) {
instanceToken := "igt_hermes_instance"
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/ws" {
t.Fatalf("unexpected upstream websocket path %s", r.URL.Path)
}
if got := r.URL.Query().Get("token"); got != "hermes-ws-token" {
t.Fatalf("upstream websocket token query = %q", got)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+instanceToken {
t.Fatalf("Authorization = %q", got)
}
if got := r.Header.Get("X-Forwarded-Prefix"); got != "/api/v1/instances/129/proxy" {
t.Fatalf("X-Forwarded-Prefix = %q", got)
}
if got := r.Header.Get("X-Forwarded-Host"); got == "" {
t.Fatalf("X-Forwarded-Host is empty")
}
if got := r.Header.Get("X-Forwarded-For"); got == "" {
t.Fatalf("X-Forwarded-For is empty")
}
if got, want := r.Header.Get("Origin"), "http://"+r.Host; got != want {
t.Fatalf("Origin = %q, want %q", got, want)
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Fatalf("upstream websocket upgrade failed: %v", err)
}
defer conn.Close()
messageType, message, err := conn.ReadMessage()
if err != nil {
t.Fatalf("upstream websocket read failed: %v", err)
}
if string(message) != "ping" {
t.Fatalf("upstream websocket message = %q", message)
}
if err := conn.WriteMessage(messageType, []byte("pong")); err != nil {
t.Fatalf("upstream websocket write failed: %v", err)
}
}))
defer upstream.Close()
podIP, gatewayPort := splitURLHostPortForProxyTest(t, upstream.URL)
workspacePath := "/workspaces/hermes/user-45/instance-129"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[129] = &models.Instance{
ID: 129,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
Status: "running",
AccessToken: &instanceToken,
WorkspacePath: &workspacePath,
RuntimeGeneration: 5,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[129] = &models.InstanceRuntimeBinding{
InstanceID: 129,
RuntimePodID: 12,
GatewayPort: gatewayPort,
State: "running",
Generation: 5,
}
podRepo := &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
12: {ID: 12, PodIP: &podIP, State: "ready"},
},
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 129, "hermes", "/api/v1/instances/129/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := service.ProxyWebSocket(r.Context(), 129, token.Token, w, r); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
}
}))
defer proxy.Close()
wsURL := "ws" + strings.TrimPrefix(proxy.URL, "http") + "/api/v1/instances/129/proxy/ws?token=hermes-ws-token"
clientConn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("client websocket dial failed: %v", err)
}
defer clientConn.Close()
if err := clientConn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
t.Fatalf("client websocket write failed: %v", err)
}
_, message, err := clientConn.ReadMessage()
if err != nil {
t.Fatalf("client websocket read failed: %v", err)
}
if string(message) != "pong" {
t.Fatalf("client websocket message = %q", message)
}
}
func TestInstanceProxyServiceUsesBaseProxyEntryForV2OpenClaw(t *testing.T) {
workspacePath := "/workspaces/openclaw/user-45/instance-123"
accessService := NewInstanceAccessService()
t.Cleanup(accessService.Stop)
service := NewInstanceProxyService(accessService)
got := service.GetProxyURLForInstance(&models.Instance{
ID: 123,
Type: "openclaw",
RuntimeType: "gateway",
WorkspacePath: &workspacePath,
}, "token+with/slash")
want := "/api/v1/instances/123/proxy/?token=token%2Bwith%2Fslash"
if got != want {
t.Fatalf("GetProxyURLForInstance() = %q, want %q", got, want)
}
}
func TestInstanceProxyServiceUsesChatEntryForHermesLite(t *testing.T) {
workspacePath := "/workspaces/hermes/user-45/instance-123"
accessService := NewInstanceAccessService()
t.Cleanup(accessService.Stop)
service := NewInstanceProxyService(accessService)
got := service.GetProxyURLForInstance(&models.Instance{
ID: 123,
Type: "hermes",
RuntimeType: "gateway",
InstanceMode: InstanceModeLite,
WorkspacePath: &workspacePath,
}, "token+with/slash")
want := "/api/v1/instances/123/proxy/chat/?token=token%2Bwith%2Fslash"
if got != want {
t.Fatalf("GetProxyURLForInstance() = %q, want %q", got, want)
}
}
func TestProxyBaseForRequestPathUsesSubdirectory(t *testing.T) {
got := proxyBaseForRequestPath("/api/v1/instances/123/proxy/__openclaw__/canvas/", 123)
want := "/api/v1/instances/123/proxy/__openclaw__/canvas/"
if got != want {
t.Fatalf("proxyBaseForRequestPath() = %q, want %q", got, want)
}
got = proxyBaseForRequestPath("/api/v1/instances/123/proxy/__openclaw__/canvas/app.js", 123)
if got != want {
t.Fatalf("proxyBaseForRequestPath(file) = %q, want %q", got, want)
}
}
func TestResolveOpenClawProxyOriginFromEnvUsesInternalOrigin(t *testing.T) {
t.Setenv("OPENCLAW_PROXY_ORIGIN", "")
t.Setenv("CLAWMANAGER_BACKEND_URL", "")
t.Setenv("CLAWMANAGER_TEAM_MANAGER_BASE_URL", "http://clawmanager-gateway.clawmanager-system.svc.cluster.local:9001/api")
got := resolveOpenClawProxyOriginFromEnv()
want := "http://clawmanager-gateway.clawmanager-system.svc.cluster.local:9001"
if got != want {
t.Fatalf("resolveOpenClawProxyOriginFromEnv() = %q, want %q", got, want)
}
}
func TestOpenClawWebSocketOriginPrefersConfiguredProxyOrigin(t *testing.T) {
accessService := NewInstanceAccessService()
t.Cleanup(accessService.Stop)
service := NewInstanceProxyService(accessService)
service.openClawProxyOrigin = "http://clawmanager-gateway.clawmanager-system.svc.cluster.local:9001"
got := service.openClawWebSocketOrigin(&url.URL{Scheme: "ws", Host: "10.42.0.63:20000"})
want := "http://clawmanager-gateway.clawmanager-system.svc.cluster.local:9001"
if got != want {
t.Fatalf("openClawWebSocketOrigin() = %q, want %q", got, want)
}
}
func TestWebSocketUpstreamOriginFallsBackToGatewayHost(t *testing.T) {
tests := []struct {
name string
url *url.URL
want string
}{
{
name: "plain websocket",
url: &url.URL{Scheme: "ws", Host: "10.42.0.63:20000"},
want: "http://10.42.0.63:20000",
},
{
name: "tls websocket",
url: &url.URL{Scheme: "wss", Host: "gateway.example.test"},
want: "https://gateway.example.test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := websocketUpstreamOrigin(tt.url); got != tt.want {
t.Fatalf("websocketUpstreamOrigin() = %q, want %q", got, tt.want)
}
})
}
}
func TestInstanceProxyServiceRejectsStoppedV2InstanceWithStaleBinding(t *testing.T) {
workspacePath := "/workspaces/openclaw/user-45/instance-125"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[125] = &models.Instance{
ID: 125,
UserID: 45,
Type: "openclaw",
RuntimeType: "gateway",
Status: "stopped",
WorkspacePath: &workspacePath,
RuntimeGeneration: 4,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[125] = &models.InstanceRuntimeBinding{
InstanceID: 125,
RuntimePodID: 9,
GatewayPort: 20025,
State: "running",
Generation: 4,
}
podIP := "10.42.0.125"
service, token := newV2ProxyTestService(t, instanceRepo, bindingRepo, &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
9: {ID: 9, PodIP: &podIP},
},
}, 45, 125, "openclaw")
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/125/proxy/", nil)
err := service.ProxyRequest(req.Context(), 125, token.Token, httptest.NewRecorder(), req)
if !errors.Is(err, ErrInstanceGatewayUnavailable) {
t.Fatalf("ProxyRequest error = %v, want ErrInstanceGatewayUnavailable", err)
}
}
func TestInstanceProxyServiceRejectsV2BindingGenerationMismatch(t *testing.T) {
workspacePath := "/workspaces/hermes/user-45/instance-126"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[126] = &models.Instance{
ID: 126,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
Status: "running",
WorkspacePath: &workspacePath,
RuntimeGeneration: 8,
}
bindingRepo := newFakeRuntimeBindingRepo()
bindingRepo.bindings[126] = &models.InstanceRuntimeBinding{
InstanceID: 126,
RuntimePodID: 10,
GatewayPort: 20026,
State: "running",
Generation: 7,
}
podIP := "10.42.0.126"
service, token := newV2ProxyTestService(t, instanceRepo, bindingRepo, &fakeRuntimePodRepo{
pods: map[int64]*models.RuntimePod{
10: {ID: 10, PodIP: &podIP},
},
}, 45, 126, "hermes")
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/126/proxy/", nil)
err := service.ProxyRequest(req.Context(), 126, token.Token, httptest.NewRecorder(), req)
if !errors.Is(err, ErrInstanceGatewayUnavailable) {
t.Fatalf("ProxyRequest error = %v, want ErrInstanceGatewayUnavailable", err)
}
}
func TestInstanceProxyServiceReturnsUnavailableWhenV2BindingMissing(t *testing.T) {
workspacePath := "/workspaces/hermes/user-45/instance-124"
instanceRepo := newV2LifecycleInstanceRepo()
instanceRepo.byID[124] = &models.Instance{
ID: 124,
UserID: 45,
Type: "hermes",
RuntimeType: "gateway",
Status: "running",
WorkspacePath: &workspacePath,
}
accessService := NewInstanceAccessService()
defer accessService.Stop()
token, err := accessService.GenerateToken(45, 124, "hermes", "/api/v1/instances/124/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = newFakeRuntimeBindingRepo()
service.runtimePodRepo = &fakeRuntimePodRepo{}
req := httptest.NewRequest(http.MethodGet, "/api/v1/instances/124/proxy/", nil)
rec := httptest.NewRecorder()
err = service.ProxyRequest(req.Context(), 124, token.Token, rec, req)
if !errors.Is(err, ErrInstanceGatewayUnavailable) {
t.Fatalf("ProxyRequest error = %v, want ErrInstanceGatewayUnavailable", err)
}
}
func newV2ProxyTestService(t *testing.T, instanceRepo repository.InstanceRepository, bindingRepo repository.InstanceRuntimeBindingRepository, podRepo repository.RuntimePodRepository, userID int, instanceID int, instanceType string) (*InstanceProxyService, *AccessToken) {
t.Helper()
accessService := NewInstanceAccessService()
t.Cleanup(accessService.Stop)
token, err := accessService.GenerateToken(userID, instanceID, instanceType, "/api/v1/instances/"+strconv.Itoa(instanceID)+"/proxy/", "", 3000, time.Hour)
if err != nil {
t.Fatalf("GenerateToken returned error: %v", err)
}
service := NewInstanceProxyService(accessService)
service.instanceRepo = instanceRepo
service.bindingRepo = bindingRepo
service.runtimePodRepo = podRepo
return service, token
}
func splitURLHostPortForProxyTest(t *testing.T, rawURL string) (string, int) {
t.Helper()
parsed, err := url.Parse(rawURL)
if err != nil {
t.Fatalf("parse upstream URL: %v", err)
}
host, portText, err := net.SplitHostPort(parsed.Host)
if err != nil {
t.Fatalf("split upstream host port: %v", err)
}
port, err := strconv.Atoi(portText)
if err != nil {
t.Fatalf("parse upstream port: %v", err)
}
return host, port
}