10 Commits
Author SHA1 Message Date
Joel Male 9002e0ebca fix: switch DataService to JSON serialization to survive QuickBooks schema drift
The SDK's XML deserializer reflects on declared properties of its Data
classes and throws a RuntimeException when QuickBooks returns a field
the installed SDK version doesn't define yet (e.g. the new invoice
AllowOnlinePayPalPayment field), breaking every invoice create/update.
The JSON path builds instances from the class's own known properties
and silently ignores unrecognised response fields instead, so it
tolerates QuickBooks adding fields ahead of the SDK.
2026-07-27 07:03:21 +03:00
pixel-workflows[bot] 473103c243 chore: update dependabot configuration 2026-05-27 06:18:22 +00:00
Joel Male f59295773d Create SECURITY.md 2026-05-27 14:48:52 +10:00
Joel Male 446f6788d2 chore: bump version to 2.0.0 2026-02-18 10:47:51 +10:00
Joel Male 831cd49719 chore: clean up 2026-02-18 10:46:22 +10:00
Joel Male bb2d375450 docs: rewrite README for current package state
- Replace broken Scrutinizer/poser.pugx badges with shields.io
- Add PHP version badge
- Add Laravel/PHP compatibility table
- Fix migration command (remove deprecated --package flag)
- Modernize usage example with FQCN resolution
- Simplify and tighten all sections
- Remove outdated Psy Shell version references
- Fix middleware class path reference
- Add testing section
2026-02-18 10:45:27 +10:00
Joel Male 861a08e083 test: add tests for upgrade-sensitive areas
- Token model structure: verify $casts has datetime fields, $fillable
  is complete, and $table is set correctly (catches $dates→$casts regression)
- ClientServiceProvider: verify DeferrableProvider interface is
  implemented and provides() returns Client::class (catches $defer regression)
- Token parseOauthToken: verify stored values and Carbon types, not
  just that mock methods were called
- Client setToken: verify DataService cache is cleared on token change
- Client delegation: verify hasValidAccessToken/hasValidRefreshToken
  delegate to Token correctly
2026-02-18 10:43:14 +10:00
Joel Male 97c04ec693 feat: migrate test suite from PHPUnit to Pest
- Replace phpunit/phpunit with pestphp/pest ^3.0
- Drop scrutinizer/ocular (incompatible with Pest's symfony/console)
- Add tests/Pest.php with MockeryPHPUnitIntegration trait binding
- Extract namespace-level function stubs (config, route, dir) to
  tests/Overrides.php, loaded via autoload-dev files
- Convert all 5 test classes to Pest closure-based syntax:
  setUp() → beforeEach(), assertions → expect(), skip() for skipped tests
- Delete tests/TestCase.php (logic moved to Pest.php)
- Stubs (TokenStub, User) unchanged
2026-02-18 10:38:20 +10:00
Joel Male 8ec2c3cc38 feat: add Laravel 11/12 and PHP 8.3+ support
- Widen illuminate/* deps to ^10.0|^11.0|^12.0, require PHP ^8.3
- Upgrade PHPUnit to ^10.5|^11.0, Mockery to ^1.6
- Rewrite phpunit.xml.dist for PHPUnit 10+ format (source/include
  replaces filter/whitelist, removed deprecated attributes)
- Replace $dates with $casts on Token model
- Implement DeferrableProvider interface instead of $defer property
  on ClientServiceProvider
- Use FQCN route definitions instead of string-based namespace()
- Fix migration getColumnType() check for Laravel 11+ native types
- Replace @test doc annotations with #[Test] attributes
- Fix dynamic property deprecation in ControllerTest
2026-02-18 10:29:34 +10:00
divakar-rajodiya 32f5c22846 update composer.json 2024-06-26 09:41:25 +05:30
24 changed files with 734 additions and 1058 deletions
-9
View File
@@ -1,9 +0,0 @@
* 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
+33
View File
@@ -0,0 +1,33 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
groups:
actions:
patterns: ["*"]
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
day: "tuesday"
open-pull-requests-limit: 10
groups:
php-minor-patch:
update-types:
- "minor"
- "patch"
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
+3
View File
@@ -5,3 +5,6 @@ phpunit.xml
.phpunit.result.cache .phpunit.result.cache
composer.lock composer.lock
/.idea/ /.idea/
.cursor
.claude
CLAUDE.md
+59 -71
View File
@@ -1,115 +1,103 @@
# Wearepixel's Laravel QuickBooks Client # Laravel QuickBooks Client
[![Latest Stable Version](https://poser.pugx.org/wearepixel/laravel-quickbooks/v/stable)](https://packagist.org/packages/wearepixel/laravel-quickbooks) [![Latest Stable Version](https://img.shields.io/packagist/v/wearepixel/laravel-quickbooks.svg)](https://packagist.org/packages/wearepixel/laravel-quickbooks)
[![Latest Unstable Version](https://poser.pugx.org/wearepixel/laravel-quickbooks/v/unstable)](https://packagist.org/packages/wearepixel/laravel-quickbooks) [![Total Downloads](https://img.shields.io/packagist/dt/wearepixel/laravel-quickbooks.svg)](https://packagist.org/packages/wearepixel/laravel-quickbooks)
[![Total Downloads](https://poser.pugx.org/wearepixel/laravel-quickbooks/downloads)](https://packagist.org/packages/wearepixel/laravel-quickbooks) [![License](https://img.shields.io/packagist/l/wearepixel/laravel-quickbooks.svg)](https://packagist.org/packages/wearepixel/laravel-quickbooks)
[![License](https://poser.pugx.org/wearepixel/laravel-quickbooks/license)](https://packagist.org/packages/wearepixel/laravel-quickbooks) [![PHP Version](https://img.shields.io/packagist/dependency-v/wearepixel/laravel-quickbooks/php.svg)](https://packagist.org/packages/wearepixel/laravel-quickbooks)
PHP client wrapping the [QuickBooks PHP SDK](https://github.com/intuit/QuickBooks-V3-PHP-SDK). A Laravel package wrapping the [QuickBooks PHP SDK](https://github.com/intuit/QuickBooks-V3-PHP-SDK). Provides OAuth 2.0 authentication, automatic token management, and access to the QuickBooks Online API.
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. ## Compatibility
## Build Status | Laravel | PHP | Package |
|---------|------|---------|
| Branch | Status | Coverage | Code Quality | | 12.x | ^8.3 | ^2.0 |
| ------ | :----: | :------: | :----------: | | 11.x | ^8.3 | ^2.0 |
| Master | [![Build Status](https://github.com/wearepixel/laravel-quickbooks/workflows/CI/badge.svg?branch=master)](https://github.com/wearepixel/laravel-quickbooks/workflows/CI/badge.svg?branch=master) | [![Code Coverage](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/?branch=master) | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/?branch=main) | | 10.x | ^8.3 | ^1.0 |
## Installation ## Installation
1. Install QuickBooks PHP Client: 1. Install the package:
```bash ```bash
$ composer require wearepixel/laravel-quickbooks composer require wearepixel/laravel-quickbooks
``` ```
2. Run our migration to install the `quickbooks_tokens` table: 2. Run the migration to create the `quickbooks_tokens` table:
```bash ```bash
$ php artisan migrate --package=wearepixel/laravel-quickbooks php artisan migrate
``` ```
The package uses the [auto registration feature](https://laravel.com/docs/packages#package-discovery) of Laravel. The package uses Laravel's [auto-discovery](https://laravel.com/docs/packages#package-discovery), so no manual provider registration is needed.
## Configuration ## 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: ### 1. Add the trait to your User model
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 ```php
use Wearepixel\QuickBooks\HasQuickBooksToken;
class User extends Authenticatable class User extends Authenticatable
{ {
use Notifiable, HasQuickBooksToken; use HasQuickBooksToken;
}
``` ```
**NOTE: If your ```User``` model is not ```App\Models\User```, then you will need to configure the path in the ```configs/quickbooks.php```.** If your User model is not `App\Models\User`, publish the config and update the `user.model` value.
2. Add the appropriate values to your ```.env``` ### 2. Set your environment variables
#### Minimal Keys ```bash
```bash QUICKBOOKS_CLIENT_ID=<your client id>
QUICKBOOKS_CLIENT_ID=<client id given by QuickBooks> QUICKBOOKS_CLIENT_SECRET=<your client secret>
QUICKBOOKS_CLIENT_SECRET=<client secret> ```
```
#### Optional Keys Optional:
```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 ```bash
QUICKBOOKS_API_URL=<Development|Production> # Defaults based on APP_ENV
QUICKBOOKS_DEBUG=<true|false> # Defaults to APP_DEBUG
```
#### Config ### 3. Publish config and views (optional)
A configuration file named ```quickbooks.php``` can be published to ```config/``` by running...
```bash ```bash
php artisan vendor:publish --tag=quickbooks-config php artisan vendor:publish --tag=quickbooks-config
``` php artisan vendor:publish --tag=quickbooks-views
```
#### Views
View files can be published by running...
```bash
php artisan vendor:publish --tag=quickbooks-views
```
## Usage ## Usage
Here is an example of getting the company information from QuickBooks: First, direct users to `/quickbooks/connect` to authorize their QuickBooks account. Once connected, you can access the API:
### NOTE: Before doing these commands, go to your connect route (default: /quickbooks/connect) to get a QuickBooks token for your user
```php ```php
php artisan tinker $quickbooks = app(\Wearepixel\QuickBooks\Client::class);
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
>>> Auth::logInUsingId(1) // Get company info
=> App\Models\User {#1668 $company = $quickbooks->getDataService()->getCompanyInfo();
id: 1,
// Other keys removed for example // Access reports
} $reportService = $quickbooks->getReportService();
>>> $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). You can call any of the resources documented in the [QuickBooks PHP SDK](https://intuit.github.io/QuickBooks-V3-PHP-SDK/quickstart.html).
## Middleware ## 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. Protect routes that require a QuickBooks connection using the `quickbooks` middleware. Users without a valid token will be redirected to the connect page.
Here is an example route definition:
```php ```php
Route::view('some/route/needing/quickbooks/token/before/using', 'some.view') Route::get('quickbooks/invoices', InvoiceController::class)
->middleware('quickbooks'); ->middleware('quickbooks');
``` ```
## Testing
```bash
./vendor/bin/pest
```
## License
MIT
+11
View File
@@ -0,0 +1,11 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Reporting a Vulnerability
If you discover a security vulnerability, please report it privately using the below channel:
**GitHub Private Vulnerability Reporting** — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
All security vulnerabilities will be promptly addressed.
-1
View File
@@ -1 +0,0 @@
4.0.4
+18 -13
View File
@@ -1,7 +1,7 @@
{ {
"name": "wearepixel/laravel-quickbooks", "name": "wearepixel/laravel-quickbooks",
"version": "1.0.0", "version": "2.0.1",
"description": "Wearepixel's Laravel Client for QuickBooks.", "description": "Laravel Client for QuickBooks.",
"keywords": [ "keywords": [
"quickbooks", "quickbooks",
"client", "client",
@@ -16,20 +16,19 @@
} }
], ],
"require": { "require": {
"php": "^8.0.2", "php": "^8.3",
"illuminate/database": "^10.0|^11.0", "illuminate/database": "^10.0|^11.0|^12.0",
"illuminate/http": "^10.0|^11.0", "illuminate/http": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0", "illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.62.1", "nesbot/carbon": "^2.62.1|^3.0",
"quickbooks/v3-php-sdk": "^5.4.6" "quickbooks/v3-php-sdk": "^5.4.6"
}, },
"require-dev": { "require-dev": {
"laravel/pint": "^1.2", "laravel/pint": "^1.2",
"mockery/mockery": "^1.5.1", "mockery/mockery": "^1.6",
"phpunit/phpunit": "^10.0", "pestphp/pest": "^3.0",
"psy/psysh": "^0.11", "psy/psysh": "^0.12"
"scrutinizer/ocular": "^1.9"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -39,7 +38,10 @@
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Wearepixel\\QuickBooks\\": "tests/" "Wearepixel\\QuickBooks\\": "tests/"
} },
"files": [
"tests/Overrides.php"
]
}, },
"extra": { "extra": {
"laravel": { "laravel": {
@@ -50,6 +52,9 @@
} }
}, },
"config": { "config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true "sort-packages": true
}, },
"prefer-stable": true "prefer-stable": true
@@ -15,8 +15,7 @@
public function up() public function up()
{ {
Schema::create('quickbooks_tokens', function (Blueprint $table) { Schema::create('quickbooks_tokens', function (Blueprint $table) {
$user_id_type = $user_id_type = str_contains(DB::getSchemaBuilder()->getColumnType('users', 'id'), 'bigint')
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
? 'unsignedBigInteger' ? 'unsignedBigInteger'
: 'unsignedInteger'; : 'unsignedInteger';
-9
View File
@@ -1,9 +0,0 @@
* 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
+6 -23
View File
@@ -8,15 +8,11 @@
--> -->
<phpunit backupGlobals="false" <phpunit backupGlobals="false"
backupStaticAttributes="false" backupStaticProperties="false"
bootstrap="vendor/autoload.php" bootstrap="vendor/autoload.php"
colors="true" colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false" processIsolation="false"
stopOnFailure="false" stopOnFailure="false">
verbose="true">
<testsuites> <testsuites>
<testsuite name="Wearepixel's Laravel QuickBooks Client"> <testsuite name="Wearepixel's Laravel QuickBooks Client">
@@ -28,26 +24,13 @@
<!-- <env name="VARIABLE" value="value"/> --> <!-- <env name="VARIABLE" value="value"/> -->
</php> </php>
<filter> <source>
<whitelist processUncoveredFilesFromWhitelist="true"> <include>
<directory suffix=".php">./src</directory> <directory suffix=".php">./src</directory>
</include>
<exclude> <exclude>
<directory>./src/Http/Exceptions</directory> <directory>./src/Http/Exceptions</directory>
<directory>./src/Providers</directory> <directory>./src/Providers</directory>
</exclude> </exclude>
</whitelist> </source>
</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> </phpunit>
+9 -3
View File
@@ -181,6 +181,12 @@ public function hasValidRefreshToken(): bool
* 2) Have valid refresh token, so renew access token & then use * 2) Have valid refresh token, so renew access token & then use
* 3) No existing token, so need to link account * 3) No existing token, so need to link account
* *
* useJson() is applied in every case: the SDK's default XML deserializer reflects
* declared properties on its Data classes and throws when QuickBooks adds a new
* response field the installed SDK version doesn't know about yet. The JSON
* deserializer builds instances from the class's own properties and silently
* ignores unrecognised keys instead, so it tolerates QuickBooks-side schema drift.
*
* @throws SdkException * @throws SdkException
* @throws ServiceException * @throws ServiceException
*/ */
@@ -203,7 +209,7 @@ protected function makeDataService(): DataService
'QBORealmID' => $this->token->realm_id, 'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token, 'refreshTokenKey' => $this->token->refresh_token,
]), ]),
); )->useJson();
} }
// Have refresh, so update access & allow app to run // Have refresh, so update access & allow app to run
@@ -214,7 +220,7 @@ protected function makeDataService(): DataService
'QBORealmID' => $this->token->realm_id, 'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token, 'refreshTokenKey' => $this->token->refresh_token,
]), ]),
); )->useJson();
$oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken(); $oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken();
@@ -226,7 +232,7 @@ protected function makeDataService(): DataService
} }
// Create new... // Create new...
return DataService::Configure($this->parseDataConfigs()); return DataService::Configure($this->parseDataConfigs())->useJson();
} }
/** /**
+1 -2
View File
@@ -22,8 +22,7 @@ public function __construct(
protected Redirector $redirector, protected Redirector $redirector,
protected Session $session, protected Session $session,
protected UrlGenerator $url_generator, protected UrlGenerator $url_generator,
) { ) {}
}
/** /**
* Handle an incoming request. * Handle an incoming request.
+2 -6
View File
@@ -3,19 +3,15 @@
namespace Wearepixel\QuickBooks\Providers; namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider; use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Client; use Wearepixel\QuickBooks\Client;
/** /**
* Class ClientServiceProvider * Class ClientServiceProvider
*/ */
class ClientServiceProvider extends LaravelServiceProvider class ClientServiceProvider extends LaravelServiceProvider implements DeferrableProvider
{ {
/**
* Indicates if loading of the provider is deferred.
*/
protected bool $defer = true;
/** /**
* Get the services provided by the provider. * Get the services provided by the provider.
*/ */
+9 -8
View File
@@ -2,8 +2,8 @@
namespace Wearepixel\QuickBooks\Providers; namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider; use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Http\Controllers\Controller;
use Wearepixel\QuickBooks\Http\Middleware\Filter; use Wearepixel\QuickBooks\Http\Middleware\Filter;
/** /**
@@ -89,19 +89,20 @@ protected function registerRoutes(): void
->prefix($config['prefix']) ->prefix($config['prefix'])
->as('quickbooks.') ->as('quickbooks.')
->middleware($config['middleware']['default']) ->middleware($config['middleware']['default'])
->namespace('Wearepixel\QuickBooks\Http\Controllers') ->group(function () use ($config) {
->group(function (Router $router) use ($config) { $this->app->router
$router ->get($config['paths']['connect'], [Controller::class, 'connect'])
->get($config['paths']['connect'], 'Controller@connect')
->middleware($config['middleware']['authenticated']) ->middleware($config['middleware']['authenticated'])
->name('connect'); ->name('connect');
$router $this->app->router
->delete($config['paths']['disconnect'], 'Controller@disconnect') ->delete($config['paths']['disconnect'], [Controller::class, 'disconnect'])
->middleware($config['middleware']['authenticated']) ->middleware($config['middleware']['authenticated'])
->name('disconnect'); ->name('disconnect');
$router->get($config['paths']['token'], 'Controller@token')->name('token'); $this->app->router
->get($config['paths']['token'], [Controller::class, 'token'])
->name('token');
}); });
} }
+6 -3
View File
@@ -34,11 +34,14 @@ class Token extends Model
protected $table = 'quickbooks_tokens'; protected $table = 'quickbooks_tokens';
/** /**
* The attributes that should be mutated to dates. * The attributes that should be cast.
* *
* @var array * @var array<string, string>
*/ */
protected $dates = ['access_token_expires_at', 'refresh_token_expires_at']; protected $casts = [
'access_token_expires_at' => 'datetime',
'refresh_token_expires_at' => 'datetime',
];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
+100 -179
View File
@@ -1,34 +1,12 @@
<?php <?php
namespace Wearepixel\QuickBooks; use QuickBooksOnline\API\Core\Http\Serialization\SerializationFormat;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService; use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Exception\ServiceException; use QuickBooksOnline\API\Exception\ServiceException;
use Wearepixel\QuickBooks\Client;
use Wearepixel\QuickBooks\Token;
/** beforeEach(function () {
* 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 = [ $this->configs = [
'data_service' => [ 'data_service' => [
'auth_mode' => 'oauth2', 'auth_mode' => 'oauth2',
@@ -40,32 +18,14 @@ protected function setUp(): void
]; ];
$this->token_mock = Mockery::mock(Token::class); $this->token_mock = Mockery::mock(Token::class);
$this->client = new Client($this->configs, $this->token_mock);
});
$this->client = $this->makeClient(); it('can be constructed', function () {
} expect($this->client)->toBeInstanceOf(Client::class);
});
private function makeClient($configs = null) it('returns a data service configured to request oauth token when token is empty', function () {
{
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 $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -78,19 +38,15 @@ public function it_returns_a_data_service_configured_to_request_oauth_token_when
->with('hasValidRefreshToken') ->with('hasValidRefreshToken')
->andReturnFalse(); ->andReturnFalse();
$this->assertFalse( expect(
$this->client $this->client
->getDataService() ->getDataService()
->getServiceContext() ->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(), ->IppConfiguration->Security->isAccessTokenSet(),
); )->toBeFalse();
} });
/** it('caches the data service once it is made', function () {
* @test
*/
public function it_caches_the_data_service_once_it_is_made()
{
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -103,15 +59,10 @@ public function it_caches_the_data_service_once_it_is_made()
->with('hasValidRefreshToken') ->with('hasValidRefreshToken')
->andReturnFalse(); ->andReturnFalse();
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received expect($this->client->getDataService())->toBeInstanceOf(DataService::class);
$this->assertEquals(DataService::class, get_class($this->client->getDataService())); });
}
/** it('returns a data service with oauth token when valid access token exists', function () {
* @test
*/
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
{
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -136,21 +87,15 @@ public function it_returns_a_data_service_with_oauth_token_when_valid_access_tok
->with('refresh_token') ->with('refresh_token')
->andReturn('refresh_token'); ->andReturn('refresh_token');
$this->assertTrue( expect(
$this->client $this->client
->getDataService() ->getDataService()
->getServiceContext() ->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(), ->IppConfiguration->Security->isAccessTokenSet(),
); )->toBeTrue();
} });
/**
* @test
*/
public function it_returns_a_data_service_with_refreshed_token_when_access_token_expired_but_refresh_token_valid()
{
$this->expectException(ServiceException::class);
it('returns a data service with refreshed token when access token expired but refresh token valid', function () {
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -180,120 +125,96 @@ public function it_returns_a_data_service_with_refreshed_token_when_access_token
->with('refresh_token') ->with('refresh_token')
->andReturn('refresh_token'); ->andReturn('refresh_token');
// TODO: This really needs to test that things are getting called correctly on the DataService class $this->client->getDataService();
$this->assertFalse( })->throws(ServiceException::class);
$this->client
it('returns a report service using the data service')
->skip('Once we figure out how to test around the static DataService::Configure');
it('configures the data service to use JSON serialization so unrecognised QuickBooks response fields do not throw', function () {
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$format = $this->client
->getDataService() ->getDataService()
->getServiceContext() ->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(), ->IppConfiguration->Message->Request->SerializationFormat;
);
}
/** expect($format)->toBe(SerializationFormat::Json);
* @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',
);
}
/** it('has logging off by default')
* @test ->skip('Have to figure out how to test this with the new code in 5.x');
*/
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 it('allows logging turned on and pointed to expected file')
->shouldReceive('getAttribute') ->skip('Have to figure out how to test this with the new code in 5.x');
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock it('returns self after deleting token', function () {
->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 $this->token_mock
->shouldReceive('remove') ->shouldReceive('remove')
->once() ->once()
->withNoArgs() ->withNoArgs()
->andReturnSelf(); ->andReturnSelf();
$this->assertInstanceOf(Client::class, $this->client->deleteToken()); expect($this->client->deleteToken())->toBeInstanceOf(Client::class);
} });
}
function dir($path) it('clears cached data service when token is changed', function () {
{ $this->token_mock
return $path === '/some/valid/path'; ->shouldReceive('getAttribute')
} ->with('hasValidAccessToken')
->andReturnFalse();
function route($name) $this->token_mock
{ ->shouldReceive('getAttribute')
return $name; ->with('hasValidRefreshToken')
} ->andReturnFalse();
$first = $this->client->getDataService();
$new_token = Mockery::mock(Token::class);
$new_token
->shouldReceive('getAttribute')
->with('hasValidAccessToken')
->andReturnFalse();
$new_token
->shouldReceive('getAttribute')
->with('hasValidRefreshToken')
->andReturnFalse();
$this->client->setToken($new_token);
$second = $this->client->getDataService();
expect($second)->not->toBe($first);
});
it('delegates hasValidAccessToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnTrue();
expect($this->client->hasValidAccessToken())->toBeTrue();
});
it('delegates hasValidRefreshToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
expect($this->client->hasValidRefreshToken())->toBeFalse();
});
+10 -33
View File
@@ -1,39 +1,16 @@
<?php <?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User; use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/** beforeEach(function () {
* Class HasQuickBooksTokenTest $this->user = new User;
*/ });
class HasQuickBooksTokenTest extends TestCase
{
/**
* @var User
*/
protected $user;
protected function setUp(): void it('can be constructed', function () {
{ expect($this->user)->toBeInstanceOf(User::class);
parent::setUp(); });
$this->user = new User(); it('has a hasOne relationship to token', function () {
} expect($this->user->quickBooksToken()->getModel())->toBeInstanceOf(Token::class);
});
/**
* @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());
}
}
+20 -98
View File
@@ -1,7 +1,5 @@
<?php <?php
namespace Wearepixel\QuickBooks\Http\Controllers;
use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@@ -9,90 +7,33 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
use Illuminate\Session\Store; use Illuminate\Session\Store;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService; use QuickBooksOnline\API\DataService\DataService;
use Wearepixel\QuickBooks\Client as QuickBooks; use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase; use Wearepixel\QuickBooks\Http\Controllers\Controller;
/** beforeEach(function () {
* 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->data_service_mock = Mockery::mock(DataService::class);
$this->quickbooks_mock = Mockery::mock(QuickBooks::class); $this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class); $this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class); $this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Store::class); $this->session_mock = Mockery::mock(Store::class);
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$this->view_factory_mock = Mockery::mock(ViewFactory::class); $this->view_factory_mock = Mockery::mock(ViewFactory::class);
$this->view_mock = Mockery::mock(View::class); $this->view_mock = Mockery::mock(View::class);
$this->controller = new Controller(); $this->controller = new Controller;
} });
/** it('can be constructed', function () {
* @test expect($this->controller)->toBeInstanceOf(Controller::class);
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Controller::class, $this->controller);
}
/** it('shows view to disconnect if account linked', function () {
* @test
*/
public function it_shows_view_to_connect_if_account_not_linked()
{
$this->data_service_mock $this->data_service_mock
->shouldReceive('getCompanyInfo') ->shouldReceive('getCompanyInfo')
->once() ->once()
->withNoArgs() ->withNoArgs()
->andReturn([ ->andReturn(['name' => 'Company']);
'name' => 'Company',
]);
$this->quickbooks_mock $this->quickbooks_mock
->shouldReceive('hasValidRefreshToken') ->shouldReceive('hasValidRefreshToken')
@@ -115,22 +56,13 @@ public function it_shows_view_to_connect_if_account_not_linked()
$this->view_mock $this->view_mock
->shouldReceive('with') ->shouldReceive('with')
->once() ->once()
->withArgs([ ->withArgs(['company', ['name' => 'Company']])
'company',
[
'name' => 'Company',
],
])
->andReturnSelf(); ->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
} });
/** it('shows view to connect if account not linked', function () {
* @test
*/
public function it_shows_view_to_disconnect_if_account_linked()
{
$this->quickbooks_mock $this->quickbooks_mock
->shouldReceive('hasValidRefreshToken') ->shouldReceive('hasValidRefreshToken')
->once() ->once()
@@ -156,13 +88,9 @@ public function it_shows_view_to_disconnect_if_account_linked()
->andReturnSelf(); ->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
} });
/** it('disconnects from quickbooks when requested', function () {
* @test
*/
public function it_disconnects_from_quickbooks_when_requested()
{
$this->request_mock $this->request_mock
->shouldReceive('session') ->shouldReceive('session')
->once() ->once()
@@ -189,15 +117,10 @@ public function it_disconnects_from_quickbooks_when_requested()
$this->quickbooks_mock, $this->quickbooks_mock,
); );
$this->assertInstanceOf(RedirectResponse::class, $result); expect($result)->toBeInstanceOf(RedirectResponse::class);
} });
/** it('finishes connecting to quickbooks when given a valid token', function () {
* @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); $realmId = random_int(1, 9999);
$this->quickbooks_mock $this->quickbooks_mock
@@ -245,6 +168,5 @@ public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by
$this->url_generator_mock, $this->url_generator_mock,
); );
$this->assertInstanceOf(RedirectResponse::class, $result); expect($result)->toBeInstanceOf(RedirectResponse::class);
} });
}
+12 -66
View File
@@ -1,53 +1,13 @@
<?php <?php
namespace Wearepixel\QuickBooks\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
use Mockery;
use Mockery\Mock;
use Wearepixel\QuickBooks\Client as QuickBooks; use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase; use Wearepixel\QuickBooks\Http\Middleware\Filter;
/** beforeEach(function () {
* 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->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class); $this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class); $this->request_mock = Mockery::mock(Request::class);
@@ -60,23 +20,15 @@ protected function setUp(): void
$this->session_mock, $this->session_mock,
$this->url_generator_mock, $this->url_generator_mock,
); );
} });
/** it('can be constructed', function () {
* @test expect($this->filter)->toBeInstanceOf(Filter::class);
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Filter::class, $this->filter);
}
/** it('passes the request to the next middleware if account linked to quickbooks', function () {
* @test
*/
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
{
$next_middleware = function ($request) { $next_middleware = function ($request) {
$this->assertEquals($this->request_mock, $request); expect($request)->toBe($this->request_mock);
}; };
$this->quickbooks_mock $this->quickbooks_mock
@@ -86,13 +38,9 @@ public function it_just_passes_the_request_to_the_next_middleware_if_account_lin
->andReturnTrue(); ->andReturnTrue();
$this->filter->handle($this->request_mock, $next_middleware); $this->filter->handle($this->request_mock, $next_middleware);
} });
/** it('redirects to quickbooks connect route after setting intended session if account not linked', function () {
* @test
*/
public function it_redirects_to_quickbooks_connect_route_after_setting_intended_session_if_account_not_linked()
{
$this->request_mock $this->request_mock
->shouldReceive('path') ->shouldReceive('path')
->once() ->once()
@@ -118,8 +66,7 @@ public function it_redirects_to_quickbooks_connect_route_after_setting_intended_
->andReturnSelf(); ->andReturnSelf();
$next_middleware = function ($request) { $next_middleware = function ($request) {
// If this is called, then fail test throw new RuntimeException('Next middleware should not be called');
$this->assertTrue(false);
}; };
$this->quickbooks_mock $this->quickbooks_mock
@@ -129,5 +76,4 @@ public function it_redirects_to_quickbooks_connect_route_after_setting_intended_
->andReturnFalse(); ->andReturnFalse();
$this->filter->handle($this->request_mock, $next_middleware); $this->filter->handle($this->request_mock, $next_middleware);
} });
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User;
function config(string $key): array
{
return [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
];
}
function route(string $name): string
{
return $name;
}
function dir(string $path): bool
{
return $path === '/some/valid/path';
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
pest()->use(MockeryPHPUnitIntegration::class);
-75
View File
@@ -1,75 +0,0 @@
<?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));
}
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
use Illuminate\Contracts\Support\DeferrableProvider;
use Wearepixel\QuickBooks\Client;
use Wearepixel\QuickBooks\Providers\ClientServiceProvider;
use Wearepixel\QuickBooks\Token;
describe('Token model structure', function () {
it('uses the quickbooks_tokens table', function () {
expect((new Token)->getTable())->toBe('quickbooks_tokens');
});
it('casts expiration fields to datetime', function () {
$casts = (new Token)->getCasts();
expect($casts)
->toHaveKey('access_token_expires_at', 'datetime')
->toHaveKey('refresh_token_expires_at', 'datetime');
});
it('has all expected fillable attributes', function () {
$fillable = (new Token)->getFillable();
expect($fillable)->toEqualCanonicalizing([
'access_token',
'access_token_expires_at',
'realm_id',
'refresh_token',
'refresh_token_expires_at',
'user_id',
]);
});
});
describe('ClientServiceProvider structure', function () {
it('implements DeferrableProvider', function () {
expect(ClientServiceProvider::class)
->toImplement(DeferrableProvider::class);
});
it('declares Client in provides array', function () {
$app = Mockery::mock(\Illuminate\Contracts\Foundation\Application::class);
$app->shouldIgnoreMissing();
$provider = new ClientServiceProvider($app);
expect($provider->provides())->toBe([Client::class]);
});
});
+59 -162
View File
@@ -1,184 +1,97 @@
<?php <?php
namespace Wearepixel\QuickBooks;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Mockery;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use Wearepixel\QuickBooks\Stubs\TokenStub; use Wearepixel\QuickBooks\Stubs\TokenStub;
use Wearepixel\QuickBooks\Stubs\User; use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/** beforeEach(function () {
* Class TokenTest $this->token = new TokenStub;
*/ });
class TokenTest extends TestCase
{
/**
* @var Token
*/
protected $token;
protected function setUp(): void it('can be constructed', function () {
{ expect($this->token)->toBeInstanceOf(Token::class);
$this->token = new TokenStub(); });
}
/** it('has accessor for valid access token', function () {
* @test expect($this->token->getHasValidAccessTokenAttribute())->not->toBeNull();
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Token::class, $this->token);
}
/** it('has accessor for valid refresh token', function () {
* @test expect($this->token->getHasValidRefreshTokenAttribute())->not->toBeNull();
*/ });
public function it_has_accessor_for_valid_access_token()
{
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
}
/** it('knows that the access token expires at has to be valid', function () {
* @test expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
*/ });
public function it_has_accessor_for_valid_refresh_token()
{
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
}
/** it('knows that the refresh token expires at has to be valid', function () {
* @test expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
*/ });
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
}
/** it('knows if access token expires at is less than now it is not expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
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->token->access_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidAccessTokenAttribute()); expect($this->token->getHasValidAccessTokenAttribute())->toBeTrue();
} });
/** it('knows if refresh token expires at is less than now it is not expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
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->token->refresh_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute()); expect($this->token->getHasValidRefreshTokenAttribute())->toBeTrue();
} });
/** it('knows if access token expires at is greater than or equal to now it is expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
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->token->access_token_expires_at = Carbon::now();
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
$this->token->access_token_expires_at->subSecond(); $this->token->access_token_expires_at->subSecond();
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
});
$this->assertFalse($this->token->getHasValidAccessTokenAttribute()); it('knows if refresh token expires at is greater than or equal to now it is expired', function () {
} Carbon::setTestNow(Carbon::now());
/**
* @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->token->refresh_token_expires_at = Carbon::now();
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
$this->token->refresh_token_expires_at->subSecond(); $this->token->refresh_token_expires_at->subSecond();
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
});
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute()); it('stores the oauth token parts in expected properties', function () {
} $oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class);
/** $oauth_token_mock->shouldReceive('getAccessToken')->once()->withNoArgs()->andReturn('access_token');
* @test $oauth_token_mock->shouldReceive('getAccessTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-06-15 12:00:00');
*/ $oauth_token_mock->shouldReceive('getRealmID')->once()->withNoArgs()->andReturn('realm_id');
public function it_stores_the_oauth_token_parts_in_expected_properties() $oauth_token_mock->shouldReceive('getRefreshToken')->once()->withNoArgs()->andReturn('refresh_token');
{ $oauth_token_mock->shouldReceive('getRefreshTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-09-24 12:00:00');
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
$oauth_token_mock $result = $this->token->parseOauthToken($oauth_token_mock);
->shouldReceive('getAccessToken')
->once()
->withNoArgs()
->andReturn('access_token');
$oauth_token_mock expect($result)->toBeInstanceOf(Token::class)
->shouldReceive('getAccessTokenExpiresAt') ->and($this->token->access_token)->toBe('access_token')
->once() ->and($this->token->realm_id)->toBe('realm_id')
->withNoArgs() ->and($this->token->refresh_token)->toBe('refresh_token')
->andReturn('now'); ->and($this->token->access_token_expires_at)->toBeInstanceOf(Carbon::class)
->and($this->token->refresh_token_expires_at)->toBeInstanceOf(Carbon::class);
});
$oauth_token_mock it('allows itself to be deleted and returns new token', function () {
->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); $this->token->user = Mockery::mock(User::class);
$has_one_mock = Mockery::mock(HasOne::class); $has_one_mock = Mockery::mock(HasOne::class);
$token_mock = Mockery::mock(Token::class); $token_mock = Mockery::mock(Token::class);
$has_one_mock $has_one_mock->shouldReceive('make')->once()->withNoArgs()->andReturn($token_mock);
->shouldReceive('make')
->once()
->withNoArgs()
->andReturn($token_mock);
$this->token->user $this->token->user
->shouldReceive('quickBooksToken') ->shouldReceive('quickBooksToken')
@@ -188,25 +101,9 @@ public function it_allows_itself_to_be_deleted_and_returns_new_token()
$this->token->user->id = 1; $this->token->user->id = 1;
$this->assertEquals($token_mock, $this->token->remove()); expect($this->token->remove())->toBe($token_mock);
} });
/** it('gets related user model from configuration', function () {
* @test expect($this->token->user()->getModel())->toBeInstanceOf(User::class);
*/ });
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,
];
}