If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Let's run the test. Copy its method name, then open your terminal. It looks like PHPUnit installed just fine. So, run:
./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted
Oh no! It blew up! Hmmm:
Unknown database 'stripe_recording_test'
Ah, my bad!
I setup our project to use a different database for testing... and I forgot to create it! Do that with:
./bin/console doctrine:database:create --env=test
And to create the tables, run:
./bin/console doctrine:schema:create --env=test
Try the test again:
./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted
Another error! Scroll to the top! The webhook returned a 500 error. And if you look closely at the dumped response HTML, you can see the reason:
No such event: evt_00000000000000
Ah, the id
of the fake event that we're sending is evt_00000000000000
. That's
not a real event in Stripe, and so when the WebhookController
reads this and
uses Stripe's API to fetch this event, it's not there:
... lines 1 - 8 | |
class WebhookController extends BaseController | |
{ | |
... lines 11 - 13 | |
public function stripeWebhookAction(Request $request) | |
{ | |
... lines 16 - 22 | |
$stripeEvent = $this->get('stripe_client') | |
->findEvent($eventId); | |
... lines 25 - 39 | |
} | |
... lines 41 - 60 | |
} |
It's kind of funny: we added this API lookup to prevent a third-party from sending fake events... and now it's stopping us from doing exactly that. Dang!
Hmm, how to fix this? In the real world, we do want to use stripe's API to
fetch the Event
object. But in the test environment, this would all work if our
code would simply use the JSON we're sending it as the event, and skip the lookup.
Let's do it! We'll set a special configuration variable in the test environment only, then use that to change our logic in the controller.
Open app/config/config.yml
and add a new parameter: verify_stripe_event
set to
true
:
... lines 1 - 7 | |
parameters: | |
... line 9 | |
verify_stripe_event: true | |
... lines 11 - 80 |
Copy that, and open config_test.yml
. Add a parameters
key, paste this parameter,
but override it to be false
:
... lines 1 - 3 | |
parameters: | |
verify_stripe_event: false | |
... lines 6 - 25 |
Now, in WebhookController
, we just need an if statement: if
$this->getParameter('verify_stripe_event')
is true, then keep the normal behavior.
Otherwise, set $stripeEvent
to json_decode($request->getContent())
:
... lines 1 - 8 | |
class WebhookController extends BaseController | |
{ | |
... lines 11 - 13 | |
public function stripeWebhookAction(Request $request) | |
{ | |
... lines 16 - 22 | |
if ($this->getParameter('verify_stripe_event')) { | |
$stripeEvent = $this->get('stripe_client') | |
->findEvent($eventId); | |
} else { | |
// fake the Stripe_Event in the test environment | |
$stripeEvent = json_decode($request->getContent()); | |
} | |
... lines 30 - 44 | |
} | |
... lines 46 - 65 | |
} |
OK, this is not technically perfect: the first $stripeEvent
is a \Stripe\Event
object, and the second will be an instance of stdClass
. But, since you fetch data
off both the same way, it should work.
Let's see if does! Try the test again:
./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted
This time, no errors! And the dumped response content looks perfect: event handled.
But, the test didn't pass:
Failed asserting that true is false on line 43
It looks like our webhook is not working, because the subscription is still active.
But actually, that's not true: Doctrine is tricking us! In reality, the database
has been updated to show that the Subscription is canceled, but this Subscription
object is out-of-date. Query for a fresh one with $subscription = $this->em
- I
set the EntityManager on that property in setup()
- then ->getRepository('AppBundle:Subscription')
with find($subscription->getId())
:
... lines 1 - 9 | |
class WebhookControllerTest extends WebTestCase | |
{ | |
... lines 12 - 22 | |
public function testStripeCustomerSubscriptionDeleted() | |
{ | |
... lines 25 - 31 | |
$client->request( | |
'POST', | |
'/webhooks/stripe', | |
[], | |
[], | |
[], | |
$eventJson | |
); | |
$this->assertEquals(200, $client->getResponse()->getStatusCode()); | |
$subscription = $this->em | |
->getRepository('AppBundle:Subscription') | |
->find($subscription->getId()); | |
$this->assertFalse($subscription->isActive()); | |
} | |
... lines 48 - 126 | |
} |
This subscription will have fresh data.
Try the test!
./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted
And we are green!
I know, that was kind of hard! But if you want to have automated webhook tests, this is the way to do it. To make matters worse, for other webhooks, you may need to fake additional API calls that you're making to Stripe.
But, there are also a couple of other, manual, but easy ways to test. Let's check 'em out!
Hey Kiuega
Your problem is at line 20. You are trying to re-define the $container
property or your test class. Instead of using a property use a local variable and it should work
Cheers!
Okay, I have analyzed the problems I encounter.
[2020-01-23 15:16:16] request.CRITICAL: Uncaught PHP Exception Exception: "There seems to be no subscription ID sub_00000000000000"
On the one hand, it seems that I can't get the manager, because he couldn't flush the subscription if I can't get it back.
Is my way to get the manager correct in my setUp () function? Besides, I noticed that when I dump after the self :: bootKernel (), it doesn't display. But if I do it before, it works. Similarly in the other functions of the class, my dumps do not pass
Hey Kiuega!
Is my way to get the manager correct in my setUp () function?
The way you're doing it should be correct. However, you shouldn't need to have the public static $container
on your class. This property is already implemented on a parent class as a protected static property - so the code should work the same without it. And setting the $em
on a property might be ok... but it's a bit safer to get this property (from self::$container->get('doctrine')->getManager()
) whenever you needed it. In some cases, after making a request with the Client, you'll need to ask for the entity manager again. Otherwise, you'll (kind of) still be using the "old" one from the "previous request" and you can get some odd results.
Besides, I noticed that when I dump after the self :: bootKernel (), it doesn't display. But if I do it before, it works. Similarly in the other functions of the class, my dumps do not pass
I'm familiar with this too :). When Symfony boots, it does some magic with dump(). This is usually cool magic, but in the case of tests, I find that it often means that the dump is hidden, which I don't actually want. I often use var_dump() in tests to make sure that I see my dumped output. That should help you debug.
Let us know how it's going!
Cheers!
Thank you for your reply ! I have made good progress on debugging. I still have a few concerns, well, especially a big one.
Since I'm on Symfony 4, the way to handle environment variables is not the same.
To be able to use the variable<b> "VERIFY_STRIPE_EVENT",</b> I therefore did simply like this:
In my <b>.env</b>, I set it to <b>true</b>.
In my <b>.env.test</b>, I set it to <b>false</b>.
That's all i did. And in my controller, if I do a <b>$logger->info(getenv("VERIFY_STRIPE_EVENT"));</b>
I've always an empty result.
PS: Sorry for my bad english, I'm French
Hey Kiuega !
You did perfectly with setting up the environment variable. Nice job!
So, I would expect it to work. And... it might be working - the problem with logging is that the "true" and "false" values will both print as empty strings. You could try this:
$logger->info(var_export(getenv("VERIFY_STRIPE_EVENT")), true);
That should print "true" or "false". There is one other detail. I don't think this is causing the issue, but it is something I want to mention. You typically don't reference environment variables in your code with getenv
. It's not a huge deal, but you're supposed to inject these as normal variables. The easiest way to do this is with a "bind":
# config/services.yaml
services:
_defaults:
# ... the other stuff
bind:
$shouldVerifyStripeEvent = '%env(VERIFY_STRIPE_EVENT)%'
With this, you can now have a $shouldVerifyStripeEvent
argument to any controller or as an argument to any service constructor and Symfony will pass you the environment variable.
Let us know if this helps!
Cheers!
Okay ! So, the good line, I think; was :$logger->info(var_export(getenv("VERIFY_STRIPE_EVENT"), true));
It's okay, I've now the good thing in my log !
Next, for the bind, great! However, I did not understand how I can retrieve the variable from my controller?
With this code
:`
$logger->info(var_export(getenv("VERIFY_STRIPE_EVENT"), true));
$verifyStripeEvent = var_export(getenv("VERIFY_STRIPE_EVENT"), true);
if($verifyStripeEvent)
{
$logger->info("env : dev");
$stripeEvent = $stripeClient->findEvent($eventId);
}
else
{
// test environment
$logger->info("env : test");
$stripeEvent = json_decode($request->getContent());
}```
In this case, if I do a simple <b>var_dump ()</b> to see what it should actually return, I get this: <b>string(5) "false"</b>
PS: Could you tell me how to format my code on this site when I paste code to make it clearer? Thank you !
<b><u>EDIT </u></b>: I tried something like `if(boolval($verifyStripeEvent))` but same. However the variable seems to be false.
On the other hand, I forced the passage in the <b>else()</b> to see what it will produce when we will have fixed the problem.
When it goes into the else, it seems that everything is going as planned compared to the video! We are not very far!
Hey Kiuega!
Next, for the bind, great! However, I did not understand how I can retrieve the variable from my controller?
You can add it as an argument to any controller method in your system:
/**
* . @Route("/any-page")
*/
public function anyPage($shouldVerifyStripeEvent)
{
}
That's it! The variable must be called $shouldVerifyStripeEvent
because that's what you set up in your bind.
In this case, if I do a simple var_dump () to see what it should actually return, I get this: string(5) "false"
Ah, actually, I think there is a problem with how you're setting the env var - and I missed it before. In .env
and .env.test
use the values "0" and "1" instead of true and false. Environment variables are always strings - that's just the nature of how they work. So using true/false will be the strings true/false, which both should actually look "true".
PS: Could you tell me how to format my code on this site when I paste code to make it clearer? Thank you !
Yea, it's super ugly :p. For inline code like this
, surround the code with <code> and </code>. For multiline code, you need to almost do the same thing, but you need to use both <pre><code> in the beginning and </code></pre> at the end... which is ugly (but then the code looks great!).
Cheers!
Perfect man! I was just writing a comment on using 0 and 1 instead of true and false!
And yes it worked!
I'll take care of putting everything back in order and I'll tell you about some things to worry about (but safe it would seem)!
EDIT : Perfect man ! We are done ! Thanks ! So, the final output : https://zupimages.net/up/20/04/vqro.png
There are some errors, but which do not seem to bother. One of which concerns Doctrine and the method -> flush (). But in the test, at no time did I give an argument, so it's weird. (And by the way, very annoying that this way of doing things is deprecated)
Next, I have the answer, so perfect it confirms that the test is a success compared to our expectations.
Now, the final files, but which certainly contain a few lines that I could have done better:
<b>WebhookController.php :</b> https://pastebin.com/VgSMHg8z
<b>WebhookControllerTest.php :</b> https://pastebin.com/8icisnj8
<b>.env :</b> VERIFY_STRIPE_EVENT=1
<b>.env.test :</b> VERIFY_STRIPE_EVENT=0
<b>services.yaml:</b>
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$shouldVerifyStripeEvent: '%env(VERIFY_STRIPE_EVENT)%'```
I think we are not bad and I hope that it will help the next who pass by! If you have something to hesitate to complete do not hesitate it is with pleasure and thank you very much for your help (2 days I'm on it anyway ahah)
I try to rework this in Flex and Symfony 4.
At the end of this leg when I run ./vendor/bin/simple-phpunit --filter testStripeCustomerSubscriptionDeleted I should have green colors. But test doesn't pass. I still have 500 status code instead of 200 status code failure.
I have the repo at address https://github.com/petre-sy.... As far as I can see ./vendor/bin/simple-phpunit --filter testStripeCustomerSubscriptionDeleted doesn't hit https://localhost:8000. I think that's the problem. Probably I must set some parameter base-url or something like that somewhere but I can't really grasp where this setting must be put.
Cheers
Hey Diaconescu,
Look at logs! You can tail them with:
$ tail -f var/log/test.log
Most probably you just set up Stripe credentials wrong or forget to set up them at all :)
Cheers!
tail -f var/log/test.log produce something like this:
request.CRITICAL: Uncaught PHP Exception Symfony\Component\DependencyInjection\Exception\EnvNotFoundException: "Environment variable not found: "STRIPE_SECRET_KEY"." at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/symfony/dependency-injection/EnvVarProcessor.php line 96 {"exception":"[object] (Symfony\\Component\\DependencyInjection\\Exception\\EnvNotFoundException(code: 0): Environment variable not found: \"STRIPE_SECRET_KEY\". at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/symfony/dependency-injection/EnvVarProcessor.php:96)"} []
I have in .env.local something like so:
STRIPE_PUBLIC_KEY=.*****************
STRIPE_SECRET_KEY=*****************
In services.yaml:
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/cur...
parameters:
locale: 'en'
stripe_public_key: '%env(STRIPE_PUBLIC_KEY)%'
stripe_secret_key: '%env(STRIPE_SECRET_KEY)%'
verify_stripe_event: '%env(VERIFY_STRIPE_EVENT)%'
In StripeClient.php I have:
class StripeClient {
/**
* @var EntityManagerInterface
*/
private $em;
public function __construct($secretKey, EntityManagerInterface $em){
$this->em = $em;
\Stripe\Stripe::setApiKey($secretKey);
}
public function createCustomer(User $user, $paymentToken){
$customer = \Stripe\Customer::create([
"email" => $user->getEmail(),
"source" => $paymentToken // obtained with Stripe.js
]);
Acordingly to tail -f var/log/test.log you seem to be right. But why this settings doesn't present any problem in web interface? Because in web interface I could charge the credit card, I could update the credit card, I could cancel a subscription, I could reactivate a subscription, I could apply for a subscription. Why all of this work? They are dependent on STRIPE_SECRET_KEY too
Hey Diaconescu
This happens only when running your tests, right? That's because env vars are isolated from environmets. You will have to create a new .env.test
file and duplicate your env vars there
You was right about duplicating env. vars in env.test file. Errors about STRIPE_SECRET_KEY disappeared, but I can't get rid of 'No such event: evt_00000000000000' critical error.
I have in .env.test:
VERIFY_STRIPE_EVENT=false
STRIPE_PUBLIC_KEY=********************************
STRIPE_SECRET_KEY=********************************
I have in .env.local:
STRIPE_PUBLIC_KEY=***************************************
STRIPE_SECRET_KEY=***************************************
VERIFY_STRIPE_EVENT=true
I have in services.yaml:
parameters:
locale: 'en'
stripe_public_key: '%env(STRIPE_PUBLIC_KEY)%'
stripe_secret_key: '%env(STRIPE_SECRET_KEY)%'
verify_stripe_event: '%env(VERIFY_STRIPE_EVENT)%'
In WebhookController I have:
if($this->getParameter('verify_stripe_event')){
$stripeEvent = $this->stripeClient->findEvent($eventId);
} else {
//fake the stripe event in the test environment
$stripeEvent = json_decode($request->getContent());
}
Hmm, that's weird, is there a stack trace? I would like to see it
Anyway, try adding this env vars to your .env.test
file
KERNEL_CLASS='App\Kernel' # Your Kernel namespace
APP_SECRET='s$cretf0rt3st'
SHELL_VERBOSITY=-1
First 2 lines were already there. i added the last 1.
The complete result of tail -f var/log/test.log is:
[2019-06-11 17:41:20] doctrine.DEBUG: "START TRANSACTION" [] []
[2019-06-11 17:41:20] doctrine.DEBUG: INSERT INTO user (stripe_customer_id, email, roles, password, card_brand, card_last4) VALUES (?, ?, ?, ?, ?, ?) {"1":null,"2":"fluffy1878526223@sheep.com","3":[],"4":"$2y$13$7nBZfmwsPQNjSImNJQS [...]","5":null,"6":null} []
[2019-06-11 17:41:20] doctrine.DEBUG: INSERT INTO subscription (stripe_subscription_id, stripe_plan_id, ends_at, billing_period_ends_at, user_id) VALUES (?, ?, ?, ?, ?) {"1":"sub_STRIPE_TEST_XYZ1183136485","2":"plan_STRIPE_TEST_ABC499083006","3":null,"4":"2019-07-11 17:41:20","5":22} []
[2019-06-11 17:41:20] doctrine.DEBUG: "COMMIT" [] []
[2019-06-11 17:41:21] request.INFO: Matched route "webhook_stripe". {"route":"webhook_stripe","route_parameters":{"_route":"webhook_stripe","_controller":"App\\Controller\\WebhookController::stripeWebhookAction"},"request_uri":"http://localhost/webhooks/stripe","method":"POST"} []
[2019-06-11 17:41:21] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
[2019-06-11 17:41:21] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginAuthenticator"} []
[2019-06-11 17:41:21] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginAuthenticator"} []
[2019-06-11 17:41:21] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
[2019-06-11 17:41:22] request.CRITICAL: Uncaught PHP Exception Stripe\Error\InvalidRequest: "No such event: evt_00000000000000" at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/stripe/stripe-php/lib/ApiRequestor.php line 210 {"exception":"[object] (Stripe\\Error\\InvalidRequest(code: 0): No such event: evt_00000000000000 at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/stripe/stripe-php/lib/ApiRequestor.php:210)"} []
Ok, so it's hitting Stripe's API the thing is such event does not exist on your Stripe account. Check your test code, probably the subscription id doesn't exist
// composer.json
{
"require": {
"php": ">=5.5.9, <7.4",
"symfony/symfony": "3.1.*", // v3.1.10
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.8
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.2
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.3.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
"stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
"stripe/stripe-php": "^3.15", // v3.23.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
"phpunit/phpunit": "^5.5", // 5.7.20
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.4
"symfony/phpunit-bridge": "^3.0", // v3.3.0
"hautelook/alice-bundle": "^1.3", // v1.4.1
"doctrine/data-fixtures": "^1.2" // v1.2.2
}
}
Hello, I try to make the test on Symfony 4.
My WebhookControllerTest seems https://pastebin.com/kGTXLXPG
But when I run test with
php bin/phpunit --filter testStripeCustomerSubscriptionDeleted
I've this error :
Fatal error: Cannot redeclare static Symfony\Bundle\FrameworkBundle\Test\WebTestCase::$container as non static App\Tests\Controller\WebhookControllerTest::$container in C:\Users\user\Desktop\StripeFormation2\tests\Controller\WebhookControllerTest.php on <br />line 11