Skip to content

Commit 3142335

Browse files
committed
Add evaluation gherkin tests
Signed-off-by: Eren Atas <[email protected]>
1 parent e56d8bb commit 3142335

File tree

4 files changed

+799
-13
lines changed

4 files changed

+799
-13
lines changed

crates/flagd/src/resolver/in_process/storage/connector/grpc.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub struct GrpcStreamConnector {
2828
retry_backoff_max_ms: u32,
2929
retry_grace_period: u32,
3030
stream_deadline_ms: u32,
31-
authority: String, // desired authority, e.g. "b-features-api.service"
31+
authority: String, // desired authority, e.g. "b-features-api.service"
3232
provider_id: String, // provider identifier for sync requests
3333
}
3434

@@ -53,7 +53,10 @@ impl GrpcStreamConnector {
5353
retry_grace_period: options.retry_grace_period,
5454
stream_deadline_ms: options.stream_deadline_ms,
5555
authority,
56-
provider_id: options.provider_id.clone().unwrap_or_else(|| "rust-flagd-provider".to_string()),
56+
provider_id: options
57+
.provider_id
58+
.clone()
59+
.unwrap_or_else(|| "rust-flagd-provider".to_string()),
5760
}
5861
}
5962

crates/flagd/src/resolver/rpc.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ fn convert_proto_metadata(metadata: prost_types::Struct) -> FlagMetadata {
8585
FlagMetadata { values }
8686
}
8787

88+
/// Maps gRPC status codes to OpenFeature error codes
89+
///
90+
/// This ensures consistent error handling across different resolver types
91+
/// and proper conformance with the OpenFeature specification.
92+
fn map_grpc_status_to_error_code(status: &tonic::Status) -> EvaluationErrorCode {
93+
use tonic::Code;
94+
match status.code() {
95+
Code::NotFound => EvaluationErrorCode::FlagNotFound,
96+
Code::InvalidArgument => EvaluationErrorCode::InvalidContext,
97+
Code::Unauthenticated | Code::PermissionDenied => {
98+
EvaluationErrorCode::General("authentication/authorization error".to_string())
99+
}
100+
Code::FailedPrecondition => EvaluationErrorCode::TypeMismatch,
101+
Code::DeadlineExceeded | Code::Cancelled => {
102+
EvaluationErrorCode::General("request timeout or cancelled".to_string())
103+
}
104+
Code::Unavailable => EvaluationErrorCode::General("service unavailable".to_string()),
105+
_ => EvaluationErrorCode::General(format!("{:?}", status.code())),
106+
}
107+
}
108+
88109
pub struct RpcResolver {
89110
client: ClientType,
90111
metadata: OnceLock<ProviderMetadata>,
@@ -214,7 +235,7 @@ impl FeatureProvider for RpcResolver {
214235
Err(status) => {
215236
error!(flag_key, error = %status, "failed to resolve boolean flag");
216237
Err(EvaluationError {
217-
code: EvaluationErrorCode::General(status.code().to_string()),
238+
code: map_grpc_status_to_error_code(&status),
218239
message: Some(status.message().to_string()),
219240
})
220241
}
@@ -247,7 +268,7 @@ impl FeatureProvider for RpcResolver {
247268
Err(status) => {
248269
error!(flag_key, error = %status, "failed to resolve string flag");
249270
Err(EvaluationError {
250-
code: EvaluationErrorCode::General(status.code().to_string()),
271+
code: map_grpc_status_to_error_code(&status),
251272
message: Some(status.message().to_string()),
252273
})
253274
}
@@ -280,7 +301,7 @@ impl FeatureProvider for RpcResolver {
280301
Err(status) => {
281302
error!(flag_key, error = %status, "failed to resolve float flag");
282303
Err(EvaluationError {
283-
code: EvaluationErrorCode::General(status.code().to_string()),
304+
code: map_grpc_status_to_error_code(&status),
284305
message: Some(status.message().to_string()),
285306
})
286307
}
@@ -313,7 +334,7 @@ impl FeatureProvider for RpcResolver {
313334
Err(status) => {
314335
error!(flag_key, error = %status, "failed to resolve integer flag");
315336
Err(EvaluationError {
316-
code: EvaluationErrorCode::General(status.code().to_string()),
337+
code: map_grpc_status_to_error_code(&status),
317338
message: Some(status.message().to_string()),
318339
})
319340
}
@@ -346,7 +367,7 @@ impl FeatureProvider for RpcResolver {
346367
Err(status) => {
347368
error!(flag_key, error = %status, "failed to resolve struct flag");
348369
Err(EvaluationError {
349-
code: EvaluationErrorCode::General(status.code().to_string()),
370+
code: map_grpc_status_to_error_code(&status),
350371
message: Some(status.message().to_string()),
351372
})
352373
}
@@ -765,4 +786,44 @@ mod tests {
765786
// Clean shutdown
766787
server_handle.abort();
767788
}
789+
790+
#[test]
791+
fn test_grpc_error_code_mapping() {
792+
use tonic::Code;
793+
794+
// Test NOT_FOUND -> FlagNotFound
795+
let status = tonic::Status::new(Code::NotFound, "Flag not found");
796+
let error_code = map_grpc_status_to_error_code(&status);
797+
assert!(matches!(error_code, EvaluationErrorCode::FlagNotFound));
798+
799+
// Test INVALID_ARGUMENT -> InvalidContext
800+
let status = tonic::Status::new(Code::InvalidArgument, "Invalid context");
801+
let error_code = map_grpc_status_to_error_code(&status);
802+
assert!(matches!(error_code, EvaluationErrorCode::InvalidContext));
803+
804+
// Test UNAUTHENTICATED -> General
805+
let status = tonic::Status::new(Code::Unauthenticated, "Not authenticated");
806+
let error_code = map_grpc_status_to_error_code(&status);
807+
assert!(matches!(error_code, EvaluationErrorCode::General(_)));
808+
809+
// Test PERMISSION_DENIED -> General
810+
let status = tonic::Status::new(Code::PermissionDenied, "Access denied");
811+
let error_code = map_grpc_status_to_error_code(&status);
812+
assert!(matches!(error_code, EvaluationErrorCode::General(_)));
813+
814+
// Test FAILED_PRECONDITION -> TypeMismatch
815+
let status = tonic::Status::new(Code::FailedPrecondition, "Type mismatch");
816+
let error_code = map_grpc_status_to_error_code(&status);
817+
assert!(matches!(error_code, EvaluationErrorCode::TypeMismatch));
818+
819+
// Test DEADLINE_EXCEEDED -> General
820+
let status = tonic::Status::new(Code::DeadlineExceeded, "Timeout");
821+
let error_code = map_grpc_status_to_error_code(&status);
822+
assert!(matches!(error_code, EvaluationErrorCode::General(_)));
823+
824+
// Test UNAVAILABLE -> General
825+
let status = tonic::Status::new(Code::Unavailable, "Service unavailable");
826+
let error_code = map_grpc_status_to_error_code(&status);
827+
assert!(matches!(error_code, EvaluationErrorCode::General(_)));
828+
}
768829
}

crates/flagd/tests/gherkin_config_test.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ impl ConfigWorld {
2727
// 1. The test is protected by #[serial_test::serial]
2828
// 2. This prevents test pollution between scenarios
2929
// 3. We clear env vars that were set in previous scenarios
30-
30+
3131
// Clear env vars that were set in the previous scenario
3232
for key in self.env_vars.keys() {
3333
unsafe {
3434
std::env::remove_var(key);
3535
}
3636
}
37-
37+
3838
// Also explicitly clear FLAGD_OFFLINE_FLAG_SOURCE_PATH because it can affect resolver type
3939
// via FlagdOptions::default() logic, even if not tracked in world.env_vars
4040
unsafe {
4141
std::env::remove_var("FLAGD_OFFLINE_FLAG_SOURCE_PATH");
4242
}
43-
43+
4444
self.env_vars.clear();
4545
self.options = FlagdOptions::default();
4646
self.provider = None;
@@ -302,7 +302,7 @@ async fn check_option_value(
302302
_ => None,
303303
};
304304
let expected = convert_type(&option_type, &value);
305-
305+
306306
// For resolver type, do case-insensitive comparison since enum normalizes to lowercase
307307
let actual_normalized = if option == "resolver" {
308308
actual.as_ref().map(|v| v.to_lowercase())
@@ -314,8 +314,12 @@ async fn check_option_value(
314314
} else {
315315
expected.clone()
316316
};
317-
318-
assert_eq!(actual_normalized, expected_normalized, "Option '{}' value mismatch", option);
317+
318+
assert_eq!(
319+
actual_normalized, expected_normalized,
320+
"Option '{}' value mismatch",
321+
option
322+
);
319323
}
320324

321325
#[test(tokio::test)]

0 commit comments

Comments
 (0)