Files
weichen 3e01614939 Require password lineage and make recovery retries metadata-only
Why: Historical full sessions could reach self-profile actions, and a lost recovery response had no safe idempotent retry contract.

Evidence: SELF receipts /tmp/iam-u3-full-password-profile.json, /tmp/iam-u3-recovery-idempotent-sqlite.json, /tmp/iam-u3-recovery-idempotent-pg.json, /tmp/iam-u3-account-sse-pass.json, /tmp/iam-u3-backend-idempotent-all.json, and /tmp/iam-u3-backend-idempotent-pg.json.

Impact: Profile actions require a password-authenticated full cookie; duplicate owner confirmation or rotation returns status without plaintext; an active SSE stream stops before its next business frame after disable.

Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
2026-10-14 04:58:31 +00:00

79 lines
2.2 KiB
Go

package server
import (
"bufio"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
core "agent-system-core"
)
func TestAccountDisableStopsNextSSEBusinessFrame(t *testing.T) {
srv, st := newAuthTestServer(t)
if err := st.CreateAccount("admin", "", "admin-hash", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("sse-user", "", "user-hash", false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("sse-user"); err != nil {
t.Fatal(err)
}
user, err := st.GetAccountByOwner("sse-user")
if err != nil {
t.Fatal(err)
}
project, err := st.CreateProject("sse-user", "stream")
if err != nil {
t.Fatal(err)
}
conv, err := st.CreateConversation("sse-user", project.ID, "stream", "")
if err != nil {
t.Fatal(err)
}
selected := make(chan core.StreamEvent, 1)
srv.Subscribe = func(string) (<-chan core.StreamEvent, func()) { return selected, func() {} }
token, err := st.CreateSession("sse-user")
if err != nil {
t.Fatal(err)
}
httpServer := httptest.NewServer(srv.Handler())
defer httpServer.Close()
request, err := http.NewRequest(http.MethodGet, httpServer.URL+"/api/events?conversationId="+conv.ID, nil)
if err != nil {
t.Fatal(err)
}
request.AddCookie(&http.Cookie{Name: SessionCookie, Value: token})
client := &http.Client{Timeout: 3 * time.Second}
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
reader := bufio.NewReader(response.Body)
first, err := reader.ReadString('\n')
if err != nil || !strings.HasPrefix(first, "retry:") {
t.Fatalf("SSE did not open: %q %v", first, err)
}
if _, err := st.SetAccountEnabled("admin", user.ID, user.StateVersion, false); err != nil {
t.Fatal(err)
}
selected <- core.StreamEvent{ConversationID: conv.ID, Seq: 1, Type: core.EvToolOutput,
Payload: json.RawMessage(`{"text":"must-not-stream"}`)}
tail, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("revoked stream did not close: %v", err)
}
if strings.Contains(string(tail), "must-not-stream") {
t.Fatal("disabled account received the next SSE business frame")
}
if _, _, ok := st.SessionInfo(token); ok {
t.Fatal("disabled account session still authenticates")
}
}