mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-20 10:24:01 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4e49b8f6 | ||
|
|
95b1beffcf | ||
|
|
7db652db57 | ||
|
|
29d92e8e76 |
@@ -78,8 +78,19 @@ impl SendableHttpRequest {
|
||||
}
|
||||
|
||||
pub fn insert_header(&mut self, header: (String, String)) {
|
||||
if header.0.eq_ignore_ascii_case("cookie") {
|
||||
if let Some(existing) =
|
||||
self.headers.iter_mut().find(|h| h.0.eq_ignore_ascii_case("cookie"))
|
||||
{
|
||||
existing.1 = format!("{}; {}", existing.1, header.1);
|
||||
} else {
|
||||
self.headers.push(header);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(existing) =
|
||||
self.headers.iter_mut().find(|h| h.0.to_lowercase() == header.0.to_lowercase())
|
||||
self.headers.iter_mut().find(|h| h.0.eq_ignore_ascii_case(&header.0))
|
||||
{
|
||||
existing.1 = header.1;
|
||||
} else {
|
||||
@@ -494,7 +505,76 @@ mod tests {
|
||||
use bytes::Bytes;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use yaak_models::models::{HttpRequest, HttpUrlParameter};
|
||||
use yaak_models::models::{HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sendable_request_preserves_independent_cookie_enabled_states() {
|
||||
let request = HttpRequest {
|
||||
url: "https://example.com/api".to_string(),
|
||||
headers: vec![
|
||||
HttpRequestHeader {
|
||||
enabled: true,
|
||||
name: "Cookie".to_string(),
|
||||
value: "session=abc".to_string(),
|
||||
id: None,
|
||||
},
|
||||
HttpRequestHeader {
|
||||
enabled: false,
|
||||
name: "Cookie".to_string(),
|
||||
value: "debug=verbose".to_string(),
|
||||
id: None,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let sendable =
|
||||
SendableHttpRequest::from_http_request(&request, SendableHttpRequestOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sendable.headers, vec![("Cookie".to_string(), "session=abc".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_header_appends_authentication_cookie() {
|
||||
let mut request = SendableHttpRequest {
|
||||
headers: vec![
|
||||
("Cookie".to_string(), "session=abc".to_string()),
|
||||
("Cookie".to_string(), "theme=dark".to_string()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
request.insert_header(("cookie".to_string(), "api_key=secret".to_string()));
|
||||
|
||||
assert_eq!(
|
||||
request.headers,
|
||||
vec![
|
||||
("Cookie".to_string(), "session=abc; api_key=secret".to_string()),
|
||||
("Cookie".to_string(), "theme=dark".to_string()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sendable_request_preserves_serialized_path_delimiters() {
|
||||
let request = HttpRequest {
|
||||
url: "https://example.com/labels/.one%2Ftwo.three/matrix/;x=1%3Bspoof%3D2;y=2"
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let sendable =
|
||||
SendableHttpRequest::from_http_request(&request, SendableHttpRequestOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sendable.url,
|
||||
"https://example.com/labels/.one%2Ftwo.three/matrix/;x=1%3Bspoof%3D2;y=2",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_no_params() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::conflict_free_name;
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::connection_or_tx::ConnectionOrTx;
|
||||
use crate::error::Result;
|
||||
@@ -144,9 +144,7 @@ impl<'a> ClientDb<'a> {
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut folder.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
Ok(merge_headers(headers, folder.headers.clone()))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_folder(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{conflict_free_name, dedupe_headers};
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
@@ -110,9 +110,7 @@ impl<'a> ClientDb<'a> {
|
||||
metadata.append(&mut workspace_metadata);
|
||||
}
|
||||
|
||||
metadata.append(&mut grpc_request.metadata.clone());
|
||||
|
||||
Ok(dedupe_headers(metadata))
|
||||
Ok(merge_headers(metadata, grpc_request.metadata.clone()))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_grpc_request(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{conflict_free_name, dedupe_headers};
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
@@ -96,9 +96,7 @@ impl<'a> ClientDb<'a> {
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut http_request.headers.clone());
|
||||
|
||||
Ok(dedupe_headers(headers))
|
||||
Ok(merge_headers(headers, http_request.headers.clone()))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_http_request(
|
||||
@@ -172,3 +170,44 @@ impl<'a> ClientDb<'a> {
|
||||
Ok(children)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::init_in_memory;
|
||||
use crate::models::{HttpRequest, HttpRequestHeader};
|
||||
|
||||
#[test]
|
||||
fn request_resolution_preserves_duplicate_request_headers() {
|
||||
let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB");
|
||||
let db = query_manager.connect();
|
||||
let workspace = db.list_workspaces().expect("Failed to list workspaces").remove(0);
|
||||
let request = HttpRequest {
|
||||
workspace_id: workspace.id,
|
||||
headers: vec![
|
||||
HttpRequestHeader {
|
||||
name: "Cookie".to_string(),
|
||||
value: "required=1".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
HttpRequestHeader {
|
||||
enabled: false,
|
||||
name: "Cookie".to_string(),
|
||||
value: "optional=1".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let resolved = db.resolve_headers_for_http_request(&request).expect("Failed to resolve");
|
||||
let cookies = resolved
|
||||
.iter()
|
||||
.filter(|header| header.name.eq_ignore_ascii_case("cookie"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(cookies.len(), 2);
|
||||
assert_eq!(cookies[0].value, "required=1");
|
||||
assert_eq!(cookies[1].value, "optional=1");
|
||||
assert!(!cookies[1].enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,21 +28,59 @@ pub(crate) use duplicate_name::conflict_free_name;
|
||||
const MAX_HISTORY_ITEMS: usize = 20;
|
||||
|
||||
use crate::models::HttpRequestHeader;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Deduplicate headers by name (case-insensitive), keeping the latest (most specific) value.
|
||||
/// Preserves the order of first occurrence for each header name.
|
||||
pub(crate) fn dedupe_headers(headers: Vec<HttpRequestHeader>) -> Vec<HttpRequestHeader> {
|
||||
let mut index_by_name: HashMap<String, usize> = HashMap::new();
|
||||
let mut deduped: Vec<HttpRequestHeader> = Vec::new();
|
||||
for header in headers {
|
||||
let key = header.name.to_lowercase();
|
||||
if let Some(&idx) = index_by_name.get(&key) {
|
||||
deduped[idx] = header;
|
||||
} else {
|
||||
index_by_name.insert(key, deduped.len());
|
||||
deduped.push(header);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
/// Merge a more-specific header layer over its parent. Names in the child replace
|
||||
/// inherited values case-insensitively, while duplicates declared together in
|
||||
/// either layer remain independent entries.
|
||||
pub(crate) fn merge_headers(
|
||||
mut parent: Vec<HttpRequestHeader>,
|
||||
child: Vec<HttpRequestHeader>,
|
||||
) -> Vec<HttpRequestHeader> {
|
||||
let child_names = child.iter().map(|header| header.name.to_lowercase()).collect::<HashSet<_>>();
|
||||
parent.retain(|header| !child_names.contains(&header.name.to_lowercase()));
|
||||
parent.extend(child);
|
||||
parent
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::merge_headers;
|
||||
use crate::models::HttpRequestHeader;
|
||||
|
||||
fn header(name: &str, value: &str) -> HttpRequestHeader {
|
||||
HttpRequestHeader { name: name.to_string(), value: value.to_string(), ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_duplicate_headers_declared_in_one_layer() {
|
||||
let merged = merge_headers(
|
||||
vec![header("Cookie", "inherited=1")],
|
||||
vec![
|
||||
header("Cookie", "required=1"),
|
||||
header("cookie", "optional=1"),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merged.iter().map(|header| header.value.as_str()).collect::<Vec<_>>(),
|
||||
vec!["required=1", "optional=1"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_names_override_parent_names_without_affecting_other_headers() {
|
||||
let merged = merge_headers(
|
||||
vec![header("Accept", "*/*"), header("X-Parent", "kept")],
|
||||
vec![header("accept", "application/json")],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merged
|
||||
.iter()
|
||||
.map(|header| (header.name.as_str(), header.value.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("X-Parent", "kept"), ("accept", "application/json")],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{conflict_free_name, dedupe_headers};
|
||||
use super::{conflict_free_name, merge_headers};
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
@@ -103,13 +103,9 @@ impl<'a> ClientDb<'a> {
|
||||
&self,
|
||||
websocket_request: &WebsocketRequest,
|
||||
) -> Result<Vec<HttpRequestHeader>> {
|
||||
let workspace = self.get_workspace(&websocket_request.workspace_id)?;
|
||||
|
||||
// Resolved headers should be from furthest to closest ancestor, to override logically.
|
||||
let mut headers = Vec::new();
|
||||
|
||||
headers.append(&mut workspace.headers.clone());
|
||||
|
||||
if let Some(folder_id) = websocket_request.folder_id.clone() {
|
||||
let parent_folder = self.get_folder(&folder_id)?;
|
||||
let mut folder_headers = self.resolve_headers_for_folder(&parent_folder)?;
|
||||
@@ -120,9 +116,7 @@ impl<'a> ClientDb<'a> {
|
||||
headers.append(&mut workspace_headers);
|
||||
}
|
||||
|
||||
headers.append(&mut websocket_request.headers.clone());
|
||||
|
||||
Ok(dedupe_headers(headers))
|
||||
Ok(merge_headers(headers, websocket_request.headers.clone()))
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_websocket_request(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::merge_headers;
|
||||
use crate::blob_manager::BlobManager;
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
@@ -144,9 +145,7 @@ impl<'a> ClientDb<'a> {
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
||||
let mut headers = default_headers();
|
||||
headers.extend(workspace.headers.clone());
|
||||
headers
|
||||
merge_headers(default_headers(), workspace.headers.clone())
|
||||
}
|
||||
|
||||
pub fn resolve_settings_for_workspace(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -615,7 +615,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -644,7 +644,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -673,7 +673,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -702,7 +702,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -731,7 +731,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -760,7 +760,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":anything",
|
||||
"value": "anything",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -820,12 +820,12 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":user",
|
||||
"value": "user",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":passwd",
|
||||
"value": "passwd",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -1071,12 +1071,12 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":name",
|
||||
"value": "name",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":value",
|
||||
"value": "value",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -1342,17 +1342,17 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":qop",
|
||||
"value": "qop",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":user",
|
||||
"value": "user",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":passwd",
|
||||
"value": "passwd",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -1385,17 +1385,17 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":qop",
|
||||
"value": "qop",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":user",
|
||||
"value": "user",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":passwd",
|
||||
"value": "passwd",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
@@ -1435,17 +1435,17 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":qop",
|
||||
"value": "qop",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":user",
|
||||
"value": "user",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":passwd",
|
||||
"value": "passwd",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
@@ -1565,7 +1565,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":etag",
|
||||
"value": "etag",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -1656,12 +1656,12 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":user",
|
||||
"value": "user",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":passwd",
|
||||
"value": "passwd",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2295,7 +2295,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2328,7 +2328,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2361,7 +2361,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2394,7 +2394,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2427,7 +2427,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2460,7 +2460,7 @@ Responses:
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":codes",
|
||||
"value": "codes",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
@@ -2755,7 +2755,7 @@ exports[`importer-openapi > Snapshots real-world fixture xkcd.yaml 1`] = `
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "http://xkcd.com",
|
||||
"value": "http://xkcd.com/",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user