package push import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // newTestFCMClient creates an FCMClient pointing at the given test server URL. func newTestFCMClient(serverURL string) *FCMClient { return &FCMClient{ serverKey: "test-server-key", httpClient: http.DefaultClient, } } // serveFCMResponse creates an httptest.Server that returns the given FCMResponse as JSON. func serveFCMResponse(t *testing.T, resp FCMResponse) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) err := json.NewEncoder(w).Encode(resp) require.NoError(t, err) })) } // sendWithEndpoint is a helper that sends an FCM notification using a custom endpoint // (the test server) instead of the real FCM endpoint. This avoids modifying the // production code to be testable and instead temporarily overrides the client's HTTP // transport to redirect requests to our test server. func sendWithEndpoint(client *FCMClient, server *httptest.Server, ctx context.Context, tokens []string, title, message string, data map[string]string) error { // Override the HTTP client to redirect all requests to the test server client.httpClient = server.Client() // We need to intercept the request and redirect it to our test server. // Use a custom RoundTripper that rewrites the URL. originalTransport := server.Client().Transport client.httpClient.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) { // Rewrite the URL to point to the test server req.URL.Scheme = "http" req.URL.Host = server.Listener.Addr().String() if originalTransport != nil { return originalTransport.RoundTrip(req) } return http.DefaultTransport.RoundTrip(req) }) return client.Send(ctx, tokens, title, message, data) } // roundTripFunc is a function that implements http.RoundTripper. type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func TestFCMSend_MoreResultsThanTokens_NoPanic(t *testing.T) { // FCM returns 5 results but we only sent 2 tokens. // Before the bounds check fix, this would panic with index out of range. fcmResp := FCMResponse{ MulticastID: 12345, Success: 2, Failure: 3, Results: []FCMResult{ {MessageID: "msg1"}, {MessageID: "msg2"}, {Error: "InvalidRegistration"}, {Error: "NotRegistered"}, {Error: "InvalidRegistration"}, }, } server := serveFCMResponse(t, fcmResp) defer server.Close() client := newTestFCMClient(server.URL) tokens := []string{"token-aaa-111", "token-bbb-222"} // This must not panic err := sendWithEndpoint(client, server, context.Background(), tokens, "Test", "Body", nil) assert.NoError(t, err) } func TestFCMSend_FewerResultsThanTokens_NoPanic(t *testing.T) { // FCM returns fewer results than tokens we sent. // This is also a malformed response but should not panic. fcmResp := FCMResponse{ MulticastID: 12345, Success: 1, Failure: 0, Results: []FCMResult{ {MessageID: "msg1"}, }, } server := serveFCMResponse(t, fcmResp) defer server.Close() client := newTestFCMClient(server.URL) tokens := []string{"token-aaa-111", "token-bbb-222", "token-ccc-333"} err := sendWithEndpoint(client, server, context.Background(), tokens, "Test", "Body", nil) assert.NoError(t, err) } func TestFCMSend_EmptyResponse_NoPanic(t *testing.T) { // FCM returns an empty Results slice. fcmResp := FCMResponse{ MulticastID: 12345, Success: 0, Failure: 0, Results: []FCMResult{}, } server := serveFCMResponse(t, fcmResp) defer server.Close() client := newTestFCMClient(server.URL) tokens := []string{"token-aaa-111"} err := sendWithEndpoint(client, server, context.Background(), tokens, "Test", "Body", nil) // No panic expected. The function returns nil because fcmResp.Success == 0 // and fcmResp.Failure == 0 (the "all failed" check requires Failure > 0). assert.NoError(t, err) } func TestFCMSend_NilResultsSlice_NoPanic(t *testing.T) { // FCM returns a response with nil Results (e.g., malformed JSON). fcmResp := FCMResponse{ MulticastID: 12345, Success: 0, Failure: 1, } server := serveFCMResponse(t, fcmResp) defer server.Close() client := newTestFCMClient(server.URL) tokens := []string{"token-aaa-111"} err := sendWithEndpoint(client, server, context.Background(), tokens, "Test", "Body", nil) // Should return error because Success == 0 and Failure > 0 assert.Error(t, err) assert.Contains(t, err.Error(), "all FCM notifications failed") } func TestFCMSend_EmptyTokens_ReturnsNil(t *testing.T) { // Verify the early return for empty tokens. client := &FCMClient{ serverKey: "test-key", httpClient: http.DefaultClient, } err := client.Send(context.Background(), []string{}, "Test", "Body", nil) assert.NoError(t, err) } func TestFCMSend_ResultsWithErrorsMatchTokens(t *testing.T) { // Normal case: results count matches tokens count, all with errors. fcmResp := FCMResponse{ MulticastID: 12345, Success: 0, Failure: 2, Results: []FCMResult{ {Error: "InvalidRegistration"}, {Error: "NotRegistered"}, }, } server := serveFCMResponse(t, fcmResp) defer server.Close() client := newTestFCMClient(server.URL) tokens := []string{"token-aaa-111", "token-bbb-222"} err := sendWithEndpoint(client, server, context.Background(), tokens, "Test", "Body", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "all FCM notifications failed") }