Files

549 lines
16 KiB
Go

package storage
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
"github.com/hanzo-fi/go-libs/v5/pkg/query"
"github.com/hanzo-fi/go-libs/v5/pkg/storage/bun/paginate"
"github.com/hanzo-fi/go-libs/v5/pkg/types/time"
"github.com/hanzo-fi/payments/pkg/domain/models"
"github.com/hanzo-fi/payments/pkg/events"
"github.com/gibson042/canonicaljson-go"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
now = time.Now()
defaultConnector = models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "default",
},
Name: "default",
CreatedAt: now.Add(-60 * time.Minute).UTC().Time,
Provider: "default",
},
Config: []byte(`{}`),
}
defaultConnector2 = models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "default2",
},
Name: "default2",
CreatedAt: now.Add(-45 * time.Minute).UTC().Time,
Provider: "default2",
},
Config: []byte(`{}`),
}
defaultConnector3 = models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "default",
},
Name: "default3",
CreatedAt: now.Add(-30 * time.Minute).UTC().Time,
Provider: "default",
},
Config: []byte(`{}`),
}
)
func upsertConnector(t *testing.T, ctx context.Context, storage Storage, connector models.Connector) {
require.NoError(t, storage.ConnectorsInstall(ctx, connector, nil))
}
// withoutUpdatedAt zeroes the UpdatedAt field before structural comparison.
// The updated_at column is set by a DB trigger on every UPDATE (including the
// internal config-write during install), so its exact value is non-deterministic
// and should not be asserted in tests that focus on other fields.
func withoutUpdatedAt(c models.Connector) models.Connector {
c.UpdatedAt = nil
return c
}
func TestConnectorsInstall(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertConnector(t, ctx, store, defaultConnector2)
t.Run("same id upsert", func(t *testing.T) {
c := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: defaultConnector.ID,
Name: "test changed",
CreatedAt: time.Now().UTC().Time,
Provider: "test",
},
Config: []byte(`{}`),
}
require.NoError(t, store.ConnectorsInstall(ctx, c, nil))
connector, err := store.ConnectorsGet(ctx, c.ID)
require.NoError(t, err)
require.NotNil(t, connector)
require.Equal(t, defaultConnector, withoutUpdatedAt(*connector))
})
t.Run("unique same upsert", func(t *testing.T) {
c := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "test",
},
Name: "default",
CreatedAt: now.Add(-23 * time.Minute).UTC().Time,
Provider: "test",
},
Config: []byte(`{}`),
}
require.Error(t, store.ConnectorsInstall(ctx, c, nil))
})
t.Run("outbox event created for connector reset", func(t *testing.T) {
// Create an old connector that will be "reset"
oldConnector := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "reset-test",
},
Name: "old connector",
CreatedAt: now.Add(-60 * time.Minute).UTC().Time,
Provider: "reset-test",
},
Config: []byte(`{}`),
}
upsertConnector(t, ctx, store, oldConnector)
// Create a new connector (simulating reset)
newConnector := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "reset-test",
},
Name: "new connector",
CreatedAt: now.Add(-1 * time.Minute).UTC().Time,
Provider: "reset-test",
},
Config: []byte(`{}`),
}
// Install with oldConnectorID (simulating reset)
require.NoError(t, store.ConnectorsInstall(ctx, newConnector, &oldConnector.ID))
// Verify outbox event was created
pendingEvents, err := store.OutboxEventsPollPending(ctx, 100)
require.NoError(t, err)
// Find our event
var ourEvent *models.OutboxEvent
for i := range pendingEvents {
if pendingEvents[i].EventType == events.EventTypeConnectorReset &&
pendingEvents[i].EntityID == oldConnector.ID.String() {
ourEvent = &pendingEvents[i]
break
}
}
require.NotNil(t, ourEvent, "expected outbox event for connector reset")
// Verify event details
assert.Equal(t, events.EventTypeConnectorReset, ourEvent.EventType)
assert.Equal(t, models.OUTBOX_STATUS_PENDING, ourEvent.Status)
assert.Equal(t, oldConnector.ID.String(), ourEvent.EntityID)
assert.Equal(t, newConnector.ID, *ourEvent.ConnectorID)
assert.Equal(t, 0, ourEvent.RetryCount)
// Verify payload
var payload map[string]interface{}
err = json.Unmarshal(ourEvent.Payload, &payload)
require.NoError(t, err)
assert.Equal(t, oldConnector.ID.String(), payload["connectorID"])
assert.NotNil(t, payload["createdAt"])
// Verify idempotency key format
expectedKeyPrefix := fmt.Sprintf("%s-", oldConnector.ID.String())
assert.Contains(t, ourEvent.ID.EventIdempotencyKey, expectedKeyPrefix)
})
t.Run("no outbox event for normal install", func(t *testing.T) {
// Count events before
eventsBefore, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countBefore := len(eventsBefore)
// Create a new connector without oldConnectorID (normal install)
normalConnector := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "normal-test",
},
Name: "normal connector",
CreatedAt: now.Add(-1 * time.Minute).UTC().Time,
Provider: "normal-test",
},
Config: []byte(`{}`),
}
require.NoError(t, store.ConnectorsInstall(ctx, normalConnector, nil))
// Verify no outbox event was created
eventsAfter, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countAfter := len(eventsAfter)
// Should have same number of events (no new reset event)
assert.Equal(t, countBefore, countAfter, "normal install should not create reset event")
})
}
func TestConnectorsUninstall(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertConnector(t, ctx, store, defaultConnector2)
t.Run("uninstall default connector", func(t *testing.T) {
require.NoError(t, store.ConnectorsUninstall(ctx, defaultConnector.ID))
connector, err := store.ConnectorsGet(ctx, defaultConnector.ID)
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
require.Nil(t, connector)
})
t.Run("uninstall unknown connector", func(t *testing.T) {
id := models.ConnectorID{
Reference: uuid.New(),
Provider: "unknown",
}
require.NoError(t, store.ConnectorsUninstall(ctx, id))
})
}
func TestConnectorsConfigUpdate(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertConnector(t, ctx, store, defaultConnector2)
t.Run("same id upsert", func(t *testing.T) {
config := json.RawMessage(`{"val":"new"}`)
c := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: defaultConnector.ID,
Name: "new name",
},
Config: config,
}
require.NoError(t, store.ConnectorsConfigUpdate(ctx, c))
connector, err := store.ConnectorsGet(ctx, c.ID)
require.NoError(t, err)
require.NotNil(t, connector)
assert.Equal(t, c.Name, connector.Name)
assert.Equal(t, defaultConnector.CreatedAt, connector.CreatedAt)
assert.Equal(t, defaultConnector.Provider, connector.Provider)
assert.Equal(t, defaultConnector.ScheduledForDeletion, connector.ScheduledForDeletion)
expectedData, err := canonicaljson.Marshal(config)
require.NoError(t, err)
data, err := canonicaljson.Marshal(connector.Config)
require.NoError(t, err)
assert.Equal(t, string(expectedData), string(data))
})
t.Run("connector doesn't exist yet", func(t *testing.T) {
c := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: models.ConnectorID{
Reference: uuid.New(),
Provider: "test",
},
},
Config: []byte(`{}`),
}
require.Error(t, store.ConnectorsConfigUpdate(ctx, c))
})
}
func TestConnectorsScheduleForDeletion(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
t.Run("schedule for deletion of unknown connector", func(t *testing.T) {
id := models.ConnectorID{
Reference: uuid.New(),
Provider: "unknown",
}
require.NoError(t, store.ConnectorsScheduleForDeletion(ctx, id))
})
t.Run("schedule for deletion of default connector", func(t *testing.T) {
require.NoError(t, store.ConnectorsScheduleForDeletion(ctx, defaultConnector.ID))
connector, err := store.ConnectorsGet(ctx, defaultConnector.ID)
require.NoError(t, err)
require.NotNil(t, connector)
require.True(t, connector.ScheduledForDeletion)
})
}
func TestConnectorsGet(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertConnector(t, ctx, store, defaultConnector2)
t.Run("get connector", func(t *testing.T) {
connector, err := store.ConnectorsGet(ctx, defaultConnector.ID)
require.NoError(t, err)
require.NotNil(t, connector)
require.Equal(t, defaultConnector, withoutUpdatedAt(*connector))
})
t.Run("get unknown connector", func(t *testing.T) {
id := models.ConnectorID{
Reference: uuid.New(),
Provider: "unknown",
}
connector, err := store.ConnectorsGet(ctx, id)
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
require.Nil(t, connector)
})
}
func TestConnectorsList(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertConnector(t, ctx, store, defaultConnector2)
upsertConnector(t, ctx, store, defaultConnector3)
t.Run("list connectors by name", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("name", "default")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector, withoutUpdatedAt(cursor.Data[0]))
})
t.Run("list connectors by unknown name", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("name", "unknown")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Empty(t, cursor.Data)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
})
t.Run("list connectors by provider", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("provider", "default")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 2)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector3, withoutUpdatedAt(cursor.Data[0]))
require.Equal(t, defaultConnector, withoutUpdatedAt(cursor.Data[1]))
})
t.Run("list connectors by provider uppercase", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("provider", "DEFAULT")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 2)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector3, withoutUpdatedAt(cursor.Data[0]))
require.Equal(t, defaultConnector, withoutUpdatedAt(cursor.Data[1]))
})
t.Run("list connectors by provider with wrong type", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("provider", 1)),
)
cursor, err := store.ConnectorsList(ctx, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list connectors by unknown provider", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("provider", "unknown")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Empty(t, cursor.Data)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
})
t.Run("list connectors by id", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("id", defaultConnector3.ID.String())),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector3, withoutUpdatedAt(cursor.Data[0]))
})
t.Run("list connectors by unknown id", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("id", "unknown")),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Empty(t, cursor.Data)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.Empty(t, cursor.Previous)
})
t.Run("list connectors test cursor", func(t *testing.T) {
q := NewListConnectorsQuery(
paginate.NewPaginatedQueryOptions(ConnectorQuery{}).
WithPageSize(1),
)
cursor, err := store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector3, withoutUpdatedAt(cursor.Data[0]))
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Next)
require.NotEmpty(t, cursor.Previous)
require.Equal(t, defaultConnector2, withoutUpdatedAt(cursor.Data[0]))
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.Empty(t, cursor.Next)
require.NotEmpty(t, cursor.Previous)
require.Equal(t, defaultConnector, withoutUpdatedAt(cursor.Data[0]))
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Next)
require.NotEmpty(t, cursor.Previous)
require.Equal(t, defaultConnector2, withoutUpdatedAt(cursor.Data[0]))
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.ConnectorsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Next)
require.Empty(t, cursor.Previous)
require.Equal(t, defaultConnector3, withoutUpdatedAt(cursor.Data[0]))
})
}