Files
igodwin eda033ff9b
CI / Lint (push) Successful in 2m29s
Build and Publish Container / build-and-publish (push) Successful in 2m58s
CI / Vulnerability scan (push) Successful in 44s
CI / Test (push) Successful in 1m45s
fix: clear golangci-lint backlog and make lint job blocking
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').

With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:32:51 -07:00

196 lines
4.8 KiB
Go

package e2e
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/igodwin/notifier/pkg/client"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
// TestSuite holds shared test state
type TestSuite struct {
Container testcontainers.Container
Client *client.RESTClient
BaseURL string
T *testing.T
}
// SetupSuite creates and starts the notifier container
func SetupSuite(t *testing.T, retention ...string) *TestSuite {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Find project root
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
// Walk up to find go.mod (project root)
projectRoot := cwd
for {
if _, err := os.Stat(filepath.Join(projectRoot, "go.mod")); err == nil {
break
}
parent := filepath.Dir(projectRoot)
if parent == projectRoot {
t.Fatalf("Could not find project root (go.mod)")
}
projectRoot = parent
}
// Build the docker image from project root
buildCmd := exec.Command("docker", "build", "-t", "notifier:test", ".")
buildCmd.Dir = projectRoot
if output, err := buildCmd.CombinedOutput(); err != nil {
t.Logf("Docker build output: %s", string(output))
t.Fatalf("Failed to build docker image: %v", err)
}
// Prepare environment variables for container
env := map[string]string{
"NOTIFIER_LOGGING_LEVEL": "debug",
"NOTIFIER_LOGGING_FORMAT": "json",
"NOTIFIER_NOTIFIERS_STDOUT": "true",
}
// Add retention config if specified
if len(retention) > 0 && retention[0] != "" {
// Parse retention string (format: "KEY1=VAL1|KEY2=VAL2|...")
parts := strings.Split(retention[0], "|")
for _, part := range parts {
kv := strings.Split(part, "=")
if len(kv) == 2 {
env[kv[0]] = kv[1]
}
}
}
// Create container
req := testcontainers.ContainerRequest{
Image: "notifier:test",
ExposedPorts: []string{"8080/tcp"},
Env: env,
WaitingFor: wait.ForHTTP("/health").WithStartupTimeout(30 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("Failed to create container: %v", err)
}
// Get container port
host, err := container.Host(ctx)
if err != nil {
if termErr := container.Terminate(ctx); termErr != nil {
t.Logf("Failed to terminate container during cleanup: %v", termErr)
}
t.Fatalf("Failed to get container host: %v", err)
}
port, err := container.MappedPort(ctx, "8080")
if err != nil {
if termErr := container.Terminate(ctx); termErr != nil {
t.Logf("Failed to terminate container during cleanup: %v", termErr)
}
t.Fatalf("Failed to get container port: %v", err)
}
baseURL := fmt.Sprintf("http://%s:%s", host, port.Port())
// Create client
cfg := client.ClientConfig{
BaseURL: baseURL,
Timeout: 30 * time.Second,
TLSInsecure: true,
}
c := client.NewRESTClient(cfg)
// Wait for service to be ready
deadline := time.Now().Add(30 * time.Second)
for {
if time.Now().After(deadline) {
if termErr := container.Terminate(ctx); termErr != nil {
t.Logf("Failed to terminate container during cleanup: %v", termErr)
}
t.Fatalf("Service failed to become ready")
}
healthy, err := c.HealthCheck(ctx)
if err == nil && healthy {
break
}
time.Sleep(100 * time.Millisecond)
}
return &TestSuite{
Container: container,
Client: c,
BaseURL: baseURL,
T: t,
}
}
// TeardownSuite stops and removes the container
func (s *TestSuite) TeardownSuite(ctx context.Context) {
if s.Container != nil {
if err := s.Container.Terminate(ctx); err != nil {
if s.T != nil {
s.T.Logf("Failed to terminate container during cleanup: %v", err)
} else {
fmt.Printf("e2e: failed to terminate container during cleanup: %v\n", err)
}
}
}
}
// WaitForCleanup waits for cleanup to have run and notifications to be removed
func (s *TestSuite) WaitForCleanup(maxWait time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), maxWait)
defer cancel()
deadline := time.Now().Add(maxWait)
for {
if time.Now().After(deadline) {
return fmt.Errorf("cleanup did not complete within %v", maxWait)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
// Check if cleanup has happened by checking logs or stats
return nil
}
}
}
// GetLogs retrieves container logs for debugging
func (s *TestSuite) GetLogs(ctx context.Context) string {
reader, err := s.Container.Logs(ctx)
if err != nil {
return fmt.Sprintf("error reading logs: %v", err)
}
defer func() { _ = reader.Close() }()
logs, err := io.ReadAll(reader)
if err != nil {
return fmt.Sprintf("error reading logs: %v", err)
}
return string(logs)
}