feat: 品牌重命名 Sub2API -> TianShuAPI
- 前端: 所有界面显示、i18n 文本、组件中的品牌名称 - 后端: 服务层、设置默认值、邮件模板、安装向导 - 数据库: 迁移脚本注释 - 保持功能完全一致,仅更改品牌名称 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,102 +1,102 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
//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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user