Laravel Cashier - Unit Tests with creating subscriptions with payment methods - laravel

Looking deeper into this, I'm not sure it's even possible, which is a shame because I'm trying to learn TDD.
I would like to test my model with Billable being created and subscribed to a plan.
/** #test */
public function an_account_can_subscribe_to_a_plan() {
$account = factory("App\Account")->create();
Stripe::setApiKey('key');
$paymentMethod = PaymentMethod::create([
'type' => 'card',
'card' => [
'number' => '4242424242424242',
'exp_month' => '2',
'exp_year' => '2021',
'cvc' => '314'
]
]);
$subscription = $account->newSubscription('default', 'starter')->create($paymentMethod);
$this->assertTrue( $subscription->valid() );
}
The Laravel Cashier docs show how to send a token via Stripe.js, but that doesn't work for Unit Testing.
I've tried to include the Stripe library directly and create a PaymentMethod object, but this also requires me set an API key manually. Now the error I'm getting is that I have to verify my phone number to send raw credit card numbers to the stripe API.
I'm hoping there's a better way. How can I use Laravel Cashier in a TDD way and mock up fake subscriptions with fake payment methods?

Stripe provides not only test card numbers, but tokens and payment method's.
https://stripe.com/docs/testing#cards
Click the PaymentMethods tab. The value (for example pm_card_visa) can be used directly on the server-side in your tests without the need of front-end payment-intent implementation.
This is an example of a feature test I have:
/**
* #test
*/
public function a_user_can_subscribe_to_a_paid_plan()
{
$this->actingAs($this->user);
$this->setUpBilling();
$enterprise = Plan::where('name', 'Enterprise')->first()->stripe_plan_id;
$response = $this->post(route('paywall.payment'), [
'payment_method' => 'pm_card_visa',
'stripe_plan_id' => $enterprise
])
->assertSessionDoesntHaveErrors()
->assertRedirect();
}
Your tests may vary, but you can make a normal request to your billing controller with these test payment methods and it goes through just as it would if you do it on the front-end.

You may want to use stripe-mock for mocking during tests. If that doesn't fit your needs, mocking the objects directly may be a better option.

Related

Can I change this feature test to a unit test?

I have written two tests: one that makes a post request to an endpoint and awaits for a specific response containing status and message; and another one making the exact same request, but instead of await a response, it verifies if the database has the data matching what I just sent. Both these test are feature tests, and so far I have no unit test in my application; that happens because I have tested endpoint only.
So my idea is the following: instead of making a call to an endpoint in my second test, I could directly test my service method that creates a new register to the database. Would this be a valid unit test?
Personally, I think it would be valid because I am isolating a specific method and testing if the code works, and not if the integration works, even though there's integration of my code with the DB (Eloquent), my service method is the closest testable thing to the DB I have in my system.
My two tests, in the order I specified above:
/** #test */
public function a_group_can_be_created()
{
$this->withoutExceptionHandling()->signIn();
$group_data = [
'name' => $this->faker->word(),
'status' => $this->faker->boolean(),
];
$modules = ['modules' => Modules::factory(1)->create()->pluck('id')];
$response = $this->post(route('cms.groups.store'), array_merge($group_data, $modules));
$response->assertSessionHas('response', cms_response(trans('cms.groups.success_create')));
}
/** #test */
public function creating_a_group_persists_its_data_to_the_database()
{
$this->withoutExceptionHandling()->signIn();
$group_data = [
'name' => $this->faker->word(),
'status' => $this->faker->boolean(),
];
$modules = ['modules' => Modules::factory(1)->create()->pluck('id')];
$this->post(route('cms.groups.store'), array_merge($group_data, $modules));
$this->assertDatabaseHas('groups', $group_data);
$this->assertDatabaseCount('modules', 2);
$this->assertDatabaseCount('group_modules', 2);
}
Unit test in laravel "do not boot your Laravel application and therefore are unable to access your application's database or other framework services"
With that said, you can't access any database with a facade or with your eloquent model
so if you change your feature testing to unit testing, it will fail.
Unit test will work well if you don't use any of laravel framework utilities. I occasionally use it for testing a little self made library
But if you want to isolate it and create feature testing without calling the API endpoint, it will works too. It's really up to you to decide whether it is necessary to do that or not. But keep in mind that unnecessary test will make the test longer, especially if you use RefreshDatabase or DatabaseMigration trait. It will quite annoying to wait for them to finish

External API auth should be a ServiceProvider or Controller in Laravel?

