package mcpgateway import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/enterpilot/gomodel/internal/encryption" "github.com/enterpilot/gomodel/internal/encryption/encryptiontest" ) func testOAuthRecord() oauthRecord { return oauthRecord{ ServerURL: "https://mcp.example.com/mcp", Resource: "https://mcp.example.com/mcp", Issuer: "https://auth.example.com", TokenURL: "https://auth.example.com/token", RedirectURI: "https://gomodel.example.com/admin/mcp-oauth/callback", ClientID: "client-1", ClientSecret: "client-secret", AccessToken: "access-secret", RefreshToken: "refresh-secret", Expiry: time.Now().Add(time.Hour).UTC().Truncate(time.Second), SignedInAt: time.Now().UTC().Truncate(time.Second), } } func TestOAuthStoreSealsRecords(t *testing.T) { runStoreSuite(t, func(t *testing.T, raw Store) { ctx := context.Background() box, _ := encryptiontest.NewBox(t) store := newOAuthStore(raw, box) require.NotNil(t, store) record := testOAuthRecord() require.NoError(t, store.put(ctx, "github", record)) rows := raw.(oauthRowStore) stored, err := rows.getOAuth(ctx, "github") require.NoError(t, err) assert.True(t, encryption.IsSealed(stored)) for _, secret := range []string{"access-secret", "refresh-secret", "client-secret"} { assert.NotContains(t, stored, secret) } got, err := store.get(ctx, "github") require.NoError(t, err) assert.Equal(t, record.AccessToken, got.AccessToken) assert.Equal(t, record.RefreshToken, got.RefreshToken) assert.True(t, record.Expiry.Equal(got.Expiry)) // A sealed record moved to another server does not open. require.NoError(t, rows.putOAuth(ctx, "gitlab", stored)) _, err = store.get(ctx, "gitlab") require.Error(t, err) listed, err := store.list(ctx) require.Error(t, err, "the moved record is reported") assert.Contains(t, listed, "github", "one bad record does not hide the others") assert.NotContains(t, listed, "gitlab") require.NoError(t, store.delete(ctx, "gitlab")) require.NoError(t, store.delete(ctx, "github")) _, err = store.get(ctx, "github") require.ErrorIs(t, err, ErrNotFound) }) } func TestOAuthStoreReencryptsPlaintext(t *testing.T) { runStoreSuite(t, func(t *testing.T, raw Store) { ctx := context.Background() plain := newOAuthStore(raw, nil) require.NoError(t, plain.put(ctx, "github", testOAuthRecord())) box, _ := encryptiontest.NewBox(t) store := newOAuthStore(raw, box) report, err := store.reencrypt(ctx) require.NoError(t, err) assert.Equal(t, 1, report.Reencrypted) stored, err := raw.(oauthRowStore).getOAuth(ctx, "github") require.NoError(t, err) assert.True(t, encryption.IsSealed(stored)) got, err := store.get(ctx, "github") require.NoError(t, err) assert.Equal(t, "access-secret", got.AccessToken) report, err = store.reencrypt(ctx) require.NoError(t, err) assert.Equal(t, 0, report.Reencrypted, "a second pass rewrites nothing") }) } // resealingRows re-seals the stored row right before the first swap, the way // a concurrent data key rotation does. type resealingRows struct { oauthRowStore reseal func() error swaps int } func (r *resealingRows) swapOAuth(ctx context.Context, name, current, next string) (bool, error) { r.swaps++ if r.swaps == 1 { if err := r.reseal(); err != nil { return false, err } } return r.oauthRowStore.swapOAuth(ctx, name, current, next) } func TestOAuthStoreUpdateRetriesAfterReseal(t *testing.T) { runStoreSuite(t, func(t *testing.T, raw Store) { ctx := context.Background() box, _ := encryptiontest.NewBox(t) plain := newOAuthStore(raw, box) record := testOAuthRecord() require.NoError(t, plain.put(ctx, "github", record)) rows := &resealingRows{oauthRowStore: raw.(oauthRowStore)} rows.reseal = func() error { return plain.put(ctx, "github", record) } store := &oauthStore{rows: rows, box: box} refreshed := record refreshed.RefreshToken = "rotated" saved, err := store.update(ctx, "github", refreshed, func(stored oauthRecord) bool { return stored.SignedInAt.Equal(record.SignedInAt) }) require.NoError(t, err) assert.True(t, saved, "a re-sealed row with the same sign-in is written on retry") assert.Equal(t, 2, rows.swaps) got, err := store.get(ctx, "github") require.NoError(t, err) assert.Equal(t, "rotated", got.RefreshToken) }) }