Skip to content

Commit 078c51e

Browse files
committed
feat: add tests
1 parent ff04fd0 commit 078c51e

File tree

3 files changed

+324
-1
lines changed

3 files changed

+324
-1
lines changed

src/external_api_keys.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,78 @@ impl Interceptor for ExternalApiKeysInterceptor {
2929
Ok(request)
3030
}
3131
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use std::collections::HashMap;
36+
37+
use tonic::service::Interceptor;
38+
use tonic::Request;
39+
40+
use super::ExternalApiKeysInterceptor;
41+
42+
#[test]
43+
fn inserts_external_api_keys_into_metadata_headers() {
44+
let api_keys = HashMap::from([
45+
("openai-api-key".to_string(), "openai-secret".to_string()),
46+
("cohere-api-key".to_string(), "cohere-secret".to_string()),
47+
]);
48+
49+
let mut interceptor = ExternalApiKeysInterceptor::new(Some(api_keys));
50+
let request = interceptor
51+
.call(Request::new(()))
52+
.expect("interceptor must accept valid external API keys");
53+
54+
let openai = request
55+
.metadata()
56+
.get("openai-api-key")
57+
.expect("missing openai-api-key header")
58+
.to_str()
59+
.expect("openai-api-key header must be valid ASCII");
60+
let cohere = request
61+
.metadata()
62+
.get("cohere-api-key")
63+
.expect("missing cohere-api-key header")
64+
.to_str()
65+
.expect("cohere-api-key header must be valid ASCII");
66+
67+
assert_eq!(openai, "openai-secret");
68+
assert_eq!(cohere, "cohere-secret");
69+
}
70+
71+
#[test]
72+
fn keeps_request_unchanged_when_external_keys_are_missing() {
73+
let mut interceptor = ExternalApiKeysInterceptor::new(None);
74+
let request = interceptor
75+
.call(Request::new(()))
76+
.expect("interceptor must accept empty external API key config");
77+
78+
assert!(request.metadata().is_empty());
79+
}
80+
81+
#[test]
82+
fn returns_invalid_argument_for_invalid_metadata_key() {
83+
let api_keys = HashMap::from([("openai api key".to_string(), "secret".to_string())]);
84+
85+
let mut interceptor = ExternalApiKeysInterceptor::new(Some(api_keys));
86+
let error = interceptor
87+
.call(Request::new(()))
88+
.expect_err("interceptor must reject invalid metadata keys");
89+
90+
assert_eq!(error.code(), tonic::Code::InvalidArgument);
91+
assert!(error.message().contains("Invalid metadata key"));
92+
}
93+
94+
#[test]
95+
fn returns_invalid_argument_for_invalid_metadata_value() {
96+
let api_keys = HashMap::from([("openai-api-key".to_string(), "bad\nkey".to_string())]);
97+
98+
let mut interceptor = ExternalApiKeysInterceptor::new(Some(api_keys));
99+
let error = interceptor
100+
.call(Request::new(()))
101+
.expect_err("interceptor must reject invalid metadata values");
102+
103+
assert_eq!(error.code(), tonic::Code::InvalidArgument);
104+
assert!(error.message().contains("Invalid metadata value"));
105+
}
106+
}

tests/snippet_tests/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod test_delete_snapshot;
2020
mod test_delete_vectors;
2121
mod test_discover_batch_points;
2222
mod test_discover_points;
23+
mod test_external_api_keys;
2324
mod test_facets;
2425
mod test_get_collection;
2526
mod test_get_collection_aliases;
@@ -56,4 +57,4 @@ mod test_upsert_image;
5657
mod test_upsert_points;
5758
mod test_upsert_points_fallback_shard_key;
5859
mod test_upsert_points_insert_only;
59-
mod test_upsert_points_with_condition;
60+
mod test_upsert_points_with_condition;
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
use std::collections::HashMap;
2+
3+
use qdrant_client::qdrant::{
4+
CreateCollectionBuilder, Distance, Document, PointStruct, Query, QueryPointsBuilder,
5+
UpsertPointsBuilder, VectorParamsBuilder,
6+
};
7+
use qdrant_client::{Payload, Qdrant};
8+
use serde_json::json;
9+
10+
const PROXY_URL: &str = "http://localhost:6334";
11+
const UPSERT_COLLECTION_NAME: &str = "test_external_api_keys_upsert";
12+
const QUERY_COLLECTION_NAME: &str = "test_external_api_keys_query";
13+
const DUAL_OPENAI_COLLECTION_NAME: &str = "test_external_api_keys_dual_openai";
14+
const DUAL_COHERE_COLLECTION_NAME: &str = "test_external_api_keys_dual_cohere";
15+
const OPENAI_MODEL: &str = "openai/text-embedding-3-small";
16+
const OPENAI_VECTOR_SIZE: u64 = 1536;
17+
const COHERE_MODEL: &str = "cohere/embed-english-v3.0";
18+
const COHERE_VECTOR_SIZE: u64 = 1024;
19+
20+
fn create_client_with_external_keys(external_api_keys: HashMap<String, String>) -> Qdrant {
21+
Qdrant::from_url(PROXY_URL)
22+
.skip_compatibility_check()
23+
.api_key("1234")
24+
.external_api_keys(external_api_keys)
25+
.timeout(30u64)
26+
.build()
27+
.expect("Failed to build client")
28+
}
29+
30+
async fn setup_collection(client: &Qdrant, collection_name: &str, vector_size: u64) {
31+
let _ = client.delete_collection(collection_name).await;
32+
33+
client
34+
.create_collection(
35+
CreateCollectionBuilder::new(collection_name)
36+
.vectors_config(VectorParamsBuilder::new(vector_size, Distance::Cosine)),
37+
)
38+
.await
39+
.expect("Failed to create collection");
40+
}
41+
42+
fn cohere_document(text: impl Into<String>, input_type: &'static str) -> Document {
43+
Document {
44+
text: text.into(),
45+
model: COHERE_MODEL.to_string(),
46+
options: HashMap::from([("input_type".to_string(), input_type.into())]),
47+
}
48+
}
49+
50+
#[tokio::test]
51+
async fn test_upsert_with_external_api_keys() {
52+
let Some(openai_api_key) = std::env::var("OPENAI_API_KEY").ok() else {
53+
eprintln!("Skipping test_upsert_with_external_api_keys: OPENAI_API_KEY is not set");
54+
return;
55+
};
56+
let collection_name = UPSERT_COLLECTION_NAME;
57+
let client = create_client_with_external_keys(HashMap::from([(
58+
"openai-api-key".to_string(),
59+
openai_api_key,
60+
)]));
61+
setup_collection(&client, collection_name, OPENAI_VECTOR_SIZE).await;
62+
63+
let doc = Document::new("Qdrant is a vector search engine", OPENAI_MODEL);
64+
65+
let result = client
66+
.upsert_points(
67+
UpsertPointsBuilder::new(
68+
collection_name,
69+
vec![PointStruct::new(
70+
1,
71+
doc,
72+
Payload::try_from(json!({"source": "test"})).unwrap(),
73+
)],
74+
)
75+
.wait(true),
76+
)
77+
.await;
78+
79+
assert!(
80+
result.is_ok(),
81+
"Upsert with external API keys failed: {result:?}"
82+
);
83+
84+
let _ = client.delete_collection(collection_name).await;
85+
}
86+
87+
#[tokio::test]
88+
async fn test_query_with_external_api_keys() {
89+
let Some(openai_api_key) = std::env::var("OPENAI_API_KEY").ok() else {
90+
eprintln!("Skipping test_query_with_external_api_keys: OPENAI_API_KEY is not set");
91+
return;
92+
};
93+
let collection_name = QUERY_COLLECTION_NAME;
94+
let client = create_client_with_external_keys(HashMap::from([(
95+
"openai-api-key".to_string(),
96+
openai_api_key,
97+
)]));
98+
setup_collection(&client, collection_name, OPENAI_VECTOR_SIZE).await;
99+
100+
// Upsert a point first
101+
let doc = Document::new("Qdrant is a vector search engine", OPENAI_MODEL);
102+
client
103+
.upsert_points(
104+
UpsertPointsBuilder::new(
105+
collection_name,
106+
vec![PointStruct::new(
107+
1,
108+
doc,
109+
Payload::try_from(json!({"source": "test"})).unwrap(),
110+
)],
111+
)
112+
.wait(true),
113+
)
114+
.await
115+
.expect("Upsert failed");
116+
117+
// Query with a document (server-side inference)
118+
let query_doc = Document::new("vector database", OPENAI_MODEL);
119+
120+
let result = client
121+
.query(
122+
QueryPointsBuilder::new(collection_name)
123+
.query(Query::new_nearest(query_doc))
124+
.limit(1)
125+
.with_payload(true),
126+
)
127+
.await;
128+
129+
assert!(
130+
result.is_ok(),
131+
"Query with external API keys failed: {result:?}"
132+
);
133+
134+
let response = result.unwrap();
135+
assert_eq!(response.result.len(), 1);
136+
assert!(response.result[0].payload.contains_key("source"));
137+
138+
let _ = client.delete_collection(collection_name).await;
139+
}
140+
141+
#[tokio::test]
142+
async fn test_query_with_two_external_api_providers() {
143+
let Some(openai_api_key) = std::env::var("OPENAI_API_KEY").ok() else {
144+
eprintln!("Skipping test_query_with_two_external_api_providers: OPENAI_API_KEY is not set");
145+
return;
146+
};
147+
let Some(cohere_api_key) = std::env::var("COHERE_API_KEY").ok() else {
148+
eprintln!("Skipping test_query_with_two_external_api_providers: COHERE_API_KEY is not set");
149+
return;
150+
};
151+
152+
let client = create_client_with_external_keys(HashMap::from([
153+
("openai-api-key".to_string(), openai_api_key),
154+
("cohere-api-key".to_string(), cohere_api_key),
155+
]));
156+
157+
setup_collection(&client, DUAL_OPENAI_COLLECTION_NAME, OPENAI_VECTOR_SIZE).await;
158+
setup_collection(&client, DUAL_COHERE_COLLECTION_NAME, COHERE_VECTOR_SIZE).await;
159+
160+
let openai_doc = Document::new("OpenAI provider document", OPENAI_MODEL);
161+
let cohere_doc = cohere_document("Cohere provider document", "search_document");
162+
163+
let openai_upsert = client
164+
.upsert_points(
165+
UpsertPointsBuilder::new(
166+
DUAL_OPENAI_COLLECTION_NAME,
167+
vec![PointStruct::new(
168+
1,
169+
openai_doc,
170+
Payload::try_from(json!({"provider": "openai"})).unwrap(),
171+
)],
172+
)
173+
.wait(true),
174+
)
175+
.await;
176+
assert!(
177+
openai_upsert.is_ok(),
178+
"OpenAI upsert with external API keys failed: {openai_upsert:?}"
179+
);
180+
181+
let cohere_upsert = client
182+
.upsert_points(
183+
UpsertPointsBuilder::new(
184+
DUAL_COHERE_COLLECTION_NAME,
185+
vec![PointStruct::new(
186+
1,
187+
cohere_doc,
188+
Payload::try_from(json!({"provider": "cohere"})).unwrap(),
189+
)],
190+
)
191+
.wait(true),
192+
)
193+
.await;
194+
assert!(
195+
cohere_upsert.is_ok(),
196+
"Cohere upsert with external API keys failed: {cohere_upsert:?}"
197+
);
198+
199+
let openai_query = client
200+
.query(
201+
QueryPointsBuilder::new(DUAL_OPENAI_COLLECTION_NAME)
202+
.query(Query::new_nearest(Document::new(
203+
"OpenAI provider query",
204+
OPENAI_MODEL,
205+
)))
206+
.limit(1)
207+
.with_payload(true),
208+
)
209+
.await;
210+
assert!(
211+
openai_query.is_ok(),
212+
"OpenAI query with external API keys failed: {openai_query:?}"
213+
);
214+
215+
let cohere_query = client
216+
.query(
217+
QueryPointsBuilder::new(DUAL_COHERE_COLLECTION_NAME)
218+
.query(Query::new_nearest(cohere_document(
219+
"Cohere provider query",
220+
"search_query",
221+
)))
222+
.limit(1)
223+
.with_payload(true),
224+
)
225+
.await;
226+
assert!(
227+
cohere_query.is_ok(),
228+
"Cohere query with external API keys failed: {cohere_query:?}"
229+
);
230+
231+
let openai_response = openai_query.unwrap();
232+
assert_eq!(openai_response.result.len(), 1);
233+
assert_eq!(
234+
openai_response.result[0].payload["provider"],
235+
"openai".into()
236+
);
237+
238+
let cohere_response = cohere_query.unwrap();
239+
assert_eq!(cohere_response.result.len(), 1);
240+
assert_eq!(
241+
cohere_response.result[0].payload["provider"],
242+
"cohere".into()
243+
);
244+
245+
let _ = client.delete_collection(DUAL_OPENAI_COLLECTION_NAME).await;
246+
let _ = client.delete_collection(DUAL_COHERE_COLLECTION_NAME).await;
247+
}

0 commit comments

Comments
 (0)