Search code examples
groovyrest-assuredserenity-bdd

REST API Testing with Screenplay: Get a value from the result of a task, and share it with other tasks


I am attempting to implement a login step with a REST API using the Serenity Screenplay Pattern. For this to work I need to be able to grab a JWT token that was returned from one Task, and use it to authenticate other tasks. I know how to do this in theory. Please consider the following Groovy code.

class ActorInTheSpotlight {
    String actor
    String token

    @Step("{0} is an actor using the system under test")
    ActorInTheSpotlight whoIsNamed(String actor) {
        this.actor = actor
        theActorCalled(actor)

        return this
    }

    @Step("#actor who can authenticate with credentials")
    ActorInTheSpotlight whoCanAuthenticateWith(String email, String password) {
        theActor().whoCan(Authenticate.withCredentials(email, password))

        return this
    }

    @Step("#actor who can call the admin API")
    ActorInTheSpotlight whoCanCallTheAdminApi() {
        theActor().whoCan(CallAnApi.at("http://localhost:3000"))
        return this
    }

    @Step("#actor was able to login to API with credentials")
    ActorInTheSpotlight wasAbleToLoginToApi() {
        return wasAbleTo(LoginWithApi.usingCredentials())
    }

    ActorInTheSpotlight wasAbleTo(Performable... todos) {
        theActor().wasAbleTo(todos)
        return this
    }
}
class LoginWithApi implements Task {

    @Shared
    ActorInTheSpotlight theActor

    static LoginWithApi usingCredentials() {
        return instrumented(LoginWithApi.class);
    }

    @Step("{0} logs into api using credentials")
    <T extends Actor> void performAs(T actor) {
        def auth = Authenticate.asPrincipal(actor)
        actor.attemptsTo(
                // NOTE: PostToApi is an alias for Post, renaming `with` to `withRequest`
                // so that Groovy does not attempt to match it to the default `with(Closure closure)`
                PostToApi.at("/login").withRequest({ RequestSpecification req ->
                    req.header("Content-Type", "application/json")
                        .body([email: auth.email, password: auth.password])
                })
        )

    }
}
class AdminApiStepDefinitions {

    @Shared
    ActorInTheSpotlight theActor

    @Before
    void set_the_stage(){
        OnStage.setTheStage(new OnlineCast())
    }

    @Given(/^that "([^"]*)" is an Admin who may call the rest api$/)
    void is_an_admin_who_may_call_the_rest_api(String actor) {
        theActor.whoIsNamed(actor)
                .whoCanCallTheAdminApi()
    }

    @Given(/^s?he was able to login to the api with the credentials$/)
    void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
        def email = credentials.get('email')
        def password = credentials.get('password')
        theActor
                .whoCanAuthenticateWith(email, password)
                .wasAbleToLoginToApi()
    }
}

So, in theory, I should be able to share the ActorInTheSpotlight steps between tasks, using it to store/retrieve my JWT token. I also see that I can grab the token value like so:

String token = SerenityRest.lastResponse() 
        .jsonPath()
        .getObject("token", String.class);

The problem is that I'm not exactly sure where to put this code within the context of the step definitions. Should I implement the retrieving of this token as its own step, or is there a way to hide this implementation detail within the LoginToApi task itself?

Thanks for your time!

Update

Here is the Authenticate ability class, which would probably be a good place to implement this functionality, but the same timing issues as above still apply. IE, how would I update an ability "mid-flight" so that it's available at the correct time for consuption in other tasks.

class Authenticate implements Ability {

    String email

    String password

    // instantiates the Ability and enables fluent DSL
    static Authenticate withCredentials(String email, String password) {
        return new Authenticate(email, password)
    }

    // NOTE: custom exception class not shown
    static Authenticate asPrincipal(Actor actor) throws CannotAuthenticateException {
        // complain if someone's asking the impossible
        if(!actor.abilityTo(Authenticate.class)){
            throw new CannotAuthenticateException(actor.getName())
        }

        return actor.abilityTo(Authenticate.class)
    }

    Authenticate(String email, String password) {
        this.email = email
        this.password = password
    }
}

Update 2

I was able to implement this as its own step, but I really dislike my implementation details leaking into the step definitions like this. I would except any answer that allows me to implement this without the was_able_to_get_a_valid_jwt_token step shown below.

note: only showing additions to original code

class ActorInTheSpotlight {
    @Step("#actor has a valid JWT token")
    ActorInTheSpotlight whoHasTheToken(String token) {
        this.token = token
        theActor().whoCan(AuthenticateApi.withToken(token))
        return this
    }
}
class AuthenticateApi implements Ability {

    String token

    static AuthenticateApi withToken(String token) {
        return new AuthenticateApi(token)
    }

    static AuthenticateApi asPrincipal(Actor actor) throws CannotAuthenticateException {
        // complain if someone's asking the impossible
        if(!actor.abilityTo(AuthenticateApi.class)){
            throw new CannotAuthenticateException(actor.getName())
        }

        return actor.abilityTo(AuthenticateApi.class)
    }

    static <T extends Actor> void attempt(final T actor, final RequestSpecification req) {
        AuthenticateApi auth = null
        try {
            auth = AuthenticateApi.asPrincipal(actor)
        }
        catch(CannotAuthenticateException e) {
            // swallow error
        }

        if(auth) {
            req.header("Authorization", "Bearer ${auth.token}")
        }
    }

    AuthenticateApi(String token) {
        this.token = token
    }
}
class AdminApiStepDefinitions {
    // This is what I want to get rid of!
    @Given(/^s?he was able to get a valid JWT token$/)
    void was_able_to_get_a_valid_jwt_token() {
        theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath()
                .getObject("token", String.class))
    }
}

And here is an example of a Task using the JWT token to authenticate requests:

class ApiGet implements Task {

    static ApiGet from(String resource) {
        return instrumented(ApiGet.class, resource)
    }

    String resource

    ApiGet(String resource) {
        this.resource = resource
    }

    @Step("{0} attempts to GET #resource")
    <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
                // NOTE: GetFromApi is an alias for Get, renaming `with` to `withRequest`
                // so that Groovy does not attempt to match it to the default `with(Closure closure)`
                GetFromApi.at(resource).withRequest({ RequestSpecification req ->
                    AuthenticateApi.attempt(actor, req)
                    req.header("Content-Type", "application/json")
                })
        )
    }
}

Solution

  • Well it doesn't seem very thread safe, but none of this really is, so... meh. Here is what I came up with.

    class AdminApiStepDefinitions {
        @Given(/^s?he was able to login to the api with the credentials$/)
        void was_able_to_login_to_the_api_with_the_credentials(Map<String, String> credentials) {
            def email = credentials.get('email')
            def password = credentials.get('password')
            theActor
                    .whoCanAuthenticateWith(email, password)
                    .wasAbleToLoginToApi()
    
            theActor.whoHasTheToken(SerenityRest.lastResponse().jsonPath().getString("token"))
        }
    }