Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock All External Requests #2

Closed
MannikJ opened this issue Jun 16, 2023 · 3 comments
Closed

Mock All External Requests #2

MannikJ opened this issue Jun 16, 2023 · 3 comments

Comments

@MannikJ
Copy link
Contributor

MannikJ commented Jun 16, 2023

First of all, thank you for this great package!

However, I'm struggling with mocking the API calls in my tests. I managed to create mocks for the different API classes like OrderV0Api. This is the basic code to create the mocks of the API classes.

    protected function credentials(): Credentials
    {
        return new Credentials($this->credentialsArray());
    }
    
    public function credentialsArray(): array
    {
        return [
            'client_id' => config('spapi.single.lwa.client_id'),
            'client_secret' => config('spapi.single.lwa.client_secret'),
            'refresh_token' => config('spapi.single.lwa.refresh_token'),
            'role_arn' => config('spapi.aws.role_arn'),
            'region' => config('spapi.single.endpoint'),
        ];
    }

    private const EMPTY_CONFIG = [
        'lwaClientId' => '',
        'lwaClientSecret' => '',
        'lwaRefreshToken' => '',
        'awsAccessKeyId' => '',
        'awsSecretAccessKey' => '',
        'endpoint' => Endpoint::EU_SANDBOX,
    ];

    public function mockSpapi(string $apiClass, array $responses, $mockTokensApi = true)
    {
        if ($mockTokensApi) {
            $this->mockTokensApi();
        }

        $mock = new MockHandler($responses);

        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);

        $creds = $this->credentials();

        $config = $creds->toSpApiConfiguration();

        $config->setEndpoint(Endpoint::EU_SANDBOX);
        //$this->mockAuthentication($config);

        $this->app->singleton($apiClass, fn () => new $apiClass($config, $client));
    }

But I realized later that this does not mock the Token requests etc. which are made in the background before calling these API endpoints, because the classes create their own instances of API classes which then create the unmocked requests.
So I tinkered a bit with it and tried to mock the authentication part as well.
But I am very confused about the different classes like AuthorizationSigner, RequestSigner, Authentication, Credentials, Configuration and what not. You can see that in the code I've come up with so far:

     public function mockAuthentication(Configuration $configuration)
    {
        $accessToken = "the-access-token";

        $authSigner = $this->createMock(AuthorizationSignerContract::class);
        $authSigner->expects($this->any())
            ->method('sign')
            ->will(
                $this->returnCallback(function ($request) {
                    return $request;
                })
            );

        $client = new Client([
            'handler' => new MockHandler([
                new Response(200, [], "{\"access_token\": \"{$accessToken}\", \"expires_in\": 60}")
            ]),
        ]);

        $auth = new Authentication(
            self::EMPTY_CONFIG + [
                'authorizationSigner' => $authSigner,
                'authenticationClient' => $client,
            ]
        );

        $configuration->setRequestSigner($auth);
    }

    public function mockTokensApi(array $responses = null)
    {
        $accessToken = "the-access-token";

        $responses = !$responses
            ? [
                new Response(200, [], "{\"access_token\": \"{$accessToken}\", \"expires_in\": 3660}"),
            ]
            : $responses;

        $this->mockSpapi(TokensV20210301Api::class, $responses, false);
    }

You see its quite chaotic because I don't understand 100% what I'm actually doing.

I found that setting the Endpoint to EU_SANDBOX should avoid requesting restrictedDataTokens, but this can not be set from .env vars, because the function SellingPartnerApi::regionToEndpoint throws an exception when SPAPI_ENDPOINT_REGION=EU_SANDBOX.

I just want a setup that ensures that there are no outgoing requests made. I would appreciate help on how to set this up in a simple but robust way.

@jlevers
Copy link
Contributor

jlevers commented Jul 2, 2023

Hey @MannikJ, sorry for taking so long to get back to you! Looks like I somehow didn't have issue notifications on for this repository.

Embarrassing truth: I have written precisely 0 tests for this library, or for the jlevers/selling-partner-api package that supports it. In order to properly mock this library, jlevers/selling-partner-api would need to be majorly updated so that it could be mocked effectively.

I found that setting the Endpoint to EU_SANDBOX should avoid requesting restrictedDataTokens, but this can not be set from .env vars, because the function SellingPartnerApi::regionToEndpoint throws an exception when SPAPI_ENDPOINT_REGION=EU_SANDBOX.

Someone else reported this issue with made a recent update to jlevers/selling-partner-api that I think will fix the Token API issue when using the sandbox endpoints. I'm working on an update to this library that will allow you to use sandbox endpoints – it's a little bit complicated in single-user mode since all API classes are auto-injected, without any user intervention. I'll update this issue once I've got that sorted :)

@MannikJ
Copy link
Contributor Author

MannikJ commented Jul 11, 2023

I have actually excluded your ServiceProvider from auto-discovery and wrote an extension of it which skips registration when in testing environment, because I realized that the token requests were made directly when the API singletons are registered. Then when I actually need the APIs in a test I create a mock for it:

It looks like this:

    public function mockSpapi(string $apiClass, array $responses, $mockAuthentication = true)
    {
        $mock = new MockHandler($responses);

        $handlerStack = HandlerStack::create($mock);
        $client = new Client(['handler' => $handlerStack]);

        $creds = $this->credentials();

        // To avoid token requests:
        Cache::put($creds->getAccessTokenCacheKey(), '123');
        Cache::put($creds->getExpiresAtCacheKey(), 3600);

        $config = $creds->toSpApiConfiguration();

        $config->setEndpoint(Endpoint::EU_SANDBOX);

        if ($mockAuthentication) {
            $this->mockAuthentication($config);
        }

        $this->app->singleton($apiClass, fn () => new $apiClass($config, $client));
    }

    public function mockAuthentication(Configuration $configuration)
    {
        $authSigner = $this->createMock(AuthorizationSignerContract::class);
        $authSigner->expects($this->any())
            ->method('sign')
            ->will(
                $this->returnCallback(function ($request) {
                    return $request;
                })
            );

        $requestSigner = $this->createMock(RequestSignerContract::class);
        $requestSigner->expects($this->any())
            ->method('signRequest')
            ->will(
                $this->returnCallback(function ($request) {
                    return $request;
                })
            );

        $this->mockTokensApi();

        $configuration->setRequestSigner($requestSigner);
    }

    public function mockTokensApi(array $responses = null)
    {
        $accessToken = 'the-access-token';

        $responses = ! $responses
            ? [
                new Response(200, [], "{\"access_token\": \"{$accessToken}\", \"expires_in\": 3660}"),
                new Response(200, [], "{\"restricted_data_token\": \"{$accessToken}\", \"expires_in\": 3660}"),

            ]
            : $responses;

        $this->mockSpapi(TokensV20210301Api::class, $responses, false);
    }

This was referenced Jul 21, 2023
Merged
@jlevers
Copy link
Contributor

jlevers commented Aug 6, 2024

It should be possible to mock requests using Saloon's mocking infrastructure as of v2.x, because v7 of jlevers/selling-partner-api uses Saloon.

@jlevers jlevers closed this as completed Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants