@@ -20,20 +20,40 @@ impl std::fmt::Display for SessionId {
2020 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result { write ! ( f, "{}" , self . 0 ) }
2121}
2222
23- #[ derive( Clone ) ]
23+ #[ derive( Clone , Debug ) ]
2424pub ( crate ) struct SenderPersister {
2525 db : Arc < Database > ,
2626 session_id : SessionId ,
2727}
2828
2929impl SenderPersister {
30- pub fn new ( db : Arc < Database > , receiver_pubkey : HpkePublicKey ) -> crate :: db:: Result < Self > {
30+ pub fn new (
31+ db : Arc < Database > ,
32+ pj_uri : & str ,
33+ receiver_pubkey : & HpkePublicKey ,
34+ ) -> crate :: db:: Result < Self > {
3135 let conn = db. get_connection ( ) ?;
36+ let receiver_pubkey_bytes = receiver_pubkey. to_compressed_bytes ( ) ;
37+
38+ let ( duplicate_uri, duplicate_rk) : ( bool , bool ) = conn. query_row (
39+ "SELECT \
40+ EXISTS(SELECT 1 FROM send_sessions WHERE completed_at IS NULL AND pj_uri = ?1), \
41+ EXISTS(SELECT 1 FROM send_sessions WHERE completed_at IS NULL AND receiver_pubkey = ?2)",
42+ params ! [ pj_uri, & receiver_pubkey_bytes] ,
43+ |row| Ok ( ( row. get ( 0 ) ?, row. get ( 1 ) ?) ) ,
44+ ) ?;
45+
46+ if duplicate_uri {
47+ return Err ( Error :: DuplicateSendSession ( DuplicateKind :: Uri ) ) ;
48+ }
49+ if duplicate_rk {
50+ return Err ( Error :: DuplicateSendSession ( DuplicateKind :: ReceiverPubkey ) ) ;
51+ }
3252
3353 // Create a new session in send_sessions and get its ID
3454 let session_id: i64 = conn. query_row (
35- "INSERT INTO send_sessions (session_id , receiver_pubkey) VALUES (NULL , ?1 ) RETURNING session_id" ,
36- params ! [ receiver_pubkey . to_compressed_bytes ( ) ] ,
55+ "INSERT INTO send_sessions (pj_uri , receiver_pubkey) VALUES (?1 , ?2 ) RETURNING session_id" ,
56+ params ! [ pj_uri , & receiver_pubkey_bytes ] ,
3757 |row| row. get ( 0 ) ,
3858 ) ?;
3959
@@ -42,7 +62,6 @@ impl SenderPersister {
4262
4363 pub fn from_id ( db : Arc < Database > , id : SessionId ) -> Self { Self { db, session_id : id } }
4464}
45-
4665impl SessionPersister for SenderPersister {
4766 type SessionEvent = SenderSessionEvent ;
4867 type InternalStorageError = crate :: db:: error:: Error ;
@@ -268,3 +287,78 @@ impl Database {
268287 Ok ( session_ids)
269288 }
270289}
290+
291+ #[ cfg( all( test, feature = "v2" ) ) ]
292+ mod tests {
293+ use std:: sync:: Arc ;
294+
295+ use payjoin:: HpkeKeyPair ;
296+
297+ use super :: * ;
298+
299+ fn create_test_db ( ) -> Arc < Database > {
300+ // Use an in-memory database for tests
301+ let manager = r2d2_sqlite:: SqliteConnectionManager :: memory ( )
302+ . with_init ( |conn| conn. execute_batch ( "PRAGMA locking_mode = EXCLUSIVE;" ) ) ;
303+ let pool = r2d2:: Pool :: new ( manager) . expect ( "pool creation should succeed" ) ;
304+ let conn = pool. get ( ) . expect ( "connection should succeed" ) ;
305+ Database :: init_schema ( & conn) . expect ( "schema init should succeed" ) ;
306+ Arc :: new ( Database ( pool) )
307+ }
308+
309+ fn make_receiver_pubkey ( ) -> payjoin:: HpkePublicKey { HpkeKeyPair :: gen_keypair ( ) . 1 }
310+
311+ /// Second call with the same URI (same active session) should return DuplicateSendSession(Uri).
312+ #[ test]
313+ fn test_duplicate_uri_returns_error ( ) {
314+ let db = create_test_db ( ) ;
315+ let rk1 = make_receiver_pubkey ( ) ;
316+ let rk2 = make_receiver_pubkey ( ) ;
317+ let uri = "bitcoin:addr1?pj=https://example.com/BBBBBBBB" ;
318+
319+ SenderPersister :: new ( db. clone ( ) , uri, & rk1) . expect ( "first session should succeed" ) ;
320+
321+ let err = SenderPersister :: new ( db, uri, & rk2) . expect_err ( "duplicate URI should fail" ) ;
322+ assert ! (
323+ matches!( err, Error :: DuplicateSendSession ( DuplicateKind :: Uri ) ) ,
324+ "expected DuplicateSendSession(Uri), got: {err:?}"
325+ ) ;
326+ }
327+
328+ /// Same receiver pubkey under a different URI should return DuplicateSendSession(ReceiverPubkey).
329+ #[ test]
330+ fn test_duplicate_rk_returns_error ( ) {
331+ let db = create_test_db ( ) ;
332+ let rk = make_receiver_pubkey ( ) ;
333+ let uri1 = "bitcoin:addr1?pj=https://example.com/CCCCCCCC" ;
334+ let uri2 = "bitcoin:addr1?pj=https://example.com/DDDDDDDD" ;
335+
336+ SenderPersister :: new ( db. clone ( ) , uri1, & rk) . expect ( "first session should succeed" ) ;
337+
338+ let err = SenderPersister :: new ( db, uri2, & rk) . expect_err ( "duplicate RK should fail" ) ;
339+ assert ! (
340+ matches!( err, Error :: DuplicateSendSession ( DuplicateKind :: ReceiverPubkey ) ) ,
341+ "expected DuplicateSendSession(ReceiverPubkey), got: {err:?}"
342+ ) ;
343+ }
344+
345+ /// After a session is marked completed, a new session with the same URI should be allowed.
346+ #[ test]
347+ fn test_completed_session_allows_reuse ( ) {
348+ let db = create_test_db ( ) ;
349+ let rk1 = make_receiver_pubkey ( ) ;
350+ let rk2 = make_receiver_pubkey ( ) ;
351+ let uri = "bitcoin:addr1?pj=https://example.com/EEEEEEEE" ;
352+
353+ let persister =
354+ SenderPersister :: new ( db. clone ( ) , uri, & rk1) . expect ( "first session should succeed" ) ;
355+
356+ // Mark the session as completed
357+ use payjoin:: persist:: SessionPersister ;
358+ persister. close ( ) . expect ( "close should succeed" ) ;
359+
360+ // Now a new session with the same URI should succeed (completed sessions don't block)
361+ let result = SenderPersister :: new ( db, uri, & rk2) ;
362+ assert ! ( result. is_ok( ) , "reuse after completion should succeed" ) ;
363+ }
364+ }
0 commit comments