Files
wxapp-guantong/backend/internal/service/schema_test.go
T
2026-09-22 22:51:19 +08:00

264 lines
8.5 KiB
Go

package service
import (
"context"
"database/sql"
"path/filepath"
"testing"
"github.com/rogeecn/wxapp-kouqiang-guahao/backend/internal/db"
_ "modernc.org/sqlite"
)
func TestApplySchemaDropsLegacyConfigColumns(t *testing.T) {
database, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
defer database.Close()
_, err = database.Exec(`
CREATE TABLE project_categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT NOT NULL DEFAULT '',
sort INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE projects (
id TEXT PRIMARY KEY,
category_id TEXT NOT NULL,
name TEXT NOT NULL,
subtitle TEXT NOT NULL DEFAULT '',
duration_minutes INTEGER NOT NULL DEFAULT 60,
price_text TEXT NOT NULL,
notice TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
sort INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE price_inquiries (
id TEXT PRIMARY KEY,
user_id TEXT,
phone TEXT NOT NULL,
province TEXT NOT NULL,
city TEXT NOT NULL,
district TEXT NOT NULL,
project_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
t.Fatalf("create legacy tables: %v", err)
}
ctx := context.Background()
schemaPath := filepath.Join("..", "..", "migrations", "schema.sql")
if err := ApplySchema(ctx, database, schemaPath); err != nil {
t.Fatalf("apply schema: %v", err)
}
removedColumns := []struct {
table string
column string
}{
{table: "project_categories", column: "icon"},
{table: "projects", column: "duration_minutes"},
{table: "projects", column: "notice"},
}
for _, item := range removedColumns {
exists, err := columnExists(ctx, database, item.table, item.column)
if err != nil {
t.Fatalf("check %s.%s: %v", item.table, item.column, err)
}
if exists {
t.Fatalf("%s.%s still exists after schema migration", item.table, item.column)
}
}
exists, err := columnExists(ctx, database, "projects", "image")
if err != nil {
t.Fatalf("check projects.image: %v", err)
}
if !exists {
t.Fatal("projects.image was not added by schema migration")
}
exists, err = columnExists(ctx, database, "price_inquiries", "remark")
if err != nil {
t.Fatalf("check price_inquiries.remark: %v", err)
}
if !exists {
t.Fatal("price_inquiries.remark was not added by schema migration")
}
exists, err = columnExists(ctx, database, "price_inquiries", "last_submitted_at")
if err != nil {
t.Fatalf("check price_inquiries.last_submitted_at: %v", err)
}
if !exists {
t.Fatal("price_inquiries.last_submitted_at was not added by schema migration")
}
}
func TestApplySchemaDedupesUserPhonesBeforeUniqueIndex(t *testing.T) {
database, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
defer database.Close()
_, err = database.Exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
openid TEXT NOT NULL UNIQUE,
unionid TEXT,
phone TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (id, openid, phone, created_at, updated_at, last_login_at)
VALUES
('user_old', 'openid_old', '13900001111', '2026-07-14 09:00:00', '2026-07-14 09:00:00', '2026-07-14 09:00:00'),
('user_new', 'openid_new', '13900001111', '2026-07-15 09:00:00', '2026-07-15 09:00:00', '2026-07-15 09:00:00');`)
if err != nil {
t.Fatalf("create duplicate users: %v", err)
}
ctx := context.Background()
schemaPath := filepath.Join("..", "..", "migrations", "schema.sql")
if err := ApplySchema(ctx, database, schemaPath); err != nil {
t.Fatalf("apply schema: %v", err)
}
var phoneOwners int
if err := database.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM users
WHERE phone = ?`, "13900001111").Scan(&phoneOwners); err != nil {
t.Fatalf("count phone owners: %v", err)
}
if phoneOwners != 1 {
t.Fatalf("phone owner count = %d, want 1", phoneOwners)
}
var keptOpenID string
if err := database.QueryRowContext(ctx, `
SELECT openid
FROM users
WHERE phone = ?`, "13900001111").Scan(&keptOpenID); err != nil {
t.Fatalf("query kept openid: %v", err)
}
if keptOpenID != "openid_new" {
t.Fatalf("kept openid = %q, want openid_new", keptOpenID)
}
var indexCount int
if err := database.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM sqlite_master
WHERE type = 'index' AND name = 'users_phone_unique_idx'`).Scan(&indexCount); err != nil {
t.Fatalf("check users_phone_unique_idx: %v", err)
}
if indexCount != 1 {
t.Fatalf("users_phone_unique_idx count = %d, want 1", indexCount)
}
}
func TestApplySchemaDedupesPriceInquiriesBeforeUniqueIndex(t *testing.T) {
database, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
defer database.Close()
_, err = database.Exec(`
CREATE TABLE price_inquiries (
id TEXT PRIMARY KEY,
user_id TEXT,
phone TEXT NOT NULL,
province TEXT NOT NULL,
city TEXT NOT NULL,
district TEXT NOT NULL,
project_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
remark TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO price_inquiries (
id, phone, province, city, district, project_name, status, remark, created_at, updated_at
) VALUES
('inquiry_old', '13900002222', '上海市', '上海市', '黄浦区', '牙齿美白', 'assigned', '旧记录', '2026-07-20 09:00:00', '2026-07-20 09:00:00'),
('inquiry_new', '13900002222', '上海市', '上海市', '浦东新区', '牙齿种植', 'completed', '最新记录', '2026-07-21 09:00:00', '2026-07-22 09:00:00');`)
if err != nil {
t.Fatalf("create duplicate price inquiries: %v", err)
}
ctx := context.Background()
schemaPath := filepath.Join("..", "..", "migrations", "schema.sql")
if err := ApplySchema(ctx, database, schemaPath); err != nil {
t.Fatalf("apply schema: %v", err)
}
var id, remark, lastSubmittedAt, createdAt, updatedAt string
if err := database.QueryRowContext(ctx, `
SELECT id, remark, last_submitted_at, created_at, updated_at
FROM price_inquiries
WHERE phone = ?`, "13900002222").Scan(&id, &remark, &lastSubmittedAt, &createdAt, &updatedAt); err != nil {
t.Fatalf("query kept price inquiry: %v", err)
}
if id != "inquiry_new" || remark != "最新记录" || lastSubmittedAt != "2026-07-21 17:00:00.000" || createdAt != "2026-07-21 17:00:00.000" || updatedAt != "2026-07-22 17:00:00.000" {
t.Fatalf("kept price inquiry = %q/%q/%q/%q/%q, want migrated latest record", id, remark, lastSubmittedAt, createdAt, updatedAt)
}
_, err = database.ExecContext(ctx, `
INSERT INTO price_inquiries (id, phone, province, city, district, project_name)
VALUES ('inquiry_duplicate', '13900002222', '上海市', '上海市', '静安区', '牙齿清洁')`)
if err == nil {
t.Fatal("duplicate price inquiry insert succeeded, want unique constraint error")
}
created, err := db.New(database).UpsertPriceInquiry(ctx, db.UpsertPriceInquiryParams{
ID: "inquiry_new_phone", Phone: "13900003333", Province: "北京市", City: "北京市",
District: "朝阳区", ProjectName: "牙齿种植", Status: "pending",
})
if err != nil {
t.Fatalf("create price inquiry after legacy schema upgrade: %v", err)
}
for name, value := range map[string]string{
"created_at": created.CreatedAt, "updated_at": created.UpdatedAt, "last_submitted_at": created.LastSubmittedAt,
} {
var timestampIsLocal bool
if err := database.QueryRowContext(ctx, `
SELECT ABS((JULIANDAY(?) - JULIANDAY('now', '+8 hours')) * 86400) < 2`, value).Scan(&timestampIsLocal); err != nil {
t.Fatalf("compare price inquiry %s with local time: %v", name, err)
}
if !timestampIsLocal {
t.Fatalf("new price inquiry %s = %q, want local time", name, value)
}
}
if err := ApplySchema(ctx, database, schemaPath); err != nil {
t.Fatalf("reapply schema: %v", err)
}
var lastSubmittedAtAfterReapply string
if err := database.QueryRowContext(ctx, `
SELECT last_submitted_at FROM price_inquiries WHERE id = ?`, id).Scan(&lastSubmittedAtAfterReapply); err != nil {
t.Fatalf("query migrated timestamp after reapply: %v", err)
}
if lastSubmittedAtAfterReapply != lastSubmittedAt {
t.Fatalf("last_submitted_at after reapply = %q, want %q", lastSubmittedAtAfterReapply, lastSubmittedAt)
}
}