|
4 | 4 | //! information from incoming requests, particularly DMA (Designated Market Area) codes. |
5 | 5 |
|
6 | 6 | use fastly::geo::geo_lookup; |
7 | | -use fastly::Request; |
| 7 | +use fastly::{Request, Response}; |
8 | 8 |
|
9 | 9 | use crate::constants::{ |
10 | 10 | HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY, |
11 | | - HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, |
| 11 | + HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, HEADER_X_GEO_REGION, |
12 | 12 | }; |
13 | 13 |
|
14 | 14 | /// Geographic information extracted from a request. |
@@ -81,72 +81,163 @@ impl GeoInfo { |
81 | 81 | pub fn has_metro_code(&self) -> bool { |
82 | 82 | self.metro_code > 0 |
83 | 83 | } |
| 84 | + |
| 85 | + /// Sets geo information headers on the response. |
| 86 | + /// |
| 87 | + /// Adds `x-geo-city`, `x-geo-country`, `x-geo-continent`, `x-geo-coordinates`, |
| 88 | + /// `x-geo-metro-code`, `x-geo-region` (when available), and |
| 89 | + /// `x-geo-info-available: true` to the given response. |
| 90 | + pub fn set_response_headers(&self, response: &mut Response) { |
| 91 | + response.set_header(HEADER_X_GEO_CITY, &self.city); |
| 92 | + response.set_header(HEADER_X_GEO_COUNTRY, &self.country); |
| 93 | + response.set_header(HEADER_X_GEO_CONTINENT, &self.continent); |
| 94 | + response.set_header(HEADER_X_GEO_COORDINATES, self.coordinates_string()); |
| 95 | + if self.has_metro_code() { |
| 96 | + response.set_header(HEADER_X_GEO_METRO_CODE, self.metro_code.to_string()); |
| 97 | + } |
| 98 | + if let Some(ref region) = self.region { |
| 99 | + response.set_header(HEADER_X_GEO_REGION, region); |
| 100 | + } |
| 101 | + response.set_header(HEADER_X_GEO_INFO_AVAILABLE, "true"); |
| 102 | + } |
84 | 103 | } |
85 | 104 |
|
86 | | -/// Extracts the DMA (Designated Market Area) code from the request's geolocation data. |
87 | | -/// |
88 | | -/// This function: |
89 | | -/// 1. Checks if running in Fastly environment |
90 | | -/// 2. Performs geo lookup based on client IP |
91 | | -/// 3. Sets various geo headers on the request |
92 | | -/// 4. Returns the metro code (DMA) if available |
93 | | -/// |
94 | | -/// # Arguments |
95 | | -/// |
96 | | -/// * `req` - The request to extract DMA code from |
97 | | -/// |
98 | | -/// # Returns |
99 | | -/// |
100 | | -/// The DMA/metro code as a string if available, None otherwise |
101 | | -pub fn get_dma_code(req: &mut Request) -> Option<String> { |
102 | | - // Debug: Check if we're running in Fastly environment |
103 | | - log::info!("Fastly Environment Check:"); |
104 | | - log::info!( |
105 | | - " FASTLY_POP: {}", |
106 | | - std::env::var("FASTLY_POP").unwrap_or_else(|_| "not in Fastly".to_string()) |
107 | | - ); |
108 | | - log::info!( |
109 | | - " FASTLY_REGION: {}", |
110 | | - std::env::var("FASTLY_REGION").unwrap_or_else(|_| "not in Fastly".to_string()) |
111 | | - ); |
112 | | - |
113 | | - // Get detailed geo information using geo_lookup |
114 | | - if let Some(geo) = req.get_client_ip_addr().and_then(geo_lookup) { |
115 | | - log::info!("Geo Information Found:"); |
116 | | - |
117 | | - // Set all available geo information in headers |
118 | | - let city = geo.city(); |
119 | | - req.set_header(HEADER_X_GEO_CITY, city); |
120 | | - log::info!(" City: {}", city); |
121 | | - |
122 | | - let country = geo.country_code(); |
123 | | - req.set_header(HEADER_X_GEO_COUNTRY, country); |
124 | | - log::info!(" Country: {}", country); |
125 | | - |
126 | | - req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent())); |
127 | | - log::info!(" Continent: {:?}", geo.continent()); |
128 | | - |
129 | | - req.set_header( |
130 | | - HEADER_X_GEO_COORDINATES, |
131 | | - format!("{},{}", geo.latitude(), geo.longitude()), |
| 105 | +#[cfg(test)] |
| 106 | +mod tests { |
| 107 | + use super::*; |
| 108 | + use fastly::Response; |
| 109 | + |
| 110 | + fn sample_geo_info() -> GeoInfo { |
| 111 | + GeoInfo { |
| 112 | + city: "San Francisco".to_string(), |
| 113 | + country: "US".to_string(), |
| 114 | + continent: "NorthAmerica".to_string(), |
| 115 | + latitude: 37.7749, |
| 116 | + longitude: -122.4194, |
| 117 | + metro_code: 807, |
| 118 | + region: Some("CA".to_string()), |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + #[test] |
| 123 | + fn set_response_headers_sets_all_geo_headers() { |
| 124 | + let geo = sample_geo_info(); |
| 125 | + let mut response = Response::new(); |
| 126 | + |
| 127 | + geo.set_response_headers(&mut response); |
| 128 | + |
| 129 | + assert_eq!( |
| 130 | + response |
| 131 | + .get_header(HEADER_X_GEO_CITY) |
| 132 | + .expect("should have city header") |
| 133 | + .to_str() |
| 134 | + .expect("should be valid str"), |
| 135 | + "San Francisco", |
| 136 | + "should set city header" |
| 137 | + ); |
| 138 | + assert_eq!( |
| 139 | + response |
| 140 | + .get_header(HEADER_X_GEO_COUNTRY) |
| 141 | + .expect("should have country header") |
| 142 | + .to_str() |
| 143 | + .expect("should be valid str"), |
| 144 | + "US", |
| 145 | + "should set country header" |
| 146 | + ); |
| 147 | + assert_eq!( |
| 148 | + response |
| 149 | + .get_header(HEADER_X_GEO_CONTINENT) |
| 150 | + .expect("should have continent header") |
| 151 | + .to_str() |
| 152 | + .expect("should be valid str"), |
| 153 | + "NorthAmerica", |
| 154 | + "should set continent header" |
| 155 | + ); |
| 156 | + assert_eq!( |
| 157 | + response |
| 158 | + .get_header(HEADER_X_GEO_COORDINATES) |
| 159 | + .expect("should have coordinates header") |
| 160 | + .to_str() |
| 161 | + .expect("should be valid str"), |
| 162 | + "37.7749,-122.4194", |
| 163 | + "should set coordinates header" |
| 164 | + ); |
| 165 | + assert_eq!( |
| 166 | + response |
| 167 | + .get_header(HEADER_X_GEO_METRO_CODE) |
| 168 | + .expect("should have metro code header") |
| 169 | + .to_str() |
| 170 | + .expect("should be valid str"), |
| 171 | + "807", |
| 172 | + "should set metro code header" |
| 173 | + ); |
| 174 | + assert_eq!( |
| 175 | + response |
| 176 | + .get_header(HEADER_X_GEO_REGION) |
| 177 | + .expect("should have region header") |
| 178 | + .to_str() |
| 179 | + .expect("should be valid str"), |
| 180 | + "CA", |
| 181 | + "should set region header" |
| 182 | + ); |
| 183 | + assert_eq!( |
| 184 | + response |
| 185 | + .get_header(HEADER_X_GEO_INFO_AVAILABLE) |
| 186 | + .expect("should have info available header") |
| 187 | + .to_str() |
| 188 | + .expect("should be valid str"), |
| 189 | + "true", |
| 190 | + "should set geo info available to true" |
132 | 191 | ); |
133 | | - log::info!(" Location: ({}, {})", geo.latitude(), geo.longitude()); |
134 | | - |
135 | | - // Get and set the metro code (DMA) |
136 | | - let metro_code = geo.metro_code(); |
137 | | - req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string()); |
138 | | - log::info!("Found DMA/Metro code: {}", metro_code); |
139 | | - return Some(metro_code.to_string()); |
140 | | - } else { |
141 | | - log::info!("No geo information available for the request"); |
142 | | - req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false"); |
143 | 192 | } |
144 | 193 |
|
145 | | - // If no metro code is found, log all request headers for debugging |
146 | | - log::info!("No DMA/Metro code found. All request headers:"); |
147 | | - for (name, value) in req.get_headers() { |
148 | | - log::info!(" {}: {:?}", name, value); |
| 194 | + #[test] |
| 195 | + fn set_response_headers_omits_metro_code_when_zero() { |
| 196 | + let geo = GeoInfo { |
| 197 | + metro_code: 0, |
| 198 | + ..sample_geo_info() |
| 199 | + }; |
| 200 | + let mut response = Response::new(); |
| 201 | + |
| 202 | + geo.set_response_headers(&mut response); |
| 203 | + |
| 204 | + assert!( |
| 205 | + response.get_header(HEADER_X_GEO_METRO_CODE).is_none(), |
| 206 | + "should not set metro code header when metro_code is 0" |
| 207 | + ); |
| 208 | + assert!( |
| 209 | + response.get_header(HEADER_X_GEO_CITY).is_some(), |
| 210 | + "should still set city header" |
| 211 | + ); |
149 | 212 | } |
150 | 213 |
|
151 | | - None |
| 214 | + #[test] |
| 215 | + fn set_response_headers_omits_region_when_none() { |
| 216 | + let geo = GeoInfo { |
| 217 | + region: None, |
| 218 | + ..sample_geo_info() |
| 219 | + }; |
| 220 | + let mut response = Response::new(); |
| 221 | + |
| 222 | + geo.set_response_headers(&mut response); |
| 223 | + |
| 224 | + assert!( |
| 225 | + response.get_header(HEADER_X_GEO_REGION).is_none(), |
| 226 | + "should not set region header when region is None" |
| 227 | + ); |
| 228 | + // Other headers should still be present |
| 229 | + assert!( |
| 230 | + response.get_header(HEADER_X_GEO_CITY).is_some(), |
| 231 | + "should still set city header" |
| 232 | + ); |
| 233 | + assert_eq!( |
| 234 | + response |
| 235 | + .get_header(HEADER_X_GEO_INFO_AVAILABLE) |
| 236 | + .expect("should have info available header") |
| 237 | + .to_str() |
| 238 | + .expect("should be valid str"), |
| 239 | + "true", |
| 240 | + "should still set geo info available to true" |
| 241 | + ); |
| 242 | + } |
152 | 243 | } |
0 commit comments