A tool for tailing a labelers' firehose, rehydrating, and storing records for future analysis of moderation decisions.
at main 205 lines 6.2 kB view raw
1import { describe, test, expect, beforeAll, afterAll } from "bun:test"; 2import { Database } from "duckdb"; 3import { initializeSchema } from "../../src/database/schema.js"; 4import { LabelsRepository } from "../../src/database/labels.repository.js"; 5import { PostsRepository } from "../../src/database/posts.repository.js"; 6import { ProfilesRepository } from "../../src/database/profiles.repository.js"; 7import { BlobsRepository } from "../../src/database/blobs.repository.js"; 8 9describe("Database Integration Tests", () => { 10 let db: Database; 11 let labelsRepo: LabelsRepository; 12 let postsRepo: PostsRepository; 13 let profilesRepo: ProfilesRepository; 14 let blobsRepo: BlobsRepository; 15 16 beforeAll(async () => { 17 db = new Database(":memory:"); 18 19 await new Promise<void>((resolve, reject) => { 20 db.exec( 21 ` 22 CREATE SEQUENCE IF NOT EXISTS labels_id_seq; 23 CREATE TABLE IF NOT EXISTS labels ( 24 id INTEGER PRIMARY KEY DEFAULT nextval('labels_id_seq'), 25 uri TEXT NOT NULL, 26 cid TEXT, 27 val TEXT NOT NULL, 28 neg BOOLEAN DEFAULT FALSE, 29 cts TIMESTAMP NOT NULL, 30 exp TIMESTAMP, 31 src TEXT NOT NULL, 32 UNIQUE(uri, val, cts) 33 ); 34 35 CREATE TABLE IF NOT EXISTS posts ( 36 uri TEXT PRIMARY KEY, 37 did TEXT NOT NULL, 38 text TEXT, 39 facets JSON, 40 embeds JSON, 41 langs JSON, 42 tags JSON, 43 created_at TIMESTAMP NOT NULL, 44 is_reply BOOLEAN DEFAULT FALSE 45 ); 46 47 CREATE TABLE IF NOT EXISTS profiles ( 48 did TEXT PRIMARY KEY, 49 handle TEXT, 50 display_name TEXT, 51 description TEXT, 52 avatar_cid TEXT, 53 banner_cid TEXT 54 ); 55 56 CREATE TABLE IF NOT EXISTS blobs ( 57 post_uri TEXT NOT NULL, 58 blob_cid TEXT NOT NULL, 59 sha256 TEXT NOT NULL, 60 phash TEXT, 61 storage_path TEXT, 62 mimetype TEXT, 63 PRIMARY KEY (post_uri, blob_cid) 64 ); 65 `, 66 (err) => { 67 if (err) reject(err); 68 else resolve(); 69 } 70 ); 71 }); 72 73 labelsRepo = new LabelsRepository(db); 74 postsRepo = new PostsRepository(db); 75 profilesRepo = new ProfilesRepository(db); 76 blobsRepo = new BlobsRepository(db); 77 }); 78 79 afterAll(async () => { 80 await new Promise<void>((resolve) => { 81 db.close(() => resolve()); 82 }); 83 }); 84 85 describe("LabelsRepository", () => { 86 test("should insert and retrieve a label", async () => { 87 const label = { 88 uri: "at://did:plc:test/app.bsky.feed.post/123", 89 val: "spam", 90 cts: "2025-01-15T12:00:00Z", 91 src: "did:plc:labeler", 92 }; 93 94 await labelsRepo.insert(label); 95 const found = await labelsRepo.findByUri(label.uri); 96 97 expect(found.length).toBe(1); 98 expect(found[0].val).toBe("spam"); 99 }); 100 101 test("should find labels by value", async () => { 102 const labels = await labelsRepo.findByValue("spam"); 103 expect(labels.length).toBeGreaterThan(0); 104 }); 105 }); 106 107 describe("PostsRepository", () => { 108 test("should insert and retrieve a post", async () => { 109 const post = { 110 uri: "at://did:plc:user/app.bsky.feed.post/abc123", 111 did: "did:plc:user", 112 text: "test post", 113 created_at: "2025-01-15T12:00:00Z", 114 is_reply: false, 115 }; 116 117 await postsRepo.insert(post); 118 const found = await postsRepo.findByUri(post.uri); 119 120 expect(found).not.toBeNull(); 121 expect(found?.text).toBe("test post"); 122 }); 123 124 test("should find posts by DID", async () => { 125 const posts = await postsRepo.findByDid("did:plc:user"); 126 expect(posts.length).toBeGreaterThan(0); 127 }); 128 }); 129 130 describe("ProfilesRepository", () => { 131 test("should insert and retrieve a profile", async () => { 132 const profile = { 133 did: "did:plc:testuser", 134 handle: "testuser.bsky.social", 135 display_name: "Test User", 136 description: "A test user", 137 }; 138 139 await profilesRepo.insert(profile); 140 const found = await profilesRepo.findByDid(profile.did); 141 142 expect(found).not.toBeNull(); 143 expect(found?.handle).toBe("testuser.bsky.social"); 144 }); 145 146 test("should find profile by handle", async () => { 147 const found = await profilesRepo.findByHandle("testuser.bsky.social"); 148 expect(found).not.toBeNull(); 149 expect(found?.did).toBe("did:plc:testuser"); 150 }); 151 152 test("should insert and retrieve profile with avatar and banner", async () => { 153 const profile = { 154 did: "did:plc:testuser2", 155 handle: "testuser2.bsky.social", 156 display_name: "Test User 2", 157 description: "A test user with avatar", 158 avatar_cid: "bafyavatartest", 159 banner_cid: "bafybannertest", 160 }; 161 162 await profilesRepo.insert(profile); 163 const found = await profilesRepo.findByDid(profile.did); 164 165 expect(found).not.toBeNull(); 166 expect(found?.avatar_cid).toBe("bafyavatartest"); 167 expect(found?.banner_cid).toBe("bafybannertest"); 168 }); 169 }); 170 171 describe("BlobsRepository", () => { 172 test("should insert and retrieve a blob", async () => { 173 const blob = { 174 post_uri: "at://did:plc:user/app.bsky.feed.post/abc123", 175 blob_cid: "bafytest123", 176 sha256: "abc123def456", 177 phash: "deadbeef", 178 mimetype: "image/jpeg", 179 }; 180 181 await blobsRepo.insert(blob); 182 const found = await blobsRepo.findByPostUri(blob.post_uri); 183 184 expect(found.length).toBe(1); 185 expect(found[0].sha256).toBe("abc123def456"); 186 }); 187 188 test("should find blob by SHA256", async () => { 189 const found = await blobsRepo.findBySha256("abc123def456"); 190 expect(found).not.toBeNull(); 191 expect(found?.blob_cid).toBe("bafytest123"); 192 }); 193 194 test("should find blobs by pHash", async () => { 195 const found = await blobsRepo.findByPhash("deadbeef"); 196 expect(found.length).toBeGreaterThan(0); 197 }); 198 199 test("should find blob by CID", async () => { 200 const found = await blobsRepo.findByCid("bafytest123"); 201 expect(found).not.toBeNull(); 202 expect(found?.sha256).toBe("abc123def456"); 203 }); 204 }); 205});