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("Hermesok")) })) 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(), ``) { 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("Hermesok")) })) 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(), ``) { 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 }