Skip to content

Add E2E test suite and fix pipe buffer aliasing bug#106

Merged
linyows merged 4 commits intomainfrom
e2e-test-enhancement
Feb 18, 2026
Merged

Add E2E test suite and fix pipe buffer aliasing bug#106
linyows merged 4 commits intomainfrom
e2e-test-enhancement

Conversation

@linyows
Copy link
Owner

@linyows linyows commented Feb 18, 2026

Summary

  • Fix buffer aliasing bug in pipe metadata extraction: sMailAddr, rMailAddr, sServerName, rServerName were stored as slices referencing the reusable read buffer, causing data corruption when subsequent reads overwrote the buffer. Now properly copied.
  • Add message capture to test SMTP server: ReceivedMessage struct and OnMessage callback enable tests to inspect relayed messages (envelope metadata + body).
  • Enhance existing integration test: Verify MailFrom and Subject header of received messages instead of only checking for no error.
  • Add E2E test suite (TestE2E) with 5 subtests: MessageRelayIntegrity, MetadataExtraction, HookCallbacks, StartTLSStripping, MultipleEmails.
  • Add Makefile targets: make e2e and updated make integration to run both TestIntegration and TestE2E.

Test plan

  • go test -short passes (existing unit tests)
  • go test -run TestIntegration passes with message verification
  • go test -run TestE2E passes all 5 subtests
  • go test -run 'TestIntegration|TestE2E' passes together

🤖 Generated with Claude Code

linyows and others added 4 commits February 18, 2026 10:32
The pipe stored slices referencing the reusable read buffer for
sMailAddr, rMailAddr, sServerName, and rServerName. Subsequent reads
overwrote the buffer, corrupting the extracted metadata passed to
AfterConn hooks. Copy the byte slices to avoid aliasing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add message capture capability to test SMTP server (ReceivedMessage,
OnMessage callback) and create comprehensive E2E test suite covering
message relay integrity, metadata extraction, hook callbacks, STARTTLS
stripping, and multiple email relay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WaitForServerListen probe connections generate AfterConn hook calls
with "unknown" addresses. Use findConnCall to search for the conn call
matching the expected MailFrom instead of relying on index order.
Also fix errcheck for s.Quit() in sendEmail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@linyows linyows requested a review from Copilot February 18, 2026 01:56
@linyows linyows self-assigned this Feb 18, 2026
@linyows linyows merged commit babba51 into main Feb 18, 2026
10 checks passed
@linyows linyows deleted the e2e-test-enhancement branch February 18, 2026 02:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical buffer aliasing bug in pipe metadata extraction and adds comprehensive E2E test infrastructure to verify SMTP proxy functionality. The aliasing bug caused data corruption when the reusable read buffer was overwritten by subsequent reads. The new test suite validates message relay integrity, metadata extraction, hook callbacks, STARTTLS stripping, and multi-email handling.

Changes:

  • Fixed buffer aliasing bug by copying metadata slices (sMailAddr, rMailAddr, sServerName, rServerName) instead of referencing the reusable buffer
  • Added message capture capability to test SMTP server with ReceivedMessage struct and OnMessage callback
  • Enhanced integration test to verify message content instead of just checking for errors
  • Added comprehensive E2E test suite with 5 subtests covering relay, metadata, hooks, STARTTLS, and multi-email scenarios
  • Updated CI workflows to use Go versions 1.24.x, 1.25.x, 1.26.x (which do not exist yet)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pipe.go Fixed buffer aliasing by copying extracted metadata slices instead of storing direct references to reusable buffer
smtp.go Added ReceivedMessage struct, OnMessage callback, and message capture logic for test SMTP server
integration_test.go Enhanced test to verify actual message content (MailFrom, Subject) instead of only checking for errors
e2e_test.go Added comprehensive E2E test suite with 5 subtests and helper infrastructure (testHook, testEnv, port allocation)
Makefile Added make e2e target and updated make integration to run both TestIntegration and TestE2E
.github/workflows/test.yml Updated Go versions to 1.24.x, 1.25.x, 1.26.x (non-existent versions)
.github/workflows/integration.yml Updated Go versions to 1.24.x, 1.25.x, 1.26.x (non-existent versions)
.github/workflows/build.yml Updated Go versions to 1.24.x, 1.25.x, 1.26.x (non-existent versions)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

