package main import ( "errors" "net/http/httptest" "reflect" "strings" "testing" "github.com/tinode/chat/server/store/types" ) func TestLoginAuditAccountDoesNotTreatTokenAsAccount(t *testing.T) { kind, account := loginAuditAccount("token", "secret-token", "") if kind != "" || account != "" { t.Fatalf("token must not be persisted as an account: kind=%q account=%q", kind, account) } kind, account = loginAuditAccount("basic", "13800138000", "") if kind != "phone" || account != "13800138000" { t.Fatalf("unexpected basic account classification: kind=%q account=%q", kind, account) } } func TestHashLoginAccount(t *testing.T) { hash := hashLoginAccount(" Alice ") if len(hash) != 64 || hash != hashLoginAccount("alice") || strings.Contains(hash, "alice") { t.Fatalf("unexpected account hash %q", hash) } } func TestClassifyLoginFailure(t *testing.T) { tests := []struct { scheme string err error want string }{ {"basic", types.ErrFailed, "password_error"}, {"sms_code", types.ErrFailed, "code_error"}, {"basic", types.ErrUserNotFound, "account_not_found"}, {"basic", types.ErrPermissionDenied, "account_disabled"}, {"basic", types.ErrPolicy, "risk_blocked"}, {"basic", errors.New("too many attempts"), "rate_limited"}, } for _, tc := range tests { got, _, _, _ := classifyLoginFailure(tc.scheme, tc.err) if got != tc.want { t.Errorf("classifyLoginFailure(%q,%v)=%q want %q", tc.scheme, tc.err, got, tc.want) } } } func TestMessageQuerySpecsExposeAdminFields(t *testing.T) { privateSpec := privateMessagesInternalQuerySpec() groupSpec := groupMessagesInternalQuerySpec() for label, spec := range map[string]*internalQuerySpec{"private": privateSpec, "group": groupSpec} { joined := strings.ToLower(spec.Select + " " + spec.From) for _, field := range []string{"from_user_id", "content_type", "content_excerpt", "status", "im_message_admin_actions"} { if !strings.Contains(joined, field) { t.Errorf("%s query missing %s", label, field) } } } if strings.Contains(strings.ToLower(privateSpec.Select), "m.content as content") || strings.Contains(strings.ToLower(groupSpec.Select), "m.content as content") { t.Fatal("message list query must not expose full message content") } if !strings.Contains(privateSpec.Select, "CAST(m.`from` AS CHAR) AS from_user_id") || !strings.Contains(privateSpec.Select, "CAST(rs.userid AS CHAR) AS to_user_id") { t.Fatal("private message query must expose 64-bit user IDs as strings") } if !strings.Contains(groupSpec.Select, "CAST(m.from_user AS CHAR) AS from_user_id") { t.Fatal("group message query must expose 64-bit user IDs as strings") } } func TestGroupMessageQuerySupportsOperationalFilters(t *testing.T) { req := httptest.NewRequest("GET", "/?seq_id=42&sender_username=alice&sender_nickname=%E5%B0%8F%E6%98%8E&group_name=%E6%B5%8B%E8%AF%95%E7%BE%A4", nil) query, _, args, _, _, err := buildInternalAdminListQuery(req, groupMessagesInternalQuerySpec(), "") if err != nil { t.Fatal(err) } for _, clause := range []string{"m.seqid = ?", "u.username LIKE ?", "p.nickname LIKE ?", "gs.name LIKE ?"} { if !strings.Contains(query, clause) { t.Errorf("group message query missing %q: %s", clause, query) } } want := []any{int64(42), "%alice%", "%小明%", "%测试群%"} if !reflect.DeepEqual(args, want) { t.Fatalf("unexpected filter arguments: got %#v, want %#v", args, want) } } func TestLoginAuditQuerySpecsExposeUserIDAsString(t *testing.T) { for label, spec := range map[string]*internalQuerySpec{ "user-login-logs": internalTenantResourceSpecs["user-login-logs"], "login-failure-logs": internalTenantResourceSpecs["login-failure-logs"], } { if spec == nil { t.Fatalf("%s query spec missing", label) } if !strings.Contains(spec.Select, "CAST(user_id AS CHAR) AS user_id") { t.Fatalf("%s query must expose 64-bit user IDs as strings: %s", label, spec.Select) } } } func TestUserQuerySupportsKeywordAndUserIDs(t *testing.T) { req := httptest.NewRequest("GET", "/?keyword=alice&user_ids=101,202&account_no=1000", nil) query, countQuery, args, _, _, err := buildInternalAdminListQuery(req, usersInternalQuerySpec(), "") if err != nil { t.Fatal(err) } for _, clause := range []string{"CONCAT_WS(' ',u.username,p.nickname,u.phone,CAST(u.account_no AS CHAR)) LIKE ?", "u.id IN (?,?)", "u.account_no = ?"} { if !strings.Contains(query, clause) || !strings.Contains(countQuery, clause) { t.Fatalf("user query missing %q: %s", clause, query) } } want := []any{int64(101), int64(202), int64(1000), "%alice%"} if !reflect.DeepEqual(args, want) { t.Fatalf("unexpected filter arguments: got %#v, want %#v", args, want) } for _, field := range []string{ "CAST(u.id AS CHAR) AS user_id", "AS avatar", "u.account_no AS display_no", "AS register_type", "im_registration_invite_uses", "im_fileuploads AS avatar", "p.avatar_file_id", "im_user_profiles AS p", } { if !strings.Contains(usersInternalQuerySpec().Select+usersInternalQuerySpec().From, field) { t.Fatalf("user query missing list field %q", field) } } } func TestRedactClientMessageForLog(t *testing.T) { raw := []byte(`{"login":{"id":"1","scheme":"basic","secret":"YWxpY2U6YWxpY2UxMjM="},"acc":{"secret":"account-secret","tmpsecret":"temp-secret"}}`) redacted := string(redactClientMessageForLog(raw)) for _, secret := range []string{"YWxpY2U6YWxpY2UxMjM=", "account-secret", "temp-secret"} { if strings.Contains(redacted, secret) { t.Fatalf("redacted log still contains secret %q: %s", secret, redacted) } } if strings.Count(redacted, "[REDACTED]") != 3 { t.Fatalf("unexpected redacted payload: %s", redacted) } }