Files
sub2api/backend/internal/repository/migrations_schema_integration_test.go
Edric Li 404bf0f8d2 refactor: migrate wechat to user attributes and enhance users list
Migrate the hardcoded wechat field to the new extensible user
attributes system and improve the users management UI.

Migration:
- Add migration 019 to move wechat data to user_attribute_values
- Remove wechat field from User entity, DTOs, and API contracts
- Clean up wechat-related code from backend and frontend

UsersView enhancements:
- Add text labels to action buttons (Filter Settings, Column Settings,
  Attributes Config) for better UX
- Change status column to show colored dot + Chinese text instead of
  English text
- Add dynamic attribute columns support with batch loading
- Add column visibility settings with localStorage persistence
- Add filter settings modal for search and filter preferences
- Update i18n translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 18:59:38 +08:00

103 lines
4.1 KiB
Go

//go:build integration
package repository
import (
"context"
"database/sql"
"testing"
"github.com/stretchr/testify/require"
)
func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) {
tx := testTx(t)
// Re-apply migrations to verify idempotency (no errors, no duplicate rows).
require.NoError(t, ApplyMigrations(context.Background(), integrationDB))
// schema_migrations should have at least the current migration set.
var applied int
require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM schema_migrations").Scan(&applied))
require.GreaterOrEqual(t, applied, 7, "expected schema_migrations to contain applied migrations")
// users: columns required by repository queries
requireColumn(t, tx, "users", "username", "character varying", 100, false)
requireColumn(t, tx, "users", "notes", "text", 0, false)
// accounts: schedulable and rate-limit fields
requireColumn(t, tx, "accounts", "schedulable", "boolean", 0, false)
requireColumn(t, tx, "accounts", "rate_limited_at", "timestamp with time zone", 0, true)
requireColumn(t, tx, "accounts", "rate_limit_reset_at", "timestamp with time zone", 0, true)
requireColumn(t, tx, "accounts", "overload_until", "timestamp with time zone", 0, true)
requireColumn(t, tx, "accounts", "session_window_status", "character varying", 20, true)
// api_keys: key length should be 128
requireColumn(t, tx, "api_keys", "key", "character varying", 128, false)
// redeem_codes: subscription fields
requireColumn(t, tx, "redeem_codes", "group_id", "bigint", 0, true)
requireColumn(t, tx, "redeem_codes", "validity_days", "integer", 0, false)
// usage_logs: billing_type used by filters/stats
requireColumn(t, tx, "usage_logs", "billing_type", "smallint", 0, false)
// settings table should exist
var settingsRegclass sql.NullString
require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.settings')").Scan(&settingsRegclass))
require.True(t, settingsRegclass.Valid, "expected settings table to exist")
// user_allowed_groups table should exist
var uagRegclass sql.NullString
require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.user_allowed_groups')").Scan(&uagRegclass))
require.True(t, uagRegclass.Valid, "expected user_allowed_groups table to exist")
// user_subscriptions: deleted_at for soft delete support (migration 012)
requireColumn(t, tx, "user_subscriptions", "deleted_at", "timestamp with time zone", 0, true)
// orphan_allowed_groups_audit table should exist (migration 013)
var orphanAuditRegclass sql.NullString
require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.orphan_allowed_groups_audit')").Scan(&orphanAuditRegclass))
require.True(t, orphanAuditRegclass.Valid, "expected orphan_allowed_groups_audit table to exist")
// account_groups: created_at should be timestamptz
requireColumn(t, tx, "account_groups", "created_at", "timestamp with time zone", 0, false)
// user_allowed_groups: created_at should be timestamptz
requireColumn(t, tx, "user_allowed_groups", "created_at", "timestamp with time zone", 0, false)
}
func requireColumn(t *testing.T, tx *sql.Tx, table, column, dataType string, maxLen int, nullable bool) {
t.Helper()
var row struct {
DataType string
MaxLen sql.NullInt64
Nullable string
}
err := tx.QueryRowContext(context.Background(), `
SELECT
data_type,
character_maximum_length,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
AND column_name = $2
`, table, column).Scan(&row.DataType, &row.MaxLen, &row.Nullable)
require.NoError(t, err, "query information_schema.columns for %s.%s", table, column)
require.Equal(t, dataType, row.DataType, "data_type mismatch for %s.%s", table, column)
if maxLen > 0 {
require.True(t, row.MaxLen.Valid, "expected maxLen for %s.%s", table, column)
require.Equal(t, int64(maxLen), row.MaxLen.Int64, "maxLen mismatch for %s.%s", table, column)
}
if nullable {
require.Equal(t, "YES", row.Nullable, "nullable mismatch for %s.%s", table, column)
} else {
require.Equal(t, "NO", row.Nullable, "nullable mismatch for %s.%s", table, column)
}
}