naco-api/internal/bluesky/client_test.go
2025-10-25 01:06:02 +00:00

229 lines
6.1 KiB
Go

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)
}
}