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,172 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
)
|
||||
|
||||
// Reports that arrived before ingestion existed are still sitting in the
|
||||
// admin's inbox. They are ordinary stored messages, so they can be read back
|
||||
// out and parsed after the fact.
|
||||
|
||||
type stubDeliveredReportStore struct {
|
||||
pending []port.StoredReportMessage
|
||||
filed []uuid.UUID
|
||||
blobs map[uuid.UUID][]byte
|
||||
listErr error
|
||||
fileErr error
|
||||
blobErr error
|
||||
}
|
||||
|
||||
func (s *stubDeliveredReportStore) ListUningestedReports(
|
||||
_ context.Context, limit int,
|
||||
) ([]port.StoredReportMessage, error) {
|
||||
if s.listErr != nil {
|
||||
return nil, s.listErr
|
||||
}
|
||||
if limit > 0 && len(s.pending) > limit {
|
||||
return s.pending[:limit], nil
|
||||
}
|
||||
return s.pending, nil
|
||||
}
|
||||
|
||||
func (s *stubDeliveredReportStore) MoveToReportsFolder(_ context.Context, messageID uuid.UUID) error {
|
||||
if s.fileErr != nil {
|
||||
return s.fileErr
|
||||
}
|
||||
s.filed = append(s.filed, messageID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubDeliveredReportStore) ReadByID(_ context.Context, blobID uuid.UUID) ([]byte, error) {
|
||||
if s.blobErr != nil {
|
||||
return nil, s.blobErr
|
||||
}
|
||||
body, ok := s.blobs[blobID]
|
||||
if !ok {
|
||||
return nil, errors.New("blob not found")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func newBackfill(store *stubDeliveredReportStore, repo *recordingReportRepo) *BackfillReportsUseCase {
|
||||
return NewBackfillReportsUseCase(
|
||||
store, store, NewIngestDeliveredReportsUseCase(NewIngestReportsUseCase(repo)))
|
||||
}
|
||||
|
||||
func TestBackfillIngestsAReportAlreadyInTheMailbox(t *testing.T) {
|
||||
blobID := uuid.New()
|
||||
messageID := uuid.New()
|
||||
payload := messageWithAttachment(googleTlsRptFilename, "application/gzip", gzipped(t, tlsRptJSON))
|
||||
|
||||
store := &stubDeliveredReportStore{
|
||||
pending: []port.StoredReportMessage{
|
||||
{MessageID: messageID, BlobID: blobID, RecipientAddress: "tlsrpt@example.test"},
|
||||
},
|
||||
blobs: map[uuid.UUID][]byte{blobID: payload},
|
||||
}
|
||||
repo := &recordingReportRepo{}
|
||||
|
||||
summary, err := newBackfill(store, repo).Run(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
|
||||
if len(repo.tlsRpt) != 1 {
|
||||
t.Fatalf("the stored report was not ingested: %d", len(repo.tlsRpt))
|
||||
}
|
||||
if repo.tlsRpt[0].Domain != "example.test" {
|
||||
t.Errorf("ingested domain is %q", repo.tlsRpt[0].Domain)
|
||||
}
|
||||
if summary.Ingested != 1 {
|
||||
t.Errorf("summary reports %d ingested, want 1", summary.Ingested)
|
||||
}
|
||||
// Filed out of the inbox, which is the point of running this at all.
|
||||
if len(store.filed) != 1 || store.filed[0] != messageID {
|
||||
t.Errorf("the message was not moved to Reports: %v", store.filed)
|
||||
}
|
||||
}
|
||||
|
||||
// Running it twice must be safe: the second pass has nothing left to do, and
|
||||
// storage is idempotent anyway.
|
||||
func TestBackfillOverAnEmptyBacklogDoesNothing(t *testing.T) {
|
||||
store := &stubDeliveredReportStore{}
|
||||
repo := &recordingReportRepo{}
|
||||
|
||||
summary, err := newBackfill(store, repo).Run(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if summary.Ingested != 0 || summary.Failed != 0 {
|
||||
t.Errorf("an empty backlog produced %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
// One unreadable report must not stop the rest of the backlog: a single
|
||||
// corrupted attachment from years ago would otherwise block every report
|
||||
// behind it forever.
|
||||
func TestBackfillContinuesPastAnUnreadableReport(t *testing.T) {
|
||||
goodBlob, badBlob := uuid.New(), uuid.New()
|
||||
goodMsg, badMsg := uuid.New(), uuid.New()
|
||||
|
||||
store := &stubDeliveredReportStore{
|
||||
pending: []port.StoredReportMessage{
|
||||
{MessageID: badMsg, BlobID: badBlob, RecipientAddress: "tlsrpt@example.test"},
|
||||
{MessageID: goodMsg, BlobID: goodBlob, RecipientAddress: "tlsrpt@example.test"},
|
||||
},
|
||||
blobs: map[uuid.UUID][]byte{
|
||||
badBlob: messageWithAttachment("broken.json.gz", "application/gzip", []byte("rubbish")),
|
||||
goodBlob: messageWithAttachment(googleTlsRptFilename, "application/gzip", gzipped(t, tlsRptJSON)),
|
||||
},
|
||||
}
|
||||
repo := &recordingReportRepo{}
|
||||
|
||||
summary, err := newBackfill(store, repo).Run(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if summary.Ingested != 1 {
|
||||
t.Errorf("the readable report was not ingested: %+v", summary)
|
||||
}
|
||||
if summary.Failed != 1 {
|
||||
t.Errorf("the unreadable one was not reported: %+v", summary)
|
||||
}
|
||||
// Both are filed either way - the inbox is being cleared, and a report
|
||||
// this server cannot parse is still a report.
|
||||
if len(store.filed) != 2 {
|
||||
t.Errorf("filed %d messages, want both", len(store.filed))
|
||||
}
|
||||
}
|
||||
|
||||
// A blob that cannot be read at all is counted and skipped, never fatal.
|
||||
func TestBackfillSurvivesAMissingBlob(t *testing.T) {
|
||||
store := &stubDeliveredReportStore{
|
||||
pending: []port.StoredReportMessage{
|
||||
{MessageID: uuid.New(), BlobID: uuid.New(), RecipientAddress: "dmarc@example.test"},
|
||||
},
|
||||
blobs: map[uuid.UUID][]byte{},
|
||||
}
|
||||
repo := &recordingReportRepo{}
|
||||
|
||||
summary, err := newBackfill(store, repo).Run(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("a missing blob must not fail the run: %v", err)
|
||||
}
|
||||
if summary.Failed != 1 {
|
||||
t.Errorf("the missing blob was not counted: %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
// A failure listing the backlog is the one thing worth stopping for: it means
|
||||
// the database is unreachable, and every message would be counted as failed.
|
||||
func TestBackfillStopsIfItCannotReadTheBacklog(t *testing.T) {
|
||||
store := &stubDeliveredReportStore{listErr: errors.New("database is down")}
|
||||
if _, err := newBackfill(store, &recordingReportRepo{}).Run(context.Background(), 0); err == nil {
|
||||
t.Error("a backlog that cannot be listed should be reported as an error")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user