package handler /* C 层:admin 全量邮件可见性的判据(2026-10-04)。 用户要求:「admin 查看全部邮件,普通用户仅查看与自己相关或显式分享的邮件」。 ★ 盯的是**分层可见性**,不是「scope 参数存在」: 1. 非 admin 显式请求 scope=all ⇒ **403**,不是静默回退到自己的收件箱。 静默降级会让调用方以为拿到了全量 —— 那是「看起来能用的错答案」。 2. 默认(无 scope)⇒ 仍是自己��收件箱,一封都不多给。 ★ 这条与 Agent 侧的收窄(15e4fe9 / 095213b 修的越权防护)无关 —— 那是 AgentAuth 通道;本文件是人���登录态。别拿「admin 要全看」 当理由去放宽 ListInboxScoped。 3. 非法 scope ⇒ 400,不是当成没给。 */ import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/agentmail/gateway/internal/db" "github.com/agentmail/gateway/internal/middleware" "github.com/agentmail/gateway/internal/models" "github.com/google/uuid" ) func setupAdminScopeDB(t *testing.T) { t.Helper() db.Close() path := filepath.Join(t.TempDir(), "admin-scope.db") if err := db.Connect(context.Background(), "sqlite://"+path); err != nil { t.Fatalf("连接测试库: %v", err) } if err := db.Migrate(context.Background()); err != nil { t.Fatalf("迁移: %v", err) } t.Cleanup(db.Close) } // seed 两封信:一封给 admin,一封只给普通用户。 func seedScopeMails(t *testing.T) (adminMailID, userMailID string) { t.Helper() ctx := context.Background() for _, u := range []string{"boss", "intern"} { if _, err := db.DB.ExecContext(ctx, `INSERT INTO users (username, password_hash, role) VALUES ($1,'x',$2)`, u, map[string]string{"boss": "admin", "intern": "user"}[u]); err != nil { t.Fatalf("建用户 %s: %v", u, err) } } var sid string if err := db.DB.QueryRowContext(ctx, `INSERT INTO sessions (session_alias, subject, status, workspace, from_agent) VALUES ('s','x','active','/tmp','pi') RETURNING session_id`).Scan(&sid); err != nil { t.Fatalf("建会话: %v", err) } // ★ mail_id 是 **UUID**:初版这里塞了 "m-admin" 这种字符串, // 报出来是 `Scan: invalid UUID length: 7` —— 看着像 SQL/扫描代码坏了, // 其实是判据自己的数据不对。判据数据错了会伪装成被测代码坏了。 for _, spec := range []struct{ tag, to, subj, at string }{ {"admin", "boss", "给管理员的信", "2026-01-02T00:00:00Z"}, {"intern", "intern", "只给普通用户的信", "2026-01-01T00:00:00Z"}, } { mid := uuid.New() if _, err := db.DB.ExecContext(ctx, `INSERT INTO mails (mail_id, session_id, from_name, from_workspace, to_name, to_workspace, subject, body, mail_type, created_at) VALUES ($1,$2,'pi','/tmp',$3,'/tmp',$4,'b','normal',$5)`, mid, sid, spec.to, spec.subj, spec.at, ); err != nil { t.Fatalf("建邮件 %s: %v", spec.tag, err) } if spec.tag == "admin" { adminMailID = mid.String() } else { userMailID = mid.String() } } return adminMailID, userMailID } func reqAsUser(t *testing.T, u models.User, url string) *httptest.ResponseRecorder { t.Helper() rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, url, nil) req = req.WithContext(context.WithValue(req.Context(), middleware.UserKey, &u)) MeGetInbox(rec, req) return rec } func TestAdminScopeAllSeesEverything(t *testing.T) { setupAdminScopeDB(t) _, userMailID := seedScopeMails(t) admin := models.User{Username: "boss", Role: "admin"} rec := reqAsUser(t, admin, "/api/v1/me/mail/inbox?scope=all") if rec.Code != http.StatusOK { t.Fatalf("admin scope=all 应 200,实际 %d:%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "只给普通用户的信") { t.Errorf("★ admin scope=all 应看到别人的信,实际:%s", rec.Body.String()) } _ = userMailID } func TestNonAdminScopeAllIs403NotSilentlyNarrowed(t *testing.T) { setupAdminScopeDB(t) adminMailID, _ := seedScopeMails(t) intern := models.User{Username: "intern", Role: "user"} rec := reqAsUser(t, intern, "/api/v1/me/mail/inbox?scope=all") // ★ 403 而不是 200:静默降级会让调用方以为拿到了全量 if rec.Code != http.StatusForbidden { t.Fatalf("★ 普通用户 scope=all 应 403,实际 %d:%s", rec.Code, rec.Body.String()) } if strings.Contains(rec.Body.String(), "给管理员的信") { t.Errorf("★ 403 的响应体里不该带任何邮件内容:%s", rec.Body.String()) } _ = adminMailID } func TestDefaultScopeStaysOwnInbox(t *testing.T) { setupAdminScopeDB(t) adminMailID, _ := seedScopeMails(t) // ★ 连 admin 在**默认**(无 scope)下也只能看自己的 —— // 「admin 全看」必须显式要求,不能靠身份隐式获得。 admin := models.User{Username: "boss", Role: "admin"} rec := reqAsUser(t, admin, "/api/v1/me/mail/inbox") if rec.Code != http.StatusOK { t.Fatalf("默认应 200,实际 %d:%s", rec.Code, rec.Body.String()) } if strings.Contains(rec.Body.String(), "只给普通用户的信") { t.Errorf("★ 默认 scope 下 admin 也不该看到别人的信(须显式 scope=all):%s", rec.Body.String()) } if !strings.Contains(rec.Body.String(), "给管理员的信") { t.Errorf("自己的信反而没了:%s", rec.Body.String()) } _ = adminMailID } func TestBadScopeIs400(t *testing.T) { setupAdminScopeDB(t) seedScopeMails(t) admin := models.User{Username: "boss", Role: "admin"} rec := reqAsUser(t, admin, "/api/v1/me/mail/inbox?scope=bogus") if rec.Code != http.StatusBadRequest { t.Errorf("非法 scope 应 400(不是当成没给),实际 %d:%s", rec.Code, rec.Body.String()) } }