Files
tasks/service/frontend/nexus_operation_http_handler_test.go
Stephan BehnkeandGitHub 47ca325241 Nexus CHASM async completion (1/2) (#9951)
## What changed?

Support Nexus async completion in CHASM.

PS: completion-before-start will be handled in a follow-up PR.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

## Potential risks

Is behind feature flag.
2026-04-21 15:46:15 +00:00

152 lines
4.9 KiB
Go

package frontend
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/nexus-rpc/sdk-go/nexus"
"github.com/stretchr/testify/require"
"go.temporal.io/api/serviceerror"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
commonnexus "go.temporal.io/server/common/nexus"
"go.temporal.io/server/common/nexus/nexusrpc"
"go.temporal.io/server/common/nexus/nexustest"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// retryableNotFoundError is a gRPC NotFound error that also implements Retryable() bool.
type retryableNotFoundError struct {
msg string
}
func (e *retryableNotFoundError) Error() string { return e.msg }
func (e *retryableNotFoundError) Retryable() bool { return true }
func (e *retryableNotFoundError) GRPCStatus() *status.Status {
return status.New(codes.NotFound, e.msg)
}
// fakeNamespaceRegistry implements namespace.Registry with just GetNamespaceName.
// All other methods panic.
type fakeNamespaceRegistry struct {
namespace.Registry
getNamespaceName func(id namespace.ID) (namespace.Name, error)
}
func (f *fakeNamespaceRegistry) GetNamespaceName(id namespace.ID) (namespace.Name, error) {
return f.getNamespaceName(id)
}
func newTestNexusOperationHTTPHandler(
endpointRegistry commonnexus.EndpointRegistry,
namespaceRegistry namespace.Registry,
) (*NexusOperationHTTPHandler, *mux.Router) {
logger := log.NewTestLogger()
h := &NexusOperationHTTPHandler{
base: nexusrpc.BaseHTTPHandler{
Logger: log.NewSlogLogger(logger),
FailureConverter: nexusrpc.DefaultFailureConverter(),
},
logger: logger,
enpointRegistry: endpointRegistry,
namespaceRegistry: namespaceRegistry,
preprocessErrorCounter: metrics.CounterFunc(func(int64, ...metrics.Tag) {}),
}
router := mux.NewRouter()
h.RegisterRoutes(router)
return h, router
}
func doNexusHTTPRequest(t *testing.T, router *mux.Router, endpointID string) *httptest.ResponseRecorder {
t.Helper()
path := "/" + commonnexus.RouteDispatchNexusTaskByEndpoint.Path(endpointID) + "/test-service/test-operation"
req := httptest.NewRequest(http.MethodPost, path, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec
}
func TestDispatchNexusTaskByEndpoint_NotFound_NonRetryable(t *testing.T) {
reg := nexustest.FakeEndpointRegistry{
OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) {
return nil, serviceerror.NewNotFound("endpoint not found")
},
}
_, router := newTestNexusOperationHTTPHandler(reg, nil)
rec := doNexusHTTPRequest(t, router, "test-endpoint-id")
require.Equal(t, http.StatusNotFound, rec.Code)
require.Equal(t, "false", rec.Header().Get("nexus-request-retryable"))
var failure nexus.Failure
require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure))
require.Equal(t, "nexus endpoint not found", failure.Message)
}
func TestDispatchNexusTaskByEndpoint_NotFound_Retryable(t *testing.T) {
reg := nexustest.FakeEndpointRegistry{
OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) {
return nil, &retryableNotFoundError{msg: "endpoint temporarily unavailable"}
},
}
_, router := newTestNexusOperationHTTPHandler(reg, nil)
rec := doNexusHTTPRequest(t, router, "test-endpoint-id")
require.Equal(t, http.StatusNotFound, rec.Code)
require.Equal(t, "true", rec.Header().Get("nexus-request-retryable"))
var failure nexus.Failure
require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure))
require.Equal(t, "nexus endpoint not found", failure.Message)
}
func TestDispatchNexusTaskByEndpoint_NamespaceNotFound_Retryable(t *testing.T) {
endpointEntry := &persistencespb.NexusEndpointEntry{
Id: "test-endpoint-id",
Endpoint: &persistencespb.NexusEndpoint{
Spec: &persistencespb.NexusEndpointSpec{
Name: "test-endpoint",
Target: &persistencespb.NexusEndpointTarget{
Variant: &persistencespb.NexusEndpointTarget_Worker_{
Worker: &persistencespb.NexusEndpointTarget_Worker{
NamespaceId: "test-ns-id",
TaskQueue: "test-task-queue",
},
},
},
},
},
}
reg := nexustest.FakeEndpointRegistry{
OnGetByID: func(_ context.Context, _ string) (*persistencespb.NexusEndpointEntry, error) {
return endpointEntry, nil
},
}
nsReg := &fakeNamespaceRegistry{
getNamespaceName: func(id namespace.ID) (namespace.Name, error) {
return "", serviceerror.NewNamespaceNotFound("test-ns-id")
},
}
_, router := newTestNexusOperationHTTPHandler(reg, nsReg)
rec := doNexusHTTPRequest(t, router, "test-endpoint-id")
require.Equal(t, http.StatusNotFound, rec.Code)
require.Equal(t, "true", rec.Header().Get("nexus-request-retryable"))
var failure nexus.Failure
require.NoError(t, json.NewDecoder(rec.Body).Decode(&failure))
require.Equal(t, "invalid endpoint target", failure.Message)
}