264 lines
8.5 KiB
Go
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(×tampIsLocal); 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)
|
|
}
|
|
}
|
|
|