diff --git a/internal/webserver/handler_bookmarks_test.go b/internal/webserver/handler_bookmarks_test.go new file mode 100644 index 0000000..1058b4b --- /dev/null +++ b/internal/webserver/handler_bookmarks_test.go @@ -0,0 +1,46 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" + + "gitlab.com/offline-twitter/twitter_offline_engine/internal/webserver" + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" +) + +func TestBookmarksTab(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + recorder := httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks", nil)) + resp := recorder.Result() + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 2) + + // Double check pagination works properly + recorder = httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks?cursor=1800452344077464795", nil)) + resp = recorder.Result() + require.Equal(resp.StatusCode, 200) + + root, err = html.Parse(resp.Body) + require.NoError(err) + tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 1) +} diff --git a/internal/webserver/handler_follow_unfollow_test.go b/internal/webserver/handler_follow_unfollow_test.go new file mode 100644 index 0000000..c199eee --- /dev/null +++ b/internal/webserver/handler_follow_unfollow_test.go @@ -0,0 +1,60 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + "strings" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" +) + +// TODO: deprecated-offline-follows + +func TestFollowUnfollow(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + user, err := profile.GetUserByHandle("kwamurai") + require.NoError(err) + require.False(user.IsFollowed) + + // Follow the user + resp := do_request(httptest.NewRequest("POST", "/follow/kwamurai", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + button := cascadia.Query(root, selector("button")) + assert.Contains(button.Attr, html.Attribute{Key: "hx-post", Val: "/unfollow/kwamurai"}) + assert.Equal(strings.TrimSpace(button.FirstChild.Data), "Unfollow") + + user, err = profile.GetUserByHandle("kwamurai") + require.NoError(err) + require.True(user.IsFollowed) + + // Unfollow the user + resp = do_request(httptest.NewRequest("POST", "/unfollow/kwamurai", nil)) + require.Equal(resp.StatusCode, 200) + + root, err = html.Parse(resp.Body) + require.NoError(err) + button = cascadia.Query(root, selector("button")) + assert.Contains(button.Attr, html.Attribute{Key: "hx-post", Val: "/follow/kwamurai"}) + assert.Equal(strings.TrimSpace(button.FirstChild.Data), "Follow") + + user, err = profile.GetUserByHandle("kwamurai") + require.NoError(err) + require.False(user.IsFollowed) +} + +func TestFollowUnfollowPostOnly(t *testing.T) { + require := require.New(t) + resp := do_request(httptest.NewRequest("GET", "/follow/kwamurai", nil)) + require.Equal(resp.StatusCode, 405) + resp = do_request(httptest.NewRequest("GET", "/unfollow/kwamurai", nil)) + require.Equal(resp.StatusCode, 405) +} diff --git a/internal/webserver/handler_lists_test.go b/internal/webserver/handler_lists_test.go new file mode 100644 index 0000000..fa830f1 --- /dev/null +++ b/internal/webserver/handler_lists_test.go @@ -0,0 +1,114 @@ +package webserver_test + +import ( + "fmt" + "strings" + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" +) + +func TestListsIndex(t *testing.T) { + require := require.New(t) + resp := do_request(httptest.NewRequest("GET", "/lists", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + + // Check that there's at least 2 Lists + assert.True(t, len(cascadia.QueryAll(root, selector(".list-preview"))) >= 2) +} + +func TestListDetail(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + // Users + resp := do_request(httptest.NewRequest("GET", "/lists/1/users", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 5) + + // Feed + resp1 := do_request(httptest.NewRequest("GET", "/lists/2", nil)) + require.Equal(resp1.StatusCode, 200) + root1, err := html.Parse(resp1.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root1, selector(".timeline > .tweet")), 3) +} + +func TestListDetailDoesntExist(t *testing.T) { + resp := do_request(httptest.NewRequest("GET", "/lists/2523478", nil)) + require.Equal(t, resp.StatusCode, 404) +} + +func TestListDetailInvalidId(t *testing.T) { + resp := do_request(httptest.NewRequest("GET", "/lists/asd", nil)) + require.Equal(t, resp.StatusCode, 400) +} + +func TestListAddAndDeleteUser(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + // Initial + resp := do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2) + + // Add a user + resp_add := do_request(httptest.NewRequest("GET", "/lists/2/add_user?user_handle=cernovich", nil)) + require.Equal(resp_add.StatusCode, 302) + require.Equal("/lists/2/users", resp_add.Header.Get("Location")) + + // Should be +1 user now + resp = do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 3) + + // Delete a user + resp_remove := do_request(httptest.NewRequest("GET", "/lists/2/remove_user?user_handle=cernovich", nil)) + require.Equal(resp_remove.StatusCode, 302) + require.Equal("/lists/2/users", resp_remove.Header.Get("Location")) + + // Should be +1 user now + resp = do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2) +} + +func TestCreateNewList(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + // Initial list-of-lists + resp := do_request(httptest.NewRequest("GET", "/lists", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + num_lists := len(cascadia.QueryAll(root, selector(".list-preview"))) + + // Create a new list + resp_add := do_request(httptest.NewRequest("POST", "/lists", strings.NewReader(`{"name": "My New List"}`))) + require.Equal(resp_add.StatusCode, 302) + require.Equal(fmt.Sprintf("/lists/%d/users", num_lists+1), resp_add.Header.Get("Location")) + + // Should be N+1 lists now + resp = do_request(httptest.NewRequest("GET", "/lists", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".list-preview")), num_lists+1) +} diff --git a/internal/webserver/handler_messages_test.go b/internal/webserver/handler_messages_test.go new file mode 100644 index 0000000..46cf384 --- /dev/null +++ b/internal/webserver/handler_messages_test.go @@ -0,0 +1,157 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" + + "gitlab.com/offline-twitter/twitter_offline_engine/internal/webserver" + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" +) + +// Loading the index page should work if you're logged in +func TestMessagesIndexPage(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat list + recorder := httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/messages", nil)) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2) + assert.Len(cascadia.QueryAll(root, selector(".chat-view .dm-message")), 0) // No messages until you click on one +} + +// Open a chat room +func TestMessagesRoom(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat detail + recorder := httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328", nil)) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2) // Chat list still renders + assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5) + + // Should have the poller at the bottom + poller := cascadia.Query(root, selector("#new-messages-poller")) + assert.NotNil(poller) + assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) + assert.Contains( + cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, + html.Attribute{Key: "value", Val: "1"}, + ) + assert.Contains( + cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, + html.Attribute{Key: "value", Val: "1686025129144"}, + ) +} + +// Loading the page since a given message +func TestMessagesRoomPollForUpdates(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat detail + recorder := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129141", nil) + req.Header.Set("HX-Request", "true") + app.ServeHTTP(recorder, req) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 3) + + // Should have the poller at the bottom + poller := cascadia.Query(root, selector("#new-messages-poller")) + assert.NotNil(poller) + assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) + assert.Contains( + cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, + html.Attribute{Key: "value", Val: "1"}, + ) + assert.Contains( + cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, + html.Attribute{Key: "value", Val: "1686025129144"}, + ) +} + +// Loading the page since latest message (no updates) +func TestMessagesRoomPollForUpdatesEmptyResult(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat detail + recorder := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144", nil) + req.Header.Set("HX-Request", "true") + app.ServeHTTP(recorder, req) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 0) + + // Should have the poller at the bottom, with the same value as previously + poller := cascadia.Query(root, selector("#new-messages-poller")) + assert.NotNil(poller) + assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) + assert.Contains( + cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, + html.Attribute{Key: "value", Val: "1"}, + ) + assert.Contains( + cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, + html.Attribute{Key: "value", Val: "1686025129144"}, + ) +} + +// Scroll back in the messages +func TestMessagesPaginate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat detail + recorder := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?cursor=1686025129142", nil) + req.Header.Set("HX-Request", "true") + app.ServeHTTP(recorder, req) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 2) +} diff --git a/internal/webserver/handler_notifications_test.go b/internal/webserver/handler_notifications_test.go new file mode 100644 index 0000000..0e1490d --- /dev/null +++ b/internal/webserver/handler_notifications_test.go @@ -0,0 +1,44 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" + + "gitlab.com/offline-twitter/twitter_offline_engine/internal/webserver" + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" +) + +func TestNotifications(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Notifications page + recorder := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/notifications", nil) + app.ServeHTTP(recorder, req) + resp := recorder.Result() + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".notification")), 6) + + // Show more + recorder = httptest.NewRecorder() + req = httptest.NewRequest("GET", "/notifications?cursor=1726604756351", nil) + req.Header.Set("HX-Request", "true") + app.ServeHTTP(recorder, req) + resp = recorder.Result() + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".notification")), 5) +} diff --git a/internal/webserver/handler_search_test.go b/internal/webserver/handler_search_test.go new file mode 100644 index 0000000..0f2c356 --- /dev/null +++ b/internal/webserver/handler_search_test.go @@ -0,0 +1,153 @@ +package webserver_test + +import ( + "fmt" + "net/url" + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" +) + +func TestSearchQueryStringRedirect(t *testing.T) { + assert := assert.New(t) + + resp := do_request(httptest.NewRequest("GET", "/search?q=asdf", nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/search/asdf") +} + +func TestSearch(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + search_txt := "to:spacex to:covfefeanon" + + resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape(search_txt)), nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "Search | Offline Twitter") + assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: search_txt}) + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 1) +} + +func TestSearchWithCursor(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // First, without the cursor + resp := do_request(httptest.NewRequest("GET", "/search/who%20are", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 3) + + // Add a cursor with the 1st tweet's posted_at time + resp = do_request(httptest.NewRequest("GET", "/search/who%20are?cursor=1628979529000", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 2) +} + +func TestSearchWithSortOrder(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/search/think?sort-order=most%20likes", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Contains(cascadia.Query(root, selector("select[name='sort-order'] option[selected]")).FirstChild.Data, "most likes") + + tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) + txts := []string{ + "Morally nuanced and complicated discussion", + "a lot of y’all embarrass yourselves on this", + "this is why the \"think tank mindset\" is a dead end", + "At this point what can we expect I guess", + "Idk if this is relevant to your department", + } + for i, txt := range txts { + assert.Contains(cascadia.Query(tweets[i], selector("p.text")).FirstChild.Data, txt) + } + + resp = do_request(httptest.NewRequest("GET", "/search/think?sort-order=most%20likes&cursor=413", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) + for i, txt := range txts[2:] { + assert.Contains(cascadia.Query(tweets[i], selector("p.text")).FirstChild.Data, txt) + } +} + +func TestSearchUsers(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/search/no?type=users", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + user_elements := cascadia.QueryAll(root, selector(".users-list .user")) + assert.Len(user_elements, 2) + assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: "no"}) +} + +// Search bar pasted link redirects +// -------------------------------- + +func TestSearchRedirectOnUserHandle(t *testing.T) { + assert := assert.New(t) + + resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("@somebody")), nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/somebody") +} + +func TestSearchRedirectOnTweetLink(t *testing.T) { + assert := assert.New(t) + + // Desktop URL + resp := do_request(httptest.NewRequest("GET", + fmt.Sprintf("/search/%s", url.PathEscape("https://twitter.com/wispem_wantex/status/1695221528617468324")), + nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/tweet/1695221528617468324") + + // Mobile URL + resp = do_request(httptest.NewRequest("GET", + fmt.Sprintf("/search/%s", url.PathEscape("https://mobile.twitter.com/wispem_wantex/status/1695221528617468324")), + nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/tweet/1695221528617468324") +} + +func TestSearchRedirectOnUserFeedLink(t *testing.T) { + assert := assert.New(t) + + // Desktop URL + resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://twitter.com/agsdf")), nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/agsdf") + + // "With Replies" page + resp = do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://x.com/agsdf/with_replies")), nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/agsdf") + + // Mobile URL + resp = do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://mobile.twitter.com/agsdfhh")), nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/agsdfhh") +} diff --git a/internal/webserver/handler_static_test.go b/internal/webserver/handler_static_test.go new file mode 100644 index 0000000..276745d --- /dev/null +++ b/internal/webserver/handler_static_test.go @@ -0,0 +1,23 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/stretchr/testify/require" +) + +func TestStaticFile(t *testing.T) { + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/static/styles.css", nil)) + require.Equal(resp.StatusCode, 200) +} + +func TestStaticFileNonexistent(t *testing.T) { + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/static/blehblehblehwfe", nil)) + require.Equal(resp.StatusCode, 404) +} diff --git a/internal/webserver/handler_timeline_test.go b/internal/webserver/handler_timeline_test.go new file mode 100644 index 0000000..cc7d1a5 --- /dev/null +++ b/internal/webserver/handler_timeline_test.go @@ -0,0 +1,79 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" + + "gitlab.com/offline-twitter/twitter_offline_engine/internal/webserver" + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" +) + +func TestTimeline(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/timeline/offline", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 20) +} + +func TestTimelineWithCursor(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=1631935701000", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 10) +} + +func TestTimelineWithCursorBadNumber(t *testing.T) { + require := require.New(t) + + // With a cursor but it sucks + resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=asdf", nil)) + require.Equal(resp.StatusCode, 400) +} + +func TestUserFeedTimeline(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat list + recorder := httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/timeline", nil)) + resp := recorder.Result() + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 1) +} diff --git a/internal/webserver/handler_tweet_detail_test.go b/internal/webserver/handler_tweet_detail_test.go new file mode 100644 index 0000000..11e345e --- /dev/null +++ b/internal/webserver/handler_tweet_detail_test.go @@ -0,0 +1,141 @@ +package webserver_test + +import ( + "strings" + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" + + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" +) + +func TestTweetDetail(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/1413773185296650241", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + tweet_nodes := cascadia.QueryAll(root, selector(".tweet")) + assert.Len(tweet_nodes, 4) +} + +func TestTweetDetailMissing(t *testing.T) { + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/100089", nil)) + require.Equal(resp.StatusCode, 404) +} + +func TestTweetDetailInvalidNumber(t *testing.T) { + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/fwjgkj", nil)) + require.Equal(resp.StatusCode, 400) +} + +func TestTweetsWithContent(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Poll + resp := do_request(httptest.NewRequest("GET", "/tweet/1465534109573390348", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".poll")), 1) + assert.Len(cascadia.QueryAll(root, selector(".poll__choice")), 4) + + // Video + resp = do_request(httptest.NewRequest("GET", "/tweet/1453461248142495744", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector("video")), 1) + + // Url + resp = do_request(httptest.NewRequest("GET", "/tweet/1438642143170646017", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".embedded-link")), 3) + + // Space + resp = do_request(httptest.NewRequest("GET", "/tweet/1624833173514293249", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".space")), 1) + assert.Len(cascadia.QueryAll(root, selector("ul.space__participants-list li")), 9) +} + +func TestTweetWithEntities(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/1489944024278523906", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + entities := cascadia.QueryAll(root, selector(".entity")) + assert.Len(entities, 2) + assert.Equal(entities[0].Data, "a") + assert.Equal(entities[0].FirstChild.Data, "@gofundme") + assert.Contains(entities[0].Attr, html.Attribute{Key: "href", Val: "/gofundme"}) + assert.Equal(entities[1].Data, "a") + assert.Equal(entities[1].FirstChild.Data, "#BankruptGoFundMe") + assert.Contains(entities[1].Attr, html.Attribute{Key: "href", Val: "/search/%23BankruptGoFundMe"}) +} + +func TestLongTweet(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/1695110851324256692", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + paragraphs := cascadia.QueryAll(root, selector(".tweet .text")) + assert.Len(paragraphs, 22) + + twt, err := profile.GetTweetById(scraper.TweetID(1695110851324256692)) + require.NoError(err) + for i, s := range strings.Split(twt.Text, "\n") { + assert.Equal(strings.TrimSpace(s), strings.TrimSpace(paragraphs[i].FirstChild.Data)) + } +} + +func TestTombstoneTweet(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/31", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + tombstone := cascadia.Query(root, selector(".tweet .tombstone")) + assert.Equal("This Tweet was deleted by the Tweet author", strings.TrimSpace(tombstone.FirstChild.Data)) +} + +func TestTweetThread(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/tweet/1698762403163304110", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + + reply_chains := cascadia.QueryAll(root, selector(".reply-chain")) + require.Len(reply_chains, 2) + + thread_chain := reply_chains[0] + assert.Len(cascadia.QueryAll(thread_chain, selector(".reply-tweet")), 7) +} diff --git a/internal/webserver/handler_user_feed_test.go b/internal/webserver/handler_user_feed_test.go new file mode 100644 index 0000000..c8e3c4c --- /dev/null +++ b/internal/webserver/handler_user_feed_test.go @@ -0,0 +1,150 @@ +package webserver_test + +import ( + "testing" + + "net/http/httptest" + + "github.com/andybalholm/cascadia" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/html" +) + +func TestUserFeed(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/cernovich", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") + + assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 8) + assert.Len(cascadia.QueryAll(root, selector(".timeline > .pinned-tweet")), 1) + assert.Len(cascadia.QueryAll(root, selector(".tweet")), 12) // Pinned tweet appears again +} + +func TestUserFeedWithEntityInBio(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/michaelmalice", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + bio_entities := cascadia.QueryAll(root, selector(".user-header__bio .entity")) + require.Len(bio_entities, 1) + assert.Equal(bio_entities[0].FirstChild.Data, "@SheathUnderwear") +} + +func TestUserFeedMissing(t *testing.T) { + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/awefhwefhwejh", nil)) + require.Equal(resp.StatusCode, 404) +} + +func TestUserFeedWithCursor(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // With a cursor + resp := do_request(httptest.NewRequest("GET", "/cernovich?cursor=1631935701000", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 2) +} + +func TestUserFeedWithCursorBadNumber(t *testing.T) { + require := require.New(t) + + // With a cursor but it sucks + resp := do_request(httptest.NewRequest("GET", "/cernovich?cursor=asdf", nil)) + require.Equal(resp.StatusCode, 400) +} + +func TestUserFeedTweetsOnlyTab(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Peter_Nimitz/without_replies", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 2) +} + +func TestUserFeedMediaTab(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Cernovich/media", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 1) +} + +func TestUserFeedLikesTab(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/MysteryGrove/likes", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 5) + + // Double check pagination works properly + resp = do_request(httptest.NewRequest("GET", "/MysteryGrove/likes?cursor=5", nil)) + require.Equal(resp.StatusCode, 200) + + root, err = html.Parse(resp.Body) + require.NoError(err) + tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweets, 4) +} + +// Followers and followees +// ----------------------- + +func TestUserFollowers(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followers", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 2) +} + +func TestUserFollowees(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followees", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 1) +} diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index efc17e6..f290d84 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -3,20 +3,14 @@ package webserver_test import ( "testing" - "fmt" "net/http" "net/http/httptest" - "net/url" - "strings" "github.com/andybalholm/cascadia" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/net/html" "gitlab.com/offline-twitter/twitter_offline_engine/internal/webserver" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence" - "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" ) type CapturingWriter struct { @@ -65,855 +59,3 @@ func TestHomepage(t *testing.T) { require.Equal(resp.StatusCode, 303) require.Equal(resp.Header.Get("Location"), "/timeline") } - -// User feed -// --------- - -func TestUserFeed(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/cernovich", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") - - assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 8) - assert.Len(cascadia.QueryAll(root, selector(".timeline > .pinned-tweet")), 1) - assert.Len(cascadia.QueryAll(root, selector(".tweet")), 12) // Pinned tweet appears again -} - -func TestUserFeedWithEntityInBio(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/michaelmalice", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - bio_entities := cascadia.QueryAll(root, selector(".user-header__bio .entity")) - require.Len(bio_entities, 1) - assert.Equal(bio_entities[0].FirstChild.Data, "@SheathUnderwear") -} - -func TestUserFeedMissing(t *testing.T) { - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/awefhwefhwejh", nil)) - require.Equal(resp.StatusCode, 404) -} - -func TestUserFeedWithCursor(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // With a cursor - resp := do_request(httptest.NewRequest("GET", "/cernovich?cursor=1631935701000", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") - - tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweet_nodes, 2) -} - -func TestUserFeedWithCursorBadNumber(t *testing.T) { - require := require.New(t) - - // With a cursor but it sucks - resp := do_request(httptest.NewRequest("GET", "/cernovich?cursor=asdf", nil)) - require.Equal(resp.StatusCode, 400) -} - -func TestUserFeedTweetsOnlyTab(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/Peter_Nimitz/without_replies", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 2) -} - -func TestUserFeedMediaTab(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/Cernovich/media", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 1) -} - -func TestUserFeedLikesTab(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/MysteryGrove/likes", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 5) - - // Double check pagination works properly - resp = do_request(httptest.NewRequest("GET", "/MysteryGrove/likes?cursor=5", nil)) - require.Equal(resp.StatusCode, 200) - - root, err = html.Parse(resp.Body) - require.NoError(err) - tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 4) -} - -func TestBookmarksTab(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - recorder := httptest.NewRecorder() - app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks", nil)) - resp := recorder.Result() - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 2) - - // Double check pagination works properly - recorder = httptest.NewRecorder() - app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks?cursor=1800452344077464795", nil)) - resp = recorder.Result() - require.Equal(resp.StatusCode, 200) - - root, err = html.Parse(resp.Body) - require.NoError(err) - tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweets, 1) -} - -// Followers and followees -// ----------------------- - -func TestUserFollowers(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followers", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 2) -} - -func TestUserFollowees(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followees", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 1) -} - -// Timeline page -// ------------- - -func TestTimeline(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/timeline/offline", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") - - tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweet_nodes, 20) -} - -func TestTimelineWithCursor(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=1631935701000", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") - - tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweet_nodes, 10) -} - -func TestTimelineWithCursorBadNumber(t *testing.T) { - require := require.New(t) - - // With a cursor but it sucks - resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=asdf", nil)) - require.Equal(resp.StatusCode, 400) -} - -func TestUserFeedTimeline(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat list - recorder := httptest.NewRecorder() - app.ServeHTTP(recorder, httptest.NewRequest("GET", "/timeline", nil)) - resp := recorder.Result() - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") - - tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweet_nodes, 1) -} - -// Search page -// ----------- - -func TestSearchQueryStringRedirect(t *testing.T) { - assert := assert.New(t) - - resp := do_request(httptest.NewRequest("GET", "/search?q=asdf", nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/search/asdf") -} - -func TestSearch(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - search_txt := "to:spacex to:covfefeanon" - - resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape(search_txt)), nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Search | Offline Twitter") - assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: search_txt}) - - tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) - assert.Len(tweet_nodes, 1) -} - -func TestSearchWithCursor(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // First, without the cursor - resp := do_request(httptest.NewRequest("GET", "/search/who%20are", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 3) - - // Add a cursor with the 1st tweet's posted_at time - resp = do_request(httptest.NewRequest("GET", "/search/who%20are?cursor=1628979529000", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 2) -} - -func TestSearchWithSortOrder(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/search/think?sort-order=most%20likes", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Contains(cascadia.Query(root, selector("select[name='sort-order'] option[selected]")).FirstChild.Data, "most likes") - - tweets := cascadia.QueryAll(root, selector(".timeline > .tweet")) - txts := []string{ - "Morally nuanced and complicated discussion", - "a lot of y’all embarrass yourselves on this", - "this is why the \"think tank mindset\" is a dead end", - "At this point what can we expect I guess", - "Idk if this is relevant to your department", - } - for i, txt := range txts { - assert.Contains(cascadia.Query(tweets[i], selector("p.text")).FirstChild.Data, txt) - } - - resp = do_request(httptest.NewRequest("GET", "/search/think?sort-order=most%20likes&cursor=413", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - tweets = cascadia.QueryAll(root, selector(".timeline > .tweet")) - for i, txt := range txts[2:] { - assert.Contains(cascadia.Query(tweets[i], selector("p.text")).FirstChild.Data, txt) - } -} - -func TestSearchUsers(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/search/no?type=users", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - user_elements := cascadia.QueryAll(root, selector(".users-list .user")) - assert.Len(user_elements, 2) - assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: "no"}) -} - -// Search bar pasted link redirects -// -------------------------------- - -func TestSearchRedirectOnUserHandle(t *testing.T) { - assert := assert.New(t) - - resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("@somebody")), nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/somebody") -} - -func TestSearchRedirectOnTweetLink(t *testing.T) { - assert := assert.New(t) - - // Desktop URL - resp := do_request(httptest.NewRequest("GET", - fmt.Sprintf("/search/%s", url.PathEscape("https://twitter.com/wispem_wantex/status/1695221528617468324")), - nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/tweet/1695221528617468324") - - // Mobile URL - resp = do_request(httptest.NewRequest("GET", - fmt.Sprintf("/search/%s", url.PathEscape("https://mobile.twitter.com/wispem_wantex/status/1695221528617468324")), - nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/tweet/1695221528617468324") -} - -func TestSearchRedirectOnUserFeedLink(t *testing.T) { - assert := assert.New(t) - - // Desktop URL - resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://twitter.com/agsdf")), nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/agsdf") - - // "With Replies" page - resp = do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://x.com/agsdf/with_replies")), nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/agsdf") - - // Mobile URL - resp = do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("https://mobile.twitter.com/agsdfhh")), nil)) - assert.Equal(resp.StatusCode, 302) - assert.Equal(resp.Header.Get("Location"), "/agsdfhh") -} - -// Tweet Detail page -// ----------------- - -func TestTweetDetail(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/1413773185296650241", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - tweet_nodes := cascadia.QueryAll(root, selector(".tweet")) - assert.Len(tweet_nodes, 4) -} - -func TestTweetDetailMissing(t *testing.T) { - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/100089", nil)) - require.Equal(resp.StatusCode, 404) -} - -func TestTweetDetailInvalidNumber(t *testing.T) { - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/fwjgkj", nil)) - require.Equal(resp.StatusCode, 400) -} - -func TestTweetsWithContent(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Poll - resp := do_request(httptest.NewRequest("GET", "/tweet/1465534109573390348", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".poll")), 1) - assert.Len(cascadia.QueryAll(root, selector(".poll__choice")), 4) - - // Video - resp = do_request(httptest.NewRequest("GET", "/tweet/1453461248142495744", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector("video")), 1) - - // Url - resp = do_request(httptest.NewRequest("GET", "/tweet/1438642143170646017", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".embedded-link")), 3) - - // Space - resp = do_request(httptest.NewRequest("GET", "/tweet/1624833173514293249", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".space")), 1) - assert.Len(cascadia.QueryAll(root, selector("ul.space__participants-list li")), 9) -} - -func TestTweetWithEntities(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/1489944024278523906", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - entities := cascadia.QueryAll(root, selector(".entity")) - assert.Len(entities, 2) - assert.Equal(entities[0].Data, "a") - assert.Equal(entities[0].FirstChild.Data, "@gofundme") - assert.Contains(entities[0].Attr, html.Attribute{Key: "href", Val: "/gofundme"}) - assert.Equal(entities[1].Data, "a") - assert.Equal(entities[1].FirstChild.Data, "#BankruptGoFundMe") - assert.Contains(entities[1].Attr, html.Attribute{Key: "href", Val: "/search/%23BankruptGoFundMe"}) -} - -func TestLongTweet(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/1695110851324256692", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - paragraphs := cascadia.QueryAll(root, selector(".tweet .text")) - assert.Len(paragraphs, 22) - - twt, err := profile.GetTweetById(scraper.TweetID(1695110851324256692)) - require.NoError(err) - for i, s := range strings.Split(twt.Text, "\n") { - assert.Equal(strings.TrimSpace(s), strings.TrimSpace(paragraphs[i].FirstChild.Data)) - } -} - -func TestTombstoneTweet(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/31", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - tombstone := cascadia.Query(root, selector(".tweet .tombstone")) - assert.Equal("This Tweet was deleted by the Tweet author", strings.TrimSpace(tombstone.FirstChild.Data)) -} - -func TestTweetThread(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/tweet/1698762403163304110", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - - reply_chains := cascadia.QueryAll(root, selector(".reply-chain")) - require.Len(reply_chains, 2) - - thread_chain := reply_chains[0] - assert.Len(cascadia.QueryAll(thread_chain, selector(".reply-tweet")), 7) -} - -// Follow and unfollow -// ------------------- - -func TestFollowUnfollow(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - user, err := profile.GetUserByHandle("kwamurai") - require.NoError(err) - require.False(user.IsFollowed) - - // Follow the user - resp := do_request(httptest.NewRequest("POST", "/follow/kwamurai", nil)) - require.Equal(resp.StatusCode, 200) - - root, err := html.Parse(resp.Body) - require.NoError(err) - button := cascadia.Query(root, selector("button")) - assert.Contains(button.Attr, html.Attribute{Key: "hx-post", Val: "/unfollow/kwamurai"}) - assert.Equal(strings.TrimSpace(button.FirstChild.Data), "Unfollow") - - user, err = profile.GetUserByHandle("kwamurai") - require.NoError(err) - require.True(user.IsFollowed) - - // Unfollow the user - resp = do_request(httptest.NewRequest("POST", "/unfollow/kwamurai", nil)) - require.Equal(resp.StatusCode, 200) - - root, err = html.Parse(resp.Body) - require.NoError(err) - button = cascadia.Query(root, selector("button")) - assert.Contains(button.Attr, html.Attribute{Key: "hx-post", Val: "/follow/kwamurai"}) - assert.Equal(strings.TrimSpace(button.FirstChild.Data), "Follow") - - user, err = profile.GetUserByHandle("kwamurai") - require.NoError(err) - require.False(user.IsFollowed) -} - -func TestFollowUnfollowPostOnly(t *testing.T) { - require := require.New(t) - resp := do_request(httptest.NewRequest("GET", "/follow/kwamurai", nil)) - require.Equal(resp.StatusCode, 405) - resp = do_request(httptest.NewRequest("GET", "/unfollow/kwamurai", nil)) - require.Equal(resp.StatusCode, 405) -} - -// Static content -// -------------- - -func TestStaticFile(t *testing.T) { - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/static/styles.css", nil)) - require.Equal(resp.StatusCode, 200) -} - -func TestStaticFileNonexistent(t *testing.T) { - require := require.New(t) - - resp := do_request(httptest.NewRequest("GET", "/static/blehblehblehwfe", nil)) - require.Equal(resp.StatusCode, 404) -} - -// Lists -// ----- - -func TestListsIndex(t *testing.T) { - require := require.New(t) - resp := do_request(httptest.NewRequest("GET", "/lists", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - - // Check that there's at least 2 Lists - assert.True(t, len(cascadia.QueryAll(root, selector(".list-preview"))) >= 2) -} - -func TestListDetail(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - // Users - resp := do_request(httptest.NewRequest("GET", "/lists/1/users", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 5) - - // Feed - resp1 := do_request(httptest.NewRequest("GET", "/lists/2", nil)) - require.Equal(resp1.StatusCode, 200) - root1, err := html.Parse(resp1.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root1, selector(".timeline > .tweet")), 3) -} - -func TestListDetailDoesntExist(t *testing.T) { - resp := do_request(httptest.NewRequest("GET", "/lists/2523478", nil)) - require.Equal(t, resp.StatusCode, 404) -} - -func TestListDetailInvalidId(t *testing.T) { - resp := do_request(httptest.NewRequest("GET", "/lists/asd", nil)) - require.Equal(t, resp.StatusCode, 400) -} - -func TestListAddAndDeleteUser(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - // Initial - resp := do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2) - - // Add a user - resp_add := do_request(httptest.NewRequest("GET", "/lists/2/add_user?user_handle=cernovich", nil)) - require.Equal(resp_add.StatusCode, 302) - require.Equal("/lists/2/users", resp_add.Header.Get("Location")) - - // Should be +1 user now - resp = do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 3) - - // Delete a user - resp_remove := do_request(httptest.NewRequest("GET", "/lists/2/remove_user?user_handle=cernovich", nil)) - require.Equal(resp_remove.StatusCode, 302) - require.Equal("/lists/2/users", resp_remove.Header.Get("Location")) - - // Should be +1 user now - resp = do_request(httptest.NewRequest("GET", "/lists/2/users", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2) -} - -func TestCreateNewList(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - // Initial list-of-lists - resp := do_request(httptest.NewRequest("GET", "/lists", nil)) - require.Equal(resp.StatusCode, 200) - root, err := html.Parse(resp.Body) - require.NoError(err) - num_lists := len(cascadia.QueryAll(root, selector(".list-preview"))) - - // Create a new list - resp_add := do_request(httptest.NewRequest("POST", "/lists", strings.NewReader(`{"name": "My New List"}`))) - require.Equal(resp_add.StatusCode, 302) - require.Equal(fmt.Sprintf("/lists/%d/users", num_lists+1), resp_add.Header.Get("Location")) - - // Should be N+1 lists now - resp = do_request(httptest.NewRequest("GET", "/lists", nil)) - require.Equal(resp.StatusCode, 200) - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".list-preview")), num_lists+1) -} - -// Messages -// -------- - -// Loading the index page should work if you're logged in -func TestMessagesIndexPage(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat list - recorder := httptest.NewRecorder() - app.ServeHTTP(recorder, httptest.NewRequest("GET", "/messages", nil)) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2) - assert.Len(cascadia.QueryAll(root, selector(".chat-view .dm-message")), 0) // No messages until you click on one -} - -// Open a chat room -func TestMessagesRoom(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat detail - recorder := httptest.NewRecorder() - app.ServeHTTP(recorder, httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328", nil)) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2) // Chat list still renders - assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5) - - // Should have the poller at the bottom - poller := cascadia.Query(root, selector("#new-messages-poller")) - assert.NotNil(poller) - assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) - assert.Contains( - cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, - html.Attribute{Key: "value", Val: "1"}, - ) - assert.Contains( - cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, - html.Attribute{Key: "value", Val: "1686025129144"}, - ) -} - -// Loading the page since a given message -func TestMessagesRoomPollForUpdates(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat detail - recorder := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129141", nil) - req.Header.Set("HX-Request", "true") - app.ServeHTTP(recorder, req) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 3) - - // Should have the poller at the bottom - poller := cascadia.Query(root, selector("#new-messages-poller")) - assert.NotNil(poller) - assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) - assert.Contains( - cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, - html.Attribute{Key: "value", Val: "1"}, - ) - assert.Contains( - cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, - html.Attribute{Key: "value", Val: "1686025129144"}, - ) -} - -// Loading the page since latest message (no updates) -func TestMessagesRoomPollForUpdatesEmptyResult(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat detail - recorder := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144", nil) - req.Header.Set("HX-Request", "true") - app.ServeHTTP(recorder, req) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 0) - - // Should have the poller at the bottom, with the same value as previously - poller := cascadia.Query(root, selector("#new-messages-poller")) - assert.NotNil(poller) - assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"}) - assert.Contains( - cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr, - html.Attribute{Key: "value", Val: "1"}, - ) - assert.Contains( - cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr, - html.Attribute{Key: "value", Val: "1686025129144"}, - ) -} - -// Scroll back in the messages -func TestMessagesPaginate(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Chat detail - recorder := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?cursor=1686025129142", nil) - req.Header.Set("HX-Request", "true") - app.ServeHTTP(recorder, req) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 2) -} - -func TestNotifications(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Boilerplate for setting an active user - app := webserver.NewApp(profile) - app.IsScrapingDisabled = true - app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login - - // Notifications page - recorder := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/notifications", nil) - app.ServeHTTP(recorder, req) - resp := recorder.Result() - root, err := html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".notification")), 6) - - // Show more - recorder = httptest.NewRecorder() - req = httptest.NewRequest("GET", "/notifications?cursor=1726604756351", nil) - req.Header.Set("HX-Request", "true") - app.ServeHTTP(recorder, req) - resp = recorder.Result() - root, err = html.Parse(resp.Body) - require.NoError(err) - assert.Len(cascadia.QueryAll(root, selector(".notification")), 5) -}