Files
goclaw/internal/http/vault_handler_upload_test.go
viettranx c8ebe9a789 refactor(comments): remove plan/phase refs from agent identity hardening code
Rewrite inline comments added during the agent identity hardening so
they explain the code as it stands today, rather than tying to internal
plan terminology (phase numbers, FR/NFR/H/M/C codes, PR references,
trap zone labels). Commit history already carries the plan archaeology.
Comments now keep the non-obvious invariants (cache boundaries, bypass
gaps, silent-nil traps, dual-tenant semantics) and drop the scaffolding.
Comment-only — no runtime behavior change.
2026-04-11 21:22:23 +07:00

140 lines
4.9 KiB
Go

package http
import (
"bytes"
"context"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// buildUploadRequest constructs a multipart POST request with optional form fields
// and one minimal file part. It does NOT attempt to reach storage — the handler's
// boundary UUID validation runs before any store or workspace access.
func buildUploadRequest(t *testing.T, fields map[string]string) *http.Request {
t.Helper()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for k, v := range fields {
if err := writer.WriteField(k, v); err != nil {
t.Fatalf("writer.WriteField(%q): %v", k, err)
}
}
// Minimal file part so ParseMultipartForm sees at least one file.
part, err := writer.CreateFormFile("files", "note.md")
if err != nil {
t.Fatalf("CreateFormFile: %v", err)
}
if _, err := part.Write([]byte("hello")); err != nil {
t.Fatalf("part.Write: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("writer.Close: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/vault/upload", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
return req
}
// assertBadRequest decodes a JSON error response and checks the HTTP status
// is 400 and the error message contains the expected substring.
func assertBadRequest(t *testing.T, rr *httptest.ResponseRecorder, wantFragment string) {
t.Helper()
if rr.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d (body: %s)", rr.Code, http.StatusBadRequest, rr.Body.String())
}
var payload map[string]string
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode json: %v (body: %s)", err, rr.Body.String())
}
msg, ok := payload["error"]
if !ok {
t.Fatalf("response missing `error` field: %s", rr.Body.String())
}
if !strings.Contains(msg, wantFragment) {
t.Errorf("error = %q, want fragment %q", msg, wantFragment)
}
}
// TestHandleUpload_InvalidAgentIDReturns400 asserts bad form `agent_id` is
// rejected at the HTTP boundary before any store call. Closes the
// owner / lite edition gap where validateTeamMembership would skip the UUID
// check.
func TestHandleUpload_InvalidAgentIDReturns400(t *testing.T) {
h := &VaultHandler{} // no store wired — boundary check runs first
req := buildUploadRequest(t, map[string]string{
"agent_id": "goctech-leader", // agent_key, not UUID
})
rr := httptest.NewRecorder()
h.handleUpload(rr, req)
assertBadRequest(t, rr, "invalid agent_id")
}
// TestHandleUpload_InvalidTeamIDReturns400 asserts bad form `team_id` is
// rejected at the HTTP boundary. validateTeamMembership short-circuits on
// owner role and on nil teamAccess (lite edition), never parsing the UUID —
// the boundary check closes that hole.
func TestHandleUpload_InvalidTeamIDReturns400(t *testing.T) {
h := &VaultHandler{} // lite edition: teamAccess is nil
req := buildUploadRequest(t, map[string]string{
"team_id": "my-team-key", // team_key, not UUID
})
rr := httptest.NewRecorder()
h.handleUpload(rr, req)
assertBadRequest(t, rr, "invalid team_id")
}
// TestHandleUpload_InvalidAgentID_OwnerContext asserts the boundary check
// fires regardless of role. The UUID hole existed for every caller — the
// boundary check ignores role entirely.
func TestHandleUpload_InvalidAgentID_OwnerContext(t *testing.T) {
h := &VaultHandler{}
req := buildUploadRequest(t, map[string]string{
"agent_id": "definitely-not-a-uuid",
})
// Simulate owner role context — the boundary check ignores role entirely.
ctx := context.WithValue(req.Context(), ownerContextKey{}, true)
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
h.handleUpload(rr, req)
assertBadRequest(t, rr, "invalid agent_id")
}
// TestHandleUpload_EmptyAgentIDAndTeamID_NoBoundaryError asserts the boundary
// check skips empty strings (shared-scope uploads). It should proceed to the
// next validation layer (workspace resolution) — which will fail with a
// different error since our test VaultHandler has no workspace. The important
// thing is that the boundary check itself does NOT return 400 with "invalid
// agent_id" or "invalid team_id" for empty values.
func TestHandleUpload_EmptyAgentIDAndTeamID_BoundaryCheckSkipped(t *testing.T) {
h := &VaultHandler{}
req := buildUploadRequest(t, map[string]string{})
rr := httptest.NewRecorder()
h.handleUpload(rr, req)
// Expect a failure AFTER boundary validation (e.g. workspace missing),
// not the boundary UUID validation errors.
if rr.Code == http.StatusBadRequest {
body := rr.Body.String()
if strings.Contains(body, "invalid agent_id") || strings.Contains(body, "invalid team_id") {
t.Errorf("boundary check fired on empty input (should skip): body=%s", body)
}
}
}
// ownerContextKey is a private test helper for simulating owner role in
// context. The real context key type lives in internal/store; we only need
// to ensure the boundary check fires independent of role-related context.
type ownerContextKey struct{}