Files
MailUI4Agents/server/internal/repo/permission_mode_test.go

370 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repo
import (
"context"
"testing"
"github.com/agentmail/gateway/internal/models"
"github.com/google/uuid"
)
// ─── InheritedMode继承、收紧、不存在的父会话 ───
// parent nil → 返回 requested 的规范化值
func TestInheritedMode_NilParent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
got := InheritedMode(ctx, nil, "plan")
if got != models.ModePlan {
t.Errorf("nil parent + plan: got %q, want plan", got)
}
got = InheritedMode(ctx, nil, "workspace")
if got != models.ModeWorkspace {
t.Errorf("nil parent + workspace: got %q, want workspace", got)
}
got = InheritedMode(ctx, nil, "full")
if got != models.ModeFull {
t.Errorf("nil parent + full: got %q, want full", got)
}
}
// parent plan → 子会话只能 plan不能提权
func TestInheritedMode_ParentPlan_Tightens(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id := createTestSession(t, ctx, "pi", "/ws")
perm, err := SetSessionPermissionMode(ctx, id, models.ModePlan)
if err != nil {
t.Fatal(err)
}
if perm.Mode != models.ModePlan {
t.Fatal("expected plan")
}
// 请求 workspace更宽松→ 应被收紧为 plan
got := InheritedMode(ctx, &id, models.ModeWorkspace)
if got != models.ModePlan {
t.Errorf("plan parent + workspace request: got %q, want plan", got)
}
// 请求 full → 同样收紧
got = InheritedMode(ctx, &id, models.ModeFull)
if got != models.ModePlan {
t.Errorf("plan parent + full request: got %q, want plan", got)
}
// 请求 plan → 保持 plan
got = InheritedMode(ctx, &id, models.ModePlan)
if got != models.ModePlan {
t.Errorf("plan parent + plan request: got %q, want plan", got)
}
}
// parent workspace → 子会话 workspace 或更严
func TestInheritedMode_ParentWorkspace(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id := createTestSession(t, ctx, "pi", "/ws")
_, err := SetSessionPermissionMode(ctx, id, models.ModeWorkspace)
if err != nil {
t.Fatal(err)
}
// 请求 full → 收紧为 workspace子不能比父更松
got := InheritedMode(ctx, &id, models.ModeFull)
if got != models.ModeWorkspace {
t.Errorf("workspace parent + full: got %q, want workspace", got)
}
// 请求 plan → 保留 plan比父更严允许
got = InheritedMode(ctx, &id, models.ModePlan)
if got != models.ModePlan {
t.Errorf("workspace parent + plan: got %q, want plan", got)
}
}
// parent full → 子会话可请求任意档位
func TestInheritedMode_ParentFull(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id := createTestSession(t, ctx, "pi", "/ws")
_, err := SetSessionPermissionMode(ctx, id, models.ModeFull)
if err != nil {
t.Fatal(err)
}
got := InheritedMode(ctx, &id, models.ModeWorkspace)
if got != models.ModeWorkspace {
t.Errorf("full parent + workspace: got %q, want workspace", got)
}
got = InheritedMode(ctx, &id, models.ModePlan)
if got != models.ModePlan {
t.Errorf("full parent + plan: got %q, want plan", got)
}
}
// 脏值 fallback非法的 requested 值在 InheritedMode 里被规范化为默认档
func TestInheritedMode_InvalidRequested(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
got := InheritedMode(ctx, nil, "elephant")
if got != models.DefaultPermissionMode {
t.Errorf("invalid requested: got %q, want %q", got, models.DefaultPermissionMode)
}
}
// parent 不存在时查不到回落ModeAtMost(default, req)
func TestInheritedMode_InvalidParent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
fakeID := uuid.New()
got := InheritedMode(ctx, &fakeID, "full")
want := models.ModeAtMost(models.DefaultPermissionMode, "full")
if got != want {
t.Errorf("invalid parent + full: got %q, want %q (modeAtMost(default, full))", got, want)
}
}
// ─── SetSessionPermissionMode roundtrip + dirty value normalization ───
func TestSetSessionPermissionMode_Roundtrip(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "dsh", 20)
id := createTestSession(t, ctx, "dsh", "/ws")
perm, err := SetSessionPermissionMode(ctx, id, models.ModeFull)
if err != nil {
t.Fatal(err)
}
if perm.Mode != models.ModeFull || perm.Enforcement != "advisory" {
t.Errorf("full mode: got mode=%q enforcement=%q", perm.Mode, perm.Enforcement)
}
// 读出来一致
got := SessionPermissionMode(ctx, id)
if got != models.ModeFull {
t.Errorf("read back: got %q, want full", got)
}
}
func TestSetSessionPermissionMode_DirtyValue_FailClosed(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id := createTestSession(t, ctx, "pi", "/ws")
perm, err := SetSessionPermissionMode(ctx, id, "BOGUS")
if err != nil {
t.Fatal(err)
}
if perm.Mode != models.DefaultPermissionMode {
t.Errorf("dirty value: got %q, want %q (fail-closed to default)", perm.Mode, models.DefaultPermissionMode)
}
}
// ─── 脏值归一化测试(覆盖 NormalizePermissionMode 本身) ───
func TestNormalizePermissionMode_Inputs(t *testing.T) {
tests := []struct {
input string
want string
}{
{"plan", "plan"},
{"workspace", "workspace"},
{"full", "full"},
// 大小写/空白不归一NormalizePermissionMode 只接受精确匹配的合法档位,
// 其余一律 fail-closed 到默认档workspace—— 不 trim 不 lowercase
// 避免「我以为给了 plan 实际拿到别的」这种隐式转换造成的安全错觉。
{"Plan", models.DefaultPermissionMode},
{" PLAN ", models.DefaultPermissionMode},
{"", models.DefaultPermissionMode},
{"bogus", models.DefaultPermissionMode},
{"F ULL", models.DefaultPermissionMode},
}
for _, tt := range tests {
got := models.NormalizePermissionMode(tt.input)
if got != tt.want {
t.Errorf("NormalizePermissionMode(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
// ─── 负向对照plan 档不能提权 ───
func TestInheritedMode_PlanCannotEscalate(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
parent := createTestSession(t, ctx, "pi", "/ws")
_, _ = SetSessionPermissionMode(ctx, parent, models.ModePlan)
child := InheritedMode(ctx, &parent, models.ModeFull)
if child != models.ModePlan {
t.Errorf("SECURITY FAIL: plan session escalated to %q via InheritedMode", child)
}
}
// ─── 三层继承链 ───
func TestInheritedMode_ThreeLevelChain(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
root := createTestSession(t, ctx, "pi", "/ws")
_, _ = SetSessionPermissionMode(ctx, root, models.ModeFull)
child := InheritedMode(ctx, &root, models.ModeWorkspace) // workspace < full → workspace
childID := createTestSession(t, ctx, "pi", "/ws")
_, _ = SetSessionPermissionMode(ctx, childID, child)
grandchild := InheritedMode(ctx, &childID, models.ModeFull) // full vs workspace → workspace
if grandchild != models.ModeWorkspace {
t.Errorf("grandchild: got %q, want workspace", grandchild)
}
// plan → workspace → plan chain
planChild := InheritedMode(ctx, &root, models.ModePlan) // plan < full → plan
planChildID := createTestSession(t, ctx, "pi", "/ws")
_, _ = SetSessionPermissionMode(ctx, planChildID, planChild)
grandchild2 := InheritedMode(ctx, &planChildID, models.ModeFull) // full vs plan → plan
if grandchild2 != models.ModePlan {
t.Errorf("plan chain grandchild: got %q, want plan", grandchild2)
}
}
// ─── calendar_events permission_mode roundtrip ───
func TestCalendarEventPermissionMode_Roundtrip(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
// CreateCalendarEvent 应规范化档位
e := &models.CalendarEvent{
Title: "测试日程",
AgentName: "pi",
ToAddress: "pi@/home/program/agentmail",
PermissionMode: "full",
Status: "active",
CreatedBy: "jianf",
}
created, err := CreateCalendarEvent(ctx, e)
if err != nil {
t.Fatal(err)
}
if created.PermissionMode != "full" {
t.Errorf("created event permission_mode: got %q, want full", created.PermissionMode)
}
// 读回来一致
got, err := GetCalendarEvent(ctx, created.EventID)
if err != nil {
t.Fatal(err)
}
if got.PermissionMode != "full" {
t.Errorf("read back: got %q, want full", got.PermissionMode)
}
}
func TestCalendarEventPermissionMode_DirtyValue_Normalized(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
e := &models.CalendarEvent{
Title: "脏值日程",
AgentName: "pi",
ToAddress: "pi@/home/program/agentmail",
PermissionMode: "INVALID",
Status: "active",
CreatedBy: "jianf",
}
created, err := CreateCalendarEvent(ctx, e)
if err != nil {
t.Fatal(err)
}
if created.PermissionMode != models.DefaultPermissionMode {
t.Errorf("dirty value: got %q, want %q", created.PermissionMode, models.DefaultPermissionMode)
}
}
func TestCalendarEventPermissionMode_UpdateRoundtrip(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
e := &models.CalendarEvent{
Title: "更新日程",
AgentName: "pi",
ToAddress: "pi@/home/program/agentmail",
PermissionMode: "workspace",
Status: "active",
CreatedBy: "jianf",
}
created, err := CreateCalendarEvent(ctx, e)
if err != nil {
t.Fatal(err)
}
created.PermissionMode = "plan"
if err := UpdateCalendarEvent(ctx, created.EventID, created); err != nil {
t.Fatal(err)
}
got, err := GetCalendarEvent(ctx, created.EventID)
if err != nil {
t.Fatal(err)
}
if got.PermissionMode != "plan" {
t.Errorf("after update: got %q, want plan", got.PermissionMode)
}
}
// ─── adopt 接管时会话档位必须写入 ───
func TestAdoptPlatformSession_WritesDefaultMode(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, err := AdoptPlatformSession(ctx, "pi", "plat-123", "my-proj", "/ws", "接管测试")
if err != nil {
t.Fatal(err)
}
// 接管会话应显式写入默认档位(不是靠 DB 默认值)
mode := SessionPermissionMode(ctx, id)
if mode != models.DefaultPermissionMode {
t.Errorf("adopt session mode: got %q, want %q", mode, models.DefaultPermissionMode)
}
}
// ─── helpers ───
func createTestSession(t *testing.T, ctx context.Context, agent, workspace string) uuid.UUID {
t.Helper()
id, err := CreateSession(ctx, nil, agent, "test subject", workspace)
if err != nil {
t.Fatalf("create session: %v", err)
}
return id
}
// init 确保每个 test 函数执行前 DB 足够干净
func init() {
// 空 —— setupTestDB 在每个测试函数内调用
}