package bluesky import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/lex/util" "github.com/bluesky-social/indigo/xrpc" ) // setupMockServer creates and returns a new httptest.Server that mocks the Bluesky API. func setupMockServer() *httptest.Server { mux := http.NewServeMux() // Mock for atproto.ServerCreateSession mux.HandleFunc("/xrpc/com.atproto.server.createSession", func(w http.ResponseWriter, r *http.Request) { // Simulate an error if the handle is "fail.test" var input atproto.ServerCreateSession_Input if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } if input.Identifier == "fail.test" { w.WriteHeader(http.StatusUnauthorized) fmt.Fprintln(w, `{"error": "InvalidCredentials", "message": "Invalid handle or password"}`) return } // Successful response resp := atproto.ServerCreateSession_Output{ AccessJwt: "test_access_jwt", RefreshJwt: "test_refresh_jwt", Handle: "test.handle", Did: "did:plc:test", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) // Mock for bsky.FeedGetAuthorFeed mux.HandleFunc("/xrpc/app.bsky.feed.getAuthorFeed", func(w http.ResponseWriter, r *http.Request) { actor := r.URL.Query().Get("actor") limitStr := r.URL.Query().Get("limit") limit, _ := strconv.ParseInt(limitStr, 10, 64) if actor == "" { http.Error(w, `{"error": "MissingParameter", "message": "actor parameter is required"}`, http.StatusBadRequest) return } // Create a mock feed feed := []*bsky.FeedDefs_FeedViewPost{} for i := 0; i < int(limit); i++ { post := &bsky.FeedDefs_PostView{ Uri: fmt.Sprintf("at://%s/app.bsky.feed.post/%d", actor, i), Cid: fmt.Sprintf("bafy...%d", i), Author: &bsky.ActorDefs_ProfileViewBasic{ Did: "did:plc:test", Handle: actor, }, Record: &util.LexiconTypeDecoder{ Val: &bsky.FeedPost{ Text: fmt.Sprintf("This is post number %d", i), CreatedAt: "2023-10-27T12:00:00Z", }, }, } feed = append(feed, &bsky.FeedDefs_FeedViewPost{Post: post}) } resp := bsky.FeedGetAuthorFeed_Output{ Feed: feed, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) // Mock for bsky.FeedGetFeed mux.HandleFunc("/xrpc/app.bsky.feed.getFeed", func(w http.ResponseWriter, r *http.Request) { feedURI := r.URL.Query().Get("feed") if feedURI == "" { http.Error(w, `{"error": "MissingParameter", "message": "feed parameter is required"}`, http.StatusBadRequest) return } resp := bsky.FeedGetFeed_Output{ Feed: []*bsky.FeedDefs_FeedViewPost{ { Post: &bsky.FeedDefs_PostView{ Uri: "at://did:plc:test/app.bsky.feed.post/1", Author: &bsky.ActorDefs_ProfileViewBasic{ Did: "did:plc:test", Handle: "someuser.test", }, }, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) return httptest.NewServer(mux) } func TestNewBlueskyClient(t *testing.T) { server := setupMockServer() defer server.Close() ctx := context.Background() t.Run("SuccessfulClientCreation", func(t *testing.T) { client, err := NewBlueskyClient(ctx, "test.user", "password", server.URL) if err != nil { t.Fatalf("Expected no error, got %v", err) } if client == nil { t.Fatal("Expected client not to be nil") } if client.client.Auth == nil { t.Fatal("Expected client.Auth not to be nil") } if client.client.Auth.AccessJwt != "test_access_jwt" { t.Errorf("Expected AccessJwt to be 'test_access_jwt', got %s", client.client.Auth.AccessJwt) } if client.client.Auth.Did != "did:plc:test" { t.Errorf("Expected Did to be 'did:plc:test', got %s", client.client.Auth.Did) } }) t.Run("FailedClientCreation", func(t *testing.T) { client, err := NewBlueskyClient(ctx, "fail.test", "wrongpassword", server.URL) if err == nil { t.Fatal("Expected an error, got nil") } if client != nil { t.Fatal("Expected client to be nil on failure") } expectedErrorMsg := "failed to create bluesky session" if !contains(err.Error(), expectedErrorMsg) { t.Errorf("Expected error message to contain '%s', got '%s'", expectedErrorMsg, err.Error()) } }) } // A helper function for checking substrings. func contains(s, substr string) bool { return len(s) >= len(substr) && s[:len(substr)] == substr } // newTestClient is a helper to quickly create a client for other tests. func newTestClient(t *testing.T, serverURL string) *BlueskyClient { return &BlueskyClient{ client: &xrpc.Client{ Host: serverURL, Auth: &xrpc.AuthInfo{ AccessJwt: "test_access_jwt", }, }, } } func TestGetLatestNPosts(t *testing.T) { server := setupMockServer() defer server.Close() client := newTestClient(t, server.URL) ctx := context.Background() actor := "did:plc:testactor" var limit int = 5 output, err := client.GetLatestNPosts(ctx, actor, limit) if err != nil { t.Fatalf("Expected no error, got %v", err) } if output == nil { t.Fatal("Expected output not to be nil") } if len(output) != int(limit) { t.Errorf("Expected feed length to be %d, got %d", limit, len(output)) } if output[0].Post.Author.Handle != actor { t.Errorf("Expected post author handle to be '%s', got '%s'", actor, output[0].Post.Author.Handle) } } func TestGetFeed(t *testing.T) { server := setupMockServer() defer server.Close() client := newTestClient(t, server.URL) ctx := context.Background() feedURI := "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends" var limit int = 10 output, err := client.GetFeed(ctx, feedURI, limit) if err != nil { t.Fatalf("Expected no error, got %v", err) } if output == nil { t.Fatal("Expected output not to be nil") } if len(output.Feed) == 0 { t.Fatal("Expected at least one post in the feed") } if output.Feed[0].Post.Uri != "at://did:plc:test/app.bsky.feed.post/1" { t.Errorf("Unexpected post URI: %s", output.Feed[0].Post.Uri) } }