package httpserver_test import ( "net/http" "testing" ) /* Subject access and erasure for long-term memory. The interesting assertions are the refusals: a route that lists what an agent inferred about a named person is a disclosure route, and it has to be gated on the same permission as the record itself. */ func TestMemoriesNeedASubject(t *testing.T) { a := newAPI(t) for _, path := range []string{ "/api/v1/memories", "/api/v1/memories?subject=everything", } { /* 422, which is this API's code for a well-formed request that cannot be acted on — see domain.Validation. */ if res := a.do(http.MethodGet, path, nil); res.code != http.StatusUnprocessableEntity { t.Errorf("GET %s = %d, want 422", path, res.code) } } } // A memory about a person that names no person cannot be produced for them, // so asking for "all candidate memories" is a mistake rather than a query. func TestAPersonalSubjectNeedsAnId(t *testing.T) { a := newAPI(t) res := a.do(http.MethodGet, "/api/v1/memories?subject=candidate", nil) if res.code != http.StatusUnprocessableEntity { t.Errorf("got %d, want 422", res.code) } } // Nothing held yet is an empty list, not an error: "we hold nothing about this // person" is a valid and important answer to a subject access request. func TestHoldingNothingIsAnEmptyList(t *testing.T) { a := newAPI(t) res := a.do(http.MethodGet, "/api/v1/memories?subject=candidate&id=11111111-1111-1111-1111-111111111111", nil) if res.code != http.StatusOK { t.Fatalf("got %d, want 200: %v", res.code, res.body) } data, ok := res.body["data"].([]any) if !ok && res.body["data"] != nil { t.Fatalf("data is not a list: %#v", res.body["data"]) } if len(data) != 0 { t.Errorf("got %d memories, want none", len(data)) } } // THE DISCLOSURE BOUNDARY. Somebody who may not read a candidate's // application must not be able to read what an agent inferred about them — // that is the same disclosure by another route. func TestAReaderWithoutTheRecordCannotReadItsMemories(t *testing.T) { a := newAPI(t) talent := signInAs(t, a.handler, a.h.Pool, a.orgID, "talent", "talent-mem@example.test", "talent") res := a.as(talent, http.MethodGet, "/api/v1/memories?subject=candidate&id=11111111-1111-1111-1111-111111111111", nil) if res.code != http.StatusForbidden { t.Errorf("got %d, want 403 — a talent read another person's inferred memories", res.code) } } func TestAReaderWithoutDeleteCannotErase(t *testing.T) { a := newAPI(t) talent := signInAs(t, a.handler, a.h.Pool, a.orgID, "talent", "talent-del@example.test", "talent") res := a.as(talent, http.MethodDelete, "/api/v1/memories?subject=candidate&id=11111111-1111-1111-1111-111111111111", nil) if res.code != http.StatusForbidden { t.Errorf("got %d, want 403", res.code) } } // "Erase every workspace memory" is a plausible thing to want and a // catastrophic thing to do by a mistyped query string. func TestErasingWorkspaceMemoriesNeedsAnExplicitId(t *testing.T) { a := newAPI(t) res := a.do(http.MethodDelete, "/api/v1/memories?subject=workspace", nil) if res.code != http.StatusUnprocessableEntity { t.Errorf("got %d, want 422 — a bare delete reached the whole workspace", res.code) } } // An erasure against nothing is still a successful erasure: the caller asked // for a state, and the state holds. func TestErasingNothingSucceeds(t *testing.T) { a := newAPI(t) res := a.do(http.MethodDelete, "/api/v1/memories?subject=candidate&id=11111111-1111-1111-1111-111111111111", nil) if res.code != http.StatusOK { t.Fatalf("got %d, want 200: %v", res.code, res.body) } } func TestMemoriesRefuseAnAnonymousCaller(t *testing.T) { a := newAPI(t) res := a.doAnon(http.MethodGet, "/api/v1/memories?subject=candidate&id=11111111-1111-1111-1111-111111111111", nil) if res.code != http.StatusUnauthorized { t.Errorf("got %d, want 401", res.code) } }