mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-20 02:13:58 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bde035885 | ||
|
|
2e14df119e | ||
|
|
9644594df6 | ||
|
|
67ead41c3e | ||
|
|
3fa4eb1591 | ||
|
|
615751ffd1 | ||
|
|
c4f96f3f11 | ||
|
|
71c217d3f0 | ||
|
|
d89831a84d | ||
|
|
3332ae263f | ||
|
|
df1fd864b2 | ||
|
|
a2d54ca774 | ||
|
|
36fec8b005 |
@@ -0,0 +1,32 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, test, vi } from "vite-plus/test";
|
||||
import { CsvViewerInner } from "./CsvViewer";
|
||||
|
||||
vi.mock("@yaakapp-internal/ui", () => ({
|
||||
Table: ({ children }: { children: ReactNode }) => <table>{children}</table>,
|
||||
TableBody: ({ children }: { children: ReactNode }) => <tbody>{children}</tbody>,
|
||||
TableCell: ({ children }: { children: ReactNode }) => <td>{children}</td>,
|
||||
TableHead: ({ children }: { children: ReactNode }) => <thead>{children}</thead>,
|
||||
TableHeaderCell: ({ children }: { children: ReactNode }) => <th>{children}</th>,
|
||||
TableRow: ({ children }: { children: ReactNode }) => <tr>{children}</tr>,
|
||||
}));
|
||||
|
||||
describe("CsvViewer", () => {
|
||||
test("renders columns that extend beyond the first row", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<CsvViewerInner
|
||||
text={[
|
||||
"startDate,2026-02-03T00:00-03:00",
|
||||
"endDate,2026-02-03T23:59:59-03:00",
|
||||
"id,Fecha de inicio,Nombre,Estado,Perfil de puesto,ID de sucursal,Sucursal,Fecha de fin,ID de usuario",
|
||||
"391118210,2026-02-03 12:58:55,atencion1,Disponible,ATD,3549,sucursal,2026-02-03 12:59:08,42041",
|
||||
].join("\n")}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("ID de usuario");
|
||||
expect(markup).toContain("42041");
|
||||
expect(markup.match(/<td>/g)).toHaveLength(20);
|
||||
});
|
||||
});
|
||||
@@ -26,27 +26,33 @@ export function CsvViewer({ text, className }: Props) {
|
||||
export function CsvViewerInner({ text, className }: { text: string | null; className?: string }) {
|
||||
const parsed = useMemo(() => {
|
||||
if (text == null) return null;
|
||||
return Papa.parse<Record<string, string>>(text, { header: true, skipEmptyLines: true });
|
||||
return Papa.parse<string[]>(text, { skipEmptyLines: true });
|
||||
}, [text]);
|
||||
|
||||
if (parsed === null) return null;
|
||||
|
||||
const header = parsed.data[0] ?? [];
|
||||
const rows = parsed.data.slice(1);
|
||||
const columnCount = parsed.data.reduce((count, row) => Math.max(count, row.length), 0);
|
||||
const columnIndexes = Array.from({ length: columnCount }, (_, index) => index);
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-full">
|
||||
<Table className={classNames(className, "text-sm")}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{parsed.meta.fields?.map((field) => (
|
||||
<TableHeaderCell key={field}>{field}</TableHeaderCell>
|
||||
{columnIndexes.map((columnIndex) => (
|
||||
<TableHeaderCell key={columnIndex}>{header[columnIndex] ?? ""}</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{parsed.data.map((row, i) => (
|
||||
{rows.map((row, i) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
<TableRow key={i}>
|
||||
{parsed.meta.fields?.map((key) => (
|
||||
<TableCell key={key}>{row[key] ?? ""}</TableCell>
|
||||
{row.map((cell, columnIndex) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
<TableCell key={columnIndex}>{cell}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -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
@@ -11,6 +11,16 @@ exports[`importer-openapi > Snapshots real-world fixture apis-guru.yaml 1`] = `
|
||||
"parentId": null,
|
||||
"parentModel": "workspace",
|
||||
"sortPriority": 0,
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_1",
|
||||
"model": "environment",
|
||||
"name": "Server 1",
|
||||
"parentId": null,
|
||||
"parentModel": "environment",
|
||||
"sortPriority": 9,
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
@@ -153,18 +163,13 @@ Responses:
|
||||
"model": "http_request",
|
||||
"name": "Retrieve one version of a particular API",
|
||||
"sortPriority": 5,
|
||||
"url": "\${[baseUrl]}/specs/:provider/:api.json",
|
||||
"url": "\${[baseUrl]}/specs/:provider/2.1.0.json",
|
||||
"urlParameters": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":provider",
|
||||
"value": "apis.guru",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":api",
|
||||
"value": "2.1.0",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
@@ -197,7 +202,7 @@ Responses:
|
||||
"model": "http_request",
|
||||
"name": "Retrieve one version of a particular API with a serviceName.",
|
||||
"sortPriority": 6,
|
||||
"url": "\${[baseUrl]}/specs/:provider/:service/:api.json",
|
||||
"url": "\${[baseUrl]}/specs/:provider/:service/2.1.0.json",
|
||||
"urlParameters": [
|
||||
{
|
||||
"enabled": true,
|
||||
@@ -209,11 +214,6 @@ Responses:
|
||||
"name": ":service",
|
||||
"value": "graph",
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":api",
|
||||
"value": "2.1.0",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
@@ -246,14 +246,8 @@ Responses:
|
||||
"model": "http_request",
|
||||
"name": "List all APIs for a particular provider",
|
||||
"sortPriority": 7,
|
||||
"url": "\${[baseUrl]}/:provider.json",
|
||||
"urlParameters": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": ":provider",
|
||||
"value": "apis.guru",
|
||||
},
|
||||
],
|
||||
"url": "\${[baseUrl]}/apis.guru.json",
|
||||
"urlParameters": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
@@ -326,6 +320,16 @@ exports[`importer-openapi > Snapshots real-world fixture httpbin.yaml 1`] = `
|
||||
"parentId": null,
|
||||
"parentModel": "workspace",
|
||||
"sortPriority": 0,
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_1",
|
||||
"model": "environment",
|
||||
"name": "Server 1",
|
||||
"parentId": null,
|
||||
"parentModel": "environment",
|
||||
"sortPriority": 90,
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
@@ -840,13 +844,7 @@ Responses:
|
||||
- 200: Sucessful authentication.
|
||||
- 401: Unsuccessful authentication.",
|
||||
"folderId": "GENERATE_ID::FOLDER_1",
|
||||
"headers": [
|
||||
{
|
||||
"enabled": false,
|
||||
"name": "Authorization",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"headers": [],
|
||||
"id": "GENERATE_ID::HTTP_REQUEST_15",
|
||||
"method": "GET",
|
||||
"model": "http_request",
|
||||
@@ -2612,11 +2610,44 @@ exports[`importer-openapi > Snapshots real-world fixture nasa-apod.yaml 1`] = `
|
||||
"parentId": null,
|
||||
"parentModel": "workspace",
|
||||
"sortPriority": 0,
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_1",
|
||||
"model": "environment",
|
||||
"name": "Server 1",
|
||||
"parentId": null,
|
||||
"parentModel": "environment",
|
||||
"sortPriority": 3,
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://api.nasa.gov/planetary",
|
||||
},
|
||||
{
|
||||
"name": "auth_api_key_key",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_2",
|
||||
"model": "environment",
|
||||
"name": "Server 2",
|
||||
"parentId": null,
|
||||
"parentModel": "environment",
|
||||
"sortPriority": 4,
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "http://api.nasa.gov/planetary",
|
||||
},
|
||||
{
|
||||
"name": "auth_api_key_key",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
@@ -2640,7 +2671,7 @@ Here's a link: https://example.com",
|
||||
"authentication": {
|
||||
"key": "api_key",
|
||||
"location": "query",
|
||||
"value": "",
|
||||
"value": "\${[auth_api_key_key]}",
|
||||
},
|
||||
"authenticationType": "apikey",
|
||||
"body": {},
|
||||
@@ -2711,6 +2742,16 @@ exports[`importer-openapi > Snapshots real-world fixture xkcd.yaml 1`] = `
|
||||
"parentId": null,
|
||||
"parentModel": "workspace",
|
||||
"sortPriority": 0,
|
||||
"variables": [],
|
||||
"workspaceId": "GENERATE_ID::WORKSPACE_0",
|
||||
},
|
||||
{
|
||||
"id": "GENERATE_ID::ENVIRONMENT_1",
|
||||
"model": "environment",
|
||||
"name": "Server 1",
|
||||
"parentId": null,
|
||||
"parentModel": "environment",
|
||||
"sortPriority": 3,
|
||||
"variables": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
|
||||
@@ -13,6 +13,36 @@ describe("importer-openapi", () => {
|
||||
.readdirSync(realWorldFixturesPath)
|
||||
.filter((fixture) => fixture.endsWith(".yaml"));
|
||||
|
||||
test("Imports OpenAPI 3.2 QUERY and additional operations", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.2.0",
|
||||
info: { title: "OpenAPI 3.2 Operations", version: "1.0.0" },
|
||||
paths: {
|
||||
"/resources": {
|
||||
query: { summary: "Query resources", responses: {} },
|
||||
additionalOperations: {
|
||||
COPY: { summary: "Copy resources", responses: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
method: "QUERY",
|
||||
name: "Query resources",
|
||||
url: "${[baseUrl]}/resources",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
method: "COPY",
|
||||
name: "Copy resources",
|
||||
url: "${[baseUrl]}/resources",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Maps operation description to request description", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
@@ -120,7 +150,15 @@ describe("importer-openapi", () => {
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }],
|
||||
variables: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
parentModel: "environment",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "https://api.example.com/v1" },
|
||||
{ name: "auth_token_auth_token", value: "" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
@@ -129,7 +167,7 @@ describe("importer-openapi", () => {
|
||||
method: "POST",
|
||||
url: "${[baseUrl]}/accounts/:accountId/members",
|
||||
authenticationType: "bearer",
|
||||
authentication: { token: "", prefix: "Bearer" },
|
||||
authentication: { token: "${[auth_token_auth_token]}", prefix: "Bearer" },
|
||||
bodyType: "application/json",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
@@ -219,6 +257,11 @@ describe("importer-openapi", () => {
|
||||
);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
variables: [{ name: "baseUrl", value: "https://api.example.com/client/v4" }],
|
||||
}),
|
||||
]);
|
||||
@@ -229,6 +272,31 @@ describe("importer-openapi", () => {
|
||||
expect(imported).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Creates an editable baseUrl variable when OpenAPI omits servers", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Serverless OpenAPI Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/api/widgets": { get: { responses: {} } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Default",
|
||||
parentModel: "environment",
|
||||
variables: [{ name: "baseUrl", value: "" }],
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests[0]?.url).toBe("${[baseUrl]}/api/widgets");
|
||||
});
|
||||
|
||||
test("Prefers operation and path servers over the spec base URL", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
@@ -251,8 +319,119 @@ describe("importer-openapi", () => {
|
||||
|
||||
expect(imported?.resources.httpRequests.map((r) => r.url)).toEqual([
|
||||
"${[baseUrl]}/root",
|
||||
"https://path.example.com/path-level",
|
||||
"https://operation.example.com/operation-level",
|
||||
"${[serverUrl]}/path-level",
|
||||
"${[serverUrl2]}/operation-level",
|
||||
]);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [
|
||||
{ name: "serverUrl", value: "https://path.example.com" },
|
||||
{ name: "serverUrl2", value: "https://operation.example.com" },
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
variables: [{ name: "baseUrl", value: "https://root.example.com" }],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Creates selectable environments for multiple OpenAPI servers", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Server Environments Test", version: "1.0.0" },
|
||||
servers: [
|
||||
{ url: "https://api.example.com/v1", description: "Production" },
|
||||
{ url: "https://sandbox.example.com/v1", description: "Sandbox" },
|
||||
],
|
||||
paths: {
|
||||
"/oauth": { get: { security: [{ oauth: [] }], responses: {} } },
|
||||
"/api-key": { get: { security: [{ apiKey: [] }], responses: {} } },
|
||||
"/fixed": {
|
||||
servers: [{ url: "https://fixed.example.com" }],
|
||||
get: { responses: {} },
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
oauth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
authorizationCode: {
|
||||
authorizationUrl: "/oauth/authorize",
|
||||
tokenUrl: "oauth/token",
|
||||
scopes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
apiKey: { type: "apiKey", in: "header", name: "X-API-Key" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "serverUrl", value: "https://fixed.example.com" }],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Production",
|
||||
parentModel: "environment",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "https://api.example.com/v1" },
|
||||
{ name: "oauth_client_id", value: "" },
|
||||
{ name: "oauth_client_secret", value: "" },
|
||||
{ name: "baseUrlOrigin", value: "https://api.example.com" },
|
||||
{ name: "auth_api_key_key", value: "" },
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Sandbox",
|
||||
parentModel: "environment",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "https://sandbox.example.com/v1" },
|
||||
{ name: "oauth_client_id", value: "" },
|
||||
{ name: "oauth_client_secret", value: "" },
|
||||
{ name: "baseUrlOrigin", value: "https://sandbox.example.com" },
|
||||
{ name: "auth_api_key_key", value: "" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests[0]?.authentication).toEqual(
|
||||
expect.objectContaining({
|
||||
authorizationUrl: "${[baseUrlOrigin]}/oauth/authorize",
|
||||
accessTokenUrl: "${[baseUrl]}/oauth/token",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Creates variables for path servers without a top-level server", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Path Server Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/items": {
|
||||
servers: [{ url: "https://path.example.com" }],
|
||||
get: { responses: {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.url).toBe("${[serverUrl]}/items");
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [{ name: "serverUrl", value: "https://path.example.com" }],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Default",
|
||||
variables: [{ name: "baseUrl", value: "" }],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -289,8 +468,8 @@ describe("importer-openapi", () => {
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "client_credentials",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
clientId: "${[oauth_oauth_client_id]}",
|
||||
clientSecret: "${[oauth_oauth_client_secret]}",
|
||||
headerPrefix: "Bearer",
|
||||
scope: "read write",
|
||||
accessTokenUrl: "https://example.com/token",
|
||||
@@ -302,12 +481,125 @@ describe("importer-openapi", () => {
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "implicit",
|
||||
clientId: "",
|
||||
clientId: "${[oauth_implicitOauth_client_id]}",
|
||||
headerPrefix: "Bearer",
|
||||
authorizationUrl: "https://example.com/authorize",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.environments[0]?.variables).toEqual([]);
|
||||
expect(imported?.resources.environments[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "Default",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "" },
|
||||
{ name: "oauth_oauth_client_id", value: "" },
|
||||
{ name: "oauth_oauth_client_secret", value: "" },
|
||||
{ name: "oauth_implicitOauth_client_id", value: "" },
|
||||
{ name: "oauth_implicitOauth_client_secret", value: "" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Uses server environment variables for OAuth2 client credentials", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "OAuth Environment Test", version: "1.0.0" },
|
||||
servers: [{ url: "https://api.example.com" }],
|
||||
paths: {
|
||||
"/users": {
|
||||
get: { security: [{ oauth: ["read"] }], responses: {} },
|
||||
post: { security: [{ oauth: ["write"] }], responses: {} },
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
oauth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
authorizationCode: {
|
||||
authorizationUrl: "/oauth/authorize",
|
||||
tokenUrl: "/oauth/token",
|
||||
scopes: { read: "Read users", write: "Write users" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({ name: "Global Variables", variables: [] }),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "https://api.example.com" },
|
||||
{ name: "oauth_client_id", value: "" },
|
||||
{ name: "oauth_client_secret", value: "" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
expect(imported?.resources.httpRequests.map((request) => request.authentication)).toEqual([
|
||||
expect.objectContaining({
|
||||
clientId: "${[oauth_client_id]}",
|
||||
clientSecret: "${[oauth_client_secret]}",
|
||||
authorizationUrl: "https://api.example.com/oauth/authorize",
|
||||
accessTokenUrl: "https://api.example.com/oauth/token",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
clientId: "${[oauth_client_id]}",
|
||||
clientSecret: "${[oauth_client_secret]}",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Uses the server environment origin for OAuth endpoints with a path-only API base", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Path-only OAuth Test", version: "1.0.0" },
|
||||
servers: [{ url: "/api/v1" }],
|
||||
paths: {
|
||||
"/users": { get: { security: [{ oauth: [] }], responses: {} } },
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
oauth: {
|
||||
type: "oauth2",
|
||||
flows: {
|
||||
authorizationCode: {
|
||||
authorizationUrl: "oauth/authorize",
|
||||
tokenUrl: "/oauth/token",
|
||||
scopes: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.authentication).toEqual(
|
||||
expect.objectContaining({
|
||||
authorizationUrl: "${[baseUrlOrigin]}/api/v1/oauth/authorize",
|
||||
accessTokenUrl: "${[baseUrlOrigin]}/oauth/token",
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({ name: "Global Variables", variables: [] }),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "/api/v1" },
|
||||
{ name: "oauth_client_id", value: "" },
|
||||
{ name: "oauth_client_secret", value: "" },
|
||||
{ name: "baseUrlOrigin", value: "" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports Swagger 2 OAuth2 flows and produces", async () => {
|
||||
@@ -335,8 +627,8 @@ describe("importer-openapi", () => {
|
||||
authenticationType: "oauth2",
|
||||
authentication: {
|
||||
grantType: "authorization_code",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
clientId: "${[oauth_client_id]}",
|
||||
clientSecret: "${[oauth_client_secret]}",
|
||||
headerPrefix: "Bearer",
|
||||
scope: "admin",
|
||||
authorizationUrl: "https://example.com/authorize",
|
||||
@@ -474,6 +766,194 @@ describe("importer-openapi", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports cookie and content-based parameters", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Parameter Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/items": {
|
||||
get: {
|
||||
parameters: [
|
||||
{
|
||||
name: "session",
|
||||
in: "cookie",
|
||||
required: true,
|
||||
schema: { type: "string", example: "abc" },
|
||||
},
|
||||
{
|
||||
name: "debug",
|
||||
in: "cookie",
|
||||
schema: { type: "string", example: "verbose" },
|
||||
},
|
||||
{
|
||||
name: "X-Filter",
|
||||
in: "header",
|
||||
required: true,
|
||||
content: { "text/plain": { example: "active" } },
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.headers).toEqual([
|
||||
{ enabled: true, name: "X-Filter", value: "active" },
|
||||
{ enabled: true, name: "Cookie", value: "session=abc" },
|
||||
{ enabled: false, name: "Cookie", value: "debug=verbose" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Preserves parameter cookies alongside cookie API-key authentication", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Authenticated Cookie Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/items": {
|
||||
get: {
|
||||
security: [{ basicAuth: [], cookieKey: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "session",
|
||||
in: "cookie",
|
||||
required: true,
|
||||
schema: { type: "string", example: "abc" },
|
||||
},
|
||||
{
|
||||
name: "debug",
|
||||
in: "cookie",
|
||||
schema: { type: "string", example: "verbose" },
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
basicAuth: { type: "http", scheme: "basic" },
|
||||
cookieKey: { type: "apiKey", in: "cookie", name: "api_key" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "basic",
|
||||
headers: [
|
||||
{ enabled: true, name: "Cookie", value: "api_key=${[auth_cookie_key_key]}" },
|
||||
{ enabled: true, name: "Cookie", value: "session=abc" },
|
||||
{ enabled: false, name: "Cookie", value: "debug=verbose" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Serializes structured query parameters according to style and explode", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Serialization Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/items": {
|
||||
get: {
|
||||
parameters: [
|
||||
{
|
||||
name: "filter",
|
||||
in: "query",
|
||||
required: true,
|
||||
style: "deepObject",
|
||||
explode: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
role: { example: "admin" },
|
||||
active: { example: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
in: "query",
|
||||
style: "form",
|
||||
explode: true,
|
||||
schema: { type: "array", example: ["one", "two"] },
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
|
||||
{ enabled: true, name: "filter[role]", value: "admin" },
|
||||
{ enabled: true, name: "filter[active]", value: "true" },
|
||||
{ enabled: false, name: "tags", value: "one" },
|
||||
{ enabled: false, name: "tags", value: "two" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Emits executable label and matrix path serializations", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.0.4",
|
||||
info: { title: "Path Serialization Test", version: "1.0.0" },
|
||||
paths: {
|
||||
"/labels/{labels}/matrix/{coordinates}/scalar/{color}/report.{format}": {
|
||||
get: {
|
||||
parameters: [
|
||||
{
|
||||
name: "labels",
|
||||
in: "path",
|
||||
required: true,
|
||||
style: "label",
|
||||
explode: true,
|
||||
schema: { type: "array", example: ["one/two", "three"] },
|
||||
},
|
||||
{
|
||||
name: "coordinates",
|
||||
in: "path",
|
||||
required: true,
|
||||
style: "matrix",
|
||||
explode: true,
|
||||
schema: { type: "object", example: { x: "1;spoof=2", y: 2 } },
|
||||
},
|
||||
{
|
||||
name: "format",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string", example: "json/evil" },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
in: "path",
|
||||
required: true,
|
||||
style: "label",
|
||||
schema: { type: "string", example: "blue" },
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
url: "${[baseUrl]}/labels/.one%2Ftwo.three/matrix/;x=1%3Bspoof%3D2;y=2/scalar/.blue/report.json%2Fevil",
|
||||
urlParameters: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("Prefers operation-level consumes for Swagger bodies", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
@@ -523,16 +1003,176 @@ describe("importer-openapi", () => {
|
||||
expect(imported?.resources.httpRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "basic",
|
||||
authentication: { username: "", password: "" },
|
||||
authentication: {
|
||||
username: "${[auth_basic_auth_username]}",
|
||||
password: "${[auth_basic_auth_password]}",
|
||||
},
|
||||
}),
|
||||
);
|
||||
// The auth plugin has no cookie location, so it becomes the Cookie header
|
||||
expect(imported?.resources.httpRequests[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
authenticationType: "apikey",
|
||||
authentication: { location: "header", key: "Cookie", value: "session=" },
|
||||
authentication: {
|
||||
location: "header",
|
||||
key: "Cookie",
|
||||
value: "session=${[auth_cookie_key_key]}",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.environments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "Global Variables",
|
||||
variables: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "Server 1",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "https://example.com/" },
|
||||
{ name: "auth_basic_auth_username", value: "" },
|
||||
{ name: "auth_basic_auth_password", value: "" },
|
||||
{ name: "auth_cookie_key_key", value: "" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Preserves anonymous security alternatives and explicit auth overrides", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.1.0",
|
||||
info: { title: "Optional Auth", version: "1.0.0" },
|
||||
security: [{ bearerAuth: [] }],
|
||||
paths: {
|
||||
"/optional-auth-first": {
|
||||
get: { security: [{ bearerAuth: [] }, {}], responses: {} },
|
||||
},
|
||||
"/optional-anonymous-first": {
|
||||
get: { security: [{}, { bearerAuth: [] }], responses: {} },
|
||||
},
|
||||
"/public": { get: { security: [], responses: {} } },
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: { type: "http", scheme: "bearer" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports AND security requirements without dropping API keys", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.1.0",
|
||||
info: { title: "Combined Auth", version: "1.0.0" },
|
||||
paths: {
|
||||
"/combined": {
|
||||
get: {
|
||||
security: [{ bearerAuth: [], tenantKey: [], queryKey: [] }],
|
||||
parameters: [
|
||||
{
|
||||
in: "header",
|
||||
name: "X-Tenant-Key",
|
||||
example: "operation-value-must-not-replace-auth",
|
||||
},
|
||||
],
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: { type: "http", scheme: "bearer" },
|
||||
tenantKey: { type: "apiKey", in: "header", name: "X-Tenant-Key" },
|
||||
queryKey: { type: "apiKey", in: "query", name: "api_key" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
authenticationType: "bearer",
|
||||
authentication: { token: "${[auth_bearer_auth_token]}", prefix: "Bearer" },
|
||||
headers: [{ enabled: true, name: "X-Tenant-Key", value: "${[auth_tenant_key_key]}" }],
|
||||
urlParameters: [{ enabled: true, name: "api_key", value: "${[auth_query_key_key]}" }],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Keeps distinct credentials for security scheme names that normalize alike", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.1.0",
|
||||
info: { title: "Auth Variable Names", version: "1.0.0" },
|
||||
paths: {
|
||||
"/hyphen": { get: { security: [{ "api-key": [] }], responses: {} } },
|
||||
"/underscore": { get: { security: [{ api_key: [] }], responses: {} } },
|
||||
"/hyphen-again": { get: { security: [{ "api-key": [] }], responses: {} } },
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
"api-key": { type: "apiKey", in: "header", name: "X-Hyphen-Key" },
|
||||
api_key: { type: "apiKey", in: "header", name: "X-Underscore-Key" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.environments[0]?.variables).toEqual([]);
|
||||
expect(imported?.resources.environments[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "Default",
|
||||
variables: [
|
||||
{ name: "baseUrl", value: "" },
|
||||
{ name: "auth_api_key_key", value: "" },
|
||||
{ name: "auth_api_key_key_2", value: "" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
authentication: expect.objectContaining({ value: "${[auth_api_key_key]}" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
authentication: expect.objectContaining({ value: "${[auth_api_key_key_2]}" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
authentication: expect.objectContaining({ value: "${[auth_api_key_key]}" }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Imports OpenID Connect as bearer authentication", async () => {
|
||||
const imported = await convertOpenApi(
|
||||
JSON.stringify({
|
||||
openapi: "3.1.0",
|
||||
info: { title: "OpenID Connect", version: "1.0.0" },
|
||||
paths: { "/me": { get: { security: [{ oidc: [] }], responses: {} } } },
|
||||
components: {
|
||||
securitySchemes: {
|
||||
oidc: {
|
||||
type: "openIdConnect",
|
||||
openIdConnectUrl: "https://accounts.example.com/.well-known/openid-configuration",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported?.resources.httpRequests).toEqual([
|
||||
expect.objectContaining({
|
||||
authenticationType: "bearer",
|
||||
authentication: { token: "${[auth_oidc_token]}", prefix: "Bearer" },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Reports references that point outside the document", async () => {
|
||||
|
||||
Reference in New Issue
Block a user