Files

208 lines
7.5 KiB
Go

package usersignup
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/utils"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
)
// versionBeforeMoveTokens is the migration version right before the "actor tokens" migration.
const versionBeforeMoveTokens = 20260722120000
// seedSignupTokensForMigration seeds two signup tokens (one with a user group, one without) into the pre-migration schema.
func seedSignupTokensForMigration(t *testing.T, db *gorm.DB, createdAt, expiresAt time.Time) {
t.Helper()
// An unrelated, non-JSON kv entry, to ensure the freeze/restore queries don't choke on other kv keys
err := db.Exec(
`INSERT INTO kv ("key", "value") VALUES ('instance_id', ?)`,
"not-json-instance-id",
).Error
require.NoError(t, err)
// A user group referenced by one of the tokens
err = db.Exec(
`INSERT INTO user_groups (id, created_at, friendly_name, name) VALUES (?, ?, ?, ?)`,
"grp-1", createdAt.Unix(), "Group One", "group-one",
).Error
require.NoError(t, err)
// A token with a user group
err = db.Exec(
`INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`,
"tok-1", createdAt.Unix(), "TOKENWITHGROUP01", expiresAt.Unix(), 3, 1,
).Error
require.NoError(t, err)
err = db.Exec(
`INSERT INTO signup_tokens_user_groups (signup_token_id, user_group_id) VALUES (?, ?)`,
"tok-1", "grp-1",
).Error
require.NoError(t, err)
// A token without user groups
err = db.Exec(
`INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`,
"tok-2", createdAt.Unix(), "TOKENNOGROUP0002", expiresAt.Unix(), 1, 0,
).Error
require.NoError(t, err)
}
func TestLoadMigratedSignupTokens(t *testing.T) {
createdAt := time.Now().Add(-time.Hour).Truncate(time.Second)
expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second)
db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) {
seedSignupTokensForMigration(t, db, createdAt, expiresAt)
})
// The migration must have dropped the signup_tokens tables
ok := db.Migrator().HasTable("signup_tokens")
require.False(t, ok, "signup_tokens table should have been dropped")
ok = db.Migrator().HasTable("signup_tokens_user_groups")
require.False(t, ok, "signup_tokens_user_groups table should have been dropped")
tokens, err := loadMigratedSignupTokens(t.Context(), db)
require.NoError(t, err)
require.Len(t, tokens, 2)
byID := make(map[string]migratedSignupToken, len(tokens))
for _, tok := range tokens {
byID[tok.ID] = tok
}
tok1 := byID["tok-1"]
require.Equal(t, "TOKENWITHGROUP01", tok1.Token)
require.Equal(t, 3, tok1.UsageLimit)
require.Equal(t, 1, tok1.UsageCount)
require.Equal(t, []string{"grp-1"}, tok1.UserGroupIDs)
require.Equal(t, expiresAt.Unix(), tok1.ExpiresAt)
require.Equal(t, createdAt.Unix(), tok1.CreatedAt)
tok2 := byID["tok-2"]
require.Equal(t, "TOKENNOGROUP0002", tok2.Token)
require.Equal(t, 1, tok2.UsageLimit)
require.Equal(t, 0, tok2.UsageCount)
require.Empty(t, tok2.UserGroupIDs)
}
// TestMigrateSignupTokens verifies that the frozen signup tokens are turned into per-token actors, and that already-expired ones are skipped
func TestMigrateSignupTokens(t *testing.T) {
createdAt := time.Now().Add(-time.Hour).Truncate(time.Second)
expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second)
db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) {
seedSignupTokensForMigration(t, db, createdAt, expiresAt)
// A token that has already expired: it must not be migrated
err := db.Exec(
`INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`,
"tok-expired", createdAt.Unix(), "EXPIREDTOKEN0003", time.Now().Add(-time.Hour).Unix(), 1, 0,
).Error
require.NoError(t, err)
})
svc := newSignupServiceForTest(t, db, fakeUserCreator{})
err := svc.migrateSignupTokens(t.Context())
require.NoError(t, err)
entries, err := svc.listSignupTokenStates(t.Context())
require.NoError(t, err)
require.Len(t, entries, 2)
byToken := make(map[string]SignupTokenState, len(entries))
for _, e := range entries {
byToken[e.Token] = e.State
}
tok1 := byToken["TOKENWITHGROUP01"]
require.Equal(t, "tok-1", tok1.ID)
require.Equal(t, 3, tok1.UsageLimit)
require.Equal(t, 1, tok1.UsageCount)
require.Equal(t, []string{"grp-1"}, tok1.UserGroupIDs)
require.Equal(t, expiresAt.Unix(), tok1.ExpiresAt.Unix())
require.Equal(t, createdAt.Unix(), tok1.CreatedAt.Unix())
tok2 := byToken["TOKENNOGROUP0002"]
require.Equal(t, "tok-2", tok2.ID)
require.Empty(t, tok2.UserGroupIDs)
// The expired token must not have been migrated
require.NotContains(t, byToken, "EXPIREDTOKEN0003")
// The migration is idempotent: re-running it doesn't reset a token that has been used since
require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc.actorService, "TOKENNOGROUP0002").Status)
err = svc.migrateSignupTokens(t.Context())
require.NoError(t, err)
require.Equal(t, signupTokenConsumeLimitReached, consumeSignupTokenForTest(t, svc.actorService, "TOKENNOGROUP0002").Status)
}
// TestLoadMigratedSignupTokensEmpty verifies that when there were no signup tokens, nothing is frozen and nothing is loaded.
func TestLoadMigratedSignupTokensEmpty(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
tokens, err := loadMigratedSignupTokens(t.Context(), db)
require.NoError(t, err)
require.Empty(t, tokens)
}
// TestMoveTokensToActorStateDown verifies that rolling the migration back recreates the signup token tables and restores their contents from the frozen kv document.
func TestMoveTokensToActorStateDown(t *testing.T) {
createdAt := time.Now().Add(-time.Hour).Truncate(time.Second)
expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second)
db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) {
seedSignupTokensForMigration(t, db, createdAt, expiresAt)
})
// The tables were frozen and dropped by the up migration
ok := db.Migrator().HasTable("signup_tokens")
require.False(t, ok)
// Roll the migration back
sqlDB, err := db.DB()
require.NoError(t, err)
m, cleanup, err := utils.GetEmbeddedMigrateInstance(t.Context(), sqlDB)
require.NoError(t, err)
defer cleanup()
err = m.Migrate(versionBeforeMoveTokens)
require.NoError(t, err)
// The tables must have been recreated and repopulated from the frozen document
ok = db.Migrator().HasTable("signup_tokens")
require.True(t, ok)
ok = db.Migrator().HasTable("signup_tokens_user_groups")
require.True(t, ok)
type row struct {
ID string
Token string
UsageLimit int
UsageCount int
}
var rows []row
err = db.Raw(`SELECT id, token, usage_limit, usage_count FROM signup_tokens ORDER BY id`).Scan(&rows).Error
require.NoError(t, err)
require.Equal(t, []row{
{ID: "tok-1", Token: "TOKENWITHGROUP01", UsageLimit: 3, UsageCount: 1},
{ID: "tok-2", Token: "TOKENNOGROUP0002", UsageLimit: 1, UsageCount: 0},
}, rows)
var groupID string
err = db.Raw(`SELECT user_group_id FROM signup_tokens_user_groups WHERE signup_token_id = ?`, "tok-1").Scan(&groupID).Error
require.NoError(t, err)
require.Equal(t, "grp-1", groupID)
// The frozen document must have been removed from the kv table
var kvCount int64
err = db.Raw(`SELECT count(*) FROM kv WHERE "key" = ?`, signupTokensMigratedKey).Scan(&kvCount).Error
require.NoError(t, err)
require.Zero(t, kvCount)
}