Files
upstream-ory-kratos/persistence/sql/migratest/migration_test.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

473 lines
13 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package migratest
import (
"context"
"encoding/json"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/bradleyjkemp/cupaloy/v2"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/persistence/sql"
gomigrations "github.com/ory/kratos/persistence/sql/migrations/go"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verification"
"github.com/ory/kratos/selfservice/strategy/code"
"github.com/ory/kratos/selfservice/strategy/link"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
"github.com/ory/pop/v6"
"github.com/ory/x/configx"
"github.com/ory/x/dbal"
"github.com/ory/x/fsx"
"github.com/ory/x/logrusx"
"github.com/ory/x/migratest"
"github.com/ory/x/networkx"
"github.com/ory/x/pagination/keysetpagination"
"github.com/ory/x/popx"
"github.com/ory/x/sqlcon"
"github.com/ory/x/sqlcon/dockertest"
)
func snapshotFor(paths ...string) *cupaloy.Config {
return cupaloy.New(
cupaloy.CreateNewAutomatically(true),
cupaloy.FailOnUpdate(true),
cupaloy.SnapshotFileExtension(".json"),
cupaloy.SnapshotSubdirectory(filepath.Join(paths...)),
)
}
func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id string) {
s := snapshotFor("fixtures", prefix)
actualJSON, err := json.MarshalIndent(actual, "", " ")
require.NoError(t, err)
err = s.SnapshotWithName(id, actualJSON)
assert.NoErrorf(t, err, "actual = %s", string(actualJSON))
}
func TestMigrations_SQLite(t *testing.T) {
t.Parallel()
sqlite, err := pop.NewConnection(&pop.ConnectionDetails{
URL: dbal.NewSQLiteTestDatabase(t),
})
require.NoError(t, err)
require.NoError(t, sqlite.Open())
testDatabase(t, "sqlite", sqlite)
}
func TestMigrations_Postgres(t *testing.T) {
if testing.Short() {
t.Skip("skipping testing in short mode")
}
t.Parallel()
testDatabase(t, "postgres", dockertest.ConnectPop(t, dockertest.RunTestPostgreSQLWithVersion(t, "16")))
}
func TestMigrations_Mysql(t *testing.T) {
if testing.Short() {
t.Skip("skipping testing in short mode")
}
t.Parallel()
testDatabase(t, "mysql", dockertest.ConnectPop(t, dockertest.RunTestMySQLWithVersion(t, "8.4")))
}
func TestMigrations_Cockroach(t *testing.T) {
if testing.Short() {
t.Skip("skipping testing in short mode")
}
t.Parallel()
testDatabase(t, "cockroach", dockertest.ConnectPop(t, dockertest.RunTestCockroachDBWithVersion(t, "latest-v25.4")))
}
func testDatabase(t *testing.T, db string, c *pop.Connection) {
ctx := context.Background()
l := logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel))
url := c.URL()
// workaround for https://github.com/ory/pop/issues/538
switch db {
case "mysql":
url = "mysql://" + url
case "sqlite":
url = "sqlite3://" + url
case "cockroach":
url = "cockroach" + strings.TrimPrefix(url, "postgres")
}
if db != "sqlite" {
dbName := "testdb" + strings.ReplaceAll(x.NewUUID().String(), "-", "")
require.NoError(t, c.RawQuery("CREATE DATABASE "+dbName).Exec())
url = regexp.MustCompile(`/[a-z0-9]+\?`).ReplaceAllString(url, "/"+dbName+"?")
}
t.Logf("URL: %s", url)
var err error
c, err = pop.NewConnection(&pop.ConnectionDetails{URL: url})
require.NoError(t, err)
require.NoError(t, c.Open())
tm, err := popx.NewMigrationBox(
fsx.Merge(sql.Migrations, networkx.Migrations),
c, l,
popx.WithGoMigrations(gomigrations.All),
popx.WithTestdata(t, os.DirFS("./testdata")),
popx.WithoutGoldenDatabase(),
)
require.NoError(t, err)
err = tm.Up(ctx) // for easy breakpointing
// _ = tm.DumpMigrationSchema(ctx) // uncomment to get the current state of the database after migrations have run
if !assert.NoError(t, err) {
assert.NoError(t, tm.DumpMigrationSchema(ctx))
t.FailNow()
}
opts := driver.WithConfigOptions(
configx.WithValues(map[string]any{
config.ViperKeyDSN: url,
config.ViperKeyPublicBaseURL: "https://www.ory.sh/",
config.ViperKeyIdentitySchemas: config.Schemas{{ID: "default", URL: "file://stub/default.schema.json"}},
config.ViperKeySecretsDefault: []string{"secret"},
}),
configx.SkipValidation(),
)
t.Run("suite=fixtures", func(t *testing.T) {
t.Run("case=identity", func(t *testing.T) {
t.Parallel()
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
ids, _, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, KeySetPagination: []keysetpagination.Option{keysetpagination.WithSize(1000)}})
require.NoError(t, err)
require.NotEmpty(t, ids)
var found []string
for y, id := range ids {
found = append(found, id.ID.String())
actual := &ids[y]
for _, a := range actual.VerifiableAddresses {
CompareWithFixture(t, a, "identity_verification_address", a.ID.String())
}
for _, a := range actual.RecoveryAddresses {
CompareWithFixture(t, a, "identity_recovery_address", a.ID.String())
}
CompareWithFixture(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), "identity", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "identity"), found)
})
t.Run("case=identity_get", func(t *testing.T) {
t.Parallel()
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
ids, _, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandNothing, KeySetPagination: []keysetpagination.Option{keysetpagination.WithSize(1000)}})
require.NoError(t, err)
require.NotEmpty(t, ids)
var found []string
for _, id := range ids {
actual, err := d.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), id.ID)
require.NoError(t, err)
found = append(found, actual.ID.String())
CompareWithFixture(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), "identity", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "identity"), found)
})
t.Run("case=verification_token", func(t *testing.T) {
t.Parallel()
var ids []link.VerificationToken
require.NoError(t, c.All(&ids))
require.NotEmpty(t, ids)
for _, id := range ids {
CompareWithFixture(t, id, "verification_token", id.ID.String())
}
})
t.Run("case=session", func(t *testing.T) {
t.Parallel()
var ids []session.Session
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.SessionPersister().GetSession(context.Background(), id.ID, session.ExpandEverything)
require.NoErrorf(t, err, "Trying to get session: %s", id.ID)
require.NotEmpty(t, actual.LogoutToken, "check if migrations have generated a logout token for existing sessions")
CompareWithFixture(t, actual, "session", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "session"), found)
})
t.Run("case=login", func(t *testing.T) {
t.Parallel()
var ids []login.Flow
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.LoginFlowPersister().GetLoginFlow(context.Background(), id.ID)
require.NoError(t, err)
CompareWithFixture(t, actual, "login_flow", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_flow"), found)
})
t.Run("case=registration", func(t *testing.T) {
t.Parallel()
var ids []registration.Flow
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.RegistrationFlowPersister().GetRegistrationFlow(context.Background(), id.ID)
require.NoError(t, err)
CompareWithFixture(t, actual, "registration_flow", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_flow"), found)
})
t.Run("case=settings_flow", func(t *testing.T) {
t.Parallel()
var ids []settings.Flow
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.SettingsFlowPersister().GetSettingsFlow(context.Background(), id.ID)
require.NoError(t, err, id.ID.String())
CompareWithFixture(t, actual, "settings_flow", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "settings_flow"), found)
})
t.Run("case=recovery_flow", func(t *testing.T) {
t.Parallel()
var ids []recovery.Flow
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), id.ID)
require.NoError(t, err)
CompareWithFixture(t, actual, "recovery_flow", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_flow"), found)
})
t.Run("case=verification_flow", func(t *testing.T) {
t.Parallel()
var ids []verification.Flow
require.NoError(t, c.Select("id").All(&ids))
require.NotEmpty(t, ids)
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
actual, err := d.VerificationFlowPersister().GetVerificationFlow(context.Background(), id.ID)
require.NoError(t, err)
CompareWithFixture(t, actual, "verification_flow", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "verification_flow"), found)
})
t.Run("case=recovery_token", func(t *testing.T) {
t.Parallel()
var ids []link.RecoveryToken
require.NoError(t, c.All(&ids))
require.NotEmpty(t, ids)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
CompareWithFixture(t, id, "recovery_token", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_token"), found)
})
t.Run("case=recovery_code", func(t *testing.T) {
t.Parallel()
var ids []code.RecoveryCode
require.NoError(t, c.All(&ids))
require.NotEmpty(t, ids)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
CompareWithFixture(t, id, "recovery_code", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "recovery_code"), found)
})
t.Run("case=registration_code", func(t *testing.T) {
t.Parallel()
var ids []code.RegistrationCode
require.NoError(t, c.All(&ids))
require.NotEmpty(t, ids)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
CompareWithFixture(t, id, "registration_code", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "registration_code"), found)
})
t.Run("case=login_code", func(t *testing.T) {
t.Parallel()
var ids []code.LoginCode
require.NoError(t, c.All(&ids))
require.NotEmpty(t, ids)
var found []string
for _, id := range ids {
found = append(found, id.ID.String())
CompareWithFixture(t, id, "login_code", id.ID.String())
}
migratest.ContainsExpectedIds(t, filepath.Join("fixtures", "login_code"), found)
})
})
t.Run("suite=constraints", func(t *testing.T) {
t.Cleanup(func() {
// clean up test duplicates - remove identity_credential_identifiers 10985ed1-5b6e-4012-ac10-03d87df65618 - otherwise down migration later fails.
require.NoError(t, c.RawQuery("DELETE FROM identity_credential_identifiers WHERE identifier = '10985ed1-5b6e-4012-ac10-03d87df65618'").Exec())
})
d, err := driver.New(
context.Background(),
os.Stderr,
opts,
)
require.NoError(t, err)
sr, err := d.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID("a79bfcf1-68ae-49de-8b23-4f96921b8341"))
require.NoError(t, err)
require.NoError(t, d.PrivilegedIdentityPool().DeleteIdentity(context.Background(), sr.IdentityID))
_, err = d.SettingsFlowPersister().GetSettingsFlow(context.Background(), x.ParseUUID("a79bfcf1-68ae-49de-8b23-4f96921b8341"))
require.Error(t, err)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
t.Run("suite=down", func(t *testing.T) {
err = tm.Down(ctx, -1) // for easy breakpointing
if !assert.NoError(t, err) {
assert.NoError(t, tm.DumpMigrationSchema(ctx))
t.FailNow()
}
})
}