//go:build mysql package mysql import ( "os" "strings" "testing" "github.com/tinode/chat/server/store/types" ) func TestCanUseManagedReportsRequiresAllOrganizationVisibility(t *testing.T) { if !canUseManagedReports(organizationVisibility{Scope: orgVisibilityAll}) { t.Fatal("all visibility should be allowed to use managed report scope") } for _, scope := range []string{orgVisibilitySelf, orgVisibilityFriends, ""} { if canUseManagedReports(organizationVisibility{Scope: scope}) { t.Fatalf("scope %q should not be allowed to use managed report scope", scope) } } } func TestReportMemberScopeWhereIncludesDepartmentAndVisibility(t *testing.T) { where, args := reportMemberScopeWhere(organizationVisibility{Scope: orgVisibilityFriends, UserID: 42}, "dep_ops") joined := strings.Join(where, " AND ") if !strings.Contains(joined, "m.department_id=?") { t.Fatalf("department filter missing from where: %s", joined) } if !strings.Contains(joined, "im_friendships") { t.Fatalf("friend visibility filter missing from where: %s", joined) } if len(args) != 4 || args[0] != "dep_ops" || args[1] != int64(42) || args[2] != int64(42) || args[3] != types.RelationshipFriend { t.Fatalf("unexpected args: %#v", args) } } 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", "ensureIMAttendanceTables", "ensureIMMeetingNotesTables", "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 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 im_friendships 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] }