feat: complete IM P0 and P1 reliability work

This commit is contained in:
richjohnrambo
2026-08-17 13:43:27 +08:00
parent ddf0f7a55e
commit f2c229135b
43 changed files with 4512 additions and 1181 deletions
+165 -66
View File
@@ -15,6 +15,7 @@ import (
"log"
"os"
"reflect"
"strings"
"testing"
"time"
@@ -48,17 +49,126 @@ var testData *test_data.TestData
var dummyUid1 = types.Uid(12345)
var dummyUid2 = types.Uid(54321)
func TestCreateDb(t *testing.T) {
if err := adp.CreateDb(config.Reset); err != nil {
t.Fatal(err)
}
// Saved db is closed, get a fresh one.
db = adp.GetTestDB().(*sqlx.DB)
}
func TestGroupBusinessTablesCreated(t *testing.T) {
tables := []string{
"im_group_settings", "im_group_members", "im_group_invitations", "im_group_join_requests",
"im_group_announcements", "im_group_announcement_reads", "im_group_mutes", "im_group_qrcodes",
"im_group_files", "im_group_admin_logs", "im_group_idempotency", "im_group_events_outbox",
}
args := make([]any, len(tables))
for i, table := range tables {
args[i] = table
}
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(tables)), ",")
var count int
query := "SELECT COUNT(*) FROM information_schema.tables " +
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ")"
if err := db.Get(&count, query, args...); err != nil {
t.Fatal(err)
}
if count != len(tables) {
t.Fatalf("group business table count: got %d, want %d", count, len(tables))
}
query = "SELECT COUNT(*) FROM information_schema.columns " +
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ") AND column_name='tenant_id'"
if err := db.Get(&count, query, args...); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("tenant business tables contain %d tenant_id columns", count)
}
}
func TestHTTPInfrastructureTablesCreated(t *testing.T) {
tables := []string{"im_http_idempotency", "im_business_events_outbox"}
args := make([]any, len(tables))
for i, table := range tables {
args[i] = table
}
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(tables)), ",")
var count int
query := "SELECT COUNT(*) FROM information_schema.tables " +
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ")"
if err := db.Get(&count, query, args...); err != nil {
t.Fatal(err)
}
if count != len(tables) {
t.Fatalf("HTTP infrastructure table count: got %d, want %d", count, len(tables))
}
query = "SELECT COUNT(*) FROM information_schema.columns " +
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ") AND column_name='tenant_id'"
if err := db.Get(&count, query, args...); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("HTTP infrastructure tables contain %d tenant_id columns", count)
}
}
func TestTenantAuthIsolation(t *testing.T) {
tenantAdp, ok := adp.(adapter.TenantAuthAdapter)
if !ok {
t.Fatal("MySQL adapter does not implement TenantAuthAdapter")
}
var routingConfig struct {
Enabled bool `json:"tenant_db_routing"`
}
if err := json.Unmarshal(config.Adapters[adp.GetName()], &routingConfig); err != nil {
t.Fatal(err)
}
if !routingConfig.Enabled {
t.Skip("tenant_db_routing is disabled in the MySQL test configuration")
}
router, ok := adp.(adapter.TenantRoutedAdapter)
if !ok {
t.Fatal("MySQL adapter does not implement TenantRoutedAdapter")
}
now := types.TimeNow()
suffix := fmt.Sprintf("%d", now.UnixNano())
createTenant := func(code string) types.TenantID {
res, err := db.Exec("INSERT INTO im_tenant(code,name,state,created_by,updated_by) VALUES(?,?,?,?,?)",
code+suffix, code, types.TenantStateActive, 0, 0)
dbNames := []string{"im_tenant_auth_a_" + suffix, "im_tenant_auth_b_" + suffix}
quoteIdent := func(value string) string {
return "`" + strings.ReplaceAll(value, "`", "``") + "`"
}
var baseDB string
if err := db.Get(&baseDB, "SELECT DATABASE()"); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
db.Exec("DELETE FROM im_tenant WHERE code IN (?,?)", "test-im_auth-a-"+suffix, "test-im_auth-b-"+suffix)
for _, dbName := range dbNames {
db.Exec("DROP DATABASE IF EXISTS " + quoteIdent(dbName))
}
})
for _, dbName := range dbNames {
if _, err := db.Exec("CREATE DATABASE " + quoteIdent(dbName) +
" CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil {
t.Fatal(err)
}
for _, table := range []string{"im_users", "im_auth", "im_credentials"} {
query := "CREATE TABLE " + quoteIdent(dbName) + "." + quoteIdent(table) +
" LIKE " + quoteIdent(baseDB) + "." + quoteIdent(table)
if _, err := db.Exec(query); err != nil {
t.Fatal(err)
}
}
}
createTenant := func(code, dbName string) types.TenantID {
res, err := db.Exec("INSERT INTO im_tenant(code,name,db_name,state,created_by,updated_by) VALUES(?,?,?,?,?,?)",
code+suffix, code, dbName, types.TenantStateActive, 0, 0)
if err != nil {
t.Fatal(err)
}
@@ -68,44 +178,41 @@ func TestTenantAuthIsolation(t *testing.T) {
}
return types.TenantID(id)
}
tenantA := createTenant("test-im_auth-a-", dbNames[0])
tenantB := createTenant("test-im_auth-b-", dbNames[1])
businessA, _, err := router.TenantAdapterFor(tenantA)
if err != nil {
t.Fatal(err)
}
if reflect.ValueOf(businessA).Pointer() == reflect.ValueOf(adp).Pointer() {
t.Skip("tenant_db_routing is disabled in the MySQL test configuration")
}
tenantA := createTenant("test-auth-a-")
tenantB := createTenant("test-auth-b-")
userAID := now.UnixNano() & 0x3fffffffffffffff
userBID := userAID + 1
t.Cleanup(func() {
db.Exec("DELETE FROM credentials WHERE tenant_id IN (?,?)", tenantA, tenantB)
db.Exec("DELETE FROM auth WHERE tenant_id IN (?,?)", tenantA, tenantB)
db.Exec("DELETE FROM users WHERE tenant_id IN (?,?)", tenantA, tenantB)
db.Exec("DELETE FROM im_tenant WHERE id IN (?,?)", tenantA, tenantB)
})
for tenantID, userID := range map[types.TenantID]int64{tenantA: userAID, tenantB: userBID} {
_, err := db.Exec("INSERT INTO users(id,tenant_id,createdat,updatedat,state,access,public,trusted,tags) "+
"VALUES(?,?,?,?,?,?,?,?,?)", userID, tenantID, now, now, types.StateOK, `{}`, nil, nil, nil)
if err != nil {
for index, userID := range []int64{userAID, userBID} {
tenantDB := quoteIdent(dbNames[index])
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,createdat,updatedat,state,access,public,trusted,tags) "+
"VALUES(?,?,?,?,?,?,?,?)", userID, now, now, types.StateOK, `{}`, nil, nil, nil); err != nil {
t.Fatal(err)
}
_, err = db.Exec("INSERT INTO auth(tenant_id,uname,userid,scheme,authlvl,secret) VALUES(?,?,?,?,?,?)",
tenantID, "basic:alice", userID, "basic", auth.LevelAuth, []byte("hash"))
if err != nil {
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(uname,userid,scheme,authlvl,secret) VALUES(?,?,?,?,?)",
"basic:alice", userID, "basic", auth.LevelAuth, []byte("hash")); err != nil {
t.Fatal(err)
}
_, err = db.Exec("INSERT INTO credentials(tenant_id,createdat,updatedat,method,value,synthetic,userid,resp,done) "+
"VALUES(?,?,?,?,?,?,?,?,?)", tenantID, now, now, "tel", "13800000000", "tel:13800000000",
userID, "", true)
if err != nil {
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_credentials(createdat,updatedat,method,value,synthetic,userid,resp,done) "+
"VALUES(?,?,?,?,?,?,?,?)", now, now, "tel", "13800000000", "tel:13800000000", userID, "", true); err != nil {
t.Fatal(err)
}
}
uidA, _, _, _, err := tenantAdp.TenantAuthGetUniqueRecord(tenantA, "basic:alice")
if err != nil || uidA != store.EncodeUid(userAID) {
t.Fatalf("tenant A auth lookup: uid=%v err=%v", uidA, err)
t.Fatalf("tenant A im_auth lookup: uid=%v err=%v", uidA, err)
}
uidB, _, _, _, err := tenantAdp.TenantAuthGetUniqueRecord(tenantB, "basic:alice")
if err != nil || uidB != store.EncodeUid(userBID) {
t.Fatalf("tenant B auth lookup: uid=%v err=%v", uidB, err)
t.Fatalf("tenant B im_auth lookup: uid=%v err=%v", uidB, err)
}
credA, err := tenantAdp.TenantUserGetByCred(tenantA, "tel", "13800000000")
if err != nil || credA != uidA {
@@ -118,18 +225,10 @@ func TestTenantAuthIsolation(t *testing.T) {
if err = tenantAdp.TenantAuthUpdRecord(tenantA, uidB, "basic", "basic:changed",
auth.LevelAuth, nil, time.Time{}); err != types.ErrNotFound {
t.Fatalf("cross-tenant auth update: got %v, want %v", err, types.ErrNotFound)
t.Fatalf("cross-tenant im_auth update: got %v, want %v", err, types.ErrNotFound)
}
}
func TestCreateDb(t *testing.T) {
if err := adp.CreateDb(config.Reset); err != nil {
t.Fatal(err)
}
// Saved db is closed, get a fresh one.
db = adp.GetTestDB().(*sqlx.DB)
}
// ================== Create tests ================================
func TestUserCreate(t *testing.T) {
for _, user := range testData.Users {
@@ -142,12 +241,12 @@ func TestUserCreate(t *testing.T) {
if err := db.Ping(); err != nil {
logs.Err.Println("Database ping failed:", err)
}
err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
err := db.QueryRow("SELECT COUNT(*) FROM im_users").Scan(&count)
if err != nil {
t.Error(err)
}
if count == 0 {
t.Error("No users created!")
t.Error("No im_users created!")
}
}
@@ -245,7 +344,7 @@ func TestTopicCreateP2P(t *testing.T) {
}
var got types.Subscription
err = db.QueryRow("SELECT createdat,updatedat,deletedat,userid,topic,delid,recvseqid,readseqid,modewant,modegiven,private FROM subscriptions WHERE topic=? AND userid=?",
err = db.QueryRow("SELECT createdat,updatedat,deletedat,userid,topic,delid,recvseqid,readseqid,modewant,modegiven,private FROM im_subscriptions WHERE topic=? AND userid=?",
testData.Subs[2].Topic, decodeUid(testData.Subs[2].User)).Scan(&got.CreatedAt,
&got.UpdatedAt, &got.DeletedAt, &got.User, &got.Topic, &got.DelId, &got.RecvSeqId, &got.ReadSeqId, &got.ModeWant, &got.ModeGiven, &got.Private)
if err != nil {
@@ -346,7 +445,7 @@ func TestUserGetAll(t *testing.T) {
t.Fatal(err)
}
if len(got) > 0 {
t.Error("result users should be zero length, got", len(got))
t.Error("result im_users should be zero length, got", len(got))
}
got, err = adp.UserGetAll(types.ParseUserId("usr"+testData.Users[0].Id), types.ParseUserId("usr"+testData.Users[1].Id))
@@ -698,7 +797,7 @@ func TestUserUpdate(t *testing.T) {
UpdatedAt time.Time
CreatedAt time.Time
}
err = db.QueryRow("SELECT useragent, updatedat, createdat FROM users WHERE id=?", decodeUid(testData.Users[0].Id)).
err = db.QueryRow("SELECT useragent, updatedat, createdat FROM im_users WHERE id=?", decodeUid(testData.Users[0].Id)).
Scan(&got.UserAgent, &got.UpdatedAt, &got.CreatedAt)
if err != nil {
t.Fatal(err)
@@ -775,7 +874,7 @@ func TestCredFail(t *testing.T) {
UpdatedAt time.Time
CreatedAt time.Time
}
err = db.QueryRow("SELECT retries, updatedat, createdat FROM credentials WHERE userid=? AND method=? AND value=?",
err = db.QueryRow("SELECT retries, updatedat, createdat FROM im_credentials WHERE userid=? AND method=? AND value=?",
decodeUid(testData.Creds[3].User), "tel", testData.Creds[3].Value).Scan(&got.Retries, &got.UpdatedAt, &got.CreatedAt)
if err != nil {
t.Fatal(err)
@@ -800,7 +899,7 @@ func TestCredConfirm(t *testing.T) {
CreatedAt time.Time
Done bool
}
err = db.QueryRow("SELECT updatedat, createdat, done FROM credentials WHERE userid=? AND method=? AND value=?",
err = db.QueryRow("SELECT updatedat, createdat, done FROM im_credentials WHERE userid=? AND method=? AND value=?",
decodeUid(testData.Creds[3].User), "tel", testData.Creds[3].Value).Scan(&got.UpdatedAt, &got.CreatedAt, &got.Done)
if err != nil {
t.Fatal(err)
@@ -822,7 +921,7 @@ func TestAuthUpdRecord(t *testing.T) {
t.Fatal(err)
}
var got []byte
err = db.QueryRow("SELECT secret FROM auth WHERE uname=?", rec.Unique).Scan(&got)
err = db.QueryRow("SELECT secret FROM im_auth WHERE uname=?", rec.Unique).Scan(&got)
if err != nil {
t.Fatal(err)
}
@@ -839,12 +938,12 @@ func TestAuthUpdRecord(t *testing.T) {
}
// Test if old ID deleted
var count int
err = db.QueryRow("SELECT COUNT(*) FROM auth WHERE uname=?", rec.Unique).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_auth WHERE uname=?", rec.Unique).Scan(&count)
if err != nil {
t.Fatal(err)
}
if count != 0 {
t.Error("Old auth record not deleted")
t.Error("Old im_auth record not deleted")
}
}
@@ -863,7 +962,7 @@ func TestTopicUpdateOnMessage(t *testing.T) {
TouchedAt time.Time
SeqId int
}
err = db.QueryRow("SELECT touchedat, seqid FROM topics WHERE name=?", testData.Topics[2].Id).
err = db.QueryRow("SELECT touchedat, seqid FROM im_topics WHERE name=?", testData.Topics[2].Id).
Scan(&got.TouchedAt, &got.SeqId)
if err != nil {
t.Fatal(err)
@@ -883,7 +982,7 @@ func TestTopicUpdate(t *testing.T) {
t.Fatal(err)
}
var got time.Time
err = db.QueryRow("SELECT updatedat FROM topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
err = db.QueryRow("SELECT updatedat FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
if err != nil {
t.Fatal(err)
}
@@ -898,7 +997,7 @@ func TestTopicOwnerChange(t *testing.T) {
t.Fatal(err)
}
var got int64
err = db.QueryRow("SELECT owner FROM topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
err = db.QueryRow("SELECT owner FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
if err != nil {
t.Fatal(err)
}
@@ -917,7 +1016,7 @@ func TestSubsUpdate(t *testing.T) {
t.Fatal(err)
}
var got time.Time
err = db.QueryRow("SELECT updatedat FROM subscriptions WHERE topic=? AND userid=?",
err = db.QueryRow("SELECT updatedat FROM im_subscriptions WHERE topic=? AND userid=?",
testData.Topics[0].Id, decodeUid(testData.Users[0].Id)).Scan(&got)
if err != nil {
t.Fatal(err)
@@ -930,7 +1029,7 @@ func TestSubsUpdate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT updatedat FROM subscriptions WHERE topic=? LIMIT 1",
err = db.QueryRow("SELECT updatedat FROM im_subscriptions WHERE topic=? LIMIT 1",
testData.Topics[1].Id).Scan(&got)
if err != nil {
t.Fatal(err)
@@ -946,7 +1045,7 @@ func TestSubsDelete(t *testing.T) {
t.Fatal(err)
}
var deletedat sql.NullTime
err = db.QueryRow("SELECT deletedat FROM subscriptions WHERE topic=? AND userid=?",
err = db.QueryRow("SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=?",
testData.Topics[1].Id, decodeUid(testData.Users[0].Id)).Scan(&deletedat)
if err != nil {
t.Fatal(err)
@@ -965,7 +1064,7 @@ func TestDeviceUpsert(t *testing.T) {
DeviceId string
Platform string
}
err = db.QueryRow("SELECT deviceid, platform FROM devices WHERE userid=? LIMIT 1",
err = db.QueryRow("SELECT deviceid, platform FROM im_devices WHERE userid=? LIMIT 1",
decodeUid(testData.Users[0].Id)).Scan(&got.DeviceId, &got.Platform)
if err != nil {
t.Fatal(err)
@@ -980,7 +1079,7 @@ func TestDeviceUpsert(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT platform FROM devices WHERE userid=? AND deviceid=?",
err = db.QueryRow("SELECT platform FROM im_devices WHERE userid=? AND deviceid=?",
decodeUid(testData.Users[0].Id), testData.Devs[0].DeviceId).Scan(&got.Platform)
if err != nil {
t.Fatal(err)
@@ -994,7 +1093,7 @@ func TestDeviceUpsert(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT platform FROM devices WHERE userid=? AND deviceid=?",
err = db.QueryRow("SELECT platform FROM im_devices WHERE userid=? AND deviceid=?",
decodeUid(testData.Users[1].Id), testData.Devs[0].DeviceId).Scan(&got.Platform)
if err != nil {
t.Fatal(err)
@@ -1030,7 +1129,7 @@ func TestMessageAttachments(t *testing.T) {
}
// Check if attachments were linked (this would require checking filemsglinks table)
var count int
if err = db.QueryRow("SELECT COUNT(*) FROM filemsglinks WHERE msgid=?",
if err = db.QueryRow("SELECT COUNT(*) FROM im_filemsglinks WHERE msgid=?",
types.ParseUid(testData.Msgs[1].Id)).Scan(&count); err != nil {
t.Fatal(err)
}
@@ -1066,7 +1165,7 @@ func TestDeviceDelete(t *testing.T) {
t.Fatal(err)
}
var count int
err = db.QueryRow("SELECT COUNT(*) FROM devices WHERE userid=?", testData.Users[1].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_devices WHERE userid=?", testData.Users[1].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1078,7 +1177,7 @@ func TestDeviceDelete(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT COUNT(*) FROM devices WHERE userid=?", testData.Users[2].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_devices WHERE userid=?", testData.Users[2].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1154,7 +1253,7 @@ func TestCredDel(t *testing.T) {
t.Fatal(err)
}
var count int
err = db.QueryRow("SELECT COUNT(*) FROM credentials WHERE method='email' AND value='alice@test.example.com'").Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_credentials WHERE method='email' AND value='alice@test.example.com'").Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1166,7 +1265,7 @@ func TestCredDel(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT COUNT(*) FROM credentials WHERE userid=?", testData.Users[1].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_credentials WHERE userid=?", testData.Users[1].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1218,13 +1317,13 @@ func TestMessageDeleteList(t *testing.T) {
// Check messages in dellog
var count int
err = db.QueryRow("SELECT COUNT(*) FROM dellog WHERE topic=? AND deletedfor=?",
err = db.QueryRow("SELECT COUNT(*) FROM im_dellog WHERE topic=? AND deletedfor=?",
toDel.Topic, decodeUid(toDel.DeletedFor)).Scan(&count)
if err != nil {
t.Fatal(err)
}
if count == 0 {
t.Error("No dellog entries created")
t.Error("No im_dellog entries created")
}
// Hard delete test
@@ -1244,7 +1343,7 @@ func TestMessageDeleteList(t *testing.T) {
}
// Check if messages content was cleared
err = db.QueryRow("SELECT COUNT(*) FROM messages WHERE topic=? AND content IS NOT NULL",
err = db.QueryRow("SELECT COUNT(*) FROM im_messages WHERE topic=? AND content IS NOT NULL",
toDel.Topic).Scan(&count)
if err != nil {
t.Fatal(err)
@@ -1257,7 +1356,7 @@ func TestMessageDeleteList(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = db.QueryRow("SELECT COUNT(*) FROM messages WHERE topic=?", testData.Topics[0].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_messages WHERE topic=?", testData.Topics[0].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1272,7 +1371,7 @@ func TestTopicDelete(t *testing.T) {
t.Fatal(err)
}
var state int
err = db.QueryRow("SELECT state FROM topics WHERE name=?", testData.Topics[1].Id).Scan(&state)
err = db.QueryRow("SELECT state FROM im_topics WHERE name=?", testData.Topics[1].Id).Scan(&state)
if err != nil {
t.Fatal(err)
}
@@ -1286,7 +1385,7 @@ func TestTopicDelete(t *testing.T) {
}
var count int
err = db.QueryRow("SELECT COUNT(*) FROM topics WHERE name=?", testData.Topics[0].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}
@@ -1311,7 +1410,7 @@ func TestUserDelete(t *testing.T) {
t.Fatal(err)
}
var state int
err = db.QueryRow("SELECT state FROM users WHERE id=?",
err = db.QueryRow("SELECT state FROM im_users WHERE id=?",
decodeUid(testData.Users[0].Id)).Scan(&state)
if err != nil {
t.Fatal(err)
@@ -1325,7 +1424,7 @@ func TestUserDelete(t *testing.T) {
t.Fatal(err)
}
var count int
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE id=?", testData.Users[1].Id).Scan(&count)
err = db.QueryRow("SELECT COUNT(*) FROM im_users WHERE id=?", testData.Users[1].Id).Scan(&count)
if err != nil {
t.Fatal(err)
}