Files
story/internal/outbox/worker_test.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

93 lines
3.4 KiB
Go

package outbox
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
func TestWorkerDefaultsAndRetryCap(t *testing.T) {
worker := NewWorker(nil, discardLogger(), WorkerOptions{})
if worker.options.PollInterval != 2*time.Second || worker.options.BatchSize != 20 || worker.options.MaxAttempts != 8 {
t.Fatalf("unexpected worker defaults: %#v", worker.options)
}
worker.options.BaseRetry = time.Second
worker.options.MaxRetry = 3 * time.Second
delay := time.Until(worker.nextAttempt(10))
if delay < 1400*time.Millisecond || delay > 3100*time.Millisecond {
t.Fatalf("capped retry delay = %s", delay)
}
if !strings.HasPrefix(worker.workerID, "worker-") {
t.Fatalf("unexpected worker id: %s", worker.workerID)
}
}
func TestStoreRejectsInvalidEventBeforeDatabaseAccess(t *testing.T) {
store := NewStore(nil)
if _, err := store.Enqueue(context.Background(), nil, NewEvent{}); err == nil {
t.Fatal("empty event should be rejected")
}
if _, err := store.Enqueue(context.Background(), nil, NewEvent{
AggregateType: "test", AggregateID: "id", EventType: "type", Payload: json.RawMessage(`not-json`),
}); err == nil {
t.Fatal("invalid JSON payload should be rejected")
}
}
func TestHashPayloadIsStable(t *testing.T) {
first := hashPayload([]byte(`{"ok":true}`))
if first != hashPayload([]byte(`{"ok":true}`)) || first == hashPayload([]byte(`{"ok":false}`)) {
t.Fatal("payload hash is not stable")
}
}
func TestRetryableErrorAllowList(t *testing.T) {
permanent := errors.New("malformed payload")
if IsRetryable(permanent) {
t.Fatal("ordinary errors must be permanent by default")
}
if !IsRetryable(Retryable(permanent)) || !errors.Is(Retryable(permanent), permanent) {
t.Fatal("explicit retryable marker was not preserved")
}
if !IsRetryable(context.DeadlineExceeded) || !IsRetryable(&net.DNSError{IsTimeout: true}) {
t.Fatal("timeouts must remain retryable")
}
if !IsRetryable(&pgconn.PgError{Code: "40001"}) || IsRetryable(&pgconn.PgError{Code: "23505"}) {
t.Fatal("PostgreSQL transient and constraint errors were misclassified")
}
if Retryable(nil) != nil {
t.Fatal("wrapping nil must return nil")
}
if !IsRetryable(fmt.Errorf("dependency: %w", Retryable(permanent))) {
t.Fatal("wrapped marker must remain discoverable")
}
}
func TestWorkerHonorsOptionalDependencyRetryDelay(t *testing.T) {
worker := NewWorker(nil, discardLogger(), WorkerOptions{BaseRetry: time.Second, MaxRetry: 3 * time.Second})
cause := errors.New("provider rate limited")
delayed := fmt.Errorf("privacy deletion: %w", RetryAfter(cause, time.Minute))
if !IsRetryable(delayed) || !errors.Is(delayed, cause) || RetryDelay(delayed) != time.Minute {
t.Fatalf("retry metadata lost through wrapping: %v", delayed)
}
if delay := time.Until(worker.nextAttemptForError(1, delayed)); delay < 59*time.Second || delay > 61*time.Second {
t.Fatalf("dependency cooldown was not honored: %s", delay)
}
if delay := time.Until(worker.nextAttemptForError(1, Retryable(cause))); delay < 400*time.Millisecond || delay > 1100*time.Millisecond {
t.Fatalf("ordinary retries changed: %s", delay)
}
if RetryAfter(nil, time.Minute) != nil || RetryDelay(cause) != 0 || RetryDelay(RetryAfter(cause, -time.Second)) != 0 {
t.Fatal("nil, ordinary or negative retry delay was not handled")
}
if RetryDelay(RetryAfter(delayed, time.Second)) != time.Minute {
t.Fatal("outer wrapper shortened dependency cooldown")
}
}