| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ecdsa" |
| 6 | "crypto/elliptic" |
| 7 | "crypto/rand" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | func fedClient(t *testing.T, fk *fakeKeymail) *Keymail { |
| 16 | t.Helper() |
| 17 | k := NewKeymail(fk.srv.URL, "https://console.example.test") |
| 18 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | k.SetSigningKey(key) |
| 23 | return k |
| 24 | } |
| 25 | |
| 26 | func TestSendFederatedUnknownSenderRegistersThenDelivers(t *testing.T) { |
| 27 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 28 | k := fedClient(t, fk) |
| 29 | id, err := k.SendFederated(context.Background(), "bill@keymail.dev", "subj", "the link") |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | if fk.registrations != 1 { |
| 34 | t.Fatalf("registrations = %d, want 1", fk.registrations) |
| 35 | } |
| 36 | if len(fk.fedSends) != 1 || fk.fedSends[0].ID != id || fk.fedSends[0].Body != "the link" || |
| 37 | fk.fedSends[0].From != "console@console.example.test" || fk.fedSends[0].To != "bill@keymail.dev" { |
| 38 | t.Fatalf("fedSends = %+v", fk.fedSends) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func TestSendFederatedKnownSenderPaysNothing(t *testing.T) { |
| 43 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 44 | k := fedClient(t, fk) |
| 45 | ctx := context.Background() |
| 46 | if _, err := k.SendFederated(ctx, "bill@keymail.dev", "s", "one"); err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | if _, err := k.SendFederated(ctx, "bill@keymail.dev", "s", "two"); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | if fk.registrations != 1 { |
| 53 | t.Fatalf("registrations = %d after two sends, want 1", fk.registrations) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // Review Focus 1: the console's key changed since it registered. |
| 58 | func TestSendFederatedReRegistersARotatedKey(t *testing.T) { |
| 59 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 60 | fk.senders["console@console.example.test"] = "an-old-key" |
| 61 | k := fedClient(t, fk) |
| 62 | if _, err := k.SendFederated(context.Background(), "bill@keymail.dev", "s", "b"); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | if fk.registrations != 1 || len(fk.fedSends) != 1 { |
| 66 | t.Fatalf("registrations=%d fedSends=%d, want 1 and 1", fk.registrations, len(fk.fedSends)) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | func TestSendFederatedRefusesAPriceAboveTheCap(t *testing.T) { |
| 71 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 72 | fk.powBits = maxFederationPowBits + 1 |
| 73 | k := fedClient(t, fk) |
| 74 | _, err := k.SendFederated(context.Background(), "bill@keymail.dev", "s", "b") |
| 75 | if !errors.Is(err, ErrFederationPriceTooHigh) { |
| 76 | t.Fatalf("err = %v, want ErrFederationPriceTooHigh", err) |
| 77 | } |
| 78 | if fk.registrations != 0 { |
| 79 | t.Fatal("registered despite refusing the price") |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // Review Focus 2 and the non-Keymail host. |
| 84 | func TestSendFederatedNoMailbox(t *testing.T) { |
| 85 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 86 | fk.noMailbox["ghost"] = true |
| 87 | k := fedClient(t, fk) |
| 88 | for _, to := range []string{"ghost@keymail.dev", "bill@gmail.com"} { |
| 89 | _, err := k.SendFederated(context.Background(), to, "s", "b") |
| 90 | if !errors.Is(err, ErrNoKeymailMailbox) { |
| 91 | t.Errorf("SendFederated(%s) = %v, want ErrNoKeymailMailbox", to, err) |
| 92 | } |
| 93 | // The lookup's own error stays reachable (%w: %w). |
| 94 | var ke *keymailError |
| 95 | if !errors.As(err, &ke) { |
| 96 | t.Errorf("SendFederated(%s) = %v, want the keymailError wrapped too", to, err) |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestSendFederatedBlocked(t *testing.T) { |
| 102 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 103 | fk.blocked["console@console.example.test"] = true |
| 104 | k := fedClient(t, fk) |
| 105 | _, err := k.SendFederated(context.Background(), "bill@keymail.dev", "s", "b") |
| 106 | if err == nil || !strings.Contains(err.Error(), "blocked") { |
| 107 | t.Fatalf("err = %v, want a blocked error", err) |
| 108 | } |
| 109 | if !IsKeymailBlocked(err) { |
| 110 | t.Fatalf("IsKeymailBlocked(%v) = false", err) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func TestIsKeymailBlocked(t *testing.T) { |
| 115 | blocked := &keymailError{Status: 403, Msg: "blocked"} |
| 116 | for _, c := range []struct { |
| 117 | err error |
| 118 | want bool |
| 119 | }{ |
| 120 | {nil, false}, |
| 121 | {blocked, true}, |
| 122 | {fmt.Errorf("keymail: deliver: %w", blocked), true}, |
| 123 | {&keymailError{Status: 403, Msg: "unknown_sender"}, false}, |
| 124 | {&keymailError{Status: 500, Msg: "blocked"}, false}, |
| 125 | {ErrFederationPriceTooHigh, false}, |
| 126 | } { |
| 127 | if got := IsKeymailBlocked(c.err); got != c.want { |
| 128 | t.Errorf("IsKeymailBlocked(%v) = %v, want %v", c.err, got, c.want) |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestSendFederatedWithoutKey(t *testing.T) { |
| 134 | fk := newFakeKeymail(t, "paul@keymail.dev") |
| 135 | k := NewKeymail(fk.srv.URL, "https://console.example.test") |
| 136 | if _, err := k.SendFederated(context.Background(), "bill@keymail.dev", "s", "b"); err == nil { |
| 137 | t.Fatal("SendFederated without a signing key succeeded") |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // Final review Important #2: proof of work is one core's worth at a time. |
| 142 | // A second registration waits for the slot and gives up at its deadline. |
| 143 | func TestPowSlotIsOneAtATimeAndHonoursTheDeadline(t *testing.T) { |
| 144 | release, err := acquirePowSlot(context.Background()) |
| 145 | if err != nil { |
| 146 | t.Fatalf("first acquire: %v", err) |
| 147 | } |
| 148 | released := false |
| 149 | releaseOnce := func() { |
| 150 | if !released { |
| 151 | release() |
| 152 | released = true |
| 153 | } |
| 154 | } |
| 155 | defer releaseOnce() |
| 156 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) |
| 157 | defer cancel() |
| 158 | start := time.Now() |
| 159 | if _, err := acquirePowSlot(ctx); !errors.Is(err, context.DeadlineExceeded) { |
| 160 | t.Fatalf("second acquire while held = %v, want DeadlineExceeded", err) |
| 161 | } |
| 162 | if d := time.Since(start); d > time.Second { |
| 163 | t.Fatalf("waiting acquire took %v", d) |
| 164 | } |
| 165 | releaseOnce() |
| 166 | again, err := acquirePowSlot(context.Background()) |
| 167 | if err != nil { |
| 168 | t.Fatalf("acquire after release: %v", err) |
| 169 | } |
| 170 | again() |
| 171 | } |
| 172 | |