Skip to content

Commit 838df63

Browse files
saagpatelclaude
andcommitted
test: add baseline Rust tests for proxy normalizer, filter, and db queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aa5c068 commit 838df63

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

src-tauri/src/db/queries.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,119 @@ mod tests {
613613
let page3 = get_requests(&conn, "s1", 2, 4).unwrap();
614614
assert_eq!(page3.len(), 1);
615615
}
616+
617+
#[test]
618+
fn test_settings_get_set_and_overwrite() {
619+
let conn = setup_test_db();
620+
db::schema::migrate_v3(&conn).unwrap();
621+
// Missing key returns None
622+
assert_eq!(get_setting(&conn, "api_key").unwrap(), None);
623+
624+
set_setting(&conn, "api_key", "sk-test-123").unwrap();
625+
assert_eq!(
626+
get_setting(&conn, "api_key").unwrap(),
627+
Some("sk-test-123".to_string())
628+
);
629+
630+
// Overwrite via upsert
631+
set_setting(&conn, "api_key", "sk-new-456").unwrap();
632+
assert_eq!(
633+
get_setting(&conn, "api_key").unwrap(),
634+
Some("sk-new-456".to_string())
635+
);
636+
}
637+
638+
#[test]
639+
fn test_rename_and_delete_session() {
640+
let conn = setup_test_db();
641+
insert_session(&conn, "s1", "Original", "extension").unwrap();
642+
643+
rename_session(&conn, "s1", "Renamed").unwrap();
644+
let session = get_session(&conn, "s1").unwrap();
645+
assert_eq!(session.name, "Renamed");
646+
647+
delete_session(&conn, "s1").unwrap();
648+
let sessions = list_sessions(&conn).unwrap();
649+
assert_eq!(sessions.len(), 0);
650+
}
651+
652+
#[test]
653+
fn test_update_session_status() {
654+
let conn = setup_test_db();
655+
insert_session(&conn, "s1", "Test", "mitm").unwrap();
656+
657+
update_session_status(&conn, "s1", "complete").unwrap();
658+
let session = get_session(&conn, "s1").unwrap();
659+
assert_eq!(session.status, "complete");
660+
// ended_at should be set for "complete" status
661+
assert!(session.ended_at.is_some());
662+
}
663+
664+
#[test]
665+
fn test_get_requests_after() {
666+
let conn = setup_test_db();
667+
insert_session(&conn, "s1", "Test", "extension").unwrap();
668+
669+
let id1 = insert_request(
670+
&conn,
671+
"s1",
672+
"extension",
673+
"GET",
674+
"https://api.example.com/a",
675+
"/a",
676+
"api.example.com",
677+
"/a",
678+
None,
679+
None,
680+
Some(200),
681+
None,
682+
None,
683+
)
684+
.unwrap();
685+
let _id2 = insert_request(
686+
&conn,
687+
"s1",
688+
"extension",
689+
"GET",
690+
"https://api.example.com/b",
691+
"/b",
692+
"api.example.com",
693+
"/b",
694+
None,
695+
None,
696+
Some(200),
697+
None,
698+
None,
699+
)
700+
.unwrap();
701+
let _id3 = insert_request(
702+
&conn,
703+
"s1",
704+
"extension",
705+
"GET",
706+
"https://api.example.com/c",
707+
"/c",
708+
"api.example.com",
709+
"/c",
710+
None,
711+
None,
712+
Some(200),
713+
None,
714+
None,
715+
)
716+
.unwrap();
717+
718+
// Only the two requests after id1 should be returned
719+
let after = get_requests_after(&conn, "s1", id1).unwrap();
720+
assert_eq!(after.len(), 2);
721+
assert!(after.iter().all(|r| r.id > id1));
722+
}
723+
724+
#[test]
725+
fn test_get_requests_by_ids_empty_slice() {
726+
let conn = setup_test_db();
727+
// Empty slice must return empty vec without error
728+
let results = get_requests_by_ids(&conn, &[]).unwrap();
729+
assert_eq!(results.len(), 0);
730+
}
616731
}

