11use std:: { fs:: create_dir_all, path:: PathBuf , sync:: Mutex } ;
22use rusqlite:: { Connection , OptionalExtension , Result as SqlResult , params} ;
33
4-
54#[ derive( Debug ) ]
65pub struct TofuData {
76 db_path : PathBuf ,
@@ -12,6 +11,10 @@ pub struct TofuData {
1211
1312impl TofuData {
1413 pub fn setData ( & self , host : & str , cert : Vec < u8 > ) -> SqlResult < ( ) > {
14+ if cert. is_empty ( ) {
15+ return Err ( rusqlite:: Error :: InvalidQuery ) ;
16+ }
17+
1518 let connection = self . connection . lock ( ) . unwrap ( ) ;
1619 let sql = "INSERT INTO tofu (host, cert) VALUES (?1, ?2)
1720 ON CONFLICT(host) DO UPDATE SET cert = excluded.cert" ;
@@ -50,3 +53,165 @@ impl TofuData {
5053 } )
5154 }
5255}
56+
57+ #[ cfg( test) ]
58+ mod tests {
59+ use super :: * ;
60+ use std:: fs;
61+ use std:: path:: Path ;
62+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
63+
64+ static TEST_COUNTER : AtomicU64 = AtomicU64 :: new ( 0 ) ;
65+
66+ fn setup_with_path ( db_path : PathBuf ) -> SqlResult < TofuData > {
67+ if let Some ( parent) = db_path. parent ( ) {
68+ create_dir_all ( parent) . ok ( ) ;
69+ }
70+ let connection = Connection :: open ( & db_path) ?;
71+ let sql = "CREATE TABLE IF NOT EXISTS tofu(
72+ host TEXT PRIMARY KEY,
73+ cert BLOB NOT NULL
74+ )" ;
75+
76+ connection. execute ( sql, [ ] ) ?;
77+
78+ Ok ( TofuData {
79+ db_path,
80+ connection : Mutex :: new ( connection)
81+ } )
82+ }
83+
84+ fn create_temp_db ( ) -> ( PathBuf , TofuData ) {
85+ let temp_dir = std:: env:: temp_dir ( ) ;
86+ let counter = TEST_COUNTER . fetch_add ( 1 , Ordering :: SeqCst ) ;
87+ let db_path = temp_dir. join ( format ! ( "tofu_test_{}_{}.sqlite" , std:: process:: id( ) , counter) ) ;
88+ // Clean up any existing test database
89+ let _ = fs:: remove_file ( & db_path) ;
90+ let store = setup_with_path ( db_path. clone ( ) ) . unwrap ( ) ;
91+ ( db_path, store)
92+ }
93+
94+ fn cleanup_temp_db ( db_path : & Path ) {
95+ // Small delay to ensure file handles are released
96+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) ;
97+ let _ = fs:: remove_file ( db_path) ;
98+ }
99+
100+ #[ test]
101+ fn test_tofu_first_use ( ) {
102+ let ( db_path, store) = create_temp_db ( ) ;
103+
104+ let host = "example.com" ;
105+ let cert = b"test certificate data" . to_vec ( ) ;
106+
107+ // First use: certificate should not exist
108+ let result = store. getData ( host) . unwrap ( ) ;
109+ assert ! ( result. is_none( ) , "Certificate should not exist on first use" ) ;
110+
111+ store. setData ( host, cert. clone ( ) ) . unwrap ( ) ;
112+
113+ let stored = store. getData ( host) . unwrap ( ) ;
114+ assert_eq ! ( stored, Some ( cert) , "Certificate should be stored" ) ;
115+
116+ drop ( store) ;
117+ cleanup_temp_db ( & db_path) ;
118+ }
119+
120+ #[ test]
121+ fn test_tofu_certificate_match ( ) {
122+ let ( db_path, store) = create_temp_db ( ) ;
123+
124+ let host = "example.com" ;
125+ let cert = b"test certificate data" . to_vec ( ) ;
126+
127+ // Store certificate
128+ store. setData ( host, cert. clone ( ) ) . unwrap ( ) ;
129+
130+ // Retrieve and verify it matches
131+ let stored = store. getData ( host) . unwrap ( ) ;
132+ assert_eq ! ( stored, Some ( cert) , "Stored certificate should match" ) ;
133+
134+ drop ( store) ;
135+ cleanup_temp_db ( & db_path) ;
136+ }
137+
138+ #[ test]
139+ fn test_tofu_certificate_change ( ) {
140+ let ( db_path, store) = create_temp_db ( ) ;
141+
142+ let host = "example.com" ;
143+ let cert1 = b"first certificate" . to_vec ( ) ;
144+ let cert2 = b"second certificate" . to_vec ( ) ;
145+
146+ // Store first certificate
147+ store. setData ( host, cert1. clone ( ) ) . unwrap ( ) ;
148+ let stored1 = store. getData ( host) . unwrap ( ) ;
149+ assert_eq ! ( stored1, Some ( cert1. clone( ) ) , "First certificate should be stored" ) ;
150+
151+ // Update with different certificate
152+ store. setData ( host, cert2. clone ( ) ) . unwrap ( ) ;
153+ let stored2 = store. getData ( host) . unwrap ( ) ;
154+ assert_eq ! ( stored2, Some ( cert2. clone( ) ) , "Second certificate should replace first" ) ;
155+ assert_ne ! ( stored2, Some ( cert1) , "Stored certificate should not match first" ) ;
156+
157+ drop ( store) ;
158+ cleanup_temp_db ( & db_path) ;
159+ }
160+
161+ #[ test]
162+ fn test_tofu_persistence ( ) {
163+ let temp_dir = std:: env:: temp_dir ( ) ;
164+ let db_path = temp_dir. join ( format ! ( "tofu_persistence_test_{}.sqlite" , std:: process:: id( ) ) ) ;
165+ let _ = fs:: remove_file ( & db_path) ;
166+
167+ let host = "example.com" ;
168+ let cert = b"persistent certificate" . to_vec ( ) ;
169+
170+ // Create first store instance and save certificate
171+ {
172+ let store1 = setup_with_path ( db_path. clone ( ) ) . unwrap ( ) ;
173+ store1. setData ( host, cert. clone ( ) ) . unwrap ( ) ;
174+ }
175+
176+ // Create second store instance and verify certificate persists
177+ {
178+ let store2 = setup_with_path ( db_path. clone ( ) ) . unwrap ( ) ;
179+ let stored = store2. getData ( host) . unwrap ( ) ;
180+ assert_eq ! ( stored, Some ( cert) , "Certificate should persist across instances" ) ;
181+ }
182+
183+ cleanup_temp_db ( & db_path) ;
184+ }
185+
186+ #[ test]
187+ fn test_tofu_empty_certificate ( ) {
188+ let ( db_path, store) = create_temp_db ( ) ;
189+
190+ let host = "example.com" ;
191+ let cert = vec ! [ ] ;
192+
193+ // Attempt to store empty certificate should fail
194+ let result = store. setData ( host, cert. clone ( ) ) ;
195+ assert ! ( result. is_err( ) , "Storing empty certificate should return an error" ) ;
196+
197+ drop ( store) ;
198+ cleanup_temp_db ( & db_path) ;
199+ }
200+
201+ #[ test]
202+ fn test_tofu_large_certificate ( ) {
203+ let ( db_path, store) = create_temp_db ( ) ;
204+
205+ let host = "example.com" ;
206+ // Create a large certificate (10KB)
207+ let cert = vec ! [ 0x42 ; 10 * 1024 ] ;
208+
209+ // Store large certificate
210+ store. setData ( host, cert. clone ( ) ) . unwrap ( ) ;
211+ let stored = store. getData ( host) . unwrap ( ) ;
212+ assert_eq ! ( stored, Some ( cert) , "Large certificate should be stored correctly" ) ;
213+
214+ drop ( store) ;
215+ cleanup_temp_db ( & db_path) ;
216+ }
217+ }
0 commit comments