Skip to main content

lastfm_edit/
vcr_test_utils.rs

1use crate::vcr_form_data::{encode_form_data, parse_form_data};
2use http_client_vcr::{Filter, FilterChain, SerializableRequest, SerializableResponse};
3
4/// Last.fm-specific utilities for test cassette management.
5/// These helpers understand the Last.fm authentication flow and handle credential replacement appropriately.
6/// A custom filter that only filters passwords while preserving usernames and CSRF tokens
7#[derive(Debug)]
8pub struct LastFmPasswordOnlyFilter;
9
10impl Filter for LastFmPasswordOnlyFilter {
11    fn filter_request(&self, request: &mut SerializableRequest) {
12        // Filter passwords in form data
13        if let Some(body) = &mut request.body {
14            if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
15                let mut params = parse_form_data(body);
16
17                // Only filter password field - keep username and CSRF token for request matching
18                if params.contains_key("password") {
19                    params.insert("password".to_string(), "test_password".to_string());
20                    *body = encode_form_data(&params);
21                }
22            }
23        }
24
25        // Filter session cookies in request headers
26        if let Some(cookie_values) = request.headers.get_mut("cookie") {
27            for cookie_header in cookie_values.iter_mut() {
28                *cookie_header = self.filter_session_cookies(cookie_header);
29            }
30        }
31    }
32
33    fn filter_response(&self, response: &mut SerializableResponse) {
34        // Filter session cookies in response set-cookie headers
35        if let Some(set_cookie_values) = response.headers.get_mut("set-cookie") {
36            for set_cookie_header in set_cookie_values.iter_mut() {
37                if set_cookie_header.contains("sessionid=") {
38                    *set_cookie_header = self.filter_set_cookie_session(set_cookie_header);
39                }
40            }
41        }
42    }
43}
44
45impl LastFmPasswordOnlyFilter {
46    /// Filter session cookies from a cookie header string
47    fn filter_session_cookies(&self, cookie_header: &str) -> String {
48        let mut filtered_cookies = Vec::new();
49
50        for cookie in cookie_header.split(';') {
51            let cookie = cookie.trim();
52            if cookie.starts_with("sessionid=") {
53                // Replace session ID with test value
54                filtered_cookies.push("sessionid=test_session_id");
55            } else {
56                filtered_cookies.push(cookie);
57            }
58        }
59
60        filtered_cookies.join("; ")
61    }
62
63    /// Filter session cookies from a set-cookie header string
64    fn filter_set_cookie_session(&self, set_cookie_header: &str) -> String {
65        if set_cookie_header.starts_with("sessionid=") {
66            // Extract everything after the session value up to the first semicolon or end
67            if let Some(semicolon_pos) = set_cookie_header.find(';') {
68                let attributes = &set_cookie_header[semicolon_pos..];
69                format!("sessionid=test_session_id{attributes}")
70            } else {
71                "sessionid=test_session_id".to_string()
72            }
73        } else {
74            set_cookie_header.to_string()
75        }
76    }
77}
78
79/// Test placeholder for API keys in recorded cassettes
80pub const TEST_API_KEY: &str = "TEST_API_KEY";
81
82/// Filter that replaces the real Last.fm API key in request URLs with a test placeholder.
83#[derive(Debug)]
84pub struct LastFmApiKeyFilter;
85
86impl Filter for LastFmApiKeyFilter {
87    fn filter_request(&self, request: &mut SerializableRequest) {
88        if let Some(real_key) = std::env::var("LASTFM_EDIT_API_KEY")
89            .ok()
90            .filter(|k| !k.is_empty())
91        {
92            request.url = request.url.replace(&real_key, TEST_API_KEY);
93        }
94    }
95
96    fn filter_response(&self, _response: &mut SerializableResponse) {}
97}
98
99/// Create a Last.fm test filter chain that:
100/// - Keeps usernames and CSRF tokens intact (needed for proper request matching)
101/// - Filters passwords from request bodies with predictable test values
102/// - Filters session tokens with predictable test values
103/// - Filters API keys from request URLs with predictable test values
104pub fn create_lastfm_test_filter_chain() -> Result<FilterChain, regex::Error> {
105    let filter_chain = FilterChain::new()
106        .add_filter(Box::new(LastFmPasswordOnlyFilter))
107        .add_filter(Box::new(LastFmApiKeyFilter));
108    Ok(filter_chain)
109}