char-slop/stars.hackery.site
git clone https://git.t4t.associates/char-slop/stars.hackery.site
92004f9
main
1import { assert , assertEquals , assertRejects } from "@std/assert" ; 2import { DatabaseSync } from "node:sqlite" ; 3import { Auth , type Session } from "./auth.ts" ; 4import { Hydrant } from "./hydrant.ts" ; 5import { handler } from "./main.ts" ; 6import { Index } from "./sync.ts" ; 7import { 8parseTitle , 9publicAddress , 10TitleLookup , 11TitleRateLimit , 12} from "./title.ts" ; 13 14Deno . test ( "title previews prefer decoded Open Graph titles, then HTML titles" , () => { 15assertEquals ( 16parseTitle ( 17'<html><head><title>HTML title</title><meta content=" Cerulea & friends " property="og:title"></head></html>' , 18), 19{ title :"Cerulea & friends" , source :"og" }, 20); 21assertEquals ( parseTitle ( "<title>A & B</title>" ), { 22title :"A & B" , 23source :"html" , 24}); 25assertEquals ( parseTitle ( "<title> </title>" ), undefined ); 26}); 27 28Deno . test ( "title lookup rejects private and non-unicast destinations" , () => { 29for ( 30const ip of [ 31"127.0.0.1" , 32"10.0.0.1" , 33"169.254.169.254" , 34"192.168.0.1" , 35"100.64.0.1" , 36"::1" , 37"fd00::1" , 38"::ffff:127.0.0.1" , 39"2002:c0a8:0101::" , 40] 41) assert ( ! publicAddress ( ip ), ip ); 42assert ( publicAddress ( "8.8.8.8" )); 43assert ( publicAddress ( "2606:4700:4700::1111" )); 44}); 45 46Deno . test ( "title lookup is limited per signed-in account" , async () => { 47const db = new DatabaseSync ( ":memory:" ); 48try { 49let calls = 0 ; 50const lookup = new TitleLookup ( db , ( url ) => { 51calls ++ ; 52return Promise . resolve ({ title :url , source :"url" }); 53}); 54for ( let i = 0 ; i < 10 ; i ++ ) { 55await lookup . lookup ( "did:plc:alice" , "https://example.org/" ); 56} 57await assertRejects ( 58() => lookup . lookup ( "did:plc:alice" , "https://example.org/" ), 59TitleRateLimit , 60); 61await lookup . lookup ( "did:plc:bob" , "https://example.org/" ); 62assertEquals ( calls , 11 ); 63} finally { 64db . close (); 65} 66}); 67 68Deno . test ( "share form previews a title and publishes the user's revision" , async () => { 69const db = new DatabaseSync ( ":memory:" ); 70try { 71const origin = "http://127.0.0.1:8000" ; 72const auth = new Auth ( db , new URL ( origin )); 73const did = "did:plc:abcdefghij" ; 74let published :unknown ; 75auth . session = () => 76Promise . resolve ({ 77 did, 78csrf :"test-csrf" , 79id :"test-session" , 80oauth :{ 81 did, 82handle :( _path :string , init :RequestInit ) => { 83published = JSON . parse ( init . body as string ); 84return Promise . resolve ( Response . json ({ 85uri :`at:// ${ did } /site.hackery.stars.star/3mgabcdefghij` , 86})); 87}, 88}, 89} as unknown as Session ); 90const hydrant = new Hydrant ( new URL ( "http://127.0.0.1:3000" )); 91const titles = new TitleLookup ( 92db , 93() => Promise . resolve ({ title :"Fetched title" , source :"og" }), 94); 95const index = new Index ( db , hydrant ); 96hydrant . track = () => Promise . resolve (); 97const app = { auth, hydrant, titles, index}; 98const request = ( path :string , fields :Record < string , string >) => 99new Request ( origin + path , { 100method :"POST" , 101headers :{ 102 origin, 103"content-type" :"application/x-www-form-urlencoded" , 104}, 105body :new URLSearchParams ({ csrf :"test-csrf" , ...fields }), 106}); 107const preview = await handler ( 108request ( "/share/title" , { 109url :"https://example.org/project" , 110comment :"Worth reading" , 111}), 112app , 113); 114assertEquals ( preview . status , 200 ); 115const html = await preview . text (); 116assert ( 117html . includes ( 'name="title" maxlength="2048" value="Fetched title"' ), 118); 119assert ( html . includes ( "Worth reading" )); 120const script = / <script>([^]*?)<\/script> / . exec ( html ) ! [ 1 ]; 121const hash = btoa ( String . fromCharCode ( 122 ...new Uint8Array ( 123await crypto . subtle . digest ( "SHA-256" , new TextEncoder (). encode ( script )), 124), 125)); 126assert ( 127preview . headers . get ( "content-security-policy" ) ! . includes ( 128`'sha256- ${ hash } '` , 129), 130); 131const rejected = await handler ( 132new Request ( origin + "/xrpc/site.hackery.stars.getTitle" , { 133method :"POST" , 134headers :{ origin, "content-type" :"application/json" }, 135body :JSON . stringify ({ url :"https://example.org/project" }), 136}), 137app , 138); 139assertEquals ( rejected . status , 403 ); 140const xrpc = await handler ( 141new Request ( origin + "/xrpc/site.hackery.stars.getTitle" , { 142method :"POST" , 143headers :{ 144 origin, 145"content-type" :"application/json" , 146"x-csrf-token" :"test-csrf" , 147}, 148body :JSON . stringify ({ url :"https://example.org/project" }), 149}), 150app , 151); 152assertEquals ( xrpc . status , 200 ); 153assertEquals (( await xrpc . json ()). title , "Fetched title" ); 154const share = await handler ( 155request ( "/share" , { 156url :"https://example.org/project" , 157title :"My revised title" , 158comment :"Worth reading" , 159}), 160app , 161); 162assertEquals ( share . status , 303 ); 163assertEquals ( 164( published as { record :{ title :string } }). record . title , 165"My revised title" , 166); 167const next = await handler ( 168new Request ( new URL ( share . headers . get ( "location" ) ! , origin )), 169app , 170); 171const form = await next . text (); 172assert ( form . includes ( "Starred!" )); 173assert ( 174form . includes ( 'name="url" type="url" maxlength="2048" required value=""' ), 175); 176} finally { 177db . close (); 178} 179});