package api
import (
"context"
"encoding/json"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// A test push says what the push services answered, from notify's own
// record — the texts here are the ones zeitforge recorded for an
// account with one Chrome and two iPhone subscriptions — and never
// carries a device's endpoint.
func TestAPushOutcomeSaysWhoRefusedAndWhy(t *testing.T) {
cases := []struct {
name string
in PushOutcome
outcome string
got, of int
why string
}{
{"not yet", PushOutcome{}, "unknown", 0, 0, ""},
{"all", PushOutcome{Found: true, Status: "sent"}, "delivered", 2, 2, ""},
{"apple signature", PushOutcome{Found: true, Status: "sent",
Error: `delivered 1/2: 1 permanent failure(s) (webpush: status 403: {"reason":"BadJwtToken"})`}, "partial", 1, 2, "403 BadJwtToken"},
{"timeout and signature", PushOutcome{Found: true, Status: "sent",
Error: `delivered 1/3: 2 permanent failure(s) (webpush: status 403: {"reason":"BadJwtToken"}; webpush: send: Post "https://web.push.apple.com/QF_secretEndpoint": context deadline exceeded (Client.Timeout exceeded while awaiting headers))`},
"partial", 1, 3, "403 BadJwtToken; timed out"},
{"none", PushOutcome{Found: true, Status: "error",
Error: `webpush: status 403: {"reason":"BadJwtToken"}; webpush: status 403: {"reason":"BadJwtToken"}`}, "failed", 0, 0, "2× 403 BadJwtToken"},
}
for _, c := range cases {
s := summarizePush(c.in, 2)
why := strings.Join(s.Why, "; ")
if s.Outcome != c.outcome || s.Delivered != c.got || s.Of != c.of || why != c.why {
t.Errorf("FAIL %s: %+v (why %q)", c.name, s, why)
}
if strings.Contains(why, "secretEndpoint") {
t.Errorf("FAIL %s: a device endpoint leaked into the summary", c.name)
}
}
}
// The op hands notify's per-device outcomes on beside the overall result.
func TestThePushTestReportsEachDevice(t *testing.T) {
np := &outcomeNotify{out: PushOutcome{Found: true, Status: "sent", Devices: []PushDeviceOutcome{
{Host: "fcm.googleapis.com", Outcome: "delivered"},
{Host: "web.push.apple.com", Outcome: "gone", Status: 410},
}}}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "t", Notify: np}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "me.push.test", mePushTestIn{})
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(got)
var wire struct {
Outcome string `json:"outcome"`
Results []struct {
Host string `json:"host"`
Outcome string `json:"outcome"`
Status int `json:"status"`
} `json:"results"`
}
if err := json.Unmarshal(b, &wire); err != nil {
t.Fatal(err)
}
if wire.Outcome == "" || len(wire.Results) != 2 ||
wire.Results[1].Host != "web.push.apple.com" || wire.Results[1].Outcome != "gone" || wire.Results[1].Status != 410 {
t.Errorf("FAIL: wire %s", b)
}
}
type outcomeNotify struct {
fakeNotify
out PushOutcome
}
func (o *outcomeNotify) PushOutcome(context.Context, string, string) (PushOutcome, error) {
return o.out, nil
}