Ctrl K
ComunidadGuías…

internal/mcpgateway/store_sql_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	})
}