Initial commit

This commit is contained in:
divakar-rajodiya
2024-06-25 17:45:35 +05:30
commit b145af20c4
28 changed files with 2250 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_style = space
indent_size = 2
+9
View File
@@ -0,0 +1,9 @@
* text=auto
/build export-ignore
/tests export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
nitpick.json export-ignore
phpunit.xml.dist export-ignore
+7
View File
@@ -0,0 +1,7 @@
/vendor
/node_modules
phpunit.xml
/build
.phpunit.result.cache
composer.lock
/.idea/
+116
View File
@@ -0,0 +1,116 @@
# Wearepixel's Laravel QuickBooks Client
[![Latest Stable Version](https://poser.pugx.org/wearepixel/laravel-quickbooks-client/v/stable)](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
[![Latest Unstable Version](https://poser.pugx.org/wearepixel/laravel-quickbooks-client/v/unstable)](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
[![Total Downloads](https://poser.pugx.org/wearepixel/laravel-quickbooks-client/downloads)](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
[![License](https://poser.pugx.org/wearepixel/laravel-quickbooks-client/license)](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
PHP client wrapping the [QuickBooks PHP SDK](https://github.com/intuit/QuickBooks-V3-PHP-SDK).
We solely use [Laravel](https://www.laravel.com) for our applications, so this package is written with Laravel in mind. If there is a request from the community to split this package into 2 parts to allow it to be used outside of Laravel, then we will consider doing that work.
## Build Status
| Branch | Status | Coverage | Code Quality |
| ------ | :----: | :------: | :----------: |
| Develop | [![Build Status](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=develop)](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=develop) | [![Code Coverage](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/badges/coverage.png?b=develop)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) |
| Master | [![Build Status](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=master)](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=master) | [![Code Coverage](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=master) | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=master) |
## Installation
1. Install QuickBooks PHP Client:
```bash
$ composer require wearepixel/laravel-quickbooks-client
```
2. Run our migration to install the `quickbooks_tokens` table:
```bash
$ php artisan migrate --package=wearepixel/laravel-quickbooks-client
```
The package uses the [auto registration feature](https://laravel.com/docs/packages#package-discovery) of Laravel.
## Configuration
1. You will need a ```quickBooksToken``` relationship on your ```User``` model. There is a trait named ```Wearepixel\QuickBooks\HasQuickBooksToken```, which you can include on your ```User``` model, which will setup the relationship. To do this implement the following:
Add ```use Wearepixel\QuickBooks\HasQuickBooksToken;``` to your service container at the top of User.php
and also add the trait within the class. For example:
```php
class User extends Authenticatable
{
use Notifiable, HasQuickBooksToken;
```
**NOTE: If your ```User``` model is not ```App\Models\User```, then you will need to configure the path in the ```configs/quickbooks.php```.**
2. Add the appropriate values to your ```.env```
#### Minimal Keys
```bash
QUICKBOOKS_CLIENT_ID=<client id given by QuickBooks>
QUICKBOOKS_CLIENT_SECRET=<client secret>
```
#### Optional Keys
```bash
QUICKBOOKS_API_URL=<Development|Production> # Defaults to App's env value
QUICKBOOKS_DEBUG=<true|false> # Defaults to App's debug value
```
3. _[Optional]_ Publish configs & views
#### Config
A configuration file named ```quickbooks.php``` can be published to ```config/``` by running...
```bash
php artisan vendor:publish --tag=quickbooks-config
```
#### Views
View files can be published by running...
```bash
php artisan vendor:publish --tag=quickbooks-views
```
## Usage
Here is an example of getting the company information from QuickBooks:
### NOTE: Before doing these commands, go to your connect route (default: /quickbooks/connect) to get a QuickBooks token for your user
```php
php artisan tinker
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
>>> Auth::logInUsingId(1)
=> App\Models\User {#1668
id: 1,
// Other keys removed for example
}
>>> $quickbooks = app('Wearepixel\QuickBooks\Client') // or app('QuickBooks')
=> Wearepixel\QuickBooks\Client {#1613}
>>> $quickbooks->getDataService()->getCompanyInfo();
=> QuickBooksOnline\API\Data\IPPCompanyInfo {#1673
+CompanyName: "Sandbox Company_US_1",
+LegalName: "Sandbox Company_US_1",
// Other properties removed for example
}
>>>
```
You can call any of the resources as documented [in the SDK](https://intuit.github.io/QuickBooks-V3-PHP-SDK/quickstart.html).
## Middleware
If you have routes that will be dependent on the user's account having a usable QuickBooks OAuth token, there is an included middleware ```Wearepixel\QuickBooks\Laravel\Filter``` that gets registered as ```quickbooks``` that will ensure the account is linked and redirect them to the `connect` route if needed.
Here is an example route definition:
```php
Route::view('some/route/needing/quickbooks/token/before/using', 'some.view')
->middleware('quickbooks');
```
+1
View File
@@ -0,0 +1 @@
4.0.4
+56
View File
@@ -0,0 +1,56 @@
{
"name": "wearepixel/laravel-quickbooks",
"description": "Wearepixel's Laravel Client for QuickBooks.",
"keywords": [
"quickbooks",
"client",
"laravel",
"wearepixel"
],
"license": "MIT",
"authors": [
{
"name": "Joel Male",
"email": "joel.male@wearepixel.com"
}
],
"require": {
"php": "^8.0.2",
"illuminate/database": "^10.0|^11.0",
"illuminate/http": "^10.0|^11.0",
"illuminate/routing": "^10.0|^11.0",
"illuminate/support": "^10.0|^11.0",
"nesbot/carbon": "^2.62.1",
"quickbooks/v3-php-sdk": "^5.4.6"
},
"require-dev": {
"laravel/pint": "^1.2",
"mockery/mockery": "^1.5.1",
"phpunit/phpunit": "^10.0",
"psy/psysh": "^0.11",
"scrutinizer/ocular": "^1.9"
},
"autoload": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "stable",
"prefer-stable": true
}
+96
View File
@@ -0,0 +1,96 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Properties for the QuickBooks SDK DataService
|--------------------------------------------------------------------------
|
| The configuration keys for the SDK are inconsistent in naming convention.
| We are adhering to snake_case. We make a sensible guess for 'base_url'
| using the app's env, but you can can it with 'QUICKBOOKS_API_URL'. Also,
| the 'redirect_uri' is made in the client from the 'quickbooks.token'
| named route, so it cannot be configured here.
|
| Most of the time, only 'QUICKBOOKS_CLIENT_ID' & 'QUICKBOOKS_CLIENT_SECRET'
| needs to be set.
|
| See: https://intuit.github.io/QuickBooks-V3-PHP-SDK/configuration.html
|
*/
'data_service' => [
'auth_mode' => 'oauth2',
'base_url' => env('QUICKBOOKS_API_URL', config('app.env') === 'production' ? 'Production' : 'Development'),
'client_id' => env('QUICKBOOKS_CLIENT_ID'),
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET'),
'scope' => 'com.intuit.quickbooks.accounting',
],
/*
|--------------------------------------------------------------------------
| Properties to control logging
|--------------------------------------------------------------------------
|
| Configures logging to <storage_path>/logs/quickbooks.log when in debug
| mode or when 'QUICKBOOKS_DEBUG' is true.
|
*/
'logging' => [
'enabled' => env('QUICKBOOKS_DEBUG', config('app.debug')),
'location' => storage_path('logs'),
],
/*
|--------------------------------------------------------------------------
| Properties to configure the routes
|--------------------------------------------------------------------------
|
| There are several routes that are needed for the package, so these
| properties allow configuring them to fit the application as needed.
|
*/
'route' => [
// Controls the middlewares for thr routes. Can be a string or array of strings
'middleware' => [
// Added to the protected routes for the package (i.e. connect & disconnect)
'authenticated' => 'auth',
// Added to all of the routes for the package
'default' => 'web',
],
'paths' => [
// Show forms to connect/disconnect
'connect' => 'connect',
// The DELETE takes place to remove token
'disconnect' => 'disconnect',
// Return URI that QuickBooks sends code to allow getting OAuth token
'token' => 'token',
],
'prefix' => 'quickbooks',
],
/*
|--------------------------------------------------------------------------
| Properties for control the "user" relationship in Token
|--------------------------------------------------------------------------
|
| The Token class has a "user" relationship, and these properties allow
| configuring the relationship.
|
*/
'user' => [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
],
];
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('quickbooks_tokens', function (Blueprint $table) {
$user_id_type =
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
? 'unsignedBigInteger'
: 'unsignedInteger';
$table->bigIncrements('id');
$table->{$user_id_type}('user_id');
$table->unsignedBigInteger('realm_id');
$table->longtext('access_token');
$table->datetime('access_token_expires_at');
$table->string('refresh_token');
$table->datetime('refresh_token_expires_at');
$table->timestamps();
$table
->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('quickbooks_tokens');
}
};
+9
View File
@@ -0,0 +1,9 @@
* text=auto
/build export-ignore
/tests export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
nitpick.json export-ignore
phpunit.xml.dist export-ignore
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the "dist(ribution)" phpunit.xml.dist file. It sets the defaults that are then over written by any files in
phpunit.xml, which is then over wrote by flags passed in via the command line. The plan is that this file is to be
used by ci to do the full suit of tests, and a developer can copy this file to phpunit.xml to trim down some of the
options.
-->
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
verbose="true">
<testsuites>
<testsuite name="Wearepixel's Laravel QuickBooks Client">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<php>
<!-- <env name="VARIABLE" value="value"/> -->
</php>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
<exclude>
<directory>./src/Http/Exceptions</directory>
<directory>./src/Providers</directory>
</exclude>
</whitelist>
</filter>
<logging>
<log type="coverage-html"
target="./build/phpunit/coverage"
lowUpperBound="35"
highLowerBound="70"/>
<log type="coverage-text"
target="php://stdout"
showOnlySummary="true"
showUncoveredFiles="false"/>
<log type="coverage-clover" target="build/phpunit/logs/clover.xml"/>
<log type="junit" target="./build/phpunit/logs/junit.xml"/>
</logging>
</phpunit>
+6
View File
@@ -0,0 +1,6 @@
{
"preset": "laravel",
"rules": {
"no_superfluous_phpdoc_tags": true
}
}
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name') }} - QuickBooks Authorization</title>
</head>
<body>
<div class="container">
<h1 class="page-heading">QuickBooks Authorization</h1>
{{-- TODO: Text here to explain what is going on --}}
{{-- TODO: Work on UI --}}
<a href="{!! $authorization_uri !!}">Connect to QuickBooks</a>
</div>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name') }} - QuickBooks Deauthorization</title>
</head>
<body>
<div class="container">
<h1 class="page-heading">QuickBooks Deauthorization</h1>
{{-- TODO: Text here to explain what is going on --}}
{{-- TODO: Work on UI --}}
<a href="{{ route('quickbooks.disconnect') }}"
onclick="event.preventDefault();document.getElementById('disconnect-form').submit();">
Disconnect from {{ $company->CompanyName }}
</a>
<form id="disconnect-form"
action="{{ route('quickbooks.disconnect') }}"
method="POST"
style="display: none;"
>
{{ method_field('DELETE') }}
{{ csrf_field() }}
</form>
</div>
</body>
</html>
+259
View File
@@ -0,0 +1,259 @@
<?php
namespace Wearepixel\QuickBooks;
use Exception;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Exception\ServiceException;
use QuickBooksOnline\API\ReportService\ReportService;
/**
* Class Client
*/
class Client
{
/**
* The configs to set up a DataService
*
* @var array
*/
protected $configs;
/**
* The DataService instance
*
* @var DataService
*/
protected $data_service;
/**
* The ReportService instance
*
* @var ReportService
*/
protected $report_service;
/**
* The Token instance
*
* @var Token
*/
protected $token;
/**
* Client constructor.
*/
public function __construct(array $configs, Token $token)
{
$this->configs = $configs;
$this->setToken($token);
}
/**
* Build URI to request authorization
*
* @throws SdkException
* @throws ServiceException
*/
public function authorizationUri(): string
{
return $this->getDataService()
->getOAuth2LoginHelper()
->getAuthorizationCodeURL();
}
/**
* Configure the logging per config/quickbooks.php
*/
public function configureLogging(): DataService
{
// In case any of the keys are not in the configs, just disable logging
try {
if (
$this->configs['logging']['enabled'] &&
dir($this->configs['logging']['location'])
) {
$this->data_service->setLogLocation($this->configs['logging']['location']);
return $this->data_service->enableLog();
}
} catch (Exception $e) {
// TODO: Figure out what to do with this exception
}
return $this->data_service->disableLog();
}
/**
* Delete the token
*
* @throws Exception
*/
public function deleteToken(): self
{
$this->setToken($this->token->remove());
return $this;
}
/**
* Convert code to an access token
*
* Upon the user allowing access to their account, there is a code sent to
* over that needs to be converted to an OAuth token.
*
* @throws SdkException
* @throws ServiceException
*/
public function exchangeCodeForToken(string $code, int $realm_id): self
{
$oauth_token = $this->getDataService()
->getOAuth2LoginHelper()
->exchangeAuthorizationCodeForToken($code, $realm_id);
$this->getDataService()->updateOAuth2Token($oauth_token);
$this->token->parseOauthToken($oauth_token)->save();
return $this;
}
/**
* Getter for the DataService
*
* Makes sure that it is setup & ready to be used.
*
* @throws SdkException
* @throws ServiceException
*/
public function getDataService(): DataService
{
if (! $this->hasValidAccessToken() || ! isset($this->data_service)) {
$this->data_service = $this->makeDataService();
$this->configureLogging();
}
return $this->data_service;
}
/**
* Getter for the ReportService
*
* Makes sure that it is setup & ready to be used.
*
* @throws SdkException
* @throws ServiceException
*/
public function getReportService(): ReportService
{
if (! $this->hasValidAccessToken() || ! isset($this->report_service)) {
$this->report_service = new ReportService($this->getDataService()->getServiceContext());
}
return $this->report_service;
}
/**
* Check to see if the token has a valid access token
*/
public function hasValidAccessToken(): bool
{
return $this->token->hasValidAccessToken;
}
/**
* Check to see if the token has a valid refresh token
*/
public function hasValidRefreshToken(): bool
{
return $this->token->hasValidRefreshToken;
}
/**
* Factory to make DataService
*
* There are 3 use cases for making a DataService....
*
* 1) Have valid access token, so ready to be used
* 2) Have valid refresh token, so renew access token & then use
* 3) No existing token, so need to link account
*
* @throws SdkException
* @throws ServiceException
*/
protected function makeDataService(): DataService
{
// Associative array to use to filter out only the needed config keys when using existing token
$existing_keys = [
'auth_mode' => null,
'baseUrl' => null,
'ClientID' => null,
'ClientSecret' => null,
];
// Have good access & refresh, so allow app to run
if ($this->hasValidAccessToken()) {
// Pull in the configs from the token into needed keys from the configs
return DataService::Configure(
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
'accessTokenKey' => $this->token->access_token,
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
}
// Have refresh, so update access & allow app to run
if ($this->hasValidRefreshToken()) {
// Pull in the configs from the token into needed keys from the configs
$data_service = DataService::Configure(
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
$oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken();
$data_service->updateOAuth2Token($oauth_token);
$this->token->parseOauthToken($oauth_token)->save();
return $data_service;
}
// Create new...
return DataService::Configure($this->parseDataConfigs());
}
/**
* QuickBooks is not consistent on their naming of variables, so map them
*/
protected function parseDataConfigs(): array
{
return [
'auth_mode' => $this->configs['data_service']['auth_mode'],
'baseUrl' => $this->configs['data_service']['base_url'],
'ClientID' => $this->configs['data_service']['client_id'],
'ClientSecret' => $this->configs['data_service']['client_secret'],
'RedirectURI' => route('quickbooks.token'),
'scope' => $this->configs['data_service']['scope'],
];
}
/**
* Allow setting a token to switch "user"
*/
public function setToken(Token $token): self
{
$this->token = $token;
// The DataService is tied to a specific token, so remove it when using a new one
unset($this->data_service);
return $this;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Wearepixel\QuickBooks;
use Illuminate\Database\Eloquent\Relations\HasOne;
trait HasQuickBooksToken
{
/**
* Have a quickBooksToken.
*/
public function quickBooksToken(): HasOne
{
return $this->hasOne(Token::class);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace Wearepixel\QuickBooks\Http\Controllers;
use Exception;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View as ViewContract;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as LaravelController;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\View;
use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Exception\ServiceException;
use Wearepixel\QuickBooks\Client as QuickBooks;
/**
* Class Controller
*/
class Controller extends LaravelController
{
// NOTE: When using constructor injection for QuickBooks, there is a race issue with the boot order of the app
/**
* Form to connect/disconnect user to QuickBooks
*
* If the user has a valid OAuth, then give form to disconnect, otherwise link to connect it
*
* @throws SdkException
* @throws ServiceException
*/
public function connect(QuickBooks $quickbooks, ViewFactory $view_factory): ViewContract|View
{
// Give view to remove token if user already linked account
if ($quickbooks->hasValidRefreshToken()) {
return $view_factory
->make('quickbooks::disconnect')
->with('company', $quickbooks->getDataService()->getCompanyInfo());
}
// Give view to link account
return $view_factory
->make('quickbooks::connect')
->with('authorization_uri', $quickbooks->authorizationUri());
}
/**
* Removes the token
*
* @throws Exception
*/
public function disconnect(
Redirector $redirector,
Request $request,
QuickBooks $quickbooks,
): RedirectResponse|View {
$quickbooks->deleteToken();
$request->session()->flash('success', 'Disconnected from QuickBooks');
return $redirector->back();
}
/**
* Accept the code from QuickBooks to request token
*
* Once a user approves linking account, then QuickBooks sends back
* a code which can be converted to an OAuth token.
*
* @throws SdkException
* @throws ServiceException
*/
public function token(
Redirector $redirector,
Request $request,
QuickBooks $quickbooks,
UrlGenerator $url_generator,
): RedirectResponse {
// TODO: Deal with exceptions
$quickbooks->exchangeCodeForToken($request->get('code'), $request->get('realmId'));
$request->session()->flash('success', 'Connected to QuickBooks');
return $redirector->intended($url_generator->route('quickbooks.connect'));
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Wearepixel\QuickBooks\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Wearepixel\QuickBooks\Client as QuickBooks;
/**
* Class Filter
*/
class Filter
{
/**
* Create a new QuickBooks filter middleware instance.
*/
public function __construct(
protected QuickBooks $quickbooks,
protected Redirector $redirector,
protected Session $session,
protected UrlGenerator $url_generator,
) {
}
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): mixed
{
if (! $this->quickbooks->hasValidRefreshToken()) {
// Set intended route, so that after linking account, user is put where they were going
$this->session->put('url.intended', $this->url_generator->to($request->path()));
return $this->redirector->route('quickbooks.connect');
}
return $next($request);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Client;
/**
* Class ClientServiceProvider
*/
class ClientServiceProvider extends LaravelServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*/
protected bool $defer = true;
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [Client::class];
}
/**
* Register the application services.
*/
public function register(): void
{
$this->app->bind(Client::class, function (Application $app) {
$token =
$app->auth->user()->quickBooksToken ?:
$app->auth
->user()
->quickBooksToken()
->make();
return new Client($app->config->get('quickbooks'), $token);
});
$this->app->alias(Client::class, 'QuickBooks');
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Http\Middleware\Filter;
/**
* Class ServiceProvider
*/
class ServiceProvider extends LaravelServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot(): void
{
$this->registerMiddleware();
$this->registerPublishes();
$this->registerRoutes();
$this->registerViews();
}
/**
* Register the application services.
*/
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../../config/quickbooks.php', 'quickbooks');
}
/**
* Register the middleware
*
* If a route needs to have the QuickBooks client, then make sure that the user has linked their account.
*/
public function registerMiddleware(): void
{
$this->app->router->aliasMiddleware('quickbooks', Filter::class);
}
/**
* There are several resources that get published
*
* Only worry about telling the application about them if running in the console.
*/
protected function registerPublishes(): void
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
$this->publishes(
[
__DIR__.'/../../config/quickbooks.php' => config_path('quickbooks.php'),
],
'quickbooks-config',
);
$this->publishes(
[
__DIR__.'/../../database/migrations' => database_path('migrations'),
],
'quickbooks-migrations',
);
$this->publishes(
[
__DIR__.'/../../resources/views' => base_path(
'resources/views/vendor/quickbooks',
),
],
'quickbooks-views',
);
}
}
/**
* Register the routes needed for the registration flow
*/
protected function registerRoutes(): void
{
$config = $this->app->config->get('quickbooks.route');
$this->app->router
->prefix($config['prefix'])
->as('quickbooks.')
->middleware($config['middleware']['default'])
->namespace('Wearepixel\QuickBooks\Http\Controllers')
->group(function (Router $router) use ($config) {
$router
->get($config['paths']['connect'], 'Controller@connect')
->middleware($config['middleware']['authenticated'])
->name('connect');
$router
->delete($config['paths']['disconnect'], 'Controller@disconnect')
->middleware($config['middleware']['authenticated'])
->name('disconnect');
$router->get($config['paths']['token'], 'Controller@token')->name('token');
});
}
/**
* Register the views
*/
protected function registerViews(): void
{
$this->loadViewsFrom(__DIR__.'/../../resources/views', 'quickbooks');
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace Wearepixel\QuickBooks;
use App\Models\User;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use QuickBooksOnline\API\Exception\SdkException;
/**
* Class Token
*
*
* @property bool $hasValidAccessToken Is the access token valid
* @property bool $hasValidRefreshToken Is the refresh token valid
* @property Carbon $access_token_expires_at Timestamp that the access token expires
* @property Carbon $refresh_token_expires_at Timestamp that the refresh token expires
* @property int $user_id Id of the related User
* @property string $access_token The access token
* @property string $realm_id Realm Id from the OAuth token
* @property string $refresh_token The refresh token
* @property User $user
*/
class Token extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'quickbooks_tokens';
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['access_token_expires_at', 'refresh_token_expires_at'];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'access_token',
'access_token_expires_at',
'realm_id',
'refresh_token',
'refresh_token_expires_at',
'user_id',
];
/**
* Check if access token is valid
*
* A token is good for 1 hour, so if it expires greater than 1 hour from now, it is still valid
*/
public function getHasValidAccessTokenAttribute(): bool
{
return $this->access_token_expires_at && Carbon::now()->lt($this->access_token_expires_at);
}
/**
* Check if refresh token is valid
*
* A token is good for 101 days, so if it expires greater than 101 days from now, it is still valid
*/
public function getHasValidRefreshTokenAttribute(): bool
{
return $this->refresh_token_expires_at &&
Carbon::now()->lt($this->refresh_token_expires_at);
}
/**
* Parse OauthToken.
*
* Process the OAuth token & store it in the persistent storage
*
* @throws SdkException
*/
public function parseOauthToken(OAuth2AccessToken $oauth_token): Token
{
// TODO: Deal with exception
$this->access_token = $oauth_token->getAccessToken();
$this->access_token_expires_at = Carbon::parse($oauth_token->getAccessTokenExpiresAt());
$this->realm_id = $oauth_token->getRealmID();
$this->refresh_token = $oauth_token->getRefreshToken();
$this->refresh_token_expires_at = Carbon::parse($oauth_token->getRefreshTokenExpiresAt());
return $this;
}
/**
* Remove the token
*
* When a token is deleted, we still need a token for the client for the user.
*
* @throws Exception
*/
public function remove(): Token
{
$user = $this->user;
$this->delete();
return $user->quickBooksToken()->make();
}
/**
* Belongs to user.
*/
public function user(): BelongsTo
{
$config = config('quickbooks.user');
return $this->belongsTo(
$config['model'],
$config['keys']['foreign'],
$config['keys']['owner'],
);
}
}
+299
View File
@@ -0,0 +1,299 @@
<?php
namespace Wearepixel\QuickBooks;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Exception\ServiceException;
/**
* Class ClientTest
*/
class ClientTest extends TestCase
{
/**
* @var Client
*/
protected $client;
/**
* @var array
*/
protected $configs = [];
/**
* @var Mock
*/
protected $token_mock;
protected function setUp(): void
{
$this->configs = [
'data_service' => [
'auth_mode' => 'oauth2',
'base_url' => 'Development',
'client_id' => 'QUICKBOOKS_CLIENT_ID',
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
'scope' => 'com.intuit.quickbooks.accounting',
],
];
$this->token_mock = Mockery::mock(Token::class);
$this->client = $this->makeClient();
}
private function makeClient($configs = null)
{
if (! is_null($configs)) {
return new Client($configs, $this->token_mock);
}
return new Client($this->configs, $this->token_mock);
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Client::class, $this->client);
}
/**
* @test
*/
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertFalse(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
/**
* @test
*/
public function it_caches_the_data_service_once_it_is_made()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received
$this->assertEquals(DataService::class, get_class($this->client->getDataService()));
}
/**
* @test
*/
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('access_token')
->andReturn('access_token');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
$this->assertTrue(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
/**
* @test
*/
public function it_returns_a_data_service_with_refreshed_token_when_access_token_expired_but_refresh_token_valid()
{
$this->expectException(ServiceException::class);
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->never()
->with('access_token');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
// TODO: This really needs to test that things are getting called correctly on the DataService class
$this->assertFalse(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
/**
* @test
*/
public function it_returns_a_report_service_using_the_data_service()
{
$this->markTestSkipped(
'Once we figure out how to test around the static DataService::Configure',
);
}
/**
* @test
*/
public function it_has_logging_off_by_default()
{
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertFalse(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
}
/**
* @test
*/
public function it_allows_logging_turned_on_and_pointed_to_expected_file()
{
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
$this->client = $this->makeClient(
array_merge(
[
'logging' => [
'enabled' => true,
'location' => '/some/valid/path',
],
],
$this->configs,
),
);
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertTrue(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
$this->assertEquals(
'/some/valid/path',
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->ServiceRequestLoggingLocation,
);
}
/**
* @test
*/
public function it_returns_self_after_deleting_token()
{
$this->token_mock
->shouldReceive('remove')
->once()
->withNoArgs()
->andReturnSelf();
$this->assertInstanceOf(Client::class, $this->client->deleteToken());
}
}
function dir($path)
{
return $path === '/some/valid/path';
}
function route($name)
{
return $name;
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User;
/**
* Class HasQuickBooksTokenTest
*/
class HasQuickBooksTokenTest extends TestCase
{
/**
* @var User
*/
protected $user;
protected function setUp(): void
{
parent::setUp();
$this->user = new User();
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(User::class, $this->user);
}
/**
* @test
*/
public function it_has_a_hasOne_relationship_to_token()
{
$this->assertInstanceOf(Token::class, $this->user->quickBooksToken()->getModel());
}
}
+250
View File
@@ -0,0 +1,250 @@
<?php
namespace Wearepixel\QuickBooks\Http\Controllers;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Session\Store;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService;
use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase;
/**
* Class ControllerTest
*/
class ControllerTest extends TestCase
{
/**
* @var Controller
*/
protected $controller;
/**
* @var Mock
*/
protected $data_service_mock;
/**
* @var Mock
*/
protected $quickbooks_mock;
/**
* @var Mock
*/
protected $redirector_mock;
/**
* @var Mock
*/
protected $request_mock;
/**
* @var Mock
*/
protected $session_mock;
/**
* @var Mock
*/
protected $view_factory_mock;
/**
* @var Mock
*/
protected $view_mock;
protected function setUp(): void
{
$this->data_service_mock = Mockery::mock(DataService::class);
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Store::class);
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
$this->view_mock = Mockery::mock(View::class);
$this->controller = new Controller();
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Controller::class, $this->controller);
}
/**
* @test
*/
public function it_shows_view_to_connect_if_account_not_linked()
{
$this->data_service_mock
->shouldReceive('getCompanyInfo')
->once()
->withNoArgs()
->andReturn([
'name' => 'Company',
]);
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
$this->quickbooks_mock
->shouldReceive('getDataService')
->once()
->withNoArgs()
->andReturn($this->data_service_mock);
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::disconnect')
->andReturn($this->view_mock);
$this->view_mock
->shouldReceive('with')
->once()
->withArgs([
'company',
[
'name' => 'Company',
],
])
->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
}
/**
* @test
*/
public function it_shows_view_to_disconnect_if_account_linked()
{
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
$this->quickbooks_mock
->shouldReceive('authorizationUri')
->once()
->withNoArgs()
->andReturn('http://uri');
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::connect')
->andReturn($this->view_mock);
$this->view_mock
->shouldReceive('with')
->once()
->withArgs(['authorization_uri', 'http://uri'])
->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
}
/**
* @test
*/
public function it_disconnects_from_quickbooks_when_requested()
{
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$this->redirector_mock
->shouldReceive('back')
->once()
->andReturn(new RedirectResponse('/test', 302));
$this->quickbooks_mock
->shouldReceive('deleteToken')
->once()
->withNoArgs();
$result = $this->controller->disconnect(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
);
$this->assertInstanceOf(RedirectResponse::class, $result);
}
/**
* @test
*/
public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by_quickbooks()
{
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$realmId = random_int(1, 9999);
$this->quickbooks_mock
->shouldReceive('exchangeCodeForToken')
->once()
->withArgs(['code', $realmId]);
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['code'])
->andReturn('code');
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['realmId'])
->andReturn($realmId);
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$this->redirector_mock
->shouldReceive('intended')
->once()
->withAnyArgs()
->andReturn(new RedirectResponse('/test', 302));
$this->url_generator_mock
->shouldReceive('route')
->withArgs(['quickbooks.connect'])
->once();
$result = $this->controller->token(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
$this->url_generator_mock,
);
$this->assertInstanceOf(RedirectResponse::class, $result);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace Wearepixel\QuickBooks\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Mockery;
use Mockery\Mock;
use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase;
/**
* Class FilterTest
*/
class FilterTest extends TestCase
{
/**
* @var Filter
*/
protected $filter;
/**
* @var Mock
*/
protected $quickbooks_mock;
/**
* @var Mock
*/
protected $redirector_mock;
/**
* @var Mock
*/
protected $request_mock;
/**
* @var Mock
*/
protected $session_mock;
/**
* @var Mock
*/
protected $url_generator_mock;
protected function setUp(): void
{
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Session::class);
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$this->filter = new Filter(
$this->quickbooks_mock,
$this->redirector_mock,
$this->session_mock,
$this->url_generator_mock,
);
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Filter::class, $this->filter);
}
/**
* @test
*/
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
{
$next_middleware = function ($request) {
$this->assertEquals($this->request_mock, $request);
};
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
$this->filter->handle($this->request_mock, $next_middleware);
}
/**
* @test
*/
public function it_redirects_to_quickbooks_connect_route_after_setting_intended_session_if_account_not_linked()
{
$this->request_mock
->shouldReceive('path')
->once()
->withNoArgs()
->andReturn('path');
$this->url_generator_mock
->shouldReceive('to')
->once()
->with('path')
->andReturn('http://to/path');
$this->session_mock
->shouldReceive('put')
->once()
->withArgs(['url.intended', 'http://to/path'])
->andReturnNull();
$this->redirector_mock
->shouldReceive('route')
->once()
->with('quickbooks.connect')
->andReturnSelf();
$next_middleware = function ($request) {
// If this is called, then fail test
$this->assertTrue(false);
};
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
$this->filter->handle($this->request_mock, $next_middleware);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Wearepixel\QuickBooks\Stubs;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Mockery;
use Wearepixel\QuickBooks\Token;
/**
* Class TokenStub
*
* Stub for a Laravel Token model
*/
class TokenStub extends Token
{
// Put public properties on this stub to keep the Model from trying to get them from db
public $access_token = null;
public $access_token_expires_at = null;
public $realm_id = null;
public $refresh_token = null;
public $refresh_token_expires_at = null;
public $user = null;
public function belongsTo(
$related,
$foreignKey = null,
$ownerKey = null,
$relation = null,
): BelongsTo {
$related_mock = Mockery::mock($related);
$related_mock->shouldIgnoreMissing();
$child_mock = Mockery::mock(Model::class);
$child_mock->shouldIgnoreMissing();
$builder_mock = Mockery::mock(Builder::class);
$builder_mock->shouldIgnoreMissing();
$builder_mock->shouldReceive('getModel')->andReturn($related_mock);
return new BelongsTo($builder_mock, $child_mock, $foreignKey, $ownerKey, $relation);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Wearepixel\QuickBooks\Stubs;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Mockery;
use Wearepixel\QuickBooks\HasQuickBooksToken;
/**
* Class User
*
* Stub for a Laravel User model
*/
class User
{
use HasQuickBooksToken;
public function hasOne($relationship): HasOne
{
$related_mock = Mockery::mock($relationship);
$related_mock->shouldIgnoreMissing();
$builder_mock = Mockery::mock(Builder::class);
$builder_mock->shouldIgnoreMissing();
$builder_mock->shouldReceive('getModel')->andReturn($related_mock);
$parent_mock = Mockery::mock(Model::class);
$parent_mock->shouldIgnoreMissing();
return new HasOne($builder_mock, $parent_mock, 'foreignKey', 'localKey');
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace Wearepixel\QuickBooks;
use ArrayAccess;
use Countable;
use Iterator;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
/**
* Class TestCase
*/
abstract class TestCase extends PHPUnitTestCase
{
use MockeryPHPUnitIntegration;
/**
* Helper to allow mocking of iterator classes.
*
* @link https://gist.github.com/VladaHejda/8299871
*
* @return void
*/
protected function mockArrayIterator(MockInterface $mock, array $items)
{
if ($mock instanceof ArrayAccess) {
foreach ($items as $key => $val) {
$mock
->shouldReceive('offsetGet')
->with($key)
->andReturn($val);
$mock
->shouldReceive('offsetExists')
->with($key)
->andReturn(true);
}
$mock->shouldReceive('offsetExists')->andReturn(false);
}
if ($mock instanceof Iterator) {
$counter = 0;
$mock->shouldReceive('rewind')->andReturnUsing(function () use (&$counter) {
$counter = 0;
});
$vals = array_values($items);
$keys = array_values(array_keys($items));
$mock->shouldReceive('valid')->andReturnUsing(function () use (&$counter, $vals) {
return isset($vals[$counter]);
});
$mock->shouldReceive('current')->andReturnUsing(function () use (&$counter, $vals) {
return $vals[$counter];
});
$mock->shouldReceive('key')->andReturnUsing(function () use (&$counter, $keys) {
return $keys[$counter];
});
$mock->shouldReceive('next')->andReturnUsing(function () use (&$counter) {
$counter++;
});
}
if ($mock instanceof Countable) {
$mock->shouldReceive('count')->andReturn(count($items));
}
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
namespace Wearepixel\QuickBooks;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Mockery;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use Wearepixel\QuickBooks\Stubs\TokenStub;
use Wearepixel\QuickBooks\Stubs\User;
/**
* Class TokenTest
*/
class TokenTest extends TestCase
{
/**
* @var Token
*/
protected $token;
protected function setUp(): void
{
$this->token = new TokenStub();
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Token::class, $this->token);
}
/**
* @test
*/
public function it_has_accessor_for_valid_access_token()
{
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
}
/**
* @test
*/
public function it_has_accessor_for_valid_refresh_token()
{
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
}
/**
* @test
*/
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
}
/**
* @test
*/
public function it_knows_that_the_refresh_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
}
/**
* @test
*/
public function it_know_if_access_token_expires_at_is_less_than_now_it_is_not_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->access_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidAccessTokenAttribute());
}
/**
* @test
*/
public function it_know_if_refresh_token_expires_at_is_less_than_now_it_is_not_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->refresh_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute());
}
/**
* @test
*/
public function it_know_if_access_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->access_token_expires_at = Carbon::now();
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
$this->token->access_token_expires_at->subSecond();
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
}
/**
* @test
*/
public function it_know_if_refresh_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->refresh_token_expires_at = Carbon::now();
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
$this->token->refresh_token_expires_at->subSecond();
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
}
/**
* @test
*/
public function it_stores_the_oauth_token_parts_in_expected_properties()
{
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
$oauth_token_mock
->shouldReceive('getAccessToken')
->once()
->withNoArgs()
->andReturn('access_token');
$oauth_token_mock
->shouldReceive('getAccessTokenExpiresAt')
->once()
->withNoArgs()
->andReturn('now');
$oauth_token_mock
->shouldReceive('getRealmID')
->once()
->withNoArgs()
->andReturn('realm_id');
$oauth_token_mock
->shouldReceive('getRefreshToken')
->once()
->withNoArgs()
->andReturn('refresh_token');
$oauth_token_mock
->shouldReceive('getRefreshTokenExpiresAt')
->once()
->withNoArgs()
->andReturn('now');
$this->token->parseOauthToken($oauth_token_mock);
}
/**
* @test
*/
public function it_allows_itself_to_be_deleted_and_returns_new_token()
{
$this->token->user = Mockery::mock(User::class);
$has_one_mock = Mockery::mock(HasOne::class);
$token_mock = Mockery::mock(Token::class);
$has_one_mock
->shouldReceive('make')
->once()
->withNoArgs()
->andReturn($token_mock);
$this->token->user
->shouldReceive('quickBooksToken')
->once()
->withNoArgs()
->andReturn($has_one_mock);
$this->token->user->id = 1;
$this->assertEquals($token_mock, $this->token->remove());
}
/**
* @test
*/
public function it_get_related_user_model_from_configuration()
{
$this->assertInstanceOf(User::class, $this->token->user()->getModel());
}
}
function config($key)
{
return [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
];
}