Add end-to-end tests for TLS RPT report ingestion and WebSocket event handling
- Implemented tests for ingesting TLS RPT reports from emails and ensuring they are stored correctly in the database. - Added tests to verify that the same report can be ingested multiple times without duplication. - Created tests for backfilling reports that were previously delivered to the inbox. - Developed WebSocket event handling tests to ensure events are correctly routed to subscribers based on mailbox ownership. - Included tests for the mail API to validate session handling, mailbox isolation, and error mapping. - Ensured that the API rejects unauthorized requests and handles malformed JSON gracefully.
This commit is contained in:
1 parent
880dced9e4
commit
f330c4324e
28 files changed
+2438
-6
No files matched your search
@@ -0,0 +1,191 @@
|
||||
package httppresentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
"lambdamail/protocols/internal/application/usecase"
|
||||
)
|
||||
|
||||
// The event stream tells a browser its mailbox changed. It is the one place
|
||||
// where one account's activity is pushed out over another's connection if the
|
||||
// filtering is wrong, and none of it was covered.
|
||||
|
||||
func newTestHub(t *testing.T, store *fakeMailStore) *EventHub {
|
||||
t.Helper()
|
||||
uc := usecase.NewWebmailUseCase(store, &fakeBlobReader{}, nil, nil, "mail.example.test")
|
||||
return NewEventHub(testVerifier(interopSecret), uc)
|
||||
}
|
||||
|
||||
// The hub matches on the aggregate's UUID string, so subscribers are keyed by
|
||||
// real UUIDs rather than the readable names the mail API fakes use.
|
||||
var (
|
||||
mailboxOne = uuid.New()
|
||||
mailboxTwo = uuid.New()
|
||||
)
|
||||
|
||||
// subscribeTo registers a subscriber directly, which is what the WebSocket
|
||||
// handler does after authenticating. Driving a real upgrade would test the
|
||||
// library; this tests the routing decision, which is ours.
|
||||
func subscribeTo(hub *EventHub, mailboxID string, buffer int) *subscriber {
|
||||
sub := &subscriber{mailboxID: mailboxID, events: make(chan EventMessage, buffer)}
|
||||
hub.add(sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
// The check that keeps one mailbox's activity off another's connection.
|
||||
func TestEventsReachOnlyTheirOwnMailbox(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
mine := subscribeTo(hub, mailboxOne.String(), 4)
|
||||
theirs := subscribeTo(hub, mailboxTwo.String(), 4)
|
||||
|
||||
hub.Publish(port.OutboxEvent{
|
||||
EventType: "EmailReceived",
|
||||
AggregateID: mailboxOne,
|
||||
Payload: []byte(`{"uid":7}`),
|
||||
})
|
||||
|
||||
select {
|
||||
case msg := <-mine.events:
|
||||
if msg.Type != "EmailReceived" {
|
||||
t.Errorf("got event type %q", msg.Type)
|
||||
}
|
||||
default:
|
||||
t.Error("the mailbox's own event never arrived")
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-theirs.events:
|
||||
t.Errorf("another mailbox's event was delivered: %+v", msg)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// A subscriber that has stopped reading must be dropped, never allowed to
|
||||
// stall the relay that every other connection shares.
|
||||
func TestASlowSubscriberIsDroppedRatherThanBlocking(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
// Buffer of one, then filled, so the next publish has nowhere to go.
|
||||
stalled := subscribeTo(hub, mailboxOne.String(), 1)
|
||||
stalled.events <- EventMessage{Type: "filler"}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
hub.Publish(port.OutboxEvent{
|
||||
EventType: "EmailReceived",
|
||||
AggregateID: mailboxOne,
|
||||
Payload: []byte(`{}`),
|
||||
})
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-context.Background().Done():
|
||||
t.Fatal("Publish blocked on a subscriber that was not reading")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriberCountFollowsAddAndRemove(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
if hub.SubscriberCount() != 0 {
|
||||
t.Fatalf("a new hub has %d subscribers", hub.SubscriberCount())
|
||||
}
|
||||
|
||||
sub := subscribeTo(hub, mailboxOne.String(), 1)
|
||||
if hub.SubscriberCount() != 1 {
|
||||
t.Errorf("after one subscriber: %d", hub.SubscriberCount())
|
||||
}
|
||||
|
||||
hub.remove(sub)
|
||||
if hub.SubscriberCount() != 0 {
|
||||
t.Errorf("after removing it: %d", hub.SubscriberCount())
|
||||
}
|
||||
}
|
||||
|
||||
// Publishing with nobody listening must be a no-op, not a panic: the relay
|
||||
// runs whether or not a browser happens to be open.
|
||||
func TestPublishWithNoSubscribersIsHarmless(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
hub.Publish(port.OutboxEvent{
|
||||
EventType: "EmailReceived",
|
||||
AggregateID: uuid.New(),
|
||||
Payload: []byte(`{}`),
|
||||
})
|
||||
}
|
||||
|
||||
// --- the upgrade's own authentication ------------------------------------
|
||||
|
||||
// The session is verified before the upgrade, so an unauthenticated caller
|
||||
// gets a plain 401 and never reaches the WebSocket machinery.
|
||||
func TestEventStreamRefusesAnUnauthenticatedUpgrade(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
hub.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/events", nil))
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("an unauthenticated upgrade answered %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventStreamRefusesAnAdminSession(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "lm_user_session", Value: adminSurfaceToken(t)})
|
||||
rec := httptest.NewRecorder()
|
||||
hub.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("an admin session reached the event stream: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// A valid session for an address this server no longer serves must not open a
|
||||
// stream either - that is a deleted account whose token has not expired yet.
|
||||
func TestEventStreamRefusesASessionWithNoMailbox(t *testing.T) {
|
||||
store := storeWithMail()
|
||||
delete(store.addresses, sessionOwner)
|
||||
hub := newTestHub(t, store)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "lm_user_session", Value: interopSession})
|
||||
rec := httptest.NewRecorder()
|
||||
hub.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("a session with no mailbox answered %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// The payload has to reach the browser unchanged: the page reads the UID out
|
||||
// of it to decide what to refresh.
|
||||
func TestEventCarriesItsPayloadThrough(t *testing.T) {
|
||||
hub := newTestHub(t, storeWithMail())
|
||||
sub := subscribeTo(hub, mailboxOne.String(), 2)
|
||||
|
||||
hub.Publish(port.OutboxEvent{
|
||||
EventType: "EmailReceived",
|
||||
AggregateID: mailboxOne,
|
||||
Payload: []byte(`{"recipient":"user@example.test","uid":42}`),
|
||||
})
|
||||
|
||||
msg := <-sub.events
|
||||
var payload struct {
|
||||
Recipient string `json:"recipient"`
|
||||
UID int `json:"uid"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
t.Fatalf("payload did not survive: %v", err)
|
||||
}
|
||||
if payload.UID != 42 || payload.Recipient != "user@example.test" {
|
||||
t.Errorf("payload arrived as %+v", payload)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user