How to unit-test a dependency in actix-web?

Hello,

I'm building a web service that requires authenticating using Oauth2 with a third party service provider. I'm trying to unit-test and mock the authentication service behind my web service, and write unit-tests for all my endpoints. I tried creating an authentication service trait and added the trait object into the AppState so that I can pass a real or mocked authentication service.

struct AppState {
    auth_service: Arc<Mutex<dyn AuthServiceTrait>>,
}

#[async_trait]
trait AuthServiceTrait {
    async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool>;
}

struct AuthService {}

#[async_trait]
impl AuthServiceTrait for AuthService {
    async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
        // perform oauth2 flow
        let _ = session.insert("user", "info");
        Ok(true)
    }
}

struct MockAuthService {}

#[async_trait]
impl AuthServiceTrait for MockAuthService {
    async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
        Ok(true)
    }
}

I added a trait async function authenticate so that I can overwrite the authentication implementation. In the real auth service, it will perform the redirect with the real service provider but in the mocked auth service, I want to be able to mock the response from the service provider.

The part where I'm stuck is that it's an async trait function (because as part of the Oauth2 implementation, it needs call the service provider to get the access token - so it needs to be async) and I need to pass and capture the actix_session::Session which is not Send to insert the session information so that the user is authenticated. Is there any way to make Session be able to send across threads? I want to make a request to /callback with the mocked auth service to bypass the authentication.

error: future cannot be sent between threads safely
  --> src/bin/example4_server.rs:51:89
   |
51 |       async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
   |  _________________________________________________________________________________________^
52 | |         // perform oauth2 flow
53 | |         let _ = session.insert("user", "info");
54 | |         Ok(true)
55 | |     }
   | |_____^ future created by async block is not `Send`
   |
   = help: within `{async block@src/bin/example4_server.rs:51:89: 55:6}`, the trait `Send` is not implemented for `Rc<RefCell<actix_session::session::SessionInner>>`, which is required by `{async block@src/bin/example4_server.rs:51:89: 55:6}: Send`
note: captured value is not `Send`
  --> src/bin/example4_server.rs:51:47
   |
51 |     async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
   |                                               ^^^^^^^ has type `actix_session::Session` which is not `Send`
   = note: required for the cast from `Pin<Box<{async block@src/bin/example4_server.rs:51:89: 55:6}>>` to `Pin<Box<(dyn std::future::Future<Output = Result<bool, anyhow::Error>> + Send + 'async_trait)>>`

In Python or Go, I can patch or inject a mocked implementation of the interface. How is this done using Rust? Thanks for the help.

Here's the sample source code: Rust Playground

I search all over actixs repos but I don't see examples on how to mock external dependencies.

You would do exactly the same in Rust. Are you getting any errors with your current approach?

The error I'm getting is this:

error: future cannot be sent between threads safely
  --> src/bin/example4_server.rs:51:89
   |
51 |       async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
   |  _________________________________________________________________________________________^
52 | |         // perform oauth2 flow
53 | |         let _ = session.insert("user", "info");
54 | |         Ok(true)
55 | |     }
   | |_____^ future created by async block is not `Send`
   |
   = help: within `{async block@src/bin/example4_server.rs:51:89: 55:6}`, the trait `Send` is not implemented for `Rc<RefCell<actix_session::session::SessionInner>>`, which is required by `{async block@src/bin/example4_server.rs:51:89: 55:6}: Send`
note: captured value is not `Send`
  --> src/bin/example4_server.rs:51:47
   |
51 |     async fn authenticate(&self, query: &str, session: Session) -> anyhow::Result<bool> {
   |                                               ^^^^^^^ has type `actix_session::Session` which is not `Send`
   = note: required for the cast from `Pin<Box<{async block@src/bin/example4_server.rs:51:89: 55:6}>>` to `Pin<Box<(dyn std::future::Future<Output = Result<bool, anyhow::Error>> + Send + 'async_trait)>>`

The actix runtime is just a bunch of single threaded tokio runtimes. This disables work-stealing in favour of being able to use !Send types. The #[async_trait] annotation translates your async trait methods into methods that return Pin<Box<dyn Future + Send + 'async_trait>>. To avoid adding the Send bound which actix_session::Session does not satisfy, either remove the #[async_trait] annotation (async trait methods were stabilized in 1.75.0) or try #[async_trait(?Send)] instead.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.