Im building an app using Laravel, that app will allow the customers to send and get contacts from/to Mautic (email marketing software) via API, but first the app need to get an autorization to use Mautic API and store the user credintals to database for future use.
This is an example for how i make an autorization to connect to that api
$settings = [
'userName' => '', // Create a new user
'password' => '', // Make it a secure password
];
// Initiate the auth object specifying to use BasicAuth
$initAuth = new ApiAuth();
$auth = $initAuth->newAuth($settings, 'BasicAuth');
and then i can get the contact by id using this
$api = new MauticApi(); //This class is from the API package
$contactApi = $api->newApi('contacts', $auth, $apiUrl);
$response = $contactApi->get($id);
So my question is how can i organize that logic, should i just put all of it in a controller or it's better to create a service provider which should be responsible of the autorization and then return $auth handle that i can use then later for each customer, and if it's better to user the serviceprovider approch then i'm wondering how can i do that, should i just put the autorization logic in the boot method?

How can I validate GET controller params in CakePHP 2?

Given this on the model:
public $validate = [
'amount' => array(
'rule' => array('comparison', '>=', 0),
'message' => 'You must buy over 0 of this item!'
)
];
How can I validate param #2 of the below?
public function buy(int $item, int $amount) {
Validation seems to be built only for POST, which I'd like to opt out of here.
First things first, modifying the database with GET requests is an anti-pattern for many different reasons. Even if you assume a friendly user agent (which you never should!), browsers can behave quirky and do unexpected stuff like for example sending GET request multiple times (that is perfectly valid as GET is not ment to modify data), which they usually won't for POST/PUT/DELETE.
I would strongly suggest to change your endpoint to handle POST requests instead.
That being said, you can generally validate whatever you want, the validation mechanisms first and foremost just validate data, they don't know or care where it stems from. You can hand over whatever data you want to your model, and let it validate it:
$data = array(
'item' => $item,
'amount' => $amount,
);
$this->ModelName->set($data);
if ($this->ModelName->validates()) {
// data is valid
} else {
// data is invalid
$errors = $this->ModelName->validationErrors;
}
Moreover you can use CakePHP's validation methods completely manually too:
App::uses('Utility', 'Validation');
$isValid = Validation::comparison($amount, '>' 0);
This example of course doesn't make too much sense, given that $isValid = $amount > 0 would do the same, however it should just show that you can validate anything everywhere without models being involved.
See also
Cookbook > Models > Data Validation > Validating Data from the Controller
Cookbook > Models > Data Validation > Core Validation Rules

Support 3D Secure card Stripe payment to start subscription

My payment model idea for my application is really simple: Having a (laravel) website with a member area and some special funcionality, whereas a member account costs 19.90 / year. I wanted to integrate Stripe to my registration flow to allow a payment to happen. When the payment has succeeded, I create a subscription which will then automatically renew this payment each year.
So good so far - I managed to get it working using the Guide on how to set up a subscription by Stripe. However, cards that required 3D Secure authentication did not work yet, and this is a must-have.
So I read further and used a PaymentIntent (API Docs). However, current behavior is the following:
I create a PaymentIntent and pass the public key to the frontend
Customer enters credentials and submits
3D Secure Authentication happens correctly, returning me a payment_method_id
On the server side, I retrieve the PaymentIntent again. It has status succeeded and the payment is recieved on my Stripe Dashboard.
I then create the customer object (with the payment method I got from the PaymentIntent), and with that customer, create the subscription
The subscription has status incomplete and it seems that the subscription tries to again charge the customer but fails because of the 3D Secure validation that would be necessary the second time.
So my actual question is: How can I create a subscription which notices somehow that the customer has already paid with my PaymentIntent and the PaymentMethod that I'm passing to it?
Some Code
Create the PaymentIntent and pass that to the frontend
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
$intent = \Stripe\PaymentIntent::create([
'amount' => '1990',
'currency' => 'chf',
]);
$request->session()->put('stripePaymentIntentId',$intent->id);
return view('payment.checkout')->with('intentClientSecret',$intent->client_secret);
Frontend Checkout when clicking "Buy"
// I have stripe elements (the card input field) ready and working
// using the variable "card". The Stripe instance is saved in "stripe".
// Using "confirmCardPayment", the 3DS authentication is performed successfully.
stripe.confirmCardPayment(intentClientSecret,{
payment_method: {card: mycard},
setup_future_usage: 'off_session'
}).then(function(result) {
$('#card-errors').text(result.error ? result.error.message : '');
if (!result.error) {
submitMyFormToBackend(result.paymentIntent.payment_method);
}
else {
unlockPaymentForm();
}
});
Backend after submitting
// Get the PaymentMethod id from the frontend that was submitted
$payment_method_id = $request->get('stripePaymentMethodId');
// Get the PaymentIntent id which we created in the beginning
$payment_intent_id = $request->session()->get('stripePaymentIntentId');
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
// Get the Laravel User
$user = auth()->user();
// Firstly load Payment Intent to have this failing first if anything is not right
$intent = \Stripe\PaymentIntent::retrieve($payment_intent_id);
if ($intent instanceof \Stripe\PaymentIntent) {
// PaymentIntent loaded successfully.
if ($intent->status == 'succeeded') {
// The intent succeeded and at this point I believe the money
// has already been transferred to my account, so it's paid.
// Setting up the user with the paymentMethod given from the frontend (from
// the 3DS confirmation).
$customer = \Stripe\Customer::create([
'payment_method' => $payment_method_id,
'email' => $user->email,
'invoice_settings' => [
'default_payment_method' => $payment_method_id,
],
]);
$stripeSub = \Stripe\Subscription::create([
'customer' => $customer->id,
'items' => [
[
'plan' => env('STRIPE_PLAN_ID'),
]
],
'collection_method' => 'charge_automatically',
'off_session' => false,
]);
// If the state of the subscription would be "active" or "trialing", we would be fine
// (depends on the trial settings on the plan), but both would be ok.
if (in_array($stripeSub->status,['active','trialing'])) {
return "SUCCESS";
}
// HOWEVER the state that I get here is "incomplete", thus it's an error.
else {
return "ERROR";
}
}
}
I finally got a working solution running for my site. It goes like this:
1 - Backend: Create a SetupIntent
I created a SetupIntent (SetupIntent API Docs) to cover the checkout flow entirely. The difference to a PaymentIntent (PaymentIntent API Docs) is that the PaymentIntent goes from collecting the card details, preparing the payment and effectively transferring the amount to the account, while the SetupIntent only prepares card collection, but does not yet execute the payment. You will get a PaymentMethod (PaymentMethod API Docs) from it, which you can use later.
$intent = SetupIntent::create([
'payment_method_types' => ['card'],
]);
Then I passed the $intent->client_secret key to my client side JavaScript.
2 - Frontend: Collect card details with Elements
On the frontend, I placed the Stripe card element to collect the card details.
var stripe = Stripe(your_stripe_public_key);
var elements = stripe.elements();
var style = { /* my custom style definitions */ };
var card = elements.create('card',{style:style});
card.mount('.my-cards-element-container');
// Add live error message listener
card.addEventListener('change',function(event) {
$('.my-card-errors-container').text(event.error ? event.error.message : '');
}
// Add payment button listener
$('.my-payment-submit-button').on('click',function() {
// Ensure to lock the Payment Form while performing async actions
lockMyPaymentForm();
// Confirm the setup without charging it yet thanks to the SetupIntent.
// With 3D Secure 2 cards, this will trigger the confirmation window.
// With 3D Secure cards, this will not trigger a confirmation.
stripe.confirmCardSetup(setup_intent_client_secret, {
payment_method: {card: card} // <- the latter is the card object variable
}).then(function(result) {
$('.my-card-errors-container').text(event.error ? event.error.message : '');
if (!result.error) {
submitPaymentMethodIdToBackend(result.setupIntent.payment_method);
}
else {
// There was an error so unlock the payment form again.
unlockMyPaymentForm();
}
});
}
function lockMyPaymentForm() {
$('.my-payment-submit-button').addClass('disabled'); // From Bootstrap
// Get the card element here and disable it
// This variable is not global so this is just sample code that does not work.
card.update({disabled: true});
}
function unlockMyPaymentForm() {
$('.my-payment-submit-button').removeClass('disabled'); // From Bootstrap
// Get the card element here and enable it again
// This variable is not global so this is just sample code that does not work.
card.update({disabled: false});
}
3 - Backend: Create Customer and Subscription
On the backend, I received the $payment_method_id which I submitted from the frontend.
Firstly, we need now to create a Customer (Customer API Docs) if it does not yet exist. On the customer, we will attach the payment method from the SetupIntent. Then, we create the Subscription (Subscription API Docs) which will start the charge from the SetupIntent.
$customer = \Stripe\Customer::create([
'email' => $user->email, // A field from my previously registered laravel user
]);
$paymentMethod = \Stripe\PaymentMethod::retrieve($payment_method_id);
$paymentMethod->attach([
'customer' => $customer->id,
]);
$customer = \Stripe\Customer::update($customer->id,[
'invoice_settings' => [
'default_payment_method' => $paymentMethod->id,
],
]);
$subscription = \Stripe\Subscription::create([
'customer' => $customer->id,
'items' => [
[
'plan' => 'MY_STRIPE_PLAN_ID',
],
],
'off_session' => TRUE, //for use when the subscription renews
]);
Now we have a Subscription object. With regular cards, the state should be active or trialing, depending on your trial days setting on the subscription. However when dealing with 3D Secure test cards, I got the subscription still in an incomplete state. According to my Stripe support contact, this can also be a problem because of not yet fully working 3D Secure test cards. However I assume that this can also happen on production environments with some sort of cards, so we have to deal with it.
On subscriptions with status incomplete you can retrieve the latest invoice from $subscription->latest_invoice like so:
$invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice);
On your invoice object, you will find a status and a hosted_invoice_url. When the status is still open, I now present the user the URL to the hosted invoice which he has to complete first. I let him open the link in a new window, which shows a nice looking invoice hosted by stripe. There, he is free to again confirm his credit card details including the 3D Secure workflow. In case he succeeds there, the $subscription->status changes to active or trialing after you re-retrieve the subscription from Stripe.
This is some sort of fool proof strategy that if anything with your implementation goes wrong, just send them to Stripe to complete it. Just be sure to hint the user that in case he has to confirm his card twice, it won't be charged twice but only once!
I was not able to create a working version of #snieguu's solution because I wanted to use Elements and not collect the credit card details separately to then create a PaymentMethod by myself.
Have You considered the opposite approach that payment intents(Also the first one) will be generated by subscription - not created manually?
So the flow will be:
Create a payment method
Create Customer(using the payment method)
Create Subscription(using Customer and payment method) - that creates also the first invoice
Retrieve payment intent from Subscription by latest_invoice.payment_intent.id. Here You can choose if this should be handled by You or Stripe. See this: How to get PaymentIntent next_action.type = redirect_to_url instead of use_stripe_sdk for Subscription
Allow finishing 3D secure flow
You have a constant price for a subscription, so it will be charged upfront:
https://stripe.com/docs/billing/subscriptions/multiplan#billing-periods-with-multiple-plans
Conventional plans that charge a fixed amount on an interval are billed at the start of each billing cycle.

Testing Laravel Nova

Currently I'm trying to write feature tests for laravel nova that assert that the page is loaded correctly and data can be seen.
However when I write the tests I can't find a way to assert that the correct text is shown due to way laravel nova's data is produce. Ontop of that I can't seem to test if a page loads correctly with laravel nova's 404 page coming back as a 200 response when a resource page that doesn't exist loads.
Has anyone found a good way to feature test nova?
TL;DR: check out this repo: https://github.com/bradenkeith/testing-nova. It has helped me find my way on how to test Laravel Nova.
Laravel Nova is basically a CRUD framework. So I'm assuming that, when you say
"that the page is loaded correctly and data can be seen"
You actually mean: my resources are loaded correctly. Or, I can create/update/delete a resource. This is because Nova is loading its resource info asynchronous via api calls.
So that's why, a good method to test your logic is to test the /nova-api/ routes.
For example:
<?php
namespace Tests\Feature\Nova;
use App\Note;
use Tests\TestCase;
class NoteTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}
/** #test */
public function it_gets_a_note()
{
// given
$note = factory(Note::class)->create();
$response = $this->get('/nova-api/notes/' . $note->id)
->assertStatus(200)
->assertJson([
'resource' => [
'id' => [
'value' => $note->id
]
]
]);
}
}
By calling the route https://my-app.test/resources/notes/1, we can assert that we're getting a 200 response (successful) and that we're actually returning our newly created Note. This is a pretty trustworthy test to be sure a resource detail page is working fine.
If, however, you are talking about Browser Testing, you might want to take a look at Laravel Dusk:
Laravel Dusk provides an expressive, easy-to-use browser automation and testing API.
Once installed, you can start real browser testing:
$user = factory(User::class)->create();
$user->assignRole('admin');
$this->browse(function (Browser $browser) use ($user) {
$browser
->loginAs($user)
->visit('/')
->assertSee('Dashboard');
});
I had the same issue, I found out that the gate in App\Providers\NovaServiceProvider.php is not letting users pass, just return true when testing only and everything must work as expected
protected function gate()
{
Gate::define('viewNova', function ($user) {
return true;
});
}
Add on app/config file in your project directory:
App\Providers\NovaServiceProvider::class,

Resources