[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 309 lines 9.2 kB view raw
1import { assertEquals, assertThrows } from "@std/assert"; 2import { 3 CreatedAtDidKeyset, 4 IndexedAtDidKeyset, 5 IsoTimeKey, 6 LikeCountCidKeyset, 7 RkeyKey, 8 TimeCidKeyset, 9} from "../data-plane/db/pagination.ts"; 10import { InvalidRequestError } from "@atp/xrpc-server"; 11 12// GenericKeyset (tested via TimeCidKeyset) 13 14Deno.test("GenericKeyset", async (t) => { 15 const keyset = new TimeCidKeyset(); 16 17 await t.step("packCursor uses colon separator", () => { 18 const cursor = { primary: "abc123", secondary: "def456" }; 19 const packed = keyset.packCursor(cursor); 20 assertEquals(packed, "abc123:def456"); 21 }); 22 23 await t.step("unpackCursor splits on first colon", () => { 24 const unpacked = keyset.unpackCursor("abc123:def456"); 25 assertEquals(unpacked?.primary, "abc123"); 26 assertEquals(unpacked?.secondary, "def456"); 27 }); 28 29 await t.step("unpackCursor handles colons in secondary", () => { 30 const unpacked = keyset.unpackCursor("abc123:def:456:789"); 31 assertEquals(unpacked?.primary, "abc123"); 32 assertEquals(unpacked?.secondary, "def:456:789"); 33 }); 34 35 await t.step("unpackCursor throws on missing separator", () => { 36 assertThrows( 37 () => keyset.unpackCursor("noseparatorhere"), 38 InvalidRequestError, 39 "Malformed cursor: missing separator", 40 ); 41 }); 42 43 await t.step("unpackCursor throws on empty primary", () => { 44 assertThrows( 45 () => keyset.unpackCursor(":secondary"), 46 InvalidRequestError, 47 "Malformed cursor: missing primary or secondary", 48 ); 49 }); 50 51 await t.step("unpackCursor throws on empty secondary", () => { 52 assertThrows( 53 () => keyset.unpackCursor("primary:"), 54 InvalidRequestError, 55 "Malformed cursor: missing primary or secondary", 56 ); 57 }); 58 59 await t.step("getFilter returns descending filter by default", () => { 60 const labeled = { 61 primary: "2024-01-15T12:00:00.000Z", 62 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 63 }; 64 const filter = keyset.getFilter(labeled); 65 assertEquals(filter, { 66 $or: [ 67 { indexedAt: { $lt: "2024-01-15T12:00:00.000Z" } }, 68 { 69 indexedAt: "2024-01-15T12:00:00.000Z", 70 cid: { $lt: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq" }, 71 }, 72 ], 73 }); 74 }); 75 76 await t.step("getFilter returns ascending filter when specified", () => { 77 const labeled = { 78 primary: "2024-01-15T12:00:00.000Z", 79 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 80 }; 81 const filter = keyset.getFilter(labeled, "asc"); 82 assertEquals(filter, { 83 $or: [ 84 { indexedAt: { $gt: "2024-01-15T12:00:00.000Z" } }, 85 { 86 indexedAt: "2024-01-15T12:00:00.000Z", 87 cid: { $gt: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq" }, 88 }, 89 ], 90 }); 91 }); 92 93 await t.step("pack and unpack roundtrip", () => { 94 const original = { 95 primary: "2024-01-15T12:00:00.000Z", 96 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 97 }; 98 99 const packed = keyset.pack(original); 100 const unpacked = keyset.unpack(packed); 101 102 assertEquals(unpacked?.primary, original.primary); 103 assertEquals(unpacked?.secondary, original.secondary); 104 }); 105 106 await t.step("packFromResult with array uses last result", () => { 107 const results = [ 108 { indexedAt: "2024-01-15T12:00:00.000Z", cid: "first" }, 109 { indexedAt: "2024-01-15T13:00:00.000Z", cid: "second" }, 110 { indexedAt: "2024-01-15T14:00:00.000Z", cid: "last" }, 111 ]; 112 113 const packed = keyset.packFromResult(results); 114 const unpacked = keyset.unpack(packed); 115 116 assertEquals(unpacked?.secondary, "last"); 117 }); 118}); 119 120// TimeCidKeyset (extends GenericKeyset) 121 122Deno.test("TimeCidKeyset", async (t) => { 123 const keyset = new TimeCidKeyset(); 124 125 await t.step( 126 "labelResult uses current time when indexedAt is missing", 127 () => { 128 const result = { 129 cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 130 }; 131 132 const before = new Date(); 133 const labeled = keyset.labelResult(result); 134 const after = new Date(); 135 136 const labeledDate = new Date(labeled.primary); 137 assertEquals(labeledDate >= before, true); 138 assertEquals(labeledDate <= after, true); 139 }, 140 ); 141 142 await t.step("labeledResultToCursor converts to base36 seconds", () => { 143 const labeled = { 144 primary: "2024-01-15T12:00:00.000Z", 145 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 146 }; 147 148 const cursor = keyset.labeledResultToCursor(labeled); 149 150 const expectedBase36 = Math.floor( 151 new Date("2024-01-15T12:00:00.000Z").getTime() / 1000, 152 ).toString(36); 153 assertEquals(cursor.primary, expectedBase36); 154 }); 155 156 await t.step("labeledResultToCursor throws on invalid date", () => { 157 const labeled = { 158 primary: "not-a-valid-date", 159 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 160 }; 161 162 assertThrows( 163 () => keyset.labeledResultToCursor(labeled), 164 InvalidRequestError, 165 "Invalid date for cursor", 166 ); 167 }); 168 169 await t.step("cursorToLabeledResult converts from base36", () => { 170 const base36Seconds = (1705320000).toString(36); 171 const cursor = { 172 primary: base36Seconds, 173 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 174 }; 175 176 const labeled = keyset.cursorToLabeledResult(cursor); 177 178 assertEquals(labeled.primary, "2024-01-15T12:00:00.000Z"); 179 }); 180}); 181 182Deno.test("TimeCidKeyset subclasses", async (t) => { 183 await t.step("CreatedAtDidKeyset works with createdAt field", () => { 184 const keyset = new CreatedAtDidKeyset(); 185 const packed = keyset.packFromResult({ 186 createdAt: "2024-01-15T12:00:00.000Z", 187 authorDid: "did:plc:testuser123", 188 }); 189 assertEquals(typeof packed, "string"); 190 }); 191 192 await t.step("IndexedAtDidKeyset works with indexedAt field", () => { 193 const keyset = new IndexedAtDidKeyset(); 194 const packed = keyset.packFromResult({ 195 indexedAt: "2024-01-15T12:00:00.000Z", 196 authorDid: "did:plc:testuser123", 197 }); 198 assertEquals(typeof packed, "string"); 199 }); 200}); 201 202// LikeCountCidKeyset (extends GenericKeyset) 203 204Deno.test("LikeCountCidKeyset", async (t) => { 205 await t.step("cursorToLabeledResult throws on invalid like count", () => { 206 const keyset = new LikeCountCidKeyset(); 207 const cursor = { 208 primary: "not-a-number", 209 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq", 210 }; 211 212 assertThrows( 213 () => keyset.cursorToLabeledResult(cursor), 214 InvalidRequestError, 215 "Malformed cursor: invalid like count", 216 ); 217 }); 218}); 219 220// GenericSingleKey (tested via RkeyKey) 221 222Deno.test("GenericSingleKey", async (t) => { 223 const keyset = new RkeyKey(); 224 225 await t.step("getFilter returns descending filter by default", () => { 226 const labeled = { primary: "3jui7kd2zcysk" }; 227 const filter = keyset.getFilter(labeled); 228 assertEquals(filter, { 229 key: { $lt: "3jui7kd2zcysk" }, 230 }); 231 }); 232 233 await t.step("getFilter returns ascending filter when specified", () => { 234 const labeled = { primary: "3jui7kd2zcysk" }; 235 const filter = keyset.getFilter(labeled, "asc"); 236 assertEquals(filter, { 237 key: { $gt: "3jui7kd2zcysk" }, 238 }); 239 }); 240 241 await t.step("unpackCursor throws on colon separator", () => { 242 assertThrows( 243 () => keyset.unpackCursor("has:colon"), 244 InvalidRequestError, 245 "Malformed cursor: unexpected separator", 246 ); 247 }); 248 249 await t.step("unpackCursor throws on double underscore separator", () => { 250 assertThrows( 251 () => keyset.unpackCursor("has__underscore"), 252 InvalidRequestError, 253 "Malformed cursor: unexpected separator", 254 ); 255 }); 256}); 257 258// IsoTimeKey (extends GenericSingleKey) 259 260Deno.test("IsoTimeKey", async (t) => { 261 const keyset = new IsoTimeKey(); 262 263 await t.step("labeledResultToCursor throws on invalid date", () => { 264 const labeled = { primary: "not-a-valid-date" }; 265 assertThrows( 266 () => keyset.labeledResultToCursor(labeled), 267 InvalidRequestError, 268 "Invalid date for cursor", 269 ); 270 }); 271 272 await t.step("cursorToLabeledResult throws on invalid date", () => { 273 const cursor = { primary: "invalid-date" }; 274 assertThrows( 275 () => keyset.cursorToLabeledResult(cursor), 276 InvalidRequestError, 277 "Malformed cursor: invalid date", 278 ); 279 }); 280 281 await t.step("pack produces ISO date but unpack fails due to colons", () => { 282 // IsoTimeKey produces ISO dates with colons, but GenericSingleKey.unpackCursor 283 // rejects strings with colons. This is a known limitation. 284 const result = { indexedAt: "2024-01-15T12:00:00.000Z" }; 285 const packed = keyset.packFromResult(result); 286 287 assertEquals(packed, "2024-01-15T12:00:00.000Z"); 288 289 assertThrows( 290 () => keyset.unpack(packed), 291 InvalidRequestError, 292 "Malformed cursor: unexpected separator", 293 ); 294 }); 295}); 296 297// RkeyKey (extends GenericSingleKey) 298 299Deno.test("RkeyKey", async (t) => { 300 await t.step("cursorToLabeledResult throws on invalid record key", () => { 301 const keyset = new RkeyKey(); 302 const cursor = { primary: "invalid/key" }; 303 assertThrows( 304 () => keyset.cursorToLabeledResult(cursor), 305 InvalidRequestError, 306 "Malformed cursor", 307 ); 308 }); 309});