174 lines
7.3 KiB
Go
174 lines
7.3 KiB
Go
//go:build mysql
|
|
|
|
package mysql
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tinode/chat/server/store"
|
|
types "github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type recordingGroupSchemaExecutor struct {
|
|
statements []string
|
|
}
|
|
|
|
func (e *recordingGroupSchemaExecutor) Exec(query string, _ ...any) (sql.Result, error) {
|
|
e.statements = append(e.statements, query)
|
|
return nil, nil
|
|
}
|
|
|
|
func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) {
|
|
exec := &recordingGroupSchemaExecutor{}
|
|
if err := ensureIMGroupBusinessTables(exec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(exec.statements) != 22 {
|
|
t.Fatalf("executed %d group schema statements, want 22", len(exec.statements))
|
|
}
|
|
for _, statement := range exec.statements {
|
|
if strings.Contains(strings.ToLower(statement), "tinode.") || strings.Contains(strings.ToLower(statement), "create database") {
|
|
t.Fatalf("group schema must use the selected tenant database: %s", statement)
|
|
}
|
|
}
|
|
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
|
|
if strings.Contains(joined, "im_group_events_outbox") {
|
|
t.Fatal("group event outbox table must not be created")
|
|
}
|
|
if strings.Contains(joined, "im_group_idempotency") {
|
|
t.Fatal("group idempotency table must not be created")
|
|
}
|
|
for _, expected := range []string{
|
|
"primary key(id), unique index uk_im_group_settings_topic(tenant_id, topic)",
|
|
"primary key(id), unique index uk_im_group_announcement_reads_announcement_user(tenant_id, announcement_id, user_id)",
|
|
"primary key(id), unique index uk_im_group_mutes_topic_user(tenant_id, topic, user_id)",
|
|
} {
|
|
if !strings.Contains(joined, expected) {
|
|
t.Fatalf("group schema is missing snowflake primary-key definition %q", expected)
|
|
}
|
|
}
|
|
if !strings.Contains(joined, "json_set(public, '$.fn'") {
|
|
t.Fatal("group schema is missing historical public.fn backfill")
|
|
}
|
|
for _, expected := range []string{"im_group_message_shards", "primary key(message_id)"} {
|
|
if !strings.Contains(joined, expected) {
|
|
t.Fatalf("group shard schema is missing %q", expected)
|
|
}
|
|
}
|
|
if strings.Contains(joined, "im_group_message_topic_shards") {
|
|
t.Fatal("group message topic shard routes must not be created in MySQL")
|
|
}
|
|
if strings.Contains(joined, "im_group_message_dedup") {
|
|
t.Fatal("group message dedup table must not be created")
|
|
}
|
|
}
|
|
|
|
func TestGroupMessageShardTableRejectsUnsafeMonth(t *testing.T) {
|
|
if table, ok := groupMessageShardTable(1, "202609"); !ok || table != "im_001_group_messages_202609" {
|
|
t.Fatalf("unexpected valid shard: table=%q ok=%v", table, ok)
|
|
}
|
|
for _, month := range []string{"", "20269", "202613", "2026_09", "202609;DROP TABLE im_users"} {
|
|
if table, ok := groupMessageShardTable(1, month); ok || table != "" {
|
|
t.Fatalf("unsafe shard month accepted: month=%q table=%q", month, table)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGroupMessageShardTablesForSeqFiltersRoutes(t *testing.T) {
|
|
routes := []store.GroupMessageShardRoute{
|
|
{Month: "202608", MinSeq: 1, MaxSeq: 20},
|
|
{Month: "202609", MinSeq: 21, MaxSeq: 100},
|
|
{Month: "202613", MinSeq: 1, MaxSeq: 1000},
|
|
}
|
|
tables := groupMessageShardTablesForSeq(7, routes, 23)
|
|
if len(tables) != 1 || tables[0] != "im_007_group_messages_202609" {
|
|
t.Fatalf("unexpected shard tables for seq: %#v", tables)
|
|
}
|
|
if tables = groupMessageShardTablesForSeq(7, routes, 101); len(tables) != 0 {
|
|
t.Fatalf("unexpected shard tables for uncovered seq: %#v", tables)
|
|
}
|
|
}
|
|
|
|
func TestTenantPartitionTableNames(t *testing.T) {
|
|
tests := []struct {
|
|
tenantID types.TenantID
|
|
friends string
|
|
messages string
|
|
topics string
|
|
members string
|
|
subs string
|
|
notices string
|
|
group string
|
|
}{
|
|
{tenantID: 1, friends: "im_001_friendships", messages: "im_001_messages", topics: "im_001_topics", members: "im_001_group_members", subs: "im_001_subscriptions", notices: "im_001_system_notifications", group: "im_001_group_messages_202609"},
|
|
{tenantID: 10, friends: "im_010_friendships", messages: "im_010_messages", topics: "im_010_topics", members: "im_010_group_members", subs: "im_010_subscriptions", notices: "im_010_system_notifications", group: "im_010_group_messages_202609"},
|
|
{tenantID: 1000, friends: "im_1000_friendships", messages: "im_1000_messages", topics: "im_1000_topics", members: "im_1000_group_members", subs: "im_1000_subscriptions", notices: "im_1000_system_notifications", group: "im_1000_group_messages_202609"},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := tenantFriendshipsTable(tc.tenantID); got != tc.friends {
|
|
t.Fatalf("tenant %d friendship table: got %q, want %q", tc.tenantID, got, tc.friends)
|
|
}
|
|
if got := tenantMessagesTable(tc.tenantID); got != tc.messages {
|
|
t.Fatalf("tenant %d message table: got %q, want %q", tc.tenantID, got, tc.messages)
|
|
}
|
|
if got := tenantTopicsTable(tc.tenantID); got != tc.topics {
|
|
t.Fatalf("tenant %d topics table: got %q, want %q", tc.tenantID, got, tc.topics)
|
|
}
|
|
if got := tenantGroupMembersTable(tc.tenantID); got != tc.members {
|
|
t.Fatalf("tenant %d group members table: got %q, want %q", tc.tenantID, got, tc.members)
|
|
}
|
|
if got := tenantSubscriptionsTable(tc.tenantID); got != tc.subs {
|
|
t.Fatalf("tenant %d subscriptions table: got %q, want %q", tc.tenantID, got, tc.subs)
|
|
}
|
|
if got := tenantSystemNotificationsTable(tc.tenantID); got != tc.notices {
|
|
t.Fatalf("tenant %d system notifications table: got %q, want %q", tc.tenantID, got, tc.notices)
|
|
}
|
|
if got, ok := tenantGroupMessagesTable(tc.tenantID, "202609"); !ok || got != tc.group {
|
|
t.Fatalf("tenant %d group message table: got %q, ok=%v, want %q", tc.tenantID, got, ok, tc.group)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTenantShardSQL(t *testing.T) {
|
|
query := tenantShardSQL(7, "SELECT * FROM im_topics t JOIN im_group_members AS gm FORCE INDEX (`idx_im_group_members_topic_role`) ON gm.topic=t.name JOIN im_subscriptions s ON s.topic=gm.topic; INSERT INTO im_system_notifications(event_id) VALUES(?)")
|
|
for _, expected := range []string{"im_007_topics", "im_007_group_members", "im_007_subscriptions", "im_007_system_notifications"} {
|
|
if !strings.Contains(query, expected) {
|
|
t.Fatalf("tenant shard query is missing %q: %s", expected, query)
|
|
}
|
|
}
|
|
for _, shared := range []string{" im_topics ", " im_group_members ", " im_subscriptions ", " im_system_notifications"} {
|
|
if strings.Contains(query, shared) {
|
|
t.Fatalf("tenant shard query should not reference shared table %q: %s", shared, query)
|
|
}
|
|
}
|
|
if !strings.Contains(query, "idx_im_group_members_topic_role") {
|
|
t.Fatalf("tenant shard query should not rewrite index names: %s", query)
|
|
}
|
|
if strings.Contains(query, "idx_im_007_group_members_topic_role") {
|
|
t.Fatalf("tenant shard query rewrote index name: %s", query)
|
|
}
|
|
}
|
|
|
|
func TestSharedDBGroupFileLinkIncludesTenantID(t *testing.T) {
|
|
source, err := os.ReadFile("tenant_groups.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := sourceBetween(t, string(source), "func (a *adapter) TenantGroupFileCreate", "\nfunc (a *adapter) enrichGroupFileFromUpload")
|
|
for _, required := range []string{
|
|
"INSERT INTO im_filemsglinks(tenant_id,createdat,fileid,topic)",
|
|
"SELECT ?,?,?,? WHERE NOT EXISTS",
|
|
"SELECT 1 FROM im_filemsglinks WHERE tenant_id=? AND fileid=? AND topic=?)",
|
|
} {
|
|
if !strings.Contains(body, required) {
|
|
t.Fatalf("TenantGroupFileCreate shared-db file link must include tenant_id; missing %q", required)
|
|
}
|
|
}
|
|
if strings.Contains(body, "INSERT INTO im_filemsglinks(createdat,fileid,topic)") {
|
|
t.Fatal("TenantGroupFileCreate must not rely on im_filemsglinks.tenant_id default")
|
|
}
|
|
}
|