internal/mcpgateway/store_sql_test.go
Rama main · réplica de lectura del repositorio original.
package mcpgateway
import (
"context"
"testing"
"time"
"go.mongodb.org/mongo-driver/v2/mongo"
"github.com/enterpilot/gomodel/internal/storage/mongotest"
"github.com/enterpilot/gomodel/internal/storage/sqlx"
"github.com/enterpilot/gomodel/internal/storage/sqlx/sqlxtest"
"github.com/stretchr/testify/require"
)
// runStoreSuite exercises behaviour every Store implementation owes its
// callers, against each backend available in this environment.
func runStoreSuite(t *testing.T, body func(t *testing.T, store Store)) {
t.Helper()
sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) {
store, err := NewSQLStore(context.Background(), db)
require.NoError(t, err)
t.Cleanup(func() { _ = store.Close() })
body(t, store)
})
mongotest.Run(t, func(t *testing.T, db *mongo.Database) {
store, err := NewMongoDBStore(db)
require.NoError(t, err)
t.Cleanup(func() { _ = store.Close() })
body(t, store)
})
}
func TestStoreRoundTrip(t *testing.T) {
runStoreSuite(t, func(t *testing.T, store Store) {
ctx := context.Background()
server := ManagedServer{
Name: "github",
DisplayName: "GitHub MCP",
URL: "https://api.githubcopilot.com/mcp",
Transport: "http",
Headers: map[string]string{"Authorization": "Bearer secret"},
Description: "GitHub tools",
Enabled: true,
AllowedTools: []string{"create_issue"},
DisallowedTools: []string{"delete_repo"},
UserPaths: []string{"/team-a"},
DisallowedUserPaths: []string{"/team-a/contractors"},
ToolTimeoutSeconds: 45,
}
err := store.Upsert(ctx, server)
require.NoError(t, err)
got, err := store.Get(ctx, "github")
require.NoError(t, err)
require.Equal(t, server.URL, got.URL)
require.Equal(t, "http", got.Transport)
require.True(t, got.Enabled, "Get() = %+v, want round-tripped row", got)
require.Equal(t, "github", got.Name)
require.Equal(t, "GitHub MCP", got.DisplayName)
require.Equal(t, "Bearer secret", got.Headers["Authorization"], "Get().Headers = %v, want secret preserved", got.Headers)
require.Len(t, got.AllowedTools, 1)
require.Equal(t, "create_issue", got.AllowedTools[0])
require.Equal(t, 45, got.ToolTimeoutSeconds)
require.Equal(t, []string{"/team-a/contractors"}, got.DisallowedUserPaths)
require.False(t, got.CreatedAt.IsZero())
require.False(t, got.UpdatedAt.IsZero(), "Get() timestamps not stamped: %+v", got)
// Update preserves CreatedAt and bumps the row.
server.Description = "updated"
server.DisplayName = "GitHub 工具"
server.Enabled = false
err = store.Upsert(ctx, server)
require.NoError(t, err)
updated, err := store.Get(ctx, "github")
require.NoError(t, err)
require.Equal(t, "updated", updated.Description)
require.False(t, updated.Enabled, "Get(updated) = %+v, want updated row", updated)
require.Equal(t, "github", updated.Name)
require.Equal(t, "GitHub 工具", updated.DisplayName)
require.True(t, updated.CreatedAt.Equal(got.CreatedAt), "Get(updated).CreatedAt = %v, want original %v preserved", updated.CreatedAt, got.CreatedAt)
list, err := store.List(ctx)
require.NoError(t, err)
require.Len(t, list, 1)
err = store.Delete(ctx, "github")
require.NoError(t, err)
_, err = store.Get(ctx, "github")
require.ErrorIs(t, err, ErrNotFound)
err = store.Delete(ctx, "github")
require.ErrorIs(t, err, ErrNotFound)
})
}
// TestSQLStoreMigratesDisplayName starts from the pre-display_name table shape
// a long-lived deployment still has on disk.
func TestSQLStoreMigratesDisplayName(t *testing.T) {
sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) {
ctx := context.Background()
err := db.Schema(ctx, `
CREATE TABLE mcp_servers (
name TEXT PRIMARY KEY, url TEXT NOT NULL DEFAULT '', transport TEXT NOT NULL DEFAULT 'http',
headers TEXT NOT NULL DEFAULT '{}', description TEXT NOT NULL DEFAULT '',
enabled `+sqlx.TypeBool+` NOT NULL DEFAULT TRUE,
allowed_tools TEXT NOT NULL DEFAULT '[]', disallowed_tools TEXT NOT NULL DEFAULT '[]',
user_paths TEXT NOT NULL DEFAULT '[]', tool_timeout_seconds INTEGER NOT NULL DEFAULT 0,
created_at `+sqlx.TypeInt64+` NOT NULL, updated_at `+sqlx.TypeInt64+` NOT NULL
)`)
require.NoError(t, err)
_, err = db.Exec(ctx,
`INSERT INTO mcp_servers (name, created_at, updated_at) VALUES (?, ?, ?)`, "linear", 1, 1)
require.NoError(t, err)
store, err := NewSQLStore(ctx, db)
require.NoError(t, err)
server, err := store.Get(ctx, "linear")
require.NoError(t, err)
require.Equal(t, "linear", server.DisplayName)
require.Empty(t, server.DisallowedUserPaths)
})
}
func TestManagedServerValidateRejectsStdio(t *testing.T) {
t.Parallel()
server := ManagedServer{Name: "local", Transport: "stdio"}
require.Error(t, server.Validate())
}
func TestManagedServerValidateRequiresURL(t *testing.T) {
t.Parallel()
server := ManagedServer{Name: "web", Transport: "http"}
require.Error(t, server.Validate())
server.URL = "ftp://nope"
require.Error(t, server.Validate())
server.URL = "https://example.com/mcp"
err := server.Validate()
require.NoError(t, err)
}
func TestManagedServerValidateRejectsInvalidUserPath(t *testing.T) {
t.Parallel()
server := ManagedServer{
Name: "web",
URL: "https://example.com/mcp",
Transport: "http",
UserPaths: []string{"/team/../admin"},
}
require.Error(t, server.Validate())
}
func TestManagedServerSpecDefaultsTimeout(t *testing.T) {
t.Parallel()
spec := ManagedServer{Name: "web", URL: "https://example.com/mcp", Transport: "http"}.Spec()
require.Greater(t, spec.ToolTimeout, time.Duration(0))
require.False(t, spec.Managed)
}
func TestStoreUpdateRequiresExistingRow(t *testing.T) {
runStoreSuite(t, func(t *testing.T, store Store) {
ctx := context.Background()
server := ManagedServer{Name: "coding", URL: "https://old.example.com/mcp", Transport: "http", Enabled: true}
require.ErrorIs(t, store.Update(ctx, server), ErrNotFound, "update must not create a missing row")
_, err := store.Get(ctx, "coding")
require.ErrorIs(t, err, ErrNotFound)
require.NoError(t, store.Upsert(ctx, server))
created, err := store.Get(ctx, "coding")
require.NoError(t, err)
server.URL = "https://new.example.com/mcp"
server.UserPaths = []string{"/team"}
require.NoError(t, store.Update(ctx, server))
got, err := store.Get(ctx, "coding")
require.NoError(t, err)
require.Equal(t, "https://new.example.com/mcp", got.URL)
require.Equal(t, []string{"/team"}, got.UserPaths)
require.Equal(t, created.CreatedAt.Unix(), got.CreatedAt.Unix(), "update keeps created_at")
require.NoError(t, store.Delete(ctx, "coding"))
require.ErrorIs(t, store.Update(ctx, server), ErrNotFound, "an edit after a delete must not recreate the row")
})
}
func TestVirtualStoreRoundTrip(t *testing.T) {
runStoreSuite(t, func(t *testing.T, store Store) {
ctx := context.Background()
virtualStore, ok := store.(VirtualStore)
require.True(t, ok, "every store backend persists virtual servers")
virtual := ManagedVirtualServer{Name: "coding", Description: "code tools", Servers: []string{"github", "jira"}, ToolDiscovery: "search"}
require.ErrorIs(t, virtualStore.UpdateVirtual(ctx, virtual), ErrNotFound, "update must not create a missing row")
require.NoError(t, virtualStore.UpsertVirtual(ctx, virtual))
got, err := virtualStore.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.Equal(t, "code tools", got.Description)
require.Equal(t, []string{"github", "jira"}, got.Servers)
require.Equal(t, "search", got.ToolDiscovery)
require.False(t, got.CreatedAt.IsZero())
virtual.Servers = []string{"github"}
virtual.ToolDiscovery = ""
require.NoError(t, virtualStore.UpdateVirtual(ctx, virtual))
rows, err := virtualStore.ListVirtual(ctx)
require.NoError(t, err)
require.Len(t, rows, 1)
require.Equal(t, []string{"github"}, rows[0].Servers)
require.Empty(t, rows[0].ToolDiscovery)
require.Equal(t, got.CreatedAt.Unix(), rows[0].CreatedAt.Unix(), "update keeps created_at")
removed, err := virtualStore.DeleteVirtual(ctx, "coding")
require.NoError(t, err)
require.Equal(t, []string{"github"}, removed.Servers, "delete returns the row it removed")
require.Equal(t, got.Generation, removed.Generation)
_, err = virtualStore.DeleteVirtual(ctx, "coding")
require.ErrorIs(t, err, ErrNotFound)
_, err = virtualStore.GetVirtual(ctx, "coding")
require.ErrorIs(t, err, ErrNotFound)
})
}
// Every creation of a virtual server row gets its own generation, which an
// edit keeps, so a name deleted and recreated within one second (SQL stores
// created_at in whole seconds) still tells its two incarnations apart.
func TestVirtualStoreGenerationIdentifiesEachCreation(t *testing.T) {
runStoreSuite(t, func(t *testing.T, store Store) {
ctx := context.Background()
virtualStore, ok := store.(VirtualStore)
require.True(t, ok)
created := time.Now().UTC().Truncate(time.Second)
virtual := ManagedVirtualServer{Name: "coding", Servers: []string{"github"}, CreatedAt: created}
require.NoError(t, virtualStore.UpsertVirtual(ctx, virtual))
first, err := virtualStore.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.NotEmpty(t, first.Generation)
virtual.Servers = []string{"github", "jira"}
require.NoError(t, virtualStore.UpsertVirtual(ctx, virtual))
require.NoError(t, virtualStore.UpdateVirtual(ctx, virtual))
edited, err := virtualStore.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.Equal(t, first.Generation, edited.Generation, "edits keep the generation")
_, err = virtualStore.DeleteVirtual(ctx, "coding")
require.NoError(t, err)
require.NoError(t, virtualStore.UpsertVirtual(ctx, virtual))
recreated, err := virtualStore.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.Equal(t, first.CreatedAt.Unix(), recreated.CreatedAt.Unix(), "created in the same second")
require.NotEqual(t, first.Generation, recreated.Generation)
require.NotEqual(t, first.Spec().Incarnation, recreated.Spec().Incarnation)
})
}
// A virtual server row from before generations gets the column with an
// empty value, and is identified by its creation time.
func TestSQLStoreMigratesVirtualGeneration(t *testing.T) {
sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) {
ctx := context.Background()
err := db.Schema(ctx, `
CREATE TABLE mcp_virtual_servers (
name TEXT PRIMARY KEY, description TEXT NOT NULL DEFAULT '',
servers TEXT NOT NULL DEFAULT '[]', tool_discovery TEXT NOT NULL DEFAULT '',
created_at `+sqlx.TypeInt64+` NOT NULL, updated_at `+sqlx.TypeInt64+` NOT NULL
)`)
require.NoError(t, err)
_, err = db.Exec(ctx,
`INSERT INTO mcp_virtual_servers (name, servers, created_at, updated_at) VALUES (?, ?, ?, ?)`, "coding", `["github"]`, 1700000000, 1700000000)
require.NoError(t, err)
store, err := NewSQLStore(ctx, db)
require.NoError(t, err)
legacy, err := store.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.Empty(t, legacy.Generation)
require.NotEmpty(t, legacy.Spec().Incarnation)
removed, err := store.DeleteVirtual(ctx, "coding")
require.NoError(t, err)
require.Equal(t, legacy.Spec().Incarnation, removed.Spec().Incarnation)
require.NoError(t, store.UpsertVirtual(ctx, ManagedVirtualServer{Name: "coding", Servers: []string{"github"}, CreatedAt: legacy.CreatedAt}))
recreated, err := store.GetVirtual(ctx, "coding")
require.NoError(t, err)
require.NotEqual(t, legacy.Spec().Incarnation, recreated.Spec().Incarnation)
})
}