Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@
OAuthAuthorizationTypeClientCredentials = "clientCredentials"
// OAuthAuthorizationTypePassword is the authorization type for password flow.
OAuthAuthorizationTypePassword = "password"

// JSON-RPC protocol versions
JsonRPC20 = "2.0"

// Application protocols
AppProtocolMCP = "mcp"
)

var (
Expand Down Expand Up @@ -666,6 +672,8 @@
ListenPort int `bson:"listen_port" json:"listen_port"`
Protocol string `bson:"protocol" json:"protocol"`
EnableProxyProtocol bool `bson:"enable_proxy_protocol" json:"enable_proxy_protocol"`
JsonRpcVersion string `bson:"json_rpc_version,omitempty" json:"json_rpc_version,omitempty"`
ApplicationProtocol string `bson:"application_protocol,omitempty" json:"application_protocol,omitempty"`
APIID string `bson:"api_id" json:"api_id"`
OrgID string `bson:"org_id" json:"org_id"`
UseKeylessAccess bool `bson:"use_keyless" json:"use_keyless"`
Expand Down Expand Up @@ -1430,6 +1438,22 @@
return a.Domain
}

// SetProtocol configures the transport and application protocol for the API.
func (a *APIDefinition) SetProtocol(transport, application string) {
a.JsonRpcVersion = transport
a.ApplicationProtocol = application

Check warning on line 1444 in apidef/api_definitions.go

View check run for this annotation

probelabs / Visor: architecture

architecture Issue

The `SetProtocol` function uses a generic `transport` parameter but assigns it to the specific `JsonRpcVersion` field. This creates a naming inconsistency that could be misleading if other transport protocols are introduced in the future, as the function would incorrectly populate a field named for JSON-RPC.
Raw output
To improve design consistency and future extensibility, either rename the function to be more specific (e.g., `SetJsonRpcApplicationProtocol`) or generalize the struct field to match the function's generic nature (e.g., rename `JsonRpcVersion` to `TransportProtocolVersion`).
}

// IsMCP returns true if this API uses the Model Context Protocol.
func (a *APIDefinition) IsMCP() bool {
return a.ApplicationProtocol == AppProtocolMCP
}

// MarkAsMCP configures the API definition as a Model Context Protocol (MCP) API.
func (a *APIDefinition) MarkAsMCP() {
a.SetProtocol(JsonRPC20, AppProtocolMCP)
}

