package ntfy import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" ) func TestNewClient(t *testing.T) { client := NewClient("http://ntfy.example.com", []string{"alerts", "updates"}) if client.baseURL != "http://ntfy.example.com" { t.Errorf("baseURL = %s, want http://ntfy.example.com", client.baseURL) } if len(client.topics) != 2 { t.Errorf("topics length = %d, want 2", len(client.topics)) } } func TestNewClient_TrimsTrailingSlash(t *testing.T) { client := NewClient("http://ntfy.example.com/", []string{"test"}) if client.baseURL != "http://ntfy.example.com" { t.Errorf("baseURL = %s, want http://ntfy.example.com (trailing slash removed)", client.baseURL) } } func TestMessage_JSON(t *testing.T) { msg := Message{ ID: "test-123", Time: 1706803200, Event: "message", Topic: "alerts", Title: "Test Alert", Message: "This is a test", Priority: 4, Tags: []string{"warning", "fire"}, Click: "https://example.com", } data, err := json.Marshal(msg) if err != nil { t.Fatalf("failed to marshal: %v", err) } var decoded Message if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal: %v", err) } if decoded.ID != msg.ID { t.Errorf("ID = %s, want %s", decoded.ID, msg.ID) } if decoded.Priority != msg.Priority { t.Errorf("Priority = %d, want %d", decoded.Priority, msg.Priority) } if len(decoded.Tags) != 2 { t.Errorf("Tags length = %d, want 2", len(decoded.Tags)) } } func TestClient_Subscribe_ReceivesMessages(t *testing.T) { messages := []Message{ {Event: "open"}, {Event: "message", ID: "msg1", Topic: "test", Title: "First", Message: "Hello"}, {Event: "keepalive"}, {Event: "message", ID: "msg2", Topic: "test", Title: "Second", Message: "World"}, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) for _, msg := range messages { data, _ := json.Marshal(msg) _, _ = fmt.Fprintf(w, "%s\n", data) } })) defer server.Close() client := NewClient(server.URL, []string{"test"}) msgCh := make(chan Message, 10) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go client.Subscribe(ctx, msgCh) // Should receive only "message" events received := make([]Message, 0) timeout := time.After(1 * time.Second) loop: for { select { case msg := <-msgCh: received = append(received, msg) if len(received) >= 2 { break loop } case <-timeout: break loop } } if len(received) != 2 { t.Errorf("received %d messages, want 2", len(received)) } if len(received) > 0 && received[0].ID != "msg1" { t.Errorf("first message ID = %s, want msg1", received[0].ID) } if len(received) > 1 && received[1].ID != "msg2" { t.Errorf("second message ID = %s, want msg2", received[1].ID) } } func TestClient_Subscribe_FilterEvents(t *testing.T) { messages := []Message{ {Event: "open"}, {Event: "keepalive"}, {Event: "message", ID: "actual", Topic: "test"}, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) for _, msg := range messages { data, _ := json.Marshal(msg) _, _ = fmt.Fprintf(w, "%s\n", data) } })) defer server.Close() client := NewClient(server.URL, []string{"test"}) msgCh := make(chan Message, 10) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() go client.Subscribe(ctx, msgCh) select { case msg := <-msgCh: if msg.Event != "message" { t.Errorf("received event = %s, want message", msg.Event) } if msg.ID != "actual" { t.Errorf("message ID = %s, want actual", msg.ID) } case <-time.After(500 * time.Millisecond): t.Error("timeout waiting for message") } } func TestClient_Subscribe_ContextCancellation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Slow server - keeps connection open time.Sleep(10 * time.Second) })) defer server.Close() client := NewClient(server.URL, []string{"test"}) msgCh := make(chan Message, 10) ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { client.Subscribe(ctx, msgCh) close(done) }() // Cancel quickly time.Sleep(50 * time.Millisecond) cancel() // Should exit promptly select { case <-done: // Success case <-time.After(2 * time.Second): t.Error("Subscribe did not exit after context cancellation") } } func TestClient_connect_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("internal error")) })) defer server.Close() client := NewClient(server.URL, []string{"test"}) msgCh := make(chan Message, 10) err := client.connect(context.Background(), msgCh) if err == nil { t.Error("expected error for server error response") } } func TestClient_connect_URLConstruction(t *testing.T) { var requestedURL string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestedURL = r.URL.Path w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewClient(server.URL, []string{"alerts", "updates"}) _ = client.connect(context.Background(), make(chan Message)) expected := "/alerts,updates/json" if requestedURL != expected { t.Errorf("URL path = %s, want %s", requestedURL, expected) } }