search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
at main 208 lines 5.7 kB view raw
1package main 2 3import ( 4 "context" 5 "database/sql" 6 "path/filepath" 7 "testing" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 _ "modernc.org/sqlite" 13) 14 15func openTestDB(t *testing.T) { 16 t.Helper() 17 18 dbPath := filepath.Join(t.TempDir(), "test.db") 19 if err := Open(dbPath); err != nil { 20 t.Fatalf("Open() error = %v", err) 21 } 22 23 t.Cleanup(func() { 24 if err := Close(); err != nil { 25 t.Fatalf("Close() error = %v", err) 26 } 27 }) 28} 29 30func TestSearchPostsBrowseMode(t *testing.T) { 31 openTestDB(t) 32 33 posts := []*Post{ 34 { 35 URI: "at://did:plc:test/app.bsky.feed.post/1", 36 CID: "cid-1", 37 AuthorDID: "did:plc:test", 38 AuthorHandle: "alice.test", 39 Text: "older saved post", 40 CreatedAt: time.Date(2026, 3, 14, 12, 0, 0, 0, time.UTC), 41 Source: "saved", 42 }, 43 { 44 URI: "at://did:plc:test/app.bsky.feed.post/2", 45 CID: "cid-2", 46 AuthorDID: "did:plc:test", 47 AuthorHandle: "alice.test", 48 Text: "newer liked post", 49 CreatedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC), 50 Source: "liked", 51 }, 52 } 53 54 for _, post := range posts { 55 if err := InsertPost(post); err != nil { 56 t.Fatalf("InsertPost() error = %v", err) 57 } 58 } 59 60 results, err := SearchPosts("", "") 61 if err != nil { 62 t.Fatalf("SearchPosts(empty) error = %v", err) 63 } 64 if len(results) != 2 { 65 t.Fatalf("SearchPosts(empty) len = %d, want 2", len(results)) 66 } 67 if results[0].URI != posts[1].URI { 68 t.Fatalf("SearchPosts(empty) first URI = %q, want %q", results[0].URI, posts[1].URI) 69 } 70 if results[0].CreatedAt.IsZero() { 71 t.Fatal("SearchPosts(empty) CreatedAt is zero, want parsed timestamp") 72 } 73 74 starResults, err := SearchPosts("*", "saved") 75 if err != nil { 76 t.Fatalf("SearchPosts(*) error = %v", err) 77 } 78 if len(starResults) != 1 { 79 t.Fatalf("SearchPosts(*) len = %d, want 1", len(starResults)) 80 } 81 if starResults[0].Source != "saved" { 82 t.Fatalf("SearchPosts(*) source = %q, want %q", starResults[0].Source, "saved") 83 } 84} 85 86func TestSQLiteOAuthStorePersistsSession(t *testing.T) { 87 openTestDB(t) 88 89 store := NewSQLiteOAuthStore() 90 did, err := syntax.ParseDID("did:plc:xg2vq45muivyy3xwatcehspu") 91 if err != nil { 92 t.Fatalf("ParseDID() error = %v", err) 93 } 94 95 session := oauth.ClientSessionData{ 96 AccountDID: did, 97 SessionID: "session-123", 98 HostURL: "https://bsky.social", 99 AuthServerURL: "https://auth.example.com", 100 AuthServerTokenEndpoint: "https://auth.example.com/token", 101 AuthServerRevocationEndpoint: "https://auth.example.com/revoke", 102 Scopes: append([]string(nil), oauthScopes...), 103 AccessToken: "access-1", 104 RefreshToken: "refresh-1", 105 DPoPAuthServerNonce: "auth-nonce", 106 DPoPHostNonce: "host-nonce", 107 DPoPPrivateKeyMultibase: "private-key", 108 } 109 110 if err := store.SaveSession(context.Background(), session); err != nil { 111 t.Fatalf("SaveSession() error = %v", err) 112 } 113 114 auth, err := GetAuthByDID(did.String()) 115 if err != nil { 116 t.Fatalf("GetAuthByDID() error = %v", err) 117 } 118 if auth == nil { 119 t.Fatal("GetAuthByDID() = nil, want auth") 120 } 121 if auth.RefreshJWT != session.RefreshToken { 122 t.Fatalf("RefreshJWT = %q, want %q", auth.RefreshJWT, session.RefreshToken) 123 } 124 if auth.DPoPHostNonce != session.DPoPHostNonce { 125 t.Fatalf("DPoPHostNonce = %q, want %q", auth.DPoPHostNonce, session.DPoPHostNonce) 126 } 127 128 got, err := store.GetSession(context.Background(), did, session.SessionID) 129 if err != nil { 130 t.Fatalf("GetSession() error = %v", err) 131 } 132 if got.AccessToken != session.AccessToken { 133 t.Fatalf("AccessToken = %q, want %q", got.AccessToken, session.AccessToken) 134 } 135 136 if err := store.DeleteSession(context.Background(), did, session.SessionID); err != nil { 137 t.Fatalf("DeleteSession() error = %v", err) 138 } 139 140 deleted, err := store.GetSession(context.Background(), did, session.SessionID) 141 if err == nil || deleted != nil { 142 t.Fatalf("GetSession() after delete = (%v, %v), want error", deleted, err) 143 } 144} 145 146func TestOpenMigratesLegacyPostsTableWithoutFacets(t *testing.T) { 147 dbPath := filepath.Join(t.TempDir(), "legacy.db") 148 149 legacyDB, err := sql.Open("sqlite", dbPath) 150 if err != nil { 151 t.Fatalf("sql.Open() error = %v", err) 152 } 153 154 legacySchema := ` 155 CREATE TABLE posts ( 156 uri TEXT PRIMARY KEY, 157 cid TEXT NOT NULL, 158 author_did TEXT NOT NULL, 159 author_handle TEXT NOT NULL, 160 text TEXT NOT NULL DEFAULT '', 161 created_at DATETIME NOT NULL, 162 like_count INTEGER DEFAULT 0, 163 repost_count INTEGER DEFAULT 0, 164 reply_count INTEGER DEFAULT 0, 165 source TEXT NOT NULL CHECK(source IN ('saved', 'liked')), 166 indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP 167 ); 168 ` 169 170 if _, err := legacyDB.Exec(legacySchema); err != nil { 171 t.Fatalf("creating legacy schema failed: %v", err) 172 } 173 if err := legacyDB.Close(); err != nil { 174 t.Fatalf("legacyDB.Close() error = %v", err) 175 } 176 177 if err := Open(dbPath); err != nil { 178 t.Fatalf("Open() error = %v", err) 179 } 180 t.Cleanup(func() { 181 if err := Close(); err != nil { 182 t.Fatalf("Close() error = %v", err) 183 } 184 }) 185 186 hasColumn, err := columnExists("posts", "facets") 187 if err != nil { 188 t.Fatalf("columnExists() error = %v", err) 189 } 190 if !hasColumn { 191 t.Fatal("posts.facets missing after migration") 192 } 193 194 post := &Post{ 195 URI: "at://did:plc:test/app.bsky.feed.post/legacy", 196 CID: "cid-legacy", 197 AuthorDID: "did:plc:test", 198 AuthorHandle: "legacy.test", 199 Text: "legacy post", 200 CreatedAt: time.Now().UTC(), 201 Source: "saved", 202 Facets: `[]`, 203 } 204 205 if err := InsertPost(post); err != nil { 206 t.Fatalf("InsertPost() after migration error = %v", err) 207 } 208}