unleash-edge 0.2.0

Unleash edge is a proxy for Unleash. It can return both evaluated feature toggles as well as the raw data from Unleash's client API
Documentation
use crate::error::EdgeError;
use crate::http::unleash_client::UnleashClient;
use crate::types::{EdgeResult, EdgeSink, EdgeSource, EdgeToken, ValidateTokensRequest};
use std::sync::Arc;
use unleash_types::Merge;
#[derive(Clone)]
pub struct TokenValidator {
    pub unleash_client: Arc<UnleashClient>,
    pub edge_source: Arc<dyn EdgeSource>,
    pub edge_sink: Arc<dyn EdgeSink>,
}

impl TokenValidator {
    async fn get_unknown_and_known_tokens(
        &self,
        tokens: Vec<String>,
    ) -> EdgeResult<(Vec<EdgeToken>, Vec<EdgeToken>)> {
        let tokens_with_valid_format: Vec<EdgeToken> = tokens
            .into_iter()
            .filter_map(|t| EdgeToken::try_from(t).ok())
            .collect();

        if tokens_with_valid_format.is_empty() {
            Err(EdgeError::TokenParseError)
        } else {
            let mut tokens = vec![];
            for token in tokens_with_valid_format {
                let known_data = self.edge_source.get_token(token.token.clone()).await?;
                tokens.push(known_data.unwrap_or(token));
            }
            Ok(tokens.into_iter().partition(|t| t.token_type.is_none()))
        }
    }

    pub async fn register_token(&self, token: String) -> EdgeResult<EdgeToken> {
        Ok(self
            .register_tokens(vec![token])
            .await?
            .first()
            .expect("Couldn't validate token")
            .clone())
    }

    pub async fn register_tokens(&self, tokens: Vec<String>) -> EdgeResult<Vec<EdgeToken>> {
        let (unknown_tokens, known_tokens) = self.get_unknown_and_known_tokens(tokens).await?;
        if unknown_tokens.is_empty() {
            Ok(known_tokens)
        } else {
            let token_strings_to_validate: Vec<String> =
                unknown_tokens.iter().map(|t| t.token.clone()).collect();

            let validation_result = self
                .unleash_client
                .validate_tokens(ValidateTokensRequest {
                    tokens: token_strings_to_validate,
                })
                .await?;

            let tokens_to_sink: Vec<EdgeToken> = unknown_tokens
                .into_iter()
                .map(|maybe_valid| {
                    if let Some(validated_token) = validation_result
                        .iter()
                        .find(|v| maybe_valid.token == v.token)
                    {
                        EdgeToken {
                            status: crate::types::TokenValidationStatus::Validated,
                            ..validated_token.clone()
                        }
                    } else {
                        EdgeToken {
                            status: crate::types::TokenValidationStatus::Invalid,
                            ..maybe_valid
                        }
                    }
                })
                .collect();
            self.edge_sink.sink_tokens(tokens_to_sink.clone()).await?;
            Ok(tokens_to_sink.merge(known_tokens))
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::data_sources::memory_provider::MemoryProvider;
    use crate::data_sources::repository::{DataSource, DataSourceFacade};
    use crate::types::{EdgeSink, EdgeSource, EdgeToken, TokenType, TokenValidationStatus};
    use actix_http::HttpService;
    use actix_http_test::{test_server, TestServer};
    use actix_service::map_config;

    use actix_web::{dev::AppConfig, web, App, HttpResponse};
    use chrono::Duration;
    use serde::{Deserialize, Serialize};
    use std::sync::Arc;

    #[derive(Clone, Debug, Serialize, Deserialize)]
    pub struct EdgeTokens {
        pub tokens: Vec<EdgeToken>,
    }

    async fn return_validated_tokens() -> HttpResponse {
        HttpResponse::Ok().json(EdgeTokens {
            tokens: valid_tokens(),
        })
    }

    fn valid_tokens() -> Vec<EdgeToken> {
        vec![EdgeToken {
            token: "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f".into(),
            projects: vec!["*".into()],
            environment: Some("development".into()),
            token_type: Some(TokenType::Client),
            status: TokenValidationStatus::Validated,
        }]
    }

    async fn test_validation_server() -> TestServer {
        test_server(move || {
            HttpService::new(map_config(
                App::new().service(
                    web::resource("/edge/validate").route(web::post().to(return_validated_tokens)),
                ),
                |_| AppConfig::default(),
            ))
            .tcp()
        })
        .await
    }

    #[tokio::test]
    pub async fn can_validate_tokens() {
        let test_provider = Arc::new(MemoryProvider::default());
        let facade = Arc::new(DataSourceFacade {
            features_refresh_interval: Some(Duration::seconds(1)),
            token_source: test_provider.clone(),
            feature_source: test_provider.clone(),
            feature_sink: test_provider.clone(),
            token_sink: test_provider.clone(),
        });

        let sink: Arc<dyn EdgeSink> = facade.clone();
        let source: Arc<dyn EdgeSource> = facade.clone();

        let srv = test_validation_server().await;
        let unleash_client =
            crate::http::unleash_client::UnleashClient::new(srv.url("/").as_str(), None)
                .expect("Couldn't build client");

        let validation_holder = super::TokenValidator {
            unleash_client: Arc::new(unleash_client),
            edge_source: source,
            edge_sink: sink,
        };
        let tokens_to_validate = vec![
            "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f".into(),
            "*:production.abcdef1234567890".into(),
        ];
        validation_holder
            .register_tokens(tokens_to_validate)
            .await
            .expect("Couldn't register tokens");
        let known_tokens = test_provider
            .get_tokens()
            .await
            .expect("Couldn't get tokens");
        assert_eq!(known_tokens.len(), 2);
        assert!(known_tokens.iter().any(|t| t.token
            == "*:development.1d38eefdd7bf72676122b008dcf330f2f2aa2f3031438e1b7e8f0d1f"
            && t.status == TokenValidationStatus::Validated));
        assert!(known_tokens
            .iter()
            .any(|t| t.token == "*:production.abcdef1234567890"
                && t.status == TokenValidationStatus::Invalid));
    }

    #[tokio::test]
    pub async fn tokens_with_wrong_format_is_not_included() {
        let test_provider = Arc::new(MemoryProvider::default());
        let facade = Arc::new(DataSourceFacade {
            features_refresh_interval: Some(Duration::seconds(1)),
            feature_source: test_provider.clone(),
            token_source: test_provider.clone(),
            feature_sink: test_provider.clone(),
            token_sink: test_provider.clone(),
        });
        let sink: Arc<dyn EdgeSink> = facade.clone();
        let source: Arc<dyn EdgeSource> = facade.clone();

        let srv = test_validation_server().await;
        let unleash_client =
            crate::http::unleash_client::UnleashClient::new(srv.url("/").as_str(), None)
                .expect("Couldn't build client");
        let validation_holder = super::TokenValidator {
            unleash_client: Arc::new(unleash_client),
            edge_source: source,
            edge_sink: sink,
        };
        let invalid_tokens = vec!["jamesbond".into(), "invalidtoken".into()];
        let validated_tokens = validation_holder.register_tokens(invalid_tokens).await;
        assert!(validated_tokens.is_err());
    }
}