laravel-spapi
laravel-spapi copied to clipboard
Mock All External Requests
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.
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 :)
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);
}
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.