// IsChildAPI returns true if this API is a child API in a versioning hierarchy.
// A child API is identified by having a BaseID that differs from its own APIID.
func (a *APIDefinition) IsChildAPI() bool {
Expand Down
220 changes: 220 additions & 0 deletions apidef/api_definitions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,226 @@ import (
"github.com/TykTechnologies/tyk/internal/service/gojsonschema"
)

func TestAPIDefinition_JsonRpcVersion(t *testing.T) {
t.Run("json-rpc version field marshaling", func(t *testing.T) {
api := APIDefinition{
JsonRpcVersion: "2.0",
}

data, err := json.Marshal(api)
assert.NoError(t, err)

var result APIDefinition
err = json.Unmarshal(data, &result)
assert.NoError(t, err)

assert.Equal(t, "2.0", result.JsonRpcVersion)
})

t.Run("empty json-rpc version omitted", func(t *testing.T) {
api := APIDefinition{
JsonRpcVersion: "",
}

data, err := json.Marshal(api)
assert.NoError(t, err)

assert.NotContains(t, string(data), "json_rpc_version")
})

t.Run("json-rpc version persists through encode/decode", func(t *testing.T) {
api := APIDefinition{
JsonRpcVersion: "2.0",
}

api.EncodeForDB()
api.DecodeFromDB()

assert.Equal(t, "2.0", api.JsonRpcVersion)
})
}

func TestAPIDefinition_ApplicationProtocol(t *testing.T) {
t.Run("application protocol field marshaling", func(t *testing.T) {
api := APIDefinition{
ApplicationProtocol: AppProtocolMCP,
}

data, err := json.Marshal(api)
assert.NoError(t, err)

var result APIDefinition
err = json.Unmarshal(data, &result)
assert.NoError(t, err)

assert.Equal(t, AppProtocolMCP, result.ApplicationProtocol)
})

t.Run("empty application protocol omitted", func(t *testing.T) {
api := APIDefinition{
ApplicationProtocol: "",
}

data, err := json.Marshal(api)
assert.NoError(t, err)

assert.NotContains(t, string(data), "application_protocol")
})

t.Run("application protocol persists through encode/decode", func(t *testing.T) {
api := APIDefinition{
ApplicationProtocol: AppProtocolMCP,
}

api.EncodeForDB()
api.DecodeFromDB()

assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
})

t.Run("supports custom protocols", func(t *testing.T) {
customProtocol := "a2a"
api := APIDefinition{
ApplicationProtocol: customProtocol,
}

data, err := json.Marshal(api)
assert.NoError(t, err)

var result APIDefinition
err = json.Unmarshal(data, &result)
assert.NoError(t, err)

assert.Equal(t, customProtocol, result.ApplicationProtocol)
})
}

func TestAPIDefinition_SetProtocol(t *testing.T) {
t.Run("sets transport and application protocol", func(t *testing.T) {
api := APIDefinition{}

api.SetProtocol(JsonRPC20, AppProtocolMCP)

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("IsMCP method returns correct value based on application protocol", func(t *testing.T) {
api := APIDefinition{}

api.SetProtocol(JsonRPC20, AppProtocolMCP)
assert.True(t, api.IsMCP())

api.SetProtocol(JsonRPC20, "a2a")
assert.False(t, api.IsMCP())

api.SetProtocol(JsonRPC20, AppProtocolMCP)
assert.True(t, api.IsMCP())
})

t.Run("overwrites existing values", func(t *testing.T) {
api := APIDefinition{
JsonRpcVersion: "1.0",
ApplicationProtocol: "old",
}

api.SetProtocol(JsonRPC20, AppProtocolMCP)

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("allows empty application protocol", func(t *testing.T) {
api := APIDefinition{}

api.SetProtocol(JsonRPC20, "")

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, "", api.ApplicationProtocol)
assert.False(t, api.IsMCP())
})

t.Run("allows empty transport protocol", func(t *testing.T) {
api := APIDefinition{}

api.SetProtocol("", "custom")

assert.Equal(t, "", api.JsonRpcVersion)
assert.Equal(t, "custom", api.ApplicationProtocol)
assert.False(t, api.IsMCP())
})
}

func TestAPIDefinition_MarkAsMCP(t *testing.T) {
t.Run("marks API as MCP", func(t *testing.T) {
api := APIDefinition{}

api.MarkAsMCP()

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("overwrites existing values", func(t *testing.T) {
api := APIDefinition{
JsonRpcVersion: "1.0",
ApplicationProtocol: "old",
}

api.MarkAsMCP()

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("is idempotent", func(t *testing.T) {
api := APIDefinition{}

api.MarkAsMCP()
api.MarkAsMCP()
api.MarkAsMCP()

assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("does not affect other fields", func(t *testing.T) {
api := APIDefinition{
Name: "test-api",
APIID: "123",
Slug: "test",
}

api.MarkAsMCP()

assert.Equal(t, "test-api", api.Name)
assert.Equal(t, "123", api.APIID)
assert.Equal(t, "test", api.Slug)
assert.Equal(t, JsonRPC20, api.JsonRpcVersion)
assert.Equal(t, AppProtocolMCP, api.ApplicationProtocol)
assert.True(t, api.IsMCP())
})

t.Run("uses SetProtocol internally", func(t *testing.T) {
api := APIDefinition{}

api.MarkAsMCP()

// Verify it's equivalent to calling SetProtocol with MCP constants
expected := APIDefinition{}
expected.SetProtocol(JsonRPC20, AppProtocolMCP)

assert.Equal(t, expected.JsonRpcVersion, api.JsonRpcVersion)
assert.Equal(t, expected.ApplicationProtocol, api.ApplicationProtocol)
assert.Equal(t, expected.IsMCP(), api.IsMCP())
})
}

func TestSchema(t *testing.T) {
schemaLoader := gojsonschema.NewBytesLoader([]byte(Schema))

Expand Down
94 changes: 52 additions & 42 deletions apidef/oas/linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,54 @@ import (
"github.com/TykTechnologies/tyk/internal/time"
)

// fixOperationsForValidation fixes operation fields to pass schema validation.
// This is needed because Fill() populates fields with random test data that may not
// conform to schema constraints (e.g., duration formats, enum values).
func fixOperationsForValidation(operations map[string]*Operation) {
for _, op := range operations {
if op.TransformRequestBody != nil {
op.TransformRequestBody.Format = "json"
}
if op.TransformResponseBody != nil {
op.TransformResponseBody.Format = "json"
}
if op.RateLimit != nil {
op.RateLimit.Per = ReadableDuration(time.Minute)
}
if op.URLRewrite != nil {
triggers := []*URLRewriteTrigger{}
for _, cond := range URLRewriteConditions {
trigger := &URLRewriteTrigger{
Condition: cond,
Rules: []*URLRewriteRule{},
}
for _, in := range URLRewriteInputs {
var rule URLRewriteRule
if in == InputRequestBody {
rule = URLRewriteRule{
In: in,
Pattern: ".*",
}
} else {
rule = URLRewriteRule{
In: in,
Name: "test",
Pattern: ".*",
}
}

trigger.Rules = append(trigger.Rules, &rule)
}
triggers = append(triggers, trigger)
}
op.URLRewrite.Triggers = triggers
}
if op.CircuitBreaker != nil {
op.CircuitBreaker.Threshold = 0.5
}
}
}

func TestXTykGateway_Lint(t *testing.T) {
var err error

Expand All @@ -25,45 +73,11 @@ func TestXTykGateway_Lint(t *testing.T) {
Fill(t, &securityScheme, 0)
{
settings.Middleware.Global.PluginConfig.Driver = "goplugin"
for _, op := range settings.Middleware.Operations {
if op.TransformRequestBody != nil {
op.TransformRequestBody.Format = "json"
}
if op.TransformResponseBody != nil {
op.TransformResponseBody.Format = "json"
}
if op.RateLimit != nil {
op.RateLimit.Per = ReadableDuration(time.Minute)
}
if op.URLRewrite != nil {
triggers := []*URLRewriteTrigger{}
for _, cond := range URLRewriteConditions {
trigger := &URLRewriteTrigger{
Condition: cond,
Rules: []*URLRewriteRule{},
}
for _, in := range URLRewriteInputs {
var rule URLRewriteRule
if in == InputRequestBody {
rule = URLRewriteRule{
In: in,
Pattern: ".*",
}
} else {
rule = URLRewriteRule{
In: in,
Name: "test",
Pattern: ".*",
}
}
fixOperationsForValidation(settings.Middleware.Operations)
fixOperationsForValidation(settings.Middleware.McpTools)
fixOperationsForValidation(settings.Middleware.McpResources)
fixOperationsForValidation(settings.Middleware.McpPrompts)

trigger.Rules = append(trigger.Rules, &rule)
}
triggers = append(triggers, trigger)
}
op.URLRewrite.Triggers = triggers
}
}
settings.Server.Authentication.BaseIdentityProvider = ""
settings.Server.Authentication.SecurityProcessingMode = SecurityProcessingModeLegacy
settings.Server.Authentication.Custom.Config.IDExtractor.Source = "body"
Expand All @@ -80,10 +94,6 @@ func TestXTykGateway_Lint(t *testing.T) {
settings.Server.EventHandlers[i].Webhook.CoolDownPeriod = ReadableDuration(time.Second * 20)
}

for idx := range settings.Middleware.Operations {
settings.Middleware.Operations[idx].CircuitBreaker.Threshold = 0.5
}

settings.Upstream.RateLimit.Per = ReadableDuration(10 * time.Second)
settings.Server.Authentication.CustomKeyLifetime.Value = ReadableDuration(10 * time.Second)

Expand Down
Loading
Loading