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

Configure Feed

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

test: add mock/context util

+1187 -71
+4 -2
deno.json
··· 31 31 "jose": "npm:jose@^6.1.3", 32 32 "mongoose": "npm:mongoose@^8.20.2", 33 33 "multiformats": "npm:multiformats@^13.4.1", 34 - "p-queue": "npm:p-queue@^8.1.1" 34 + "p-queue": "npm:p-queue@^8.1.1", 35 + "mongodb-memory-server-core": "npm:mongodb-memory-server-core@^11.0.0" 35 36 }, 36 37 "test": { 37 38 "permissions": { 38 - "env": true 39 + "env": true, 40 + "read": true 39 41 } 40 42 } 41 43 }
+191 -5
deno.lock
··· 39 39 "npm:dotenv@^17.2.3": "17.2.3", 40 40 "npm:jose@^6.1.3": "6.1.3", 41 41 "npm:lodash@*": "4.17.21", 42 + "npm:mongodb-memory-server-core@11": "11.0.0", 42 43 "npm:mongoose@^8.20.2": "8.20.2", 43 44 "npm:multiformats@^13.4.1": "13.4.1", 44 45 "npm:p-queue@^8.1.1": "8.1.1", ··· 276 277 "@types/webidl-conversions" 277 278 ] 278 279 }, 280 + "@types/whatwg-url@13.0.0": { 281 + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", 282 + "dependencies": [ 283 + "@types/webidl-conversions" 284 + ] 285 + }, 286 + "agent-base@7.1.4": { 287 + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" 288 + }, 289 + "async-mutex@0.5.0": { 290 + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", 291 + "dependencies": [ 292 + "tslib" 293 + ] 294 + }, 279 295 "await-lock@2.2.2": { 280 296 "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" 281 297 }, 298 + "b4a@1.7.3": { 299 + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==" 300 + }, 301 + "bare-events@2.8.2": { 302 + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==" 303 + }, 282 304 "bson@6.10.4": { 283 305 "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" 284 306 }, 307 + "bson@7.0.0": { 308 + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==" 309 + }, 310 + "buffer-crc32@0.2.13": { 311 + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" 312 + }, 313 + "camelcase@6.3.0": { 314 + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" 315 + }, 285 316 "cborg@4.3.2": { 286 317 "integrity": "sha512-l+QzebEAG0vb09YKkaOrMi2zmm80UNjmbvocMIeW5hO7JOXWdrQ/H49yOKfYX0MBgrj/KWgatBnEgRXyNyKD+A==", 287 318 "bin": true 288 319 }, 320 + "commondir@1.0.1": { 321 + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" 322 + }, 289 323 "debug@4.4.3": { 290 324 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 291 325 "dependencies": [ ··· 298 332 "eventemitter3@5.0.1": { 299 333 "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" 300 334 }, 335 + "events-universal@1.0.1": { 336 + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", 337 + "dependencies": [ 338 + "bare-events" 339 + ] 340 + }, 341 + "fast-fifo@1.3.2": { 342 + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" 343 + }, 344 + "find-cache-dir@3.3.2": { 345 + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", 346 + "dependencies": [ 347 + "commondir", 348 + "make-dir", 349 + "pkg-dir" 350 + ] 351 + }, 352 + "find-up@4.1.0": { 353 + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 354 + "dependencies": [ 355 + "locate-path", 356 + "path-exists" 357 + ] 358 + }, 359 + "follow-redirects@1.15.11": { 360 + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==" 361 + }, 301 362 "graphemer@1.4.0": { 302 363 "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 303 364 }, 365 + "https-proxy-agent@7.0.6": { 366 + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 367 + "dependencies": [ 368 + "agent-base", 369 + "debug" 370 + ] 371 + }, 304 372 "iso-datestring-validator@2.2.2": { 305 373 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 306 374 }, ··· 309 377 }, 310 378 "kareem@2.6.3": { 311 379 "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" 380 + }, 381 + "locate-path@5.0.0": { 382 + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 383 + "dependencies": [ 384 + "p-locate" 385 + ] 312 386 }, 313 387 "lodash@4.17.21": { 314 388 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 315 389 }, 390 + "make-dir@3.1.0": { 391 + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 392 + "dependencies": [ 393 + "semver@6.3.1" 394 + ] 395 + }, 316 396 "memory-pager@1.5.0": { 317 397 "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" 318 398 }, 319 399 "mongodb-connection-string-url@3.0.2": { 320 400 "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 321 401 "dependencies": [ 322 - "@types/whatwg-url", 402 + "@types/whatwg-url@11.0.5", 323 403 "whatwg-url" 324 404 ] 325 405 }, 406 + "mongodb-connection-string-url@7.0.0": { 407 + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", 408 + "dependencies": [ 409 + "@types/whatwg-url@13.0.0", 410 + "whatwg-url" 411 + ] 412 + }, 413 + "mongodb-memory-server-core@11.0.0": { 414 + "integrity": "sha512-qg1XWrmX+7IL6SyRKiknHEcNYJomnvw14WxjEkRaAXh3IsJ/DPwEykQqAW5vzGedeSSETZtyWwxyXHQLPqnA/Q==", 415 + "dependencies": [ 416 + "async-mutex", 417 + "camelcase", 418 + "debug", 419 + "find-cache-dir", 420 + "follow-redirects", 421 + "https-proxy-agent", 422 + "mongodb@7.0.0", 423 + "new-find-package-json", 424 + "semver@7.7.3", 425 + "tar-stream", 426 + "tslib", 427 + "yauzl" 428 + ] 429 + }, 326 430 "mongodb@6.20.0": { 327 431 "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", 328 432 "dependencies": [ 329 433 "@mongodb-js/saslprep", 330 - "bson", 331 - "mongodb-connection-string-url" 434 + "bson@6.10.4", 435 + "mongodb-connection-string-url@3.0.2" 436 + ] 437 + }, 438 + "mongodb@7.0.0": { 439 + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", 440 + "dependencies": [ 441 + "@mongodb-js/saslprep", 442 + "bson@7.0.0", 443 + "mongodb-connection-string-url@7.0.0" 332 444 ] 333 445 }, 334 446 "mongoose@8.20.2": { 335 447 "integrity": "sha512-U0TPupnqBOAI3p9H9qdShX8/nJUBylliRcHFKuhbewEkM7Y0qc9BbrQR9h4q6+1easoZqej7cq2Ee36AZ0gMzQ==", 336 448 "dependencies": [ 337 - "bson", 449 + "bson@6.10.4", 338 450 "kareem", 339 - "mongodb", 451 + "mongodb@6.20.0", 340 452 "mpath", 341 453 "mquery", 342 454 "ms", ··· 361 473 "multiformats@9.9.0": { 362 474 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 363 475 }, 476 + "new-find-package-json@2.0.0": { 477 + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", 478 + "dependencies": [ 479 + "debug" 480 + ] 481 + }, 482 + "p-limit@2.3.0": { 483 + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 484 + "dependencies": [ 485 + "p-try" 486 + ] 487 + }, 488 + "p-locate@4.1.0": { 489 + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 490 + "dependencies": [ 491 + "p-limit" 492 + ] 493 + }, 364 494 "p-queue@8.1.1": { 365 495 "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", 366 496 "dependencies": [ ··· 371 501 "p-timeout@6.1.4": { 372 502 "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==" 373 503 }, 504 + "p-try@2.2.0": { 505 + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 506 + }, 507 + "path-exists@4.0.0": { 508 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 509 + }, 510 + "pend@1.2.0": { 511 + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" 512 + }, 513 + "pkg-dir@4.2.0": { 514 + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 515 + "dependencies": [ 516 + "find-up" 517 + ] 518 + }, 374 519 "punycode@2.3.1": { 375 520 "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 376 521 }, 377 522 "rate-limiter-flexible@9.0.0": { 378 523 "integrity": "sha512-Dz+NKRx+V9WVLY6QJv2soo/eN+cYL/+/1XggE/tgnGuA+D/Q1em0hWVp6AZd8lfhzZ6+2wrk7TEwYN+x9AsHeg==" 379 524 }, 525 + "semver@6.3.1": { 526 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 527 + "bin": true 528 + }, 529 + "semver@7.7.3": { 530 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 531 + "bin": true 532 + }, 380 533 "sift@17.1.3": { 381 534 "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" 382 535 }, ··· 386 539 "memory-pager" 387 540 ] 388 541 }, 542 + "streamx@2.23.0": { 543 + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", 544 + "dependencies": [ 545 + "events-universal", 546 + "fast-fifo", 547 + "text-decoder" 548 + ] 549 + }, 550 + "tar-stream@3.1.7": { 551 + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", 552 + "dependencies": [ 553 + "b4a", 554 + "fast-fifo", 555 + "streamx" 556 + ] 557 + }, 558 + "text-decoder@1.2.3": { 559 + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", 560 + "dependencies": [ 561 + "b4a" 562 + ] 563 + }, 389 564 "tlds@1.260.0": { 390 565 "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 391 566 "bin": true ··· 395 570 "dependencies": [ 396 571 "punycode" 397 572 ] 573 + }, 574 + "tslib@2.8.1": { 575 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 398 576 }, 399 577 "uint8arrays@3.0.0": { 400 578 "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", ··· 415 593 "webidl-conversions" 416 594 ] 417 595 }, 596 + "yauzl@3.2.0": { 597 + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", 598 + "dependencies": [ 599 + "buffer-crc32", 600 + "pend" 601 + ] 602 + }, 418 603 "zod@3.25.76": { 419 604 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 420 605 } ··· 439 624 "npm:@atproto/api@~0.16.11", 440 625 "npm:dotenv@^17.2.3", 441 626 "npm:jose@^6.1.3", 627 + "npm:mongodb-memory-server-core@11", 442 628 "npm:mongoose@^8.20.2", 443 629 "npm:multiformats@^13.4.1", 444 630 "npm:p-queue@^8.1.1"
+26 -64
tests/main_test.ts
··· 1 1 import { assertEquals } from "@std/assert"; 2 2 import { assertMatch } from "@std/assert/match"; 3 - import { createApp } from "../main.ts"; 4 - import { AppContext } from "../context.ts"; 5 - import { Database } from "../data-plane/db/index.ts"; 6 - import { createAuthVerifier } from "../auth-verifier.ts"; 7 - import { getLogger } from "@logtape/logtape"; 8 - import { DataPlane } from "../data-plane/index.ts"; 9 - import { Hydrator } from "../hydration/index.ts"; 10 - import { Views } from "../views/index.ts"; 11 - import { IdResolver } from "@atp/identity"; 12 - import { ServerConfig } from "../config.ts"; 13 - 14 - const cfg = new ServerConfig({ 15 - relayUrl: "http://localhost:8080", 16 - serverDid: "did:web:localhost", 17 - modServiceDid: "did:web:test", 18 - adminPasswords: ["test"], 19 - privateKey: 20 - "5676df35fd3a185a1771a43536635ad90057e0c0d1fd91436344bb50ce23a460", 21 - publicUrl: "http://localhost:4000", 22 - alternateAudienceDids: [], 23 - bigThreadUris: new Set(["did:web:test"]), 24 - maxThreadParents: 10, 25 - }); 26 - 27 - // Create a mock context for testing without database 28 - function createMockContext(): AppContext { 29 - const appLogger = getLogger(["appview"]); 30 - const idResolver = new IdResolver(); 31 - const serviceDid = "did:web:test"; 32 - 33 - // Create mock database that doesn't actually connect 34 - const mockDb = { 35 - connect: () => Promise.resolve(), 36 - disconnect: () => Promise.resolve(), 37 - models: {}, 38 - getCursorState: () => Promise.resolve(null), 39 - saveCursorState: () => Promise.resolve(), 40 - } as unknown as Database; 41 - 42 - const dataplane = new DataPlane(mockDb, idResolver); 43 - const hydrator = new Hydrator(dataplane); 44 - const views = new Views(cfg); 45 - const authVerifier = createAuthVerifier(dataplane, { 46 - ownDid: serviceDid, 47 - alternateAudienceDids: [], 48 - modServiceDid: "did:web:test", 49 - adminPasses: ["test"], 50 - }); 51 - 52 - return { 53 - db: mockDb, 54 - dataplane, 55 - hydrator, 56 - views, 57 - logger: appLogger, 58 - idResolver, 59 - cfg, 60 - authVerifier, 61 - }; 62 - } 3 + import { createMockApp, createMockContext } from "./util.ts"; 63 4 64 5 Deno.test("Basic App Creation", async () => { 65 - const ctx = createMockContext(); 66 - const app = createApp(ctx); 6 + const app = createMockApp(); 67 7 68 8 const res = await app.request("/", { 69 9 headers: { ··· 75 15 }); 76 16 77 17 Deno.test("Well Known Endpoint", async () => { 78 - const ctx = createMockContext(); 79 - const app = createApp(ctx); 18 + const app = createMockApp(); 80 19 81 20 const res = await app.request("/.well-known/did.json", { 82 21 headers: { ··· 107 46 ), 108 47 ); 109 48 }); 49 + 50 + Deno.test("Mock Context Creation", () => { 51 + const ctx = createMockContext(); 52 + 53 + assertEquals(typeof ctx.db, "object"); 54 + assertEquals(typeof ctx.dataplane, "object"); 55 + assertEquals(typeof ctx.hydrator, "object"); 56 + assertEquals(typeof ctx.views, "object"); 57 + assertEquals(typeof ctx.authVerifier, "function"); // AuthVerifier is a callable function 58 + assertEquals(typeof ctx.logger, "object"); 59 + assertEquals(typeof ctx.idResolver, "object"); 60 + assertEquals(ctx.cfg.serverDid, "did:web:localhost"); 61 + }); 62 + 63 + Deno.test("Mock Context with Config Overrides", () => { 64 + const ctx = createMockContext({ 65 + serverDid: "did:web:custom.test", 66 + adminPasswords: ["custom-password"], 67 + }); 68 + 69 + assertEquals(ctx.cfg.serverDid, "did:web:custom.test"); 70 + assertEquals(ctx.cfg.adminPasswords, ["custom-password"]); 71 + });
+966
tests/util.ts
··· 1 + import { MongoMemoryServer } from "mongodb-memory-server-core"; 2 + import mongoose, { Connection } from "mongoose"; 3 + import * as models from "../data-plane/db/models.ts"; 4 + import { Hono } from "hono"; 5 + import { createApp } from "../main.ts"; 6 + import { AppContext, AppEnv } from "../context.ts"; 7 + import { Database } from "../data-plane/db/index.ts"; 8 + import { createAuthVerifier } from "../auth-verifier.ts"; 9 + import { getLogger } from "@logtape/logtape"; 10 + import { DataPlane } from "../data-plane/index.ts"; 11 + import { Hydrator } from "../hydration/index.ts"; 12 + import { Views } from "../views/index.ts"; 13 + import { IdResolver } from "@atp/identity"; 14 + import { ServerConfig, ServerConfigValues } from "../config.ts"; 15 + 16 + // Configure mongodb-memory-server to use a specific download directory 17 + // This prevents issues with empty paths when running with restricted permissions 18 + const downloadDir = Deno.env.get("MONGOMS_DOWNLOAD_DIR") || 19 + `${Deno.env.get("HOME")}/.cache/mongodb-binaries`; 20 + Deno.env.set("MONGOMS_DOWNLOAD_DIR", downloadDir); 21 + 22 + // ============================================================================ 23 + // Test Data Options 24 + // ============================================================================ 25 + 26 + export interface TestDataOptions { 27 + actors?: boolean; 28 + profiles?: boolean; 29 + posts?: boolean; 30 + replies?: boolean; 31 + stories?: boolean; 32 + likes?: boolean; 33 + reposts?: boolean; 34 + follows?: boolean; 35 + blocks?: boolean; 36 + audio?: boolean; 37 + generators?: boolean; 38 + preferences?: boolean; 39 + records?: boolean; 40 + actorSync?: boolean; 41 + } 42 + 43 + // ============================================================================ 44 + // Test Database Types 45 + // ============================================================================ 46 + 47 + export interface TestDatabase { 48 + mongoServer: MongoMemoryServer; 49 + connection: Connection; 50 + models: models.DatabaseModels; 51 + cleanup: () => Promise<void>; 52 + } 53 + 54 + // ============================================================================ 55 + // Test App Types 56 + // ============================================================================ 57 + 58 + export interface TestApp { 59 + app: Hono<AppEnv>; 60 + ctx: AppContext; 61 + cleanup: () => Promise<void>; 62 + } 63 + 64 + // ============================================================================ 65 + // Default Test Config 66 + // ============================================================================ 67 + 68 + const DEFAULT_TEST_CONFIG: ServerConfigValues = { 69 + relayUrl: "http://localhost:8080", 70 + serverDid: "did:web:localhost", 71 + modServiceDid: "did:web:test", 72 + adminPasswords: ["test"], 73 + privateKey: 74 + "5676df35fd3a185a1771a43536635ad90057e0c0d1fd91436344bb50ce23a460", 75 + publicUrl: "http://localhost:4000", 76 + alternateAudienceDids: [], 77 + bigThreadUris: new Set(["did:web:test"]), 78 + maxThreadParents: 10, 79 + }; 80 + 81 + // ============================================================================ 82 + // Test Users 83 + // ============================================================================ 84 + 85 + export const TEST_USERS = [ 86 + { did: "did:plc:testuser1", handle: "alice.test" }, 87 + { did: "did:plc:testuser2", handle: "bob.test" }, 88 + { did: "did:plc:testuser3", handle: "charlie.test" }, 89 + { did: "did:plc:testuser4", handle: "diana.test" }, 90 + ] as const; 91 + 92 + // ============================================================================ 93 + // Database Setup Functions 94 + // ============================================================================ 95 + 96 + /** 97 + * Creates an in-memory MongoDB instance with fake sample data for testing 98 + * @param options - Options to control which data categories to create 99 + * @returns TestDatabase instance with connection and cleanup method 100 + */ 101 + export async function createTestDatabase( 102 + options: TestDataOptions = {}, 103 + ): Promise<TestDatabase> { 104 + // Default to creating all data types 105 + const opts = { 106 + actors: true, 107 + profiles: true, 108 + posts: true, 109 + replies: true, 110 + stories: true, 111 + likes: true, 112 + reposts: true, 113 + follows: true, 114 + blocks: true, 115 + audio: true, 116 + generators: true, 117 + preferences: true, 118 + records: true, 119 + actorSync: true, 120 + ...options, 121 + }; 122 + 123 + // Start MongoDB Memory Server 124 + const mongoServer = await MongoMemoryServer.create(); 125 + const uri = mongoServer.getUri(); 126 + 127 + // Create connection 128 + const connection = mongoose.createConnection(uri, { 129 + autoIndex: true, 130 + autoCreate: true, 131 + }); 132 + 133 + // Initialize models 134 + const dbModels: models.DatabaseModels = { 135 + Record: connection.model<models.RecordDocument>( 136 + "Record", 137 + models.recordSchema, 138 + ), 139 + DuplicateRecord: connection.model<models.DuplicateRecordDocument>( 140 + "DuplicateRecord", 141 + models.duplicateRecordSchema, 142 + ), 143 + Like: connection.model<models.LikeDocument>("Like", models.likeSchema), 144 + Post: connection.model<models.PostDocument>("Post", models.postSchema), 145 + Reply: connection.model<models.ReplyDocument>("Reply", models.replySchema), 146 + Story: connection.model<models.StoryDocument>("Story", models.storySchema), 147 + Follow: connection.model<models.FollowDocument>( 148 + "Follow", 149 + models.followSchema, 150 + ), 151 + Block: connection.model<models.BlockDocument>("Block", models.blockSchema), 152 + Profile: connection.model<models.ProfileDocument>( 153 + "Profile", 154 + models.profileSchema, 155 + ), 156 + Audio: connection.model<models.AudioDocument>("Audio", models.audioSchema), 157 + Repost: connection.model<models.RepostDocument>( 158 + "Repost", 159 + models.repostSchema, 160 + ), 161 + Generator: connection.model<models.GeneratorDocument>( 162 + "Generator", 163 + models.generatorSchema, 164 + ), 165 + Takedown: connection.model<models.TakedownDocument>( 166 + "Takedown", 167 + models.takedownSchema, 168 + ), 169 + RepoTakedown: connection.model<models.RepoTakedownDocument>( 170 + "RepoTakedown", 171 + models.repoTakedownSchema, 172 + ), 173 + BlobTakedown: connection.model<models.BlobTakedownDocument>( 174 + "BlobTakedown", 175 + models.blobTakedownSchema, 176 + ), 177 + Actor: connection.model<models.ActorDocument>("Actor", models.actorSchema), 178 + ActorSync: connection.model<models.ActorSyncDocument>( 179 + "ActorSync", 180 + models.actorSyncSchema, 181 + ), 182 + Preference: connection.model<models.PreferenceDocument>( 183 + "Preference", 184 + models.preferenceSchema, 185 + ), 186 + CursorState: connection.model<models.CursorStateDocument>( 187 + "CursorState", 188 + models.cursorStateSchema, 189 + ), 190 + }; 191 + 192 + // Seed data 193 + await seedTestData(dbModels, opts); 194 + 195 + const cleanup = async () => { 196 + try { 197 + await connection.close(); 198 + } catch (_err) { 199 + // Ignore close errors 200 + } 201 + await mongoServer.stop(); 202 + }; 203 + 204 + return { 205 + mongoServer, 206 + connection, 207 + models: dbModels, 208 + cleanup, 209 + }; 210 + } 211 + 212 + /** 213 + * Helper function to get a test database URI without creating the full database 214 + * Useful for testing Database class initialization 215 + */ 216 + export async function getTestDatabaseUri(): Promise<{ 217 + uri: string; 218 + cleanup: () => Promise<void>; 219 + }> { 220 + const mongoServer = await MongoMemoryServer.create(); 221 + const uri = mongoServer.getUri(); 222 + 223 + const cleanup = async () => { 224 + await mongoServer.stop(); 225 + }; 226 + 227 + return { uri, cleanup }; 228 + } 229 + 230 + // ============================================================================ 231 + // App Setup Functions 232 + // ============================================================================ 233 + 234 + /** 235 + * Creates a mock context for testing without a real database connection 236 + * @param configOverrides - Optional config values to override defaults 237 + * @returns AppContext instance with mock database 238 + */ 239 + export function createMockContext( 240 + configOverrides: Partial<ServerConfigValues> = {}, 241 + ): AppContext { 242 + const cfg = new ServerConfig({ ...DEFAULT_TEST_CONFIG, ...configOverrides }); 243 + const appLogger = getLogger(["appview"]); 244 + const idResolver = new IdResolver(); 245 + 246 + // Create mock database that doesn't actually connect 247 + const mockDb = { 248 + connect: () => Promise.resolve(), 249 + disconnect: () => Promise.resolve(), 250 + models: {}, 251 + getCursorState: () => Promise.resolve(null), 252 + saveCursorState: () => Promise.resolve(), 253 + idResolver, 254 + } as unknown as Database; 255 + 256 + const dataplane = new DataPlane(mockDb, idResolver); 257 + const hydrator = new Hydrator(dataplane); 258 + const views = new Views(cfg); 259 + const authVerifier = createAuthVerifier(dataplane, { 260 + ownDid: cfg.serverDid, 261 + alternateAudienceDids: cfg.alternateAudienceDids, 262 + modServiceDid: cfg.modServiceDid, 263 + adminPasses: cfg.adminPasswords, 264 + }); 265 + 266 + return { 267 + db: mockDb, 268 + dataplane, 269 + hydrator, 270 + views, 271 + logger: appLogger, 272 + idResolver, 273 + cfg, 274 + authVerifier, 275 + }; 276 + } 277 + 278 + /** 279 + * Creates a full test context with an in-memory MongoDB database 280 + * @param options - Options to control test data seeding 281 + * @param configOverrides - Optional config values to override defaults 282 + * @returns AppContext instance with real database and cleanup function 283 + */ 284 + export async function createTestContext( 285 + options: TestDataOptions = {}, 286 + configOverrides: Partial<ServerConfigValues> = {}, 287 + ): Promise<{ ctx: AppContext; cleanup: () => Promise<void> }> { 288 + const testDb = await createTestDatabase(options); 289 + const cfg = new ServerConfig({ ...DEFAULT_TEST_CONFIG, ...configOverrides }); 290 + const appLogger = getLogger(["appview"]); 291 + const idResolver = new IdResolver(); 292 + 293 + // Create a wrapper Database object that uses the test connection and models 294 + const db = { 295 + connection: testDb.connection, 296 + models: testDb.models, 297 + idResolver, 298 + logger: getLogger(["appview", "database"]), 299 + connect: () => Promise.resolve(), 300 + disconnect: async () => { 301 + await testDb.cleanup(); 302 + }, 303 + getCursorState: async () => { 304 + const cursorState = await testDb.models.CursorState.findOne({ 305 + identifier: "last_processed_cursor", 306 + }); 307 + return cursorState?.cursorValue || null; 308 + }, 309 + saveCursorState: async (cursorPosition: number) => { 310 + await testDb.models.CursorState.findOneAndUpdate( 311 + { identifier: "last_processed_cursor" }, 312 + { 313 + cursorValue: cursorPosition, 314 + updatedAt: new Date(), 315 + }, 316 + { upsert: true }, 317 + ); 318 + }, 319 + resolveHandle: async (handle: string) => { 320 + return await idResolver.handle.resolve(handle); 321 + }, 322 + resolveDid: async (did: string) => { 323 + const data = await idResolver.did.resolveAtprotoData(did); 324 + return { did: data.did, handle: data.handle }; 325 + }, 326 + getIdentityByDid: async ({ did }: { did: string }) => { 327 + const doc = await idResolver.did.resolve(did); 328 + if (!doc) throw new Error("DID not found"); 329 + return { did }; 330 + }, 331 + } as unknown as Database; 332 + 333 + const dataplane = new DataPlane(db, idResolver); 334 + const hydrator = new Hydrator(dataplane); 335 + const views = new Views(cfg); 336 + const authVerifier = createAuthVerifier(dataplane, { 337 + ownDid: cfg.serverDid, 338 + alternateAudienceDids: cfg.alternateAudienceDids, 339 + modServiceDid: cfg.modServiceDid, 340 + adminPasses: cfg.adminPasswords, 341 + }); 342 + 343 + const ctx: AppContext = { 344 + db, 345 + dataplane, 346 + hydrator, 347 + views, 348 + logger: appLogger, 349 + idResolver, 350 + cfg, 351 + authVerifier, 352 + }; 353 + 354 + const cleanup = async () => { 355 + await testDb.cleanup(); 356 + }; 357 + 358 + return { ctx, cleanup }; 359 + } 360 + 361 + /** 362 + * Creates a full test app with an in-memory MongoDB database 363 + * This is the primary function to use for integration tests 364 + * @param options - Options to control test data seeding 365 + * @param configOverrides - Optional config values to override defaults 366 + * @returns TestApp instance with app, context, and cleanup function 367 + */ 368 + export async function createTestApp( 369 + options: TestDataOptions = {}, 370 + configOverrides: Partial<ServerConfigValues> = {}, 371 + ): Promise<TestApp> { 372 + const { ctx, cleanup } = await createTestContext(options, configOverrides); 373 + const app = createApp(ctx); 374 + 375 + return { 376 + app, 377 + ctx, 378 + cleanup, 379 + }; 380 + } 381 + 382 + /** 383 + * Creates a test app with a mock database (no real MongoDB connection) 384 + * Useful for testing routes that don't require database access 385 + * @param configOverrides - Optional config values to override defaults 386 + * @returns Hono app instance with mock context 387 + */ 388 + export function createMockApp( 389 + configOverrides: Partial<ServerConfigValues> = {}, 390 + ): Hono<AppEnv> { 391 + const ctx = createMockContext(configOverrides); 392 + return createApp(ctx); 393 + } 394 + 395 + // ============================================================================ 396 + // Test Data Seeding 397 + // ============================================================================ 398 + 399 + /** 400 + * Seeds the test database with fake sample data 401 + */ 402 + async function seedTestData( 403 + models: models.DatabaseModels, 404 + options: Required<TestDataOptions>, 405 + ): Promise<void> { 406 + const now = new Date().toISOString(); 407 + const baseCid = "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq"; 408 + 409 + // Create Actors 410 + if (options.actors) { 411 + for (const user of TEST_USERS) { 412 + await models.Actor.create({ 413 + did: user.did, 414 + handle: user.handle, 415 + indexedAt: now, 416 + takedownRef: "", 417 + upstreamStatus: "active", 418 + keys: JSON.stringify({ signing: "key123" }), 419 + services: JSON.stringify({ pds: "https://pds.test" }), 420 + }); 421 + } 422 + } 423 + 424 + // Create ActorSync records 425 + if (options.actorSync) { 426 + for (const user of TEST_USERS) { 427 + await models.ActorSync.create({ 428 + did: user.did, 429 + commitCid: `${baseCid}commit${user.did.slice(-1)}`, 430 + repoRev: "rev123", 431 + }); 432 + } 433 + } 434 + 435 + // Create Profiles 436 + if (options.profiles) { 437 + await models.Profile.create({ 438 + uri: `at://${TEST_USERS[0].did}/app.bsky.actor.profile/self`, 439 + cid: `${baseCid}profile1`, 440 + authorDid: TEST_USERS[0].did, 441 + createdAt: now, 442 + indexedAt: now, 443 + displayName: "Alice", 444 + description: "Software engineer and coffee enthusiast", 445 + avatar: { 446 + $type: "blob", 447 + ref: { $link: `${baseCid}avatar1` }, 448 + mimeType: "image/jpeg", 449 + size: 50000, 450 + }, 451 + banner: { 452 + $type: "blob", 453 + ref: { $link: `${baseCid}banner1` }, 454 + mimeType: "image/jpeg", 455 + size: 100000, 456 + }, 457 + labels: [], 458 + pinnedPost: "", 459 + postsCount: 5, 460 + followersCount: 100, 461 + followsCount: 50, 462 + }); 463 + 464 + await models.Profile.create({ 465 + uri: `at://${TEST_USERS[1].did}/app.bsky.actor.profile/self`, 466 + cid: `${baseCid}profile2`, 467 + authorDid: TEST_USERS[1].did, 468 + createdAt: now, 469 + indexedAt: now, 470 + displayName: "Bob", 471 + description: "Music lover and tech geek", 472 + labels: [], 473 + pinnedPost: "", 474 + postsCount: 3, 475 + followersCount: 75, 476 + followsCount: 80, 477 + }); 478 + 479 + await models.Profile.create({ 480 + uri: `at://${TEST_USERS[2].did}/app.bsky.actor.profile/self`, 481 + cid: `${baseCid}profile3`, 482 + authorDid: TEST_USERS[2].did, 483 + createdAt: now, 484 + indexedAt: now, 485 + displayName: "Charlie", 486 + description: "Adventure seeker", 487 + labels: [], 488 + pinnedPost: "", 489 + postsCount: 10, 490 + followersCount: 200, 491 + followsCount: 150, 492 + }); 493 + } 494 + 495 + // Create Audio 496 + const audioUris: string[] = []; 497 + if (options.audio) { 498 + const audio1 = await models.Audio.create({ 499 + uri: `at://${TEST_USERS[1].did}/app.sprk.audio/audio1`, 500 + cid: `${baseCid}audio1`, 501 + authorDid: TEST_USERS[1].did, 502 + createdAt: now, 503 + indexedAt: now, 504 + sound: { 505 + $type: "blob", 506 + ref: { $link: `${baseCid}sound1` }, 507 + mimeType: "audio/mpeg", 508 + size: 1000000, 509 + }, 510 + origin: "original", 511 + title: "Chill Beats", 512 + details: "Relaxing music for coding", 513 + labels: [], 514 + useCount: 10, 515 + }); 516 + audioUris.push(audio1.uri); 517 + 518 + const audio2 = await models.Audio.create({ 519 + uri: `at://${TEST_USERS[1].did}/app.sprk.audio/audio2`, 520 + cid: `${baseCid}audio2`, 521 + authorDid: TEST_USERS[1].did, 522 + createdAt: now, 523 + indexedAt: now, 524 + sound: { 525 + $type: "blob", 526 + ref: { $link: `${baseCid}sound2` }, 527 + mimeType: "audio/mpeg", 528 + size: 800000, 529 + }, 530 + origin: "original", 531 + title: "Summer Vibes", 532 + details: "Upbeat summer track", 533 + labels: [], 534 + useCount: 5, 535 + }); 536 + audioUris.push(audio2.uri); 537 + } 538 + 539 + // Create Posts 540 + const postUris: string[] = []; 541 + if (options.posts) { 542 + const post1 = await models.Post.create({ 543 + uri: `at://${TEST_USERS[0].did}/app.sprk.post/post1`, 544 + cid: `${baseCid}post1`, 545 + authorDid: TEST_USERS[0].did, 546 + createdAt: now, 547 + indexedAt: now, 548 + caption: { 549 + text: "Just posted my first video! #excited", 550 + facets: [ 551 + { 552 + index: { byteStart: 30, byteEnd: 38 }, 553 + features: [{ 554 + $type: "app.bsky.richtext.facet#tag", 555 + tag: "excited", 556 + }], 557 + }, 558 + ], 559 + }, 560 + media: { 561 + $type: "app.sprk.post#videoMedia", 562 + video: { 563 + $type: "blob", 564 + ref: { $link: `${baseCid}video1` }, 565 + alt: "My first video", 566 + aspectRatio: { width: 1080, height: 1920 }, 567 + mimeType: "video/mp4", 568 + size: 5000000, 569 + }, 570 + }, 571 + sound: audioUris.length > 0 572 + ? { uri: audioUris[0], cid: `${baseCid}audio1` } 573 + : undefined, 574 + langs: ["en"], 575 + labels: [], 576 + tags: ["excited"], 577 + likeCount: 25, 578 + replyCount: 3, 579 + repostCount: 5, 580 + }); 581 + postUris.push(post1.uri); 582 + 583 + const post2 = await models.Post.create({ 584 + uri: `at://${TEST_USERS[2].did}/app.sprk.post/post2`, 585 + cid: `${baseCid}post2`, 586 + authorDid: TEST_USERS[2].did, 587 + createdAt: now, 588 + indexedAt: now, 589 + caption: { 590 + text: "Check out these amazing photos from my hike!", 591 + facets: [], 592 + }, 593 + media: { 594 + $type: "app.sprk.post#imageMedia", 595 + images: [ 596 + { 597 + $type: "blob", 598 + ref: { $link: `${baseCid}img1` }, 599 + alt: "Mountain view", 600 + aspectRatio: { width: 4, height: 3 }, 601 + mimeType: "image/jpeg", 602 + size: 200000, 603 + }, 604 + { 605 + $type: "blob", 606 + ref: { $link: `${baseCid}img2` }, 607 + alt: "Trail path", 608 + aspectRatio: { width: 4, height: 3 }, 609 + mimeType: "image/jpeg", 610 + size: 180000, 611 + }, 612 + ], 613 + }, 614 + langs: ["en"], 615 + labels: [], 616 + tags: [], 617 + likeCount: 50, 618 + replyCount: 8, 619 + repostCount: 12, 620 + }); 621 + postUris.push(post2.uri); 622 + 623 + const post3 = await models.Post.create({ 624 + uri: `at://${TEST_USERS[1].did}/app.sprk.post/post3`, 625 + cid: `${baseCid}post3`, 626 + authorDid: TEST_USERS[1].did, 627 + createdAt: now, 628 + indexedAt: now, 629 + caption: { 630 + text: "Simple text post without media", 631 + facets: [], 632 + }, 633 + langs: ["en"], 634 + labels: [], 635 + tags: [], 636 + likeCount: 10, 637 + replyCount: 2, 638 + repostCount: 1, 639 + }); 640 + postUris.push(post3.uri); 641 + } 642 + 643 + // Create Replies 644 + if (options.replies && postUris.length > 0) { 645 + await models.Reply.create({ 646 + uri: `at://${TEST_USERS[1].did}/app.sprk.reply/reply1`, 647 + cid: `${baseCid}reply1`, 648 + authorDid: TEST_USERS[1].did, 649 + createdAt: now, 650 + indexedAt: now, 651 + text: "This is awesome!", 652 + facets: [], 653 + reply: { 654 + root: { uri: postUris[0], cid: `${baseCid}post1` }, 655 + parent: { uri: postUris[0], cid: `${baseCid}post1` }, 656 + }, 657 + langs: ["en"], 658 + labels: [], 659 + likeCount: 5, 660 + replyCount: 0, 661 + }); 662 + 663 + await models.Reply.create({ 664 + uri: `at://${TEST_USERS[2].did}/app.sprk.reply/reply2`, 665 + cid: `${baseCid}reply2`, 666 + authorDid: TEST_USERS[2].did, 667 + createdAt: now, 668 + indexedAt: now, 669 + text: "Great content!", 670 + facets: [], 671 + reply: { 672 + root: { uri: postUris[0], cid: `${baseCid}post1` }, 673 + parent: { uri: postUris[0], cid: `${baseCid}post1` }, 674 + }, 675 + media: { 676 + $type: "app.sprk.reply#imageMedia", 677 + images: [ 678 + { 679 + $type: "blob", 680 + ref: { $link: `${baseCid}replyimg1` }, 681 + alt: "Reply image", 682 + aspectRatio: { width: 1, height: 1 }, 683 + mimeType: "image/jpeg", 684 + size: 100000, 685 + }, 686 + ], 687 + }, 688 + langs: ["en"], 689 + labels: [], 690 + likeCount: 3, 691 + replyCount: 0, 692 + }); 693 + } 694 + 695 + // Create Stories 696 + if (options.stories) { 697 + await models.Story.create({ 698 + uri: `at://${TEST_USERS[0].did}/app.sprk.story/story1`, 699 + cid: `${baseCid}story1`, 700 + authorDid: TEST_USERS[0].did, 701 + createdAt: now, 702 + indexedAt: now, 703 + media: { 704 + $type: "app.sprk.story#videoMedia", 705 + video: { 706 + $type: "blob", 707 + ref: { $link: `${baseCid}storyvid1` }, 708 + alt: "Story video", 709 + aspectRatio: { width: 1080, height: 1920 }, 710 + mimeType: "video/mp4", 711 + size: 3000000, 712 + }, 713 + }, 714 + sound: audioUris.length > 0 715 + ? { uri: audioUris[1], cid: `${baseCid}audio2` } 716 + : undefined, 717 + labels: [], 718 + }); 719 + 720 + await models.Story.create({ 721 + uri: `at://${TEST_USERS[2].did}/app.sprk.story/story2`, 722 + cid: `${baseCid}story2`, 723 + authorDid: TEST_USERS[2].did, 724 + createdAt: now, 725 + indexedAt: now, 726 + media: { 727 + $type: "app.sprk.story#imageMedia", 728 + image: { 729 + $type: "blob", 730 + ref: { $link: `${baseCid}storyimg1` }, 731 + alt: "Story image", 732 + aspectRatio: { width: 1080, height: 1920 }, 733 + mimeType: "image/jpeg", 734 + size: 250000, 735 + }, 736 + }, 737 + labels: [], 738 + }); 739 + } 740 + 741 + // Create Likes 742 + if (options.likes && postUris.length > 0) { 743 + await models.Like.create({ 744 + uri: `at://${TEST_USERS[1].did}/app.bsky.feed.like/like1`, 745 + cid: `${baseCid}like1`, 746 + authorDid: TEST_USERS[1].did, 747 + createdAt: now, 748 + indexedAt: now, 749 + subject: postUris[0], 750 + subjectCid: `${baseCid}post1`, 751 + }); 752 + 753 + await models.Like.create({ 754 + uri: `at://${TEST_USERS[2].did}/app.bsky.feed.like/like2`, 755 + cid: `${baseCid}like2`, 756 + authorDid: TEST_USERS[2].did, 757 + createdAt: now, 758 + indexedAt: now, 759 + subject: postUris[0], 760 + subjectCid: `${baseCid}post1`, 761 + }); 762 + 763 + await models.Like.create({ 764 + uri: `at://${TEST_USERS[0].did}/app.bsky.feed.like/like3`, 765 + cid: `${baseCid}like3`, 766 + authorDid: TEST_USERS[0].did, 767 + createdAt: now, 768 + indexedAt: now, 769 + subject: postUris[1], 770 + subjectCid: `${baseCid}post2`, 771 + }); 772 + } 773 + 774 + // Create Reposts 775 + if (options.reposts && postUris.length > 0) { 776 + await models.Repost.create({ 777 + uri: `at://${TEST_USERS[1].did}/app.bsky.feed.repost/repost1`, 778 + cid: `${baseCid}repost1`, 779 + authorDid: TEST_USERS[1].did, 780 + createdAt: now, 781 + indexedAt: now, 782 + subject: postUris[0], 783 + subjectCid: `${baseCid}post1`, 784 + }); 785 + 786 + await models.Repost.create({ 787 + uri: `at://${TEST_USERS[3].did}/app.bsky.feed.repost/repost2`, 788 + cid: `${baseCid}repost2`, 789 + authorDid: TEST_USERS[3].did, 790 + createdAt: now, 791 + indexedAt: now, 792 + subject: postUris[1], 793 + subjectCid: `${baseCid}post2`, 794 + }); 795 + } 796 + 797 + // Create Follows 798 + if (options.follows) { 799 + // Alice follows Bob 800 + await models.Follow.create({ 801 + uri: `at://${TEST_USERS[0].did}/app.bsky.graph.follow/follow1`, 802 + cid: `${baseCid}follow1`, 803 + authorDid: TEST_USERS[0].did, 804 + createdAt: now, 805 + indexedAt: now, 806 + subject: TEST_USERS[1].did, 807 + }); 808 + 809 + // Bob follows Alice 810 + await models.Follow.create({ 811 + uri: `at://${TEST_USERS[1].did}/app.bsky.graph.follow/follow2`, 812 + cid: `${baseCid}follow2`, 813 + authorDid: TEST_USERS[1].did, 814 + createdAt: now, 815 + indexedAt: now, 816 + subject: TEST_USERS[0].did, 817 + }); 818 + 819 + // Charlie follows Alice 820 + await models.Follow.create({ 821 + uri: `at://${TEST_USERS[2].did}/app.bsky.graph.follow/follow3`, 822 + cid: `${baseCid}follow3`, 823 + authorDid: TEST_USERS[2].did, 824 + createdAt: now, 825 + indexedAt: now, 826 + subject: TEST_USERS[0].did, 827 + }); 828 + 829 + // Alice follows Charlie 830 + await models.Follow.create({ 831 + uri: `at://${TEST_USERS[0].did}/app.bsky.graph.follow/follow4`, 832 + cid: `${baseCid}follow4`, 833 + authorDid: TEST_USERS[0].did, 834 + createdAt: now, 835 + indexedAt: now, 836 + subject: TEST_USERS[2].did, 837 + }); 838 + } 839 + 840 + // Create Blocks 841 + if (options.blocks) { 842 + await models.Block.create({ 843 + uri: `at://${TEST_USERS[0].did}/app.bsky.graph.block/block1`, 844 + cid: `${baseCid}block1`, 845 + authorDid: TEST_USERS[0].did, 846 + createdAt: now, 847 + indexedAt: now, 848 + subject: TEST_USERS[3].did, 849 + }); 850 + } 851 + 852 + // Create Generators 853 + if (options.generators) { 854 + await models.Generator.create({ 855 + uri: `at://${TEST_USERS[0].did}/app.bsky.feed.generator/gen1`, 856 + cid: `${baseCid}gen1`, 857 + authorDid: TEST_USERS[0].did, 858 + createdAt: now, 859 + indexedAt: now, 860 + displayName: "Trending Videos", 861 + description: "Hot video content from the community", 862 + descriptionFacets: [], 863 + avatar: { 864 + $type: "blob", 865 + ref: { $link: `${baseCid}genav1` }, 866 + mimeType: "image/png", 867 + size: 40000, 868 + }, 869 + acceptsInteractions: true, 870 + labels: [], 871 + likeCount: 150, 872 + }); 873 + 874 + await models.Generator.create({ 875 + uri: `at://${TEST_USERS[2].did}/app.bsky.feed.generator/gen2`, 876 + cid: `${baseCid}gen2`, 877 + authorDid: TEST_USERS[2].did, 878 + createdAt: now, 879 + indexedAt: now, 880 + displayName: "Nature & Adventure", 881 + description: "Outdoor content and adventure videos", 882 + descriptionFacets: [], 883 + acceptsInteractions: true, 884 + labels: [], 885 + likeCount: 80, 886 + }); 887 + } 888 + 889 + // Create Preferences 890 + if (options.preferences) { 891 + await models.Preference.create({ 892 + userDid: TEST_USERS[0].did, 893 + contentLabelPrefs: JSON.stringify({ 894 + labelerDids: [], 895 + labels: {}, 896 + }), 897 + savedFeeds: JSON.stringify([]), 898 + personalDetailsPref: JSON.stringify({}), 899 + feedViewPrefs: JSON.stringify({}), 900 + threadViewPref: JSON.stringify({}), 901 + interestsPref: JSON.stringify({ tags: ["tech", "music"] }), 902 + mutedWordsPref: JSON.stringify([]), 903 + hiddenPostsPref: JSON.stringify([]), 904 + labelersPref: JSON.stringify([]), 905 + postInteractionSettingsPref: JSON.stringify({}), 906 + createdAt: now, 907 + updatedAt: now, 908 + }); 909 + 910 + await models.Preference.create({ 911 + userDid: TEST_USERS[1].did, 912 + contentLabelPrefs: JSON.stringify({ 913 + labelerDids: [], 914 + labels: {}, 915 + }), 916 + savedFeeds: JSON.stringify([]), 917 + personalDetailsPref: JSON.stringify({}), 918 + feedViewPrefs: JSON.stringify({}), 919 + threadViewPref: JSON.stringify({}), 920 + interestsPref: JSON.stringify({ tags: ["music", "art"] }), 921 + mutedWordsPref: JSON.stringify([]), 922 + hiddenPostsPref: JSON.stringify([]), 923 + labelersPref: JSON.stringify([]), 924 + postInteractionSettingsPref: JSON.stringify({}), 925 + createdAt: now, 926 + updatedAt: now, 927 + }); 928 + } 929 + 930 + // Create Records 931 + if (options.records && postUris.length > 0) { 932 + await models.Record.create({ 933 + uri: postUris[0], 934 + cid: `${baseCid}post1`, 935 + did: TEST_USERS[0].did, 936 + collectionName: "app.sprk.post", 937 + rkey: "post1", 938 + createdAt: now, 939 + indexedAt: now, 940 + json: JSON.stringify({ 941 + $type: "app.sprk.post", 942 + caption: { text: "Just posted my first video! #excited" }, 943 + createdAt: now, 944 + }), 945 + takenDown: false, 946 + takedownRef: "", 947 + }); 948 + 949 + await models.Record.create({ 950 + uri: postUris[1], 951 + cid: `${baseCid}post2`, 952 + did: TEST_USERS[2].did, 953 + collectionName: "app.sprk.post", 954 + rkey: "post2", 955 + createdAt: now, 956 + indexedAt: now, 957 + json: JSON.stringify({ 958 + $type: "app.sprk.post", 959 + caption: { text: "Check out these amazing photos from my hike!" }, 960 + createdAt: now, 961 + }), 962 + takenDown: false, 963 + takedownRef: "", 964 + }); 965 + } 966 + }