src-tauri/src/proxy/filter.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,62 @@ mod tests {
253253
None
254254
);
255255
}
256+
257+
#[test]
258+
fn test_domain_matches_exact_and_subdomain() {
259+
assert!(domain_matches("example.com", "example.com"));
260+
assert!(domain_matches("sub.example.com", "example.com"));
261+
assert!(domain_matches("deep.sub.example.com", "example.com"));
262+
// Must not match partial suffix — "notexample.com" should NOT match "example.com"
263+
assert!(!domain_matches("notexample.com", "example.com"));
264+
}
265+
266+
#[test]
267+
fn test_has_noise_extension_all_types() {
268+
for ext in &[
269+
".js", ".css", ".woff", ".woff2", ".ttf", ".png", ".jpg", ".jpeg", ".gif", ".svg",
270+
".ico", ".map", ".br",
271+
] {
272+
let path = format!("/static/file{ext}");
273+
assert!(has_noise_extension(&path), "expected noise for path {path}");
274+
}
275+
// Non-noise extensions
276+
assert!(!has_noise_extension("/api/data.json"));
277+
assert!(!has_noise_extension("/v1/users"));
278+
}
279+
280+
#[test]
281+
fn test_preset_domains_known_and_unknown() {
282+
assert!(!preset_domains("analytics").is_empty());
283+
assert!(!preset_domains("cdn").is_empty());
284+
assert!(!preset_domains("social").is_empty());
285+
assert!(!preset_domains("fonts").is_empty());
286+
// Unknown preset returns empty slice
287+
assert!(preset_domains("nonexistent").is_empty());
288+
}
289+
290+
#[test]
291+
fn test_detect_noise_reason_allowlist() {
292+
let config = crate::models::FilterConfig {
293+
allowlist: vec!["trusted.com".to_string()],
294+
..Default::default()
295+
};
296+
assert_eq!(
297+
detect_noise_reason("other.com", "/api", &config),
298+
Some("not in allowlist")
299+
);
300+
assert_eq!(detect_noise_reason("trusted.com", "/api", &config), None);
301+
}
302+
303+
#[test]
304+
fn test_detect_noise_reason_user_denylist() {
305+
let config = crate::models::FilterConfig {
306+
denylist: vec!["tracker.internal".to_string()],
307+
..Default::default()
308+
};
309+
assert_eq!(
310+
detect_noise_reason("tracker.internal", "/ping", &config),
311+
Some("user denylist")
312+
);
313+
}
256314
}

src-tauri/src/proxy/normalizer.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,35 @@ mod tests {
173173
);
174174
assert_eq!(normalize_path("/commits/ABCDEF01"), "/commits/{id}");
175175
}
176+
177+
#[test]
178+
fn test_hex_boundary_exactly_seven_chars_preserved() {
179+
// Hex strings shorter than 8 chars must NOT be replaced
180+
assert_eq!(normalize_path("/api/abcdef0"), "/api/abcdef0"); // 7 hex chars → preserved
181+
// 7-digit pure integer IS replaced (matched as integer, not hex)
182+
assert_eq!(normalize_path("/api/1234567"), "/api/{id}");
183+
}
184+
185+
#[test]
186+
fn test_version_variants() {
187+
assert_eq!(normalize_path("/v1/resources"), "/v1/resources");
188+
assert_eq!(normalize_path("/v100/data"), "/v100/data");
189+
// "version" word (not v\d+) is preserved as-is
190+
assert_eq!(normalize_path("/version/info"), "/version/info");
191+
}
192+
193+
#[test]
194+
fn test_all_digits_segment_always_replaced() {
195+
assert_eq!(normalize_path("/orders/0"), "/orders/{id}");
196+
assert_eq!(normalize_path("/orders/1234567890"), "/orders/{id}");
197+
}
198+
199+
#[test]
200+
fn test_mixed_alphanumeric_non_hex_boundary() {
201+
// 9-char segment with non-hex letters (g-z) — can't match RE_HEX, below 10-char threshold
202+
// "abcXYZde5" has 'X','Y','Z' which are non-hex uppercase (lowercased: x,y,z — non hex)
203+
assert_eq!(normalize_path("/api/abcXYZde5"), "/api/abcXYZde5"); // 9 chars, non-hex → preserved
204+
// 10-char non-hex mixed → replaced by mixed-alphanumeric rule
205+
assert_eq!(normalize_path("/api/abcXYZde56"), "/api/{id}"); // 10 chars, mixed → replaced
206+
}
176207
}

0 commit comments

Comments
 (0)