Add red tests for EXT-8b read-path consumption
Why: U9 第二切片——server 读面须按资源级六要素咨询 V2(红:SetResourceAuthorizerV2 未定义)。 Evidence: go vet 编译红(槽位/消费缺失);断言下载/list/建会话三路径查询要素、V2 拒绝压过 V1、V2 缺席回落 V1。 Impact: 仅测试。 Refs: Backend#178;方案 r1 U9
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package server
|
||||
|
||||
// EXT-8b 读面消费红测(方案 r1 U9 第二切片):五处共享读路径在 V2 注册后按
|
||||
// 资源级查询咨询(Actor/Action/ResourceType/ResourceID/Owner/ProjectID 由
|
||||
// 受信上下文构造);V2 拒绝压过 V1 放行(资源级权威);V2 未注册时五处全部
|
||||
// 回落 V1(兼容现行,RDM 现网行为不变)。红:当前 server 尚无 V2 槽位与
|
||||
// AuthorizeResource 消费——注册接口不存在,编译红先行。
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/store"
|
||||
"agent-system-backend/pkg/extension"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
func newV2CaptureServer(t *testing.T) (*Server, *store.Store, string, string, string) {
|
||||
t.Helper()
|
||||
srv, st := newAuthTestServer(t)
|
||||
alice, err := st.CreateAPIKey("alice", "e2e")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bob, err := st.CreateAPIKey("bob", "e2e")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pj, err := st.CreateProject("alice", "资源级项目")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return srv, st, alice, bob, pj.ID
|
||||
}
|
||||
|
||||
func TestSharedReadPathsConsultV2WithResourceContext(t *testing.T) {
|
||||
srv, st, alice, bob, pid := newV2CaptureServer(t)
|
||||
_ = alice
|
||||
|
||||
// 项目内造一个工件(供 download 路径取元数据)。
|
||||
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "res.txt", Kind: "file", MIME: "text/plain"}
|
||||
if err := st.CreateArtifact(art, []byte("body")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var got []extension.ResourceAuthzQuery
|
||||
srv.SetResourceAuthorizerV2(func(q extension.ResourceAuthzQuery) (bool, error) {
|
||||
got = append(got, q)
|
||||
return true, nil
|
||||
})
|
||||
h := srv.Handler()
|
||||
call := func(method, path string) int {
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+bob)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(&rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
// ① 工件下载(owner 自有未命中 → 资源级咨询)。
|
||||
code := call(http.MethodGet, "/api/projects/"+pid+"/artifacts/"+string(art.ID)+"/download")
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("download: %d", code)
|
||||
}
|
||||
// ② 项目工件清单。
|
||||
call(http.MethodGet, "/api/projects/"+pid+"/artifacts")
|
||||
// ③ 共享项目建会话。
|
||||
call(http.MethodPost, "/api/projects/"+pid+"/conversations", nil)
|
||||
|
||||
byAction := map[string]extension.ResourceAuthzQuery{}
|
||||
for _, q := range got {
|
||||
byAction[q.Action] = q
|
||||
}
|
||||
dl := byAction["download"]
|
||||
if dl.Actor != "bob" || dl.ResourceType != "artifact" || dl.ResourceID != art.ID || dl.ProjectID != pid || dl.Owner != "alice" {
|
||||
t.Fatalf("download 查询六要素不符: %+v", dl)
|
||||
}
|
||||
ls := byAction["list"]
|
||||
if ls.Actor != "bob" || ls.ResourceType != "project" || ls.ResourceID != pid || ls.ProjectID != pid {
|
||||
t.Fatalf("list 查询不符: %+v", ls)
|
||||
}
|
||||
rd := byAction["read"]
|
||||
if rd.Actor != "bob" || rd.ResourceType != "project" || rd.ResourceID != pid {
|
||||
t.Fatalf("read(conversation) 查询不符: %+v", rd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2DenyOverridesV1Allow(t *testing.T) {
|
||||
srv, st, alice, bob, pid := newV2CaptureServer(t)
|
||||
_ = alice
|
||||
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "d.txt", Kind: "file"}
|
||||
if err := st.CreateArtifact(art, []byte("body")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// V1 全放行、V2 全拒绝——资源级权威必须压过 V1。
|
||||
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
srv.SetResourceAuthorizerV2(func(q extension.ResourceAuthzQuery) (bool, error) {
|
||||
return false, nil
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects/"+pid+"/artifacts/"+string(art.ID)+"/download", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+bob)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(&rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("V2 拒绝须压过 V1 放行并保持 404(不泄露存在性), got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2AbsentFallsBackToV1(t *testing.T) {
|
||||
srv, st, alice, bob, pid := newV2CaptureServer(t)
|
||||
_ = alice
|
||||
art := &core.Artifact{ProjectID: core.ID(pid), Owner: "alice", Name: "f.txt", Kind: "file"}
|
||||
if err := st.CreateArtifact(art, []byte("body")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
|
||||
return owner == "bob" && string(projectID) == pid, nil
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects/"+pid+"/artifacts/"+string(art.ID)+"/download", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+bob)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(&rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("V2 未注册应回落 V1(现行行为), got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user