Files
galaxy-game/mail/internal/app/runtime_smoke_test.go
T
2026-04-26 20:34:39 +02:00

263 lines
7.3 KiB
Go

package app
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io"
"log/slog"
"math/big"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
smtpadapter "galaxy/mail/internal/adapters/smtp"
"galaxy/mail/internal/config"
"galaxy/mail/internal/ports"
"github.com/stretchr/testify/require"
testcontainers "github.com/testcontainers/testcontainers-go"
rediscontainer "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
realRuntimeSmokeEnv = "MAIL_REAL_RUNTIME_SMOKE"
realRuntimeRedisImage = "redis:7"
realRuntimeMailpitImage = "axllent/mailpit:v1.28.2"
realRuntimeMailpitCert = "/tmp/mailpit/server.crt"
realRuntimeMailpitKey = "/tmp/mailpit/server.key"
)
func TestRealRuntimeCompatibility(t *testing.T) {
if os.Getenv(realRuntimeSmokeEnv) != "1" {
t.Skipf("set %s=1 to run the real runtime smoke suite", realRuntimeSmokeEnv)
}
ctx := context.Background()
redisContainer, err := rediscontainer.Run(ctx, realRuntimeRedisImage)
require.NoError(t, err)
testcontainers.CleanupContainer(t, redisContainer)
redisAddr, err := redisContainer.Endpoint(ctx, "")
require.NoError(t, err)
certFiles := writeMailpitTLSFiles(t)
mailpitContainer, err := testcontainers.Run(
ctx,
realRuntimeMailpitImage,
testcontainers.WithExposedPorts("1025/tcp", "8025/tcp"),
testcontainers.WithFiles(
testcontainers.ContainerFile{
HostFilePath: certFiles.certPath,
ContainerFilePath: realRuntimeMailpitCert,
FileMode: 0o644,
},
testcontainers.ContainerFile{
HostFilePath: certFiles.keyPath,
ContainerFilePath: realRuntimeMailpitKey,
FileMode: 0o600,
},
),
testcontainers.WithEnv(map[string]string{
"MP_SMTP_TLS_CERT": realRuntimeMailpitCert,
"MP_SMTP_TLS_KEY": realRuntimeMailpitKey,
"MP_SMTP_REQUIRE_STARTTLS": "true",
}),
testcontainers.WithWaitStrategy(
wait.ForAll(
wait.ForListeningPort("1025/tcp"),
wait.ForListeningPort("8025/tcp"),
).WithDeadline(30*time.Second),
),
)
require.NoError(t, err)
testcontainers.CleanupContainer(t, mailpitContainer)
smtpAddr, err := mailpitContainer.PortEndpoint(ctx, "1025/tcp", "")
require.NoError(t, err)
mailpitHTTPBaseURL, err := mailpitContainer.PortEndpoint(ctx, "8025/tcp", "http")
require.NoError(t, err)
truncateRuntimeMail(t)
cfg := runtimeBaseConfig(t, redisAddr)
cfg.Templates.Dir = writeRuntimeTemplates(t)
cfg.InternalHTTP.Addr = mustFreeAddr(t)
cfg.ShutdownTimeout = time.Second
cfg.StreamBlockTimeout = 100 * time.Millisecond
cfg.AttemptWorkerConcurrency = 1
cfg.OperatorRequestTimeout = time.Second
cfg.SMTP.Mode = config.SMTPModeSMTP
cfg.SMTP.Addr = smtpAddr
cfg.SMTP.FromEmail = "noreply@example.com"
cfg.SMTP.Timeout = 2 * time.Second
instance := startSmokeRuntime(t, cfg, runtimeDependencies{
providerFactory: func(cfg config.SMTPConfig, _ *slog.Logger) (ports.Provider, error) {
return smtpadapter.New(smtpadapter.Config{
Addr: cfg.Addr,
FromEmail: cfg.FromEmail,
FromName: cfg.FromName,
Timeout: cfg.Timeout,
TLSConfig: certFiles.clientTLSConfig,
})
},
schedulerPoll: 25 * time.Millisecond,
})
response := postLoginCodeDelivery(t, instance.baseURL, loginCodeDeliveryRequest{
idempotencyKey: "real-runtime-smoke",
email: "pilot@example.com",
code: "246810",
locale: "fr-FR",
})
require.Equal(t, "sent", string(response.Outcome))
list := eventuallyListDeliveries(t, instance.baseURL, url.Values{
"source": []string{"authsession"},
"idempotency_key": []string{"real-runtime-smoke"},
})
require.Len(t, list.Items, 1)
detail := eventuallyDeliveryStatus(t, instance.baseURL, list.Items[0].DeliveryID, "sent")
require.Equal(t, "authsession", detail.Source)
require.Equal(t, "auth.login_code", detail.TemplateID)
require.Equal(t, "fr-FR", detail.Locale)
require.True(t, detail.LocaleFallbackUsed)
require.Equal(t, []string{"pilot@example.com"}, detail.To)
attempts := getDeliveryAttempts(t, instance.baseURL, detail.DeliveryID)
require.Len(t, attempts.Items, 1)
require.Equal(t, "provider_accepted", attempts.Items[0].Status)
messageText := waitForMailpitLatestText(t, mailpitHTTPBaseURL)
require.Contains(t, messageText, "246810")
}
type smokeTLSFiles struct {
certPath string
keyPath string
clientTLSConfig *tls.Config
}
func writeMailpitTLSFiles(t *testing.T) smokeTLSFiles {
t.Helper()
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "localhost",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
root := t.TempDir()
certPath := filepath.Join(root, "server.crt")
keyPath := filepath.Join(root, "server.key")
require.NoError(t, os.WriteFile(certPath, certPEM, 0o644))
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
rootCAs := x509.NewCertPool()
require.True(t, rootCAs.AppendCertsFromPEM(certPEM))
return smokeTLSFiles{
certPath: certPath,
keyPath: keyPath,
clientTLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: rootCAs,
ServerName: "localhost",
},
}
}
func startSmokeRuntime(t *testing.T, cfg config.Config, deps runtimeDependencies) *runtimeInstance {
t.Helper()
runtime, err := newRuntime(context.Background(), cfg, testLogger(), deps)
require.NoError(t, err)
instance := &runtimeInstance{
baseURL: "http://" + cfg.InternalHTTP.Addr,
runtime: runtime,
done: make(chan error, 1),
}
runCtx, cancel := context.WithCancel(context.Background())
instance.cancel = cancel
go func() {
instance.done <- runtime.Run(runCtx)
}()
waitForRuntimeReady(t, instance.baseURL)
t.Cleanup(func() {
instance.stop(t)
})
return instance
}
func waitForMailpitLatestText(t *testing.T, baseURL string) string {
t.Helper()
client := &http.Client{
Timeout: 500 * time.Millisecond,
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
t.Cleanup(client.CloseIdleConnections)
var payload string
require.Eventually(t, func() bool {
request, err := http.NewRequest(http.MethodGet, baseURL+"/view/latest.txt", nil)
require.NoError(t, err)
response, err := client.Do(request)
if err != nil {
return false
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
require.NoError(t, err)
if response.StatusCode != http.StatusOK {
return false
}
payload = string(body)
return strings.TrimSpace(payload) != ""
}, 20*time.Second, 100*time.Millisecond)
return payload
}