package web_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "code.severnaya.net/kordophone-mock/v2/data" "code.severnaya.net/kordophone-mock/v2/model" "code.severnaya.net/kordophone-mock/v2/server" "code.severnaya.net/kordophone-mock/v2/web" "golang.org/x/net/websocket" ) func TestVersion(t *testing.T) { s := httptest.NewServer(web.NewMockHTTPServer(web.MockHTTPServerConfiguration{})) resp, err := http.Get(s.URL + "/version") if err != nil { t.Fatalf("TestVersion error: %s", err) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } if string(body) != server.VERSION { t.Fatalf("Unexpected return value: %s (expected %s)", body, "1.0") } } func TestStatus(t *testing.T) { s := httptest.NewServer(web.NewMockHTTPServer(web.MockHTTPServerConfiguration{})) resp, err := http.Get(s.URL + "/status") if err != nil { t.Fatalf("TestStatus error: %s", err) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } if string(body) != "OK" { t.Fatalf("Unexpected return value: %s (expected %s)", body, "OK") } } func TestConversations(t *testing.T) { server := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{}) httpServer := httptest.NewServer(server) conversation := model.Conversation{ Date: time.Now(), Participants: []string{"Alice", "Bob"}, UnreadCount: 1, LastMessagePreview: "Hello world", Guid: "1234567890", } server.Server.AddConversation(conversation) resp, err := http.Get(httpServer.URL + "/conversations") if err != nil { t.Fatalf("TestConversations error: %s", err) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } var convos []model.Conversation err = json.Unmarshal(body, &convos) if err != nil { t.Fatalf("Error unmarshalling JSON: %s", err) } if len(convos) != 1 { t.Fatalf("Unexpected number of conversations: %d (expected %d)", len(convos), 1) } testConversation := &convos[0] if testConversation.Equal(&conversation) != true { t.Fatalf("Unexpected conversation: %v (expected %v)", convos[0], conversation) } } func TestMessages(t *testing.T) { server := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{}) httpServer := httptest.NewServer(server) const sender = "Alice" const text = "This is a test." conversation := model.Conversation{ Date: time.Now(), Participants: []string{sender}, UnreadCount: 1, Guid: "1234567890", } server.Server.AddConversation(conversation) message := model.Message{ Text: text, Sender: &conversation.Participants[0], Date: time.Now(), } server.Server.AppendMessageToConversation(&conversation, message) resp, err := http.Get(httpServer.URL + "/messages?guid=" + *&conversation.Guid) if err != nil { t.Fatalf("TestMessages error: %s", err) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } var messages []model.Message err = json.Unmarshal(body, &messages) if err != nil { t.Fatalf("Error unmarshalling JSON: %s", err) } if len(messages) != 1 { t.Fatalf("Unexpected num messages: %d (expected %d)", len(messages), 1) } fetchedMessage := messages[0] if fetchedMessage.Text != message.Text { t.Fatalf("Unexpected message text: %s (expected %s)", fetchedMessage.Text, message.Text) } if *fetchedMessage.Sender != *message.Sender { t.Fatalf("Unexpected message sender: %s (expected %s)", *fetchedMessage.Sender, *message.Sender) } } func TestAuthentication(t *testing.T) { s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true}) httpServer := httptest.NewServer(s) // First, try authenticated request and make sure it fails resp, err := http.Get(httpServer.URL + "/status") if err != nil { t.Fatalf("TestAuthentication status error: %s", err) } if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized) } tryAuthenticate := func(username string, password string) *http.Response { authRequest := web.AuthenticationRequest{ Username: username, Password: password, } authRequestJSON, err := json.Marshal(authRequest) if err != nil { t.Fatalf("Error marshalling JSON: %s", err) } resp, err := http.Post(httpServer.URL+"/authenticate", "application/json", io.NopCloser(bytes.NewReader(authRequestJSON))) if err != nil { t.Fatalf("TestAuthentication error: %s", err) } return resp } // Send authentication request with bad credentials resp = tryAuthenticate("bad", "credentials") if resp.StatusCode == http.StatusOK { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized) } // Now try good credentials resp = tryAuthenticate(server.AUTH_USERNAME, server.AUTH_PASSWORD) if resp.StatusCode != http.StatusOK { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK) } // Decode the token from the body. body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } var authToken model.AuthToken err = json.Unmarshal(body, &authToken) if err != nil { t.Fatalf("Error unmarshalling JSON: %s, body: %s", err, body) } if authToken.SignedToken == "" { t.Fatalf("Unexpected empty signed token") } // Send a request with the signed token req, err := http.NewRequest(http.MethodGet, httpServer.URL+"/status", nil) if err != nil { t.Fatalf("Error creating request: %s", err) } req.Header.Set("Authorization", "Bearer "+authToken.SignedToken) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error sending request: %s", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized) } body, err = io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } if string(body) != "OK" { t.Fatalf("Unexpected body: %s (expected %s)", body, "OK") } } func TestUpdates(t *testing.T) { s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true}) httpServer := httptest.NewServer(s) messageSeq := 0 // Mock conversation guid := "1234567890" conversation := model.Conversation{ Date: time.Now(), Participants: []string{"Alice"}, UnreadCount: 0, Guid: guid, } s.Server.AddConversation(conversation) // Receive a message message := model.Message{ Text: "This is a test.", Sender: &conversation.Participants[0], Date: time.Now(), } // This should enqueue an update item s.Server.ReceiveMessage(&conversation, message) resp, err := http.Get(httpServer.URL + fmt.Sprintf("/pollUpdates?seq=%d", messageSeq)) if err != nil { t.Fatalf("TestUpdates error: %s", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } var updates []model.UpdateItem err = json.Unmarshal(body, &updates) if err != nil { t.Fatalf("Error unmarshalling JSON: %s", err) } if len(updates) != 1 { t.Fatalf("Unexpected num updates: %d (expected %d)", len(updates), 1) } update := updates[0] // Message seq should be >= messageSeq messageSeq = update.MessageSequenceNumber if messageSeq != 1 { t.Fatalf("Unexpected message seq: %d (expected >= 0)", messageSeq) } if update.Conversation.Guid != conversation.Guid { t.Fatalf("Unexpected conversation guid: %s (expected %s)", update.Conversation.Guid, conversation.Guid) } if update.Message.Text != message.Text { t.Fatalf("Unexpected message text: %s (expected %s)", update.Message.Text, message.Text) } } type MessageUpdateError struct { Message string } func (e MessageUpdateError) Error() string { return e.Message } func TestUpdatesWebsocket(t *testing.T) { s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true}) httpServer := httptest.NewServer(s) // Mock conversation guid := "1234567890" conversation := model.Conversation{ Date: time.Now(), Participants: []string{"Alice"}, UnreadCount: 0, Guid: guid, } s.Server.AddConversation(conversation) // Receive a message message := model.Message{ Text: "This is a test.", Sender: &conversation.Participants[0], Date: time.Now(), } // Open websocket connection wsURL := "ws" + strings.TrimPrefix(httpServer.URL, "http") + "/updates" ws, err := websocket.Dial(wsURL, "ws", httpServer.URL) if err != nil { t.Fatalf("Error opening websocket: %s", err) } // Await messages on the websocket messageReceived := make(chan bool) errorEncountered := make(chan error) go func() { // Read from websocket var updates []model.UpdateItem err := websocket.JSON.Receive(ws, &updates) if err != nil { errorEncountered <- err return } else { if len(updates) != 1 { errorEncountered <- MessageUpdateError{ fmt.Sprintf("Unexpected num updates: %d (expected %d)", len(updates), 1), } return } update := updates[0] if update.Conversation.Guid != conversation.Guid { errorEncountered <- MessageUpdateError{ fmt.Sprintf("Unexpected conversation guid: %s (expected %s)", update.Conversation.Guid, conversation.Guid), } return } if update.Message.Text != message.Text { errorEncountered <- MessageUpdateError{ fmt.Sprintf("Unexpected message text: %s (expected %s)", update.Message.Text, message.Text), } return } messageReceived <- true } }() // sleep for a bit to allow the websocket to connect time.Sleep(100 * time.Millisecond) // This should enqueue an update item s.Server.ReceiveMessage(&conversation, message) // Await expectation select { case <-messageReceived: // COOL case err := <-errorEncountered: t.Fatalf("Error encountered reading from websocket: %s", err) case <-time.After(1 * time.Second): t.Fatalf("Timed out waiting for websocket message") } } func TestMarkConversation(t *testing.T) { s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true}) httpServer := httptest.NewServer(s) // Mock conversation guid := "1234567890" conversation := model.Conversation{ Date: time.Now(), Participants: []string{"Alice"}, UnreadCount: 0, Guid: guid, } s.Server.AddConversation(conversation) // Receive message to mark as unread message := model.Message{ Text: "This is a test.", Sender: &conversation.Participants[0], Date: time.Now(), } s.Server.ReceiveMessage(&conversation, message) if convo, _ := s.Server.ConversationForGUID(guid); convo.UnreadCount != 1 { t.Fatalf("Unexpected unread count: %d (expected %d)", convo.UnreadCount, 1) } // Mark conversation as read resp, err := http.Post(httpServer.URL+"/markConversation?guid="+guid, "", nil) if err != nil { t.Fatalf("TestMarkConversation error: %s", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK) } if convo, _ := s.Server.ConversationForGUID(guid); convo.UnreadCount != 0 { t.Fatalf("Unexpected unread count: %d (expected %d)", convo.UnreadCount, 0) } } func TestMessageQueries(t *testing.T) { s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true}) httpServer := httptest.NewServer(s) // Mock conversation guid := "1234567890" conversation := model.Conversation{ Date: time.Now(), Participants: []string{"Alice"}, UnreadCount: 0, Guid: guid, } s.Server.AddConversation(conversation) // Mock messages numMessages := 20 for i := 0; i < numMessages; i++ { message := data.GenerateRandomMessage(conversation.Participants) s.Server.AppendMessageToConversation(&conversation, message) } // Pick a pivot message from the sorted list sortedMessages := s.Server.MessagesForConversation(&conversation) pivotMessage := sortedMessages[len(sortedMessages)/2] // Query messages before the pivot, test limit also limitMessageCount := 5 resp, err := http.Get(httpServer.URL + fmt.Sprintf("/messages?guid=%s&beforeMessageGUID=%s&limit=%d", guid, pivotMessage.Guid, limitMessageCount)) if err != nil { t.Fatalf("TestMessageQueries error: %s", err) } // Decode response body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } var messages []model.Message err = json.Unmarshal(body, &messages) if err != nil { t.Fatalf("Error unmarshalling JSON: %s", err) } if len(messages) != limitMessageCount { t.Fatalf("Unexpected num messages: %d (expected %d)", len(messages), limitMessageCount) } // Make sure before query is exclusive of the pivot message for _, message := range messages { if message.Guid == pivotMessage.Guid { t.Fatalf("Found pivot guid in before query: %s (expected != %s)", message.Guid, pivotMessage.Guid) } } // Make sure messages are actually before the pivot for _, message := range messages { if message.Date.After(pivotMessage.Date) { t.Fatalf("Unexpected message date: %s (expected before %s)", message.Date, pivotMessage.Date) } } // Query messages after the pivot resp, err = http.Get(httpServer.URL + fmt.Sprintf("/messages?guid=%s&afterMessageGUID=%s", guid, pivotMessage.Guid)) if err != nil { t.Fatalf("TestMessageQueries error: %s", err) } // Decode response body, err = io.ReadAll(resp.Body) if err != nil { t.Fatalf("Error decoding body: %s", body) } messages = []model.Message{} err = json.Unmarshal(body, &messages) if err != nil { t.Fatalf("Error unmarshalling JSON: %s", err) } // Make sure after query is exclusive of the pivot message for _, message := range messages { if message.Guid == pivotMessage.Guid { t.Fatalf("Found pivot guid in after query: %s (expected != %s)", message.Guid, pivotMessage.Guid) } } // Make sure messages are actually after the pivot for _, message := range messages { if message.Date.Before(pivotMessage.Date) { t.Fatalf("Unexpected message date: %s (expected after %s)", message.Date, pivotMessage.Date) } } }