Files
mesh-catalog/modules/mesh-mail/cmd/mesh-mail/ask_test.go
T

219 lines
9.4 KiB
Go

package main
// mail_send raises its ask on the operator channel, and messenger's content rule (ADR 0253 §3) refuses any
// message that carries a mail address, a URL or a bare host name. ADR 0361 §2 needs the ask to show the
// account, the recipients, the subject and the first lines regardless — so the ask must say all of that in
// a form the content rule lets through (novox/hq issue 868, "Decided" note and review round 1). These tests
// build real asks through mail_send and judge them with the real rule, copied verbatim as content_rule.go.
import (
"encoding/json"
"strings"
"testing"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// wholeAsk is every word the operator can ever see for one ask: its Headline, Explanation and Details
// together. Useful for a readability check (nothing unmasked is visible anywhere), but never for judging
// whether messenger would refuse the ask as a whole — judgedWords is that (novox/hq issue 868, review
// round 1): Details is filtered line by line, not judged with the rest (holder.go's sayOn).
func wholeAsk(r *sendRouter, id string) string {
a := r.published[id]
return a.Headline + "\n" + a.Explanation + "\n" + a.Details
}
// judgedWords is exactly the text messenger refuses (or lets through) an ask as a whole on (asks.go's
// TakeAsk): its headline, its explanation, what happens at expiry, and each option's label and effect.
// Details is never part of it.
func judgedWords(a oc.Ask) string {
words := []string{a.Headline, a.Explanation, a.OnExpiry}
for _, o := range a.Options {
words = append(words, o.Label, o.Does)
}
return strings.Join(words, "\n")
}
// mustNotBeRefused fails the test naming what the content rule refused, and why — judged the way messenger
// itself judges an ask as a whole, on judgedWords alone.
func mustNotBeRefused(t *testing.T, a oc.Ask) {
t.Helper()
if refusal, ok := Check(judgedWords(a)); !ok {
t.Fatalf("the content rule refuses the ask: %s\n%s", refusal, judgedWords(a))
}
}
// (1) A recipient that is one of the operator's own mesh-mail accounts (sendTestBundle's "mesh", address
// jschoubben@novox.be) is named by the account's name alone: the ask carries no mail address at all, and
// passes the real content rule. This is the first test issue 868's decision asked for; it fails today
// because AskMailSend puts the raw address into the ask.
func TestMailSendAskForOwnAccountRecipientCarriesNoAddress(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
args := map[string]any{"account": "mesh", "to": []any{"jschoubben@novox.be"},
"subject": "Testing issue 868", "body": "Does the ask reach the operator now?"}
text, isErr := rt.call(SendTool, "novox.triage", args)
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
if json.Unmarshal([]byte(text), &out) != nil {
t.Fatalf("not JSON: %s", text)
}
id, _ := out["ask"].(string)
if id == "" {
t.Fatalf("%s", text)
}
whole := wholeAsk(router, id)
if strings.Contains(whole, "@") {
t.Errorf("the ask carries a mail address for its own account recipient:\n%s", whole)
}
if !strings.Contains(whole, "your account mesh") {
t.Errorf("the ask does not name the own account:\n%s", whole)
}
mustNotBeRefused(t, router.published[id])
}
// (2) A recipient outside the operator's own accounts is said without its address or host: its first
// letter, an ellipsis and the domain's labels as words.
func TestMailSendAskForOutsideRecipientCarriesNoAddress(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
text, isErr := rt.call(SendTool, "novox.triage", sendArgs) // to: ops@example.org, lead@example.org
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
id := out["ask"].(string)
whole := wholeAsk(router, id)
if strings.Contains(whole, "@") || strings.Contains(whole, "example.org") {
t.Errorf("the ask carries the recipients' address or host:\n%s", whole)
}
for _, want := range []string{"o… at example org", "l… at example org"} {
if !strings.Contains(whole, want) {
t.Errorf("the ask does not say %q:\n%s", want, whole)
}
}
mustNotBeRefused(t, router.published[id])
}
// (3) More than three recipients are counted rather than said one by one.
func TestMailSendAskForManyRecipientsCountsTheRest(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
args := map[string]any{"account": "mesh",
"to": []any{"a@example.org", "b@example.org", "c@example.org", "d@example.org", "e@example.org"},
"subject": "Many recipients", "body": "body"}
text, isErr := rt.call(SendTool, "novox.triage", args)
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
id := out["ask"].(string)
whole := wholeAsk(router, id)
if strings.Contains(whole, "@") {
t.Errorf("the ask carries a mail address among many recipients:\n%s", whole)
}
if !strings.Contains(whole, "and 2 more") {
t.Errorf("the ask does not count the recipients past the first three:\n%s", whole)
}
mustNotBeRefused(t, router.published[id])
}
// (4) A subject holding a mail address and a link has both masked before the ask carries it.
func TestMailSendAskMasksAnAddressAndALinkInTheSubject(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"},
"subject": "Contact ops@example.org or see https://example.com/issue/42",
"body": "See https://example.com/issue/42 and write to ops@example.org for details."}
text, isErr := rt.call(SendTool, "novox.triage", args)
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
id := out["ask"].(string)
whole := wholeAsk(router, id)
if strings.Contains(whole, "@") || strings.Contains(whole, "example.com") || strings.Contains(whole, "://") {
t.Errorf("the ask carries an unmasked address or link:\n%s", whole)
}
if !strings.Contains(whole, "[address]") || !strings.Contains(whole, "[link]") {
t.Errorf("the ask does not say [address] and [link] in place of what it masked:\n%s", whole)
}
mustNotBeRefused(t, router.published[id])
}
// (5) review round 1: a subject the masking does not catch (an IP, a two-slash path-shaped reference, a
// 40-character hex hash) does not refuse the whole ask: the real content rule (content_rule.go) still judges
// the masked explanation as refused, so the subject and first lines are left out of it and pointed at Details
// instead — the ask still names the account and recipients, and still reaches the operator.
func TestMailSendAskFallsBackToDetailsWhenTheSubjectStillRefuses(t *testing.T) {
for _, subject := range []string{
"Invoice 2026/10/15",
"server 10.0.0.1",
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", // 40 hex characters: reHex's shape
} {
t.Run(subject, func(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"}, "subject": subject, "body": "body"}
text, isErr := rt.call(SendTool, "novox.triage", args)
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
id := out["ask"].(string)
a := router.published[id]
mustNotBeRefused(t, a)
if !strings.Contains(a.Explanation, "mesh") {
t.Errorf("the explanation does not name the account:\n%s", a.Explanation)
}
if !strings.Contains(a.Explanation, "o… at example org") {
t.Errorf("the explanation does not name the recipient:\n%s", a.Explanation)
}
if strings.Contains(a.Explanation, subject) {
t.Errorf("the explanation still carries the subject that refused the ask:\n%s", a.Explanation)
}
if !strings.Contains(a.Details, "Details") && !strings.Contains(a.Explanation, "Details") {
t.Errorf("the explanation does not point at Details:\n%s", a.Explanation)
}
if !strings.Contains(a.Details, subject) {
t.Errorf("Details does not carry the subject (filtered line by line by messenger, as the subject's own line): %s", a.Details)
}
})
}
}
// (6) review round 1: a recipient written with a display name ("Name <addr>") is matched against the
// operator's own accounts, and masked, by its address alone.
func TestMailSendAskMatchesADisplayNameRecipientByItsAddress(t *testing.T) {
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
rt := startBundle(t, b.Tools())
args := map[string]any{"account": "mesh", "to": []any{"Jochen Schoubben <jschoubben@novox.be>"},
"subject": "Testing a display name", "body": "body"}
text, isErr := rt.call(SendTool, "novox.triage", args)
if isErr {
t.Fatalf("%s", text)
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
id := out["ask"].(string)
whole := wholeAsk(router, id)
if strings.Contains(whole, "@") || strings.Contains(whole, "Jochen Schoubben") {
t.Errorf("the ask carries the display-name recipient's address or name:\n%s", whole)
}
if !strings.Contains(whole, "your account mesh") {
t.Errorf("the ask does not match the display-name recipient to its own account:\n%s", whole)
}
mustNotBeRefused(t, router.published[id])
}