Files
honeyDueAPI/internal/push/push_coverage_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

360 lines
10 KiB
Go

package push
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// === truncateToken tests ===
func TestTruncateToken_LongToken(t *testing.T) {
token := "abcdefghijklmnopqrstuvwxyz1234567890"
result := truncateToken(token)
assert.Equal(t, "abcdefgh...", result)
}
func TestTruncateToken_ShortToken(t *testing.T) {
token := "abc"
result := truncateToken(token)
assert.Equal(t, "abc", result)
}
func TestTruncateToken_ExactlyEightChars(t *testing.T) {
token := "12345678"
result := truncateToken(token)
assert.Equal(t, "12345678", result)
}
func TestTruncateToken_NineChars(t *testing.T) {
token := "123456789"
result := truncateToken(token)
assert.Equal(t, "12345678...", result)
}
func TestTruncateToken_Empty(t *testing.T) {
result := truncateToken("")
assert.Equal(t, "", result)
}
// === Client tests ===
func TestClient_SendToIOS_Disabled(t *testing.T) {
client := &Client{
enabled: false,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.NoError(t, err) // Returns nil when disabled
}
func TestClient_SendToIOS_NilAPNs(t *testing.T) {
client := &Client{
enabled: true,
apns: nil, // Not initialized
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.NoError(t, err) // Returns nil when not initialized
}
func TestClient_SendToIOS_CircuitBreakerOpen(t *testing.T) {
breaker := NewCircuitBreaker("apns", WithFailureThreshold(1))
breaker.RecordFailure() // Open the circuit
client := &Client{
enabled: true,
apns: &APNsClient{}, // Non-nil so we pass the nil check
apnsBreaker: breaker,
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToIOS(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.ErrorIs(t, err, ErrCircuitOpen)
}
func TestClient_SendToAndroid_Disabled(t *testing.T) {
client := &Client{
enabled: false,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_SendToAndroid_NilFCM(t *testing.T) {
client := &Client{
enabled: true,
fcm: nil,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_SendToAndroid_CircuitBreakerOpen(t *testing.T) {
breaker := NewCircuitBreaker("fcm", WithFailureThreshold(1))
breaker.RecordFailure()
client := &Client{
enabled: true,
fcm: &FCMClient{}, // Non-nil
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: breaker,
}
err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.ErrorIs(t, err, ErrCircuitOpen)
}
func TestClient_SendToAndroid_Success(t *testing.T) {
server := serveFCMV1Success(t)
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
client := &Client{
enabled: true,
fcm: fcmClient,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_SendToAndroid_Failure_RecordsInBreaker(t *testing.T) {
server := serveFCMV1Error(t, http.StatusInternalServerError, "INTERNAL", "internal error")
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
breaker := NewCircuitBreaker("fcm", WithFailureThreshold(3))
client := &Client{
enabled: true,
fcm: fcmClient,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: breaker,
}
err := client.SendToAndroid(context.Background(), []string{"token1"}, "Title", "Body", nil)
assert.Error(t, err)
assert.Equal(t, 1, breaker.Counts())
}
func TestClient_SendToAll_Disabled(t *testing.T) {
client := &Client{
enabled: false,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToAll(context.Background(), []string{"ios-token"}, []string{"android-token"}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_SendToAll_EmptyTokens(t *testing.T) {
server := serveFCMV1Success(t)
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
client := &Client{
enabled: true,
fcm: fcmClient,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
// No tokens at all — should just return nil
err := client.SendToAll(context.Background(), []string{}, []string{}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_SendToAll_AndroidOnly(t *testing.T) {
server := serveFCMV1Success(t)
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
client := &Client{
enabled: true,
fcm: fcmClient,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendToAll(context.Background(), []string{}, []string{"android-token"}, "Title", "Body", nil)
assert.NoError(t, err)
}
func TestClient_IsIOSEnabled(t *testing.T) {
clientWithAPNS := &Client{apns: &APNsClient{}}
clientWithoutAPNS := &Client{apns: nil}
assert.True(t, clientWithAPNS.IsIOSEnabled())
assert.False(t, clientWithoutAPNS.IsIOSEnabled())
}
func TestClient_IsAndroidEnabled(t *testing.T) {
clientWithFCM := &Client{fcm: &FCMClient{}}
clientWithoutFCM := &Client{fcm: nil}
assert.True(t, clientWithFCM.IsAndroidEnabled())
assert.False(t, clientWithoutFCM.IsAndroidEnabled())
}
func TestClient_HealthCheck(t *testing.T) {
client := &Client{
apns: nil,
fcm: nil,
}
err := client.HealthCheck(context.Background())
assert.NoError(t, err)
client.fcm = &FCMClient{}
err = client.HealthCheck(context.Background())
assert.NoError(t, err)
}
func TestClient_SendActionableNotification_Disabled(t *testing.T) {
client := &Client{
enabled: false,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
assert.NoError(t, err)
}
func TestClient_SendActionableNotification_NilAPNs(t *testing.T) {
server := serveFCMV1Success(t)
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
client := &Client{
enabled: true,
apns: nil,
fcm: fcmClient,
apnsBreaker: NewCircuitBreaker("apns"),
fcmBreaker: NewCircuitBreaker("fcm"),
}
// Should skip iOS and send Android
err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
assert.NoError(t, err)
}
func TestClient_SendActionableNotification_APNsBreakerOpen(t *testing.T) {
server := serveFCMV1Success(t)
defer server.Close()
fcmClient := newTestFCMClient(server.URL)
apnsBreaker := NewCircuitBreaker("apns", WithFailureThreshold(1))
apnsBreaker.RecordFailure()
client := &Client{
enabled: true,
apns: &APNsClient{},
fcm: fcmClient,
apnsBreaker: apnsBreaker,
fcmBreaker: NewCircuitBreaker("fcm"),
}
err := client.SendActionableNotification(context.Background(), []string{"ios"}, []string{"android"}, "Title", "Body", nil, "TASK_DUE")
// Should return ErrCircuitOpen because that was the lastErr set
assert.ErrorIs(t, err, ErrCircuitOpen)
}
// === FCM additional tests ===
func TestFCMV1Send_WithDataPayload(t *testing.T) {
var receivedData map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req fcmV1Request
_ = json.NewDecoder(r.Body).Decode(&req)
receivedData = req.Message.Data
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(fcmV1Response{Name: "projects/test/messages/0:12345"})
}))
defer server.Close()
client := newTestFCMClient(server.URL)
data := map[string]string{
"task_id": "42",
"action": "complete",
"deep_link": "/tasks/42",
}
err := client.Send(context.Background(), []string{"token"}, "Title", "Body", data)
require.NoError(t, err)
assert.Equal(t, "42", receivedData["task_id"])
assert.Equal(t, "complete", receivedData["action"])
assert.Equal(t, "/tasks/42", receivedData["deep_link"])
}
func TestFCMV1Send_ContextCancelled(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The server just hangs — context cancellation should cause the request to fail
select {}
}))
defer server.Close()
client := newTestFCMClient(server.URL)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
err := client.Send(ctx, []string{"token"}, "Title", "Body", nil)
assert.Error(t, err)
}
func TestFCMSendError_ErrorFormatting(t *testing.T) {
// Short token (no truncation)
shortErr := &FCMSendError{
Token: "abc",
StatusCode: 500,
ErrorCode: FCMErrInternal,
Message: "server error",
}
assert.Contains(t, shortErr.Error(), "abc")
assert.Contains(t, shortErr.Error(), "500")
assert.Contains(t, shortErr.Error(), "INTERNAL")
assert.Contains(t, shortErr.Error(), "server error")
}
func TestParseFCMV1Error_MalformedJSON(t *testing.T) {
result := parseFCMV1Error("token123", 500, []byte("not json"))
assert.Equal(t, 500, result.StatusCode)
assert.Contains(t, result.Message, "unparseable error response")
}
func TestParseFCMV1Error_ValidJSON(t *testing.T) {
body := `{"error":{"code":404,"message":"not found","status":"NOT_FOUND"}}`
result := parseFCMV1Error("token123", 404, []byte(body))
assert.Equal(t, 404, result.StatusCode)
assert.Equal(t, FCMErrorCode("NOT_FOUND"), result.ErrorCode)
assert.Equal(t, "not found", result.Message)
}
// === Platform constants ===
func TestPlatformConstants(t *testing.T) {
assert.Equal(t, "ios", PlatformIOS)
assert.Equal(t, "android", PlatformAndroid)
}