default:
if !c.data {
if c.data {
c.dataBody.WriteString(v + "\n")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data capture logic writes v + "\n" instead of preserving the original line ending. Since the incoming line was already read with ReadString('\n') and then trimmed, the original CRLF line ending is lost and replaced with just LF. This could cause issues if downstream code expects proper CRLF line endings in SMTP messages. Consider preserving the original line ending or consistently using CRLF for SMTP compliance.

Suggested change
c.dataBody.WriteString(v + "\n")
c.dataBody.WriteString(v + crlf)

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +314
func TestE2E(t *testing.T) {
if testing.Short() {
t.SkipNow()
}

env := setupTestEnv(t)

t.Run("MessageRelayIntegrity", func(t *testing.T) {
from := "sender@example.test"
to := "receiver@example.local"
subject := "Integrity Test"
body := "Hello, this is a relay integrity test."

env.sendEmail(t, from, to, subject, body)
msg := env.waitForMessage(t, 5*time.Second)

if msg.MailFrom != from {
t.Errorf("MailFrom = %q, want %q", msg.MailFrom, from)
}
if len(msg.RcptTo) != 1 || msg.RcptTo[0] != to {
t.Errorf("RcptTo = %v, want [%q]", msg.RcptTo, to)
}

data := string(msg.Data)
if !strings.Contains(data, "Subject: "+subject) {
t.Errorf("Data does not contain expected Subject header:\n%s", data[:min(len(data), 300)])
}
if !strings.Contains(data, body) {
t.Errorf("Data does not contain expected body:\n%s", data[:min(len(data), 300)])
}
})

t.Run("MetadataExtraction", func(t *testing.T) {
// Find the conn call matching the email we sent (probe connections have "unknown")
emailConn := env.hook.findConnCall("sender@example.test", 5*time.Second)
if emailConn == nil {
t.Fatal("timed out waiting for AfterConn hook call with expected MailFrom")
}

if string(emailConn.MailFrom) != "sender@example.test" {
t.Errorf("AfterConn MailFrom = %q, want %q", emailConn.MailFrom, "sender@example.test")
}
if string(emailConn.MailTo) != "receiver@example.local" {
t.Errorf("AfterConn MailTo = %q, want %q", emailConn.MailTo, "receiver@example.local")
}
})

t.Run("HookCallbacks", func(t *testing.T) {
comms := env.hook.getCommCalls()
if len(comms) == 0 {
t.Fatal("no AfterComm calls recorded")
}

directions := make(map[Direction]bool)
for _, c := range comms {
directions[c.Direction] = true
if c.ConnID == "" {
t.Error("AfterComm has empty ConnID")
}
if c.OccurredAt.IsZero() {
t.Error("AfterComm has zero OccurredAt")
}
}

for _, d := range []Direction{onPxy, srcToDst} {
if !directions[d] {
t.Errorf("AfterComm not called with Direction %q", d)
}
}
})

t.Run("StartTLSStripping", func(t *testing.T) {
comms := env.hook.getCommCalls()

var dstToPxyHasStartTLS bool
var pxyToSrcHasStartTLS bool
for _, c := range comms {
dataStr := string(c.Data)
if c.Direction == dstToPxy && strings.Contains(dataStr, "STARTTLS") {
dstToPxyHasStartTLS = true
}
if c.Direction == pxyToSrc && strings.Contains(dataStr, "STARTTLS") {
pxyToSrcHasStartTLS = true
}
}

if !dstToPxyHasStartTLS {
t.Error("expected STARTTLS in dstToPxy communication, but not found")
}
if pxyToSrcHasStartTLS {
t.Error("STARTTLS should be stripped from pxyToSrc communication, but was found")
}
})

t.Run("MultipleEmails", func(t *testing.T) {
connCountBefore := len(env.hook.getConnCalls())

env.sendEmail(t, "user2@example.test", "dest2@example.local", "Second Email", "Body of second email")
msg2 := env.waitForMessage(t, 5*time.Second)
if msg2.MailFrom != "user2@example.test" {
t.Errorf("2nd email MailFrom = %q, want %q", msg2.MailFrom, "user2@example.test")
}
if !strings.Contains(string(msg2.Data), "Body of second email") {
t.Error("2nd email Data does not contain expected body")
}

env.sendEmail(t, "user3@example.test", "dest3@example.local", "Third Email", "Body of third email")
msg3 := env.waitForMessage(t, 5*time.Second)
if msg3.MailFrom != "user3@example.test" {
t.Errorf("3rd email MailFrom = %q, want %q", msg3.MailFrom, "user3@example.test")
}
if !strings.Contains(string(msg3.Data), "Body of third email") {
t.Error("3rd email Data does not contain expected body")
}

if !env.hook.waitForConnCalls(connCountBefore+2, 5*time.Second) {
t.Error("timed out waiting for AfterConn hook calls for multiple emails")
}
})
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subtests in TestE2E are not isolated - they all share the same testEnv instance and accumulated hook data. This means subtests depend on the execution order and cannot be run independently with go test -run TestE2E/MetadataExtraction. The MetadataExtraction subtest relies on finding data from MessageRelayIntegrity, and HookCallbacks/StartTLSStripping subtests examine all accumulated communications. Consider either: (1) making each subtest independent by setting up fresh environments, or (2) acknowledging this dependency by not using separate subtests (use a single sequential test), or (3) documenting that subtests must run in order.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants