blob: f17abd357ac1b3a7b965f07bd9e449396d09c461 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![cfg(test)]
use crate::{AuthProviderSupplier, TokenManagerError};
use failure::Fail;
use fidl::endpoints::create_request_stream;
use fidl::endpoints::ClientEnd;
use fidl_fuchsia_auth::{
AuthProviderMarker, AuthProviderRequest, AuthProviderRequestStream, AuthProviderStatus, Status,
};
use futures::future::BoxFuture;
use futures::future::{join, FutureObj};
use futures::prelude::*;
use futures::stream::FuturesUnordered;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
/// FakeAuthProviderSupplier implements AuthProviderSupplier, which is needed by TokenManager
/// during instantiation. This fake has a bit of logic: (1) auth provider clients can be
/// pre-populated during test setup, (2) clients will only be given out once through the `get()`
/// method and (3) the `run()` method checks that all clients were given out during the
/// test. The reason for this design is that TokenManager caches auth provider clients and
/// reuses them throughout its lifetime, hence it will never ask for the same auth provider
/// (as defined by its `auth_provider_type`) twice.
pub struct FakeAuthProviderSupplier {
auth_providers: Mutex<HashMap<String, ClientEnd<AuthProviderMarker>>>,
servers: Mutex<FuturesUnordered<BoxFuture<'static, Result<(), fidl::Error>>>>,
}
#[derive(Debug, Fail)]
pub enum FakeAuthProviderError {
#[fail(display = "FakeAuthProvider error: Some auth providers were never used")]
UnusedAuthProviders,
#[fail(display = "FakeAuthProvider error: A server error occurred: {:?}", _0)]
ServerError(fidl::Error),
}
impl FakeAuthProviderSupplier {
pub fn new() -> Self {
Self {
auth_providers: Mutex::new(HashMap::new()),
servers: Mutex::new(FuturesUnordered::new()),
}
}
/// Add an auth provider, by supplying two arguments: the type, and a function which acts
/// as the server end for the auth provider. This function will be invoked when `run()` is
/// called.
pub fn add_auth_provider<'a, F, Fut>(&'a self, auth_provider_type: &'a str, server_fn: F)
where
F: (FnOnce(AuthProviderRequestStream) -> Fut),
Fut: Future<Output = Result<(), fidl::Error>> + Send + 'static,
{
let (client_end, stream) = create_request_stream().unwrap();
let serve = server_fn(stream);
self.auth_providers.lock().insert(auth_provider_type.to_string(), client_end);
self.servers.lock().push(serve.boxed());
}
/// Run the added auth providers to completion. We return an error if a server function
/// returns an error or if not all clients have been given out during the call.
/// This is intended to run concurrently with the client code under test.
pub async fn run(&self) -> Result<(), FakeAuthProviderError> {
let futs = std::mem::replace(&mut *self.servers.lock(), FuturesUnordered::new());
futs.collect::<Vec<_>>().await
.into_iter()
.collect::<Result<Vec<_>, fidl::Error>>()
.map_err(|err| FakeAuthProviderError::ServerError(err))?;
if !self.auth_providers.lock().is_empty() {
return Err(FakeAuthProviderError::UnusedAuthProviders);
}
Ok(())
}
}
impl AuthProviderSupplier for FakeAuthProviderSupplier {
/// Just returns the pre-populated AuthProvider client end. Note that this consumes the
/// auth provider; a second identical call will fail. This method should be invoked by the
/// code under test.
fn get<'b>(
&'b self,
auth_provider_type: &'b str,
) -> FutureObj<'b, Result<ClientEnd<AuthProviderMarker>, TokenManagerError>> {
let client_end = self.auth_providers.lock().remove(auth_provider_type);
FutureObj::new(Box::new(async move {
client_end.ok_or(TokenManagerError::new(Status::AuthProviderServiceUnavailable))
}))
}
}
impl<'a> AuthProviderSupplier for &'a FakeAuthProviderSupplier {
fn get<'b>(
&'b self,
auth_provider_type: &'b str,
) -> FutureObj<'b, Result<ClientEnd<AuthProviderMarker>, TokenManagerError>> {
AuthProviderSupplier::get(*self, auth_provider_type)
}
}
mod tests {
use super::*;
use fuchsia_async;
/// This is a meta-test, of the FakeAuthProviderSupplier itself, since it has a bit of logic.
/// First we create a FakeAuthProviderSupplier with two auth providers, Hooli and Pied Piper,
/// for which we create two servers with expects on the incoming messages, and pre-populated
/// responses. Finally, we create a client that retrieves the auth providers from the fake,
/// sends the expected messages to them and checks that the responses match.
#[fuchsia_async::run_until_stalled(test)]
async fn auth_provider_fake_test() {
let auth_provider_supplier = Arc::new(FakeAuthProviderSupplier::new());
assert!(auth_provider_supplier.get("myspace").await.is_err()); // Non-existing provider
auth_provider_supplier.add_auth_provider("hooli", |mut stream| {
async move {
match stream.try_next().await? {
Some(AuthProviderRequest::RevokeAppOrPersistentCredential {
responder,
credential,
}) => {
assert_eq!(credential, "HOOLI_CREDENTIAL");
responder.send(AuthProviderStatus::BadRequest)?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
}
});
auth_provider_supplier.add_auth_provider("pied-piper", |mut stream| {
async move {
match stream.try_next().await? {
Some(AuthProviderRequest::GetAppIdToken {
responder,
credential,
audience,
}) => {
assert_eq!(credential, "PIED_PIPER_CREDENTIAL");
assert!(audience.is_none());
responder.send(AuthProviderStatus::Ok, None)?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
}
});
let auth_provider_supplier_clone = Arc::clone(&auth_provider_supplier);
let client_fn = async move {
let ap_proxy =
auth_provider_supplier_clone.get("hooli").await.unwrap().into_proxy().unwrap();
let status =
ap_proxy.revoke_app_or_persistent_credential("HOOLI_CREDENTIAL").await.unwrap();
assert_eq!(status, AuthProviderStatus::BadRequest);
let ap_proxy = auth_provider_supplier_clone.get("pied-piper").await
.unwrap()
.into_proxy()
.unwrap();
let (status, auth_token) =
ap_proxy.get_app_id_token("PIED_PIPER_CREDENTIAL", None).await.unwrap();
assert_eq!(status, AuthProviderStatus::Ok);
assert!(auth_token.is_none());
};
let (run_result, _) = join(auth_provider_supplier.run(), client_fn).await;
assert!(run_result.is_ok());
assert!(auth_provider_supplier.get("hooli").await.is_err());
assert!(auth_provider_supplier.get("pied-piper").await.is_err());
}
}