//go:build mysql package mysql import ( "os" "strings" "testing" "github.com/tinode/chat/server/store/types" ) func TestGroupAdminAccessModeDoesNotGrantOwner(t *testing.T) { mode := groupAdminAccessMode() if !mode.IsAdmin() { t.Fatalf("admin mode should grant admin access: %s", mode.String()) } if mode.IsOwner() { t.Fatalf("admin mode must not grant owner access: %s", mode.String()) } } func TestMessageSearchDoesNotClearScannedHead(t *testing.T) { source, err := os.ReadFile("tenant_adapter.go") if err != nil { t.Fatal(err) } if strings.Contains(string(source), "msg.Head = kvMapFromJSON(msg.Head)") { t.Fatal("message search must not clear Head after sqlx StructScan has already decoded it") } } func TestRuntimeOpenDoesNotAutoCreateOrMigrateTables(t *testing.T) { source, err := os.ReadFile("adapter.go") if err != nil { t.Fatal(err) } body := string(source) for _, tc := range []struct { name string function string end string }{ { name: "Open", function: "func (a *adapter) Open", end: "\nfunc (a *adapter) Close", }, { name: "TenantAdapterFor", function: "func (a *adapter) TenantAdapterFor", end: "\nfunc (a *adapter) tenantByID", }, } { fn := sourceBetween(t, body, tc.function, tc.end) for _, forbidden := range []string{ "ensureIMRegistrationInviteControlTables", "ensureIMTenantEntryControlTables", "ensureIMGroupBusinessTables", "ensureIMHTTPInfrastructureTables", "ensureIMAdminGovernanceTables", "ensureIMFileReviewTables", "ensureIMOrganizationVisibilityTables", "ensureSnowflakePrimaryKeys", "ensureUserIdentitySchema", "ensureIMRegistrationInviteUseTable", "CREATE DATABASE", "CREATE TABLE", "ALTER TABLE", } { if strings.Contains(fn, forbidden) { t.Fatalf("%s must not auto-create or auto-migrate schema; found %q", tc.name, forbidden) } } } } func sourceBetween(t *testing.T, source, startMarker, endMarker string) string { t.Helper() start := strings.Index(source, startMarker) if start < 0 { t.Fatalf("missing source marker %q", startMarker) } end := strings.Index(source[start:], endMarker) if end < 0 { t.Fatalf("missing source marker %q after %q", endMarker, startMarker) } return source[start : start+end] } func TestSearchPaginationCursorUsesLastReturnedRow(t *testing.T) { source, err := os.ReadFile("tenant_adapter.go") if err != nil { t.Fatal(err) } body := string(source) for _, tc := range []struct { name string function string forbidden string required string }{ { name: "p2p message search", function: "func (a *adapter) TenantMessageSearch", forbidden: "nextCursor = strconv.FormatInt(msg.SeqId, 10)", required: "nextCursor = strconv.FormatInt(lastSeq, 10)", }, { name: "group message search", function: "func (a *adapter) tenantGroupMessageSearch", forbidden: "nextCursor = strconv.FormatInt(msg.SeqId, 10)", required: "nextCursor = strconv.FormatInt(lastSeq, 10)", }, { name: "favorite list", function: "func (a *adapter) TenantFavoriteList", forbidden: "nextCursor = strconv.FormatInt(item.ID, 10)", required: "nextCursor = strconv.FormatInt(lastID, 10)", }, } { fn := functionSource(t, body, tc.function) if strings.Contains(fn, tc.forbidden) { t.Fatalf("%s must not use the extra fetched row as next cursor", tc.name) } if !strings.Contains(fn, tc.required) { t.Fatalf("%s must use the last returned row as next cursor", tc.name) } } } func TestMessageSearchTextMatchesStructuredContent(t *testing.T) { msg := types.Message{ Content: types.KVMap{ "file": map[string]any{ "file_name": "项目方案.pdf", }, "link": map[string]any{ "title": "季度复盘", "summary": "增长分析", }, }, } for _, query := range []string{"项目方案", "季度", "增长 分析"} { if !messageMatchesSearch(msg, query) { t.Fatalf("structured message should match query %q, text=%q", query, messageSearchText(msg.Content)) } } if messageMatchesSearch(msg, "不存在") { t.Fatal("structured message matched unrelated query") } } func TestTenantFileAccessIncludesAvatarAudienceRules(t *testing.T) { source, err := os.ReadFile("tenant_adapter.go") if err != nil { t.Fatal(err) } body := string(source) for _, want := range []string{ "INNER JOIN %s AS fs", "fs.user_id=? AND fs.peer_id=fml.userid AND fs.status=?", "INNER JOIN im_group_members AS gm", "gm.topic=fml.topic AND gm.user_id=? AND gm.member_status=?", } { if !strings.Contains(body, want) { t.Fatalf("file ACL must include avatar audience rule %q", want) } } } func functionSource(t *testing.T, source, marker string) string { t.Helper() start := strings.Index(source, marker) if start < 0 { t.Fatalf("function %q not found", marker) } next := strings.Index(source[start+len(marker):], "\nfunc ") if next < 0 { return source[start:] } return source[start : start+len(marker)+next] }