mirror of
https://github.com/wearepixel/laravel-quickbooks.git
synced 2026-11-14 14:01:46 +00:00
Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
/vendor
|
||||
/node_modules
|
||||
phpunit.xml
|
||||
/build
|
||||
.phpunit.result.cache
|
||||
composer.lock
|
||||
/.idea/
|
||||
@@ -0,0 +1,116 @@
|
||||
# Wearepixel's Laravel QuickBooks Client
|
||||
|
||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks-client)
|
||||
[](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 | [](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=develop) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=develop) |
|
||||
| Master | [](https://github.com/wearepixel/laravel-quickbooks-client/workflows/CI/badge.svg?branch=master) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks-client/?branch=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');
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"no_superfluous_phpdoc_tags": true
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user