Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: frontend all endpoints now accept context correctly #558

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions server/src/frontend_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use unleash_types::{
};
use unleash_yggdrasil::{EngineState, ResolvedToggle};

use crate::error::EdgeError::ContextParseError;
use crate::types::{ClientIp, IncomingContext, PostContext};
use crate::{
error::{EdgeError, FrontendHydrationMissing},
Expand Down Expand Up @@ -48,13 +47,14 @@ pub async fn get_proxy_all_features(
edge_token: EdgeToken,
engine_cache: Data<DashMap<String, EngineState>>,
token_cache: Data<DashMap<String, EdgeToken>>,
context: QsQuery<IncomingContext>,
req: HttpRequest,
) -> EdgeJsonResult<FrontendResult> {
get_all_features(
edge_token,
engine_cache,
token_cache,
req.query_string(),
&context.into_inner().into(),
req.extensions().get::<ClientIp>(),
)
}
Expand All @@ -75,13 +75,14 @@ pub async fn get_frontend_all_features(
edge_token: EdgeToken,
engine_cache: Data<DashMap<String, EngineState>>,
token_cache: Data<DashMap<String, EdgeToken>>,
context: QsQuery<IncomingContext>,
req: HttpRequest,
) -> EdgeJsonResult<FrontendResult> {
get_all_features(
edge_token,
engine_cache,
token_cache,
req.query_string(),
&context.into_inner().into(),
req.extensions().get::<ClientIp>(),
)
}
Expand Down Expand Up @@ -746,17 +747,13 @@ pub fn get_all_features(
edge_token: EdgeToken,
engine_cache: Data<DashMap<String, EngineState>>,
token_cache: Data<DashMap<String, EdgeToken>>,
query_string: &str,
context: &Context,
client_ip: Option<&ClientIp>,
) -> EdgeJsonResult<FrontendResult> {
let raw_context: IncomingContext = serde_qs::Config::new(0, false)
.deserialize_str(query_string)
.map_err(|_| ContextParseError)?;
let context: Context = raw_context.into();
let context_with_ip = if context.remote_address.is_none() {
Context {
&Context {
remote_address: client_ip.map(|ip| ip.to_string()),
..context
..context.clone()
}
} else {
context
Expand All @@ -769,7 +766,7 @@ pub fn get_all_features(
let engine = engine_cache.get(&key).ok_or_else(|| {
EdgeError::FrontendNotYetHydrated(FrontendHydrationMissing::from(&edge_token))
})?;
let feature_results = engine.resolve_all(&context_with_ip, &None).ok_or_else(|| {
let feature_results = engine.resolve_all(context_with_ip, &None).ok_or_else(|| {
EdgeError::FrontendExpectedToBeHydrated(
"Feature cache has not been hydrated yet, but it was expected to be. This can be due to a race condition from calling edge before it's ready. This error might auto resolve as soon as edge is able to fetch from upstream".into(),
)
Expand Down Expand Up @@ -1578,7 +1575,9 @@ mod tests {
.insert_header(("Authorization", auth_key.clone()))
.set_json(json!({ "properties": {"companyId": "bricks"}}))
.to_request();
let result: FrontendResult = test::try_call_and_read_body_json(&app, req).await.expect("Failed to call endpoint");
let result: FrontendResult = test::try_call_and_read_body_json(&app, req)
.await
.expect("Failed to call endpoint");
tracing::info!("{result:?}");
assert_eq!(result.toggles.len(), 1);
}
Expand Down Expand Up @@ -1665,4 +1664,45 @@ mod tests {
let result = test::call_service(&app, proxy_req).await;
assert_eq!(result.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn can_handle_custom_context_fields_on_all_endpoint() {
let client_features_with_custom_context_field =
crate::tests::features_from_disk("../examples/with_custom_constraint.json");
let auth_key = "default:development.secret123".to_string();
let (token_cache, feature_cache, engine_cache) = build_offline_mode(
client_features_with_custom_context_field.clone(),
vec![auth_key.clone()],
vec![],
vec![],
)
.unwrap();
let config =
serde_qs::actix::QsQueryConfig::default().qs_config(serde_qs::Config::new(5, false));
let app = test::init_service(
App::new()
.app_data(config)
.app_data(Data::from(token_cache))
.app_data(Data::from(feature_cache))
.app_data(Data::from(engine_cache))
.service(
web::scope("/api").configure(|cfg| super::configure_frontend_api(cfg, false)),
),
)
.await;
let req = test::TestRequest::get()
.uri("/api/frontend/all?properties[companyId]=bricks")
.insert_header(ContentType::json())
.insert_header(("Authorization", auth_key.clone()))
.to_request();
let feature_results: FrontendResult = test::call_and_read_body_json(&app, req).await;
assert!(feature_results.toggles.iter().any(|f| f.enabled));
let req = test::TestRequest::get()
.uri("/api/frontend?properties%5BcompanyId%5D=bricks")
.insert_header(ContentType::json())
.insert_header(("Authorization", auth_key.clone()))
.to_request();
let feature_results: FrontendResult = test::call_and_read_body_json(&app, req).await;
assert!(feature_results.toggles.iter().any(|f| f.enabled));
}
}