Files
upstream-ory-kratos/courier/test/persistence.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

374 lines
13 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package test
import (
"context"
"errors"
"fmt"
"slices"
"testing"
"time"
"github.com/go-faker/faker/v4"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/courier"
"github.com/ory/kratos/x"
"github.com/ory/pop/v6"
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
"github.com/ory/x/sqlcon"
)
type PersisterWrapper interface {
GetConnection(ctx context.Context) *pop.Connection
NetworkID(ctx context.Context) uuid.UUID
courier.Persister
}
type NetworkWrapper func(t *testing.T, ctx context.Context) (uuid.UUID, PersisterWrapper)
func TestPersister(ctx context.Context, newNetworkUnlessExisting NetworkWrapper, newNetwork NetworkWrapper) func(t *testing.T) {
return func(t *testing.T) {
nid, p := newNetworkUnlessExisting(t, ctx)
// backdateMessage moves a message's creation time into the past so that
// it clears the CockroachDB anti-contention grace period in
// NextMessages, which defers messages enqueued within the last second
// to a later poll.
backdateMessage := func(t *testing.T, id uuid.UUID) {
createdAt := time.Now().UTC().Add(-2 * time.Second)
require.NoError(t, p.GetConnection(ctx).RawQuery(
"UPDATE courier_messages SET created_at = ?, updated_at = ? WHERE id = ? AND nid = ?",
createdAt, createdAt, id, nid).Exec())
}
t.Run("case=no messages in queue", func(t *testing.T) {
m, err := p.NextMessages(ctx, 10)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
assert.Len(t, m, 0)
_, err = p.LatestQueuedMessage(ctx)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
})
messages := make([]courier.Message, 5)
t.Run("case=add messages to the queue", func(t *testing.T) {
now := time.Now().UTC()
for k := range messages {
require.NoError(t, faker.FakeData(&messages[k]))
require.NoError(t, p.AddMessage(ctx, &messages[k]))
// The timestamps are in the past (preserving the insertion
// order) so that the messages clear the CockroachDB
// anti-contention grace period in NextMessages.
createdAt := now.Add(time.Duration(k)*time.Hour - 25*time.Hour).Round(time.Second)
require.NoError(t, p.GetConnection(ctx).
RawQuery(
"UPDATE courier_messages SET created_at = ?, updated_at = ? WHERE id = ? AND nid = ?",
createdAt,
createdAt,
messages[k].ID, nid).
Exec())
}
})
t.Run("case=latest message in queue", func(t *testing.T) {
expected, err := p.LatestQueuedMessage(ctx)
require.NoError(t, err)
actual := messages[len(messages)-1]
assert.Equal(t, expected.ID, actual.ID)
assert.Equal(t, expected.Subject, actual.Subject)
})
t.Run("case=pull messages from the queue", func(t *testing.T) {
for k, expected := range messages {
expected.Status = courier.MessageStatusProcessing
t.Run(fmt.Sprintf("message=%d", k), func(t *testing.T) {
messages, err := p.NextMessages(ctx, 1)
require.NoError(t, err)
require.Len(t, messages, 1)
actual := messages[0]
assert.Equal(t, expected.ID, actual.ID)
assert.Equal(t, expected.Subject, actual.Subject)
assert.Equal(t, expected.Body, actual.Body)
assert.Equal(t, expected.Status, actual.Status)
assert.Equal(t, expected.Type, actual.Type)
assert.Equal(t, expected.Recipient, actual.Recipient)
})
}
_, err := p.NextMessages(ctx, 10)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
})
t.Run("case=setting message status", func(t *testing.T) {
require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusQueued))
ms, err := p.NextMessages(ctx, 1)
require.NoError(t, err)
require.Len(t, ms, 1)
assert.Equal(t, messages[0].ID, ms[0].ID)
require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusSent))
_, err = p.NextMessages(ctx, 1)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusAbandoned))
_, err = p.NextMessages(ctx, 1)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
})
t.Run("case=incrementing send count", func(t *testing.T) {
originalSendCount := messages[0].SendCount
require.NoError(t, p.SetMessageStatus(ctx, messages[0].ID, courier.MessageStatusQueued))
require.NoError(t, p.IncrementMessageSendCount(ctx, messages[0].ID))
ms, err := p.NextMessages(ctx, 1)
require.NoError(t, err)
require.Len(t, ms, 1)
assert.Equal(t, messages[0].ID, ms[0].ID)
assert.Equal(t, originalSendCount+1, ms[0].SendCount)
})
t.Run("case=list messages", func(t *testing.T) {
// List by status.
{
status := courier.MessageStatusProcessing
filter := courier.ListCourierMessagesParameters{
Status: &status,
}
ms, _, err := p.ListMessages(ctx, filter, []keysetpagination.Option{})
require.NoError(t, err)
require.Len(t, ms, len(messages), messages)
// Check that the 'filter by status' works.
for _, m := range ms {
require.Equal(t, status, m.Status)
}
// Check that the 'order by created_at desc' works.
require.True(t, slices.IsSortedFunc(ms, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
}
// Query fewer items than the total, multiple times.
{
filter := courier.ListCourierMessagesParameters{}
maxSize := 2
ms1, pagination, err := p.ListMessages(ctx, filter, []keysetpagination.Option{
keysetpagination.WithSize(2),
})
require.NoError(t, err)
require.NotNil(t, pagination)
require.False(t, pagination.IsLast())
require.Len(t, ms1, maxSize)
// Check that the 'order by created_at desc' works.
require.True(t, slices.IsSortedFunc(ms1, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
// Second call.
// Marshal -> unmarshal the pagination token to be more realistic.
encrypted := pagination.PageToken().Encrypt(nil)
unmarshalled, err := keysetpagination.ParsePageToken(nil, encrypted)
require.NoError(t, err)
ms2, pagination, err := p.ListMessages(ctx, filter,
[]keysetpagination.Option{
keysetpagination.WithSize(2),
keysetpagination.WithToken(unmarshalled),
})
require.NoError(t, err)
require.NotNil(t, pagination)
require.False(t, pagination.IsLast())
require.Len(t, ms2, maxSize)
// Check that the 'order by created_at desc' works.
require.True(t, slices.IsSortedFunc(ms2, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
// Check that the second call returned different elements.
require.NotEqual(t, ms1[0].ID, ms2[0].ID)
require.NotEqual(t, ms1[1].ID, ms2[1].ID)
allElements := append(ms1, ms2...)
require.True(t, slices.IsSortedFunc(allElements, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
// Last call
ms3, pagination, err := p.ListMessages(ctx, filter, pagination.ToOptions())
require.NoError(t, err)
require.NotNil(t, pagination)
require.True(t, pagination.IsLast())
require.Len(t, ms3, 1)
// Check that the 'order by created_at desc' works.
require.True(t, slices.IsSortedFunc(ms3, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
// Check that the third call returned different elements.
require.NotEqual(t, ms2[0].ID, ms3[0].ID)
allElements = append(ms1, ms2...)
allElements = append(allElements, ms3...)
require.True(t, slices.IsSortedFunc(allElements, func(a, b courier.Message) int { return b.CreatedAt.Compare(a.CreatedAt) }))
}
t.Run("on another network", func(t *testing.T) {
nid1, p1 := newNetwork(t, ctx)
status := courier.MessageStatusProcessing
filter := courier.ListCourierMessagesParameters{
Status: &status,
}
ms, _, err := p1.ListMessages(ctx, filter, []keysetpagination.Option{})
require.NoError(t, err)
require.Len(t, ms, 0)
// Due to a bug in the pagination query definition, it was possible to retrieve messages from another `network`
// using the pagination query. That required that 2 message's `created_at` timestamps were equal, to trigger
// the `OR` clause of the paginated query.
// This part of the tests "simulates" this behavior, by forcing the same timestamps on multiple messages across
// different networks.
nid2, p2 := newNetwork(t, ctx)
msg1 := courier.Message{
ID: uuid.FromStringOrNil("10000000-0000-0000-0000-000000000000"),
NID: nid1,
Status: courier.MessageStatusProcessing,
}
err = p1.GetConnection(ctx).Create(&msg1)
require.NoError(t, err)
msg2 := courier.Message{
ID: uuid.FromStringOrNil("20000000-0000-0000-0000-000000000000"),
NID: nid1,
Status: courier.MessageStatusProcessing,
}
err = p1.GetConnection(ctx).Create(&msg2)
require.NoError(t, err)
msg3 := courier.Message{
ID: uuid.FromStringOrNil("30000000-0000-0000-0000-000000000000"),
NID: nid2,
Status: courier.MessageStatusProcessing,
}
err = p2.GetConnection(ctx).Create(&msg3)
require.NoError(t, err)
now := time.Now().UTC().Truncate(time.Second)
// Set all `created_at` timestamps to the same value to force the `OR` clause of the paginated query.
// `created_at` is set by "pop" and does not allow a manual override, apart from using `pop.SetNowFunc`, but that also influences the other tests in this
// suite, as it just overrides a global function.
require.NoError(t, p1.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg1.ID, nid1).Exec())
// get the "updated" message from the
require.NoError(t, p1.GetConnection(ctx).Where("id = ? AND nid = ?", msg1.ID, msg1.NID).First(&msg1))
require.NoError(t, p1.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg2.ID, nid1).Exec())
require.NoError(t, p2.GetConnection(ctx).RawQuery("UPDATE courier_messages SET created_at = ? WHERE id = ? AND nid = ?", now, msg3.ID, nid2).Exec())
// Use the updated first message's PageToken as the basis for the paginated request.
ms, _, err = p1.ListMessages(ctx, filter, []keysetpagination.Option{keysetpagination.WithToken(msg1.PageToken())})
require.NoError(t, err)
// The response should just contain messages from network1, and not from network2.
require.Len(t, ms, 1)
require.Equal(t, ms[0].NID, nid1)
require.Equal(t, ms[0].ID, msg2.ID)
})
})
t.Run("case=network", func(t *testing.T) {
t.Run("generates id on creation", func(t *testing.T) {
expected := courier.Message{ID: uuid.Nil}
require.NoError(t, p.AddMessage(ctx, &expected))
backdateMessage(t, expected.ID)
assert.NotEqual(t, uuid.Nil, expected.ID)
assert.EqualValues(t, nid, expected.NID)
assert.EqualValues(t, nid, p.NetworkID(ctx))
actual, err := p.LatestQueuedMessage(ctx)
require.NoError(t, err)
assert.EqualValues(t, expected.ID, actual.ID)
assert.EqualValues(t, nid, actual.NID)
actuals, err := p.NextMessages(ctx, 255)
require.NoError(t, err)
actual = &actuals[0]
assert.EqualValues(t, expected.ID, actual.ID)
assert.EqualValues(t, nid, actual.NID)
})
id := x.NewUUID()
t.Run("persists id on creation", func(t *testing.T) {
expected := courier.Message{ID: id}
require.NoError(t, p.AddMessage(ctx, &expected))
backdateMessage(t, expected.ID)
assert.EqualValues(t, id, expected.ID)
assert.EqualValues(t, nid, expected.NID)
assert.EqualValues(t, nid, p.NetworkID(ctx))
actual, err := p.LatestQueuedMessage(ctx)
require.NoError(t, err)
assert.EqualValues(t, id, actual.ID)
assert.EqualValues(t, nid, actual.NID)
actuals, err := p.NextMessages(ctx, 255)
require.NoError(t, err)
actual = &actuals[0]
assert.EqualValues(t, id, actual.ID)
assert.EqualValues(t, nid, actual.NID)
})
t.Run("can not get on another network", func(t *testing.T) {
_, p := newNetwork(t, ctx)
_, err := p.LatestQueuedMessage(ctx)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
_, err = p.NextMessages(ctx, 255)
require.ErrorIs(t, err, courier.ErrQueueEmpty)
})
t.Run("can not update on another network", func(t *testing.T) {
_, p := newNetwork(t, ctx)
err := p.SetMessageStatus(ctx, id, courier.MessageStatusProcessing)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
t.Run("case=FetchMessage", func(t *testing.T) {
msgID := messages[0].ID
message, err := p.FetchMessage(ctx, msgID)
require.NoError(t, err)
require.Equal(t, msgID, message.ID)
t.Run("can not get on another network", func(t *testing.T) {
_, p := newNetwork(t, ctx)
_, err := p.FetchMessage(ctx, msgID)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
t.Run("case=RecordDispatch", func(t *testing.T) {
msgID := messages[0].ID
err := p.RecordDispatch(ctx, msgID, courier.CourierMessageDispatchStatusFailed, errors.New("testerror"))
require.NoError(t, err)
message, err := p.FetchMessage(ctx, msgID)
require.NoError(t, err)
require.Len(t, message.Dispatches, 1)
assert.Equal(t, "testerror", gjson.GetBytes(message.Dispatches[0].Error, "message").String())
t.Run("can not get on another network", func(t *testing.T) {
_, p := newNetwork(t, ctx)
_, err := p.FetchMessage(ctx, msgID)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
}
}