Files
im_server/server/auth_audit_test.go
T
Austin 6bbe955e75 feat: 新增共库模式下登录失败日志表及相关功能
- 新增登录失败日志表(共库多租户版),添加 tenant_id 列实现租户隔离
- 调整登录失败日志逻辑,仅记录触发账号锁定的事件
- 新增管理端接口功能,为登录失败日志补充实时锁定状态
- 更新测试用例适配共库模式下的租户过滤逻辑
2026-09-23 13:18:28 +08:00

154 lines
5.9 KiB
Go

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(), "", types.TenantID(1))
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(1), int64(42), "%alice%", "%小明%", "%测试群%"}
if !reflect.DeepEqual(args, want) {
t.Fatalf("unexpected filter arguments: got %#v, want %#v", args, want)
}
}
func TestLoginAuditQuerySpecsExposeUserIDAsString(t *testing.T) {
spec := internalTenantResourceSpecs["user-login-logs"]
if spec == nil {
t.Fatal("user-login-logs query spec missing")
}
if !strings.Contains(spec.Select, "CAST(user_id AS CHAR) AS user_id") {
t.Fatalf("user-login-logs query must expose 64-bit user IDs as strings: %s", spec.Select)
}
// login-failure-logs 保留(仅记录触发锁定的事件,见 session.go lockedNow 分支),
// 但共库模式必须带 tenant_id 隔离列,防止跨租户看到彼此的失败日志。
failureSpec := internalTenantResourceSpecs["login-failure-logs"]
if failureSpec == nil {
t.Fatal("login-failure-logs query spec missing")
}
if failureSpec.TenantColumn != "tenant_id" {
t.Fatalf("login-failure-logs query spec must filter by tenant_id: %+v", failureSpec)
}
}
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(), "", types.TenantID(1))
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(1), 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)
}
}