11 Commits
Author SHA1 Message Date
Joel Male 32266f6dd5 fix: revert JSON serialization workaround, upgrade SDK to 6.3.1 instead
v2.0.1's useJson() workaround was wrong: the JSON request serializer
silently drops nested single-object fields (e.g. CustomerRef, which
QuickBooks requires), and the JSON response deserializer never returns
a typed entity, so $result->Id is always null downstream. That breaks
every dsCall() site, worse than the bug it was meant to fix.

The actual root cause was that quickbooks/v3-php-sdk 5.4.7 predates
QuickBooks Online's new invoice AllowOnlinePayPalPayment field, so its
XML deserializer throws on responses that include it. SDK 6.3.1
declares the field, so upgrading resolves the original error while
keeping the fully-supported XML path (and closes out the SDK bump
Dependabot already had queued in #3).

Verified: deserializing a response containing AllowOnlinePayPalPayment
throws RuntimeException on 5.4.7 and returns a correctly typed
IPPInvoice (with Id populated) on 6.3.1.
2026-07-27 07:15:59 +03:00
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
23 changed files with 707 additions and 1059 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
+59 -54
View File
@@ -1,56 +1,61 @@
{ {
"name": "wearepixel/laravel-quickbooks", "name": "wearepixel/laravel-quickbooks",
"version": "1.0.0", "version": "2.0.2",
"description": "Wearepixel's Laravel Client for QuickBooks.", "description": "Laravel Client for QuickBooks.",
"keywords": [ "keywords": [
"quickbooks", "quickbooks",
"client", "client",
"laravel", "laravel",
"wearepixel" "wearepixel"
], ],
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Pixel", "name": "Pixel",
"email": "hello@wearepixel.com.au" "email": "hello@wearepixel.com.au"
} }
], ],
"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": "^6.3.1"
}, },
"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": { "Wearepixel\\QuickBooks\\": "src/"
"Wearepixel\\QuickBooks\\": "src/" }
} },
}, "autoload-dev": {
"autoload-dev": { "psr-4": {
"psr-4": { "Wearepixel\\QuickBooks\\": "tests/"
"Wearepixel\\QuickBooks\\": "tests/" },
} "files": [
}, "tests/Overrides.php"
"extra": { ]
"laravel": { },
"providers": [ "extra": {
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider", "laravel": {
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider" "providers": [
] "Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
} "Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
}, ]
"config": { }
"sort-packages": true },
}, "config": {
"prefer-stable": true "allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
},
"prefer-stable": true
} }
@@ -15,10 +15,9 @@
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';
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->{$user_id_type}('user_id'); $table->{$user_id_type}('user_id');
-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
+10 -27
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>
<exclude> </include>
<directory>./src/Http/Exceptions</directory> <exclude>
<directory>./src/Providers</directory> <directory>./src/Http/Exceptions</directory>
</exclude> <directory>./src/Providers</directory>
</whitelist> </exclude>
</filter> </source>
<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>
+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.
+157 -258
View File
@@ -1,299 +1,198 @@
<?php <?php
namespace Wearepixel\QuickBooks;
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 $this->configs = [
*/ 'data_service' => [
class ClientTest extends TestCase 'auth_mode' => 'oauth2',
{ 'base_url' => 'Development',
/** 'client_id' => 'QUICKBOOKS_CLIENT_ID',
* @var Client 'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
*/ 'scope' => 'com.intuit.quickbooks.accounting',
protected $client; ],
];
/** $this->token_mock = Mockery::mock(Token::class);
* @var array $this->client = new Client($this->configs, $this->token_mock);
*/ });
protected $configs = [];
/** it('can be constructed', function () {
* @var Mock expect($this->client)->toBeInstanceOf(Client::class);
*/ });
protected $token_mock;
protected function setUp(): void it('returns a data service configured to request oauth token when token is empty', function () {
{ $this->token_mock
$this->configs = [ ->shouldReceive('getAttribute')
'data_service' => [ ->twice()
'auth_mode' => 'oauth2', ->with('hasValidAccessToken')
'base_url' => 'Development', ->andReturnFalse();
'client_id' => 'QUICKBOOKS_CLIENT_ID',
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
'scope' => 'com.intuit.quickbooks.accounting',
],
];
$this->token_mock = Mockery::mock(Token::class); $this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->client = $this->makeClient(); expect(
} $this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
)->toBeFalse();
});
private function makeClient($configs = null) it('caches the data service once it is made', function () {
{ $this->token_mock
if (! is_null($configs)) { ->shouldReceive('getAttribute')
return new Client($configs, $this->token_mock); ->twice()
} ->with('hasValidAccessToken')
->andReturnFalse();
return new Client($this->configs, $this->token_mock); $this->token_mock
} ->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
/** expect($this->client->getDataService())->toBeInstanceOf(DataService::class);
* @test });
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Client::class, $this->client);
}
/** it('returns a data service with oauth token when valid access token exists', function () {
* @test $this->token_mock
*/ ->shouldReceive('getAttribute')
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty() ->twice()
{ ->with('hasValidAccessToken')
$this->token_mock ->andReturnTrue();
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->once() ->once()
->with('hasValidRefreshToken') ->with('access_token')
->andReturnFalse(); ->andReturn('access_token');
$this->assertFalse( $this->token_mock
$this->client ->shouldReceive('getAttribute')
->getDataService() ->once()
->getServiceContext() ->with('realm_id')
->IppConfiguration->Security->isAccessTokenSet(), ->andReturn('realm_id');
);
}
/** $this->token_mock
* @test ->shouldReceive('getAttribute')
*/ ->once()
public function it_caches_the_data_service_once_it_is_made() ->with('refresh_token')
{ ->andReturn('refresh_token');
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock expect(
->shouldReceive('getAttribute') $this->client
->once() ->getDataService()
->with('hasValidRefreshToken') ->getServiceContext()
->andReturnFalse(); ->IppConfiguration->Security->isAccessTokenSet(),
)->toBeTrue();
});
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received it('returns a data service with refreshed token when access token expired but refresh token valid', function () {
$this->assertEquals(DataService::class, get_class($this->client->getDataService())); $this->token_mock
} ->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
/** $this->token_mock
* @test ->shouldReceive('getAttribute')
*/ ->once()
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist() ->with('hasValidRefreshToken')
{ ->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnTrue();
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->once() ->never()
->with('access_token') ->with('access_token');
->andReturn('access_token');
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->once() ->once()
->with('realm_id') ->with('realm_id')
->andReturn('realm_id'); ->andReturn('realm_id');
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->once() ->once()
->with('refresh_token') ->with('refresh_token')
->andReturn('refresh_token'); ->andReturn('refresh_token');
$this->assertTrue( $this->client->getDataService();
$this->client })->throws(ServiceException::class);
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
/** it('returns a report service using the data service')
* @test ->skip('Once we figure out how to test around the static DataService::Configure');
*/
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 it('has logging off by default')
->shouldReceive('getAttribute') ->skip('Have to figure out how to test this with the new code in 5.x');
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$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('hasValidRefreshToken')
->andReturnTrue();
$this->token_mock it('returns self after deleting token', function () {
->shouldReceive('getAttribute') $this->token_mock
->never() ->shouldReceive('remove')
->with('access_token'); ->once()
->withNoArgs()
->andReturnSelf();
$this->token_mock expect($this->client->deleteToken())->toBeInstanceOf(Client::class);
->shouldReceive('getAttribute') });
->once()
->with('realm_id')
->andReturn('realm_id');
$this->token_mock it('clears cached data service when token is changed', function () {
->shouldReceive('getAttribute') $this->token_mock
->once() ->shouldReceive('getAttribute')
->with('refresh_token') ->with('hasValidAccessToken')
->andReturn('refresh_token'); ->andReturnFalse();
// TODO: This really needs to test that things are getting called correctly on the DataService class $this->token_mock
$this->assertFalse( ->shouldReceive('getAttribute')
$this->client ->with('hasValidRefreshToken')
->getDataService() ->andReturnFalse();
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
/** $first = $this->client->getDataService();
* @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',
);
}
/** $new_token = Mockery::mock(Token::class);
* @test $new_token
*/ ->shouldReceive('getAttribute')
public function it_has_logging_off_by_default() ->with('hasValidAccessToken')
{ ->andReturnFalse();
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
$this->token_mock $new_token
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->once() ->with('hasValidRefreshToken')
->with('hasValidAccessToken') ->andReturnFalse();
->andReturnFalse();
$this->token_mock $this->client->setToken($new_token);
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertFalse( $second = $this->client->getDataService();
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
}
/** expect($second)->not->toBe($first);
* @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( it('delegates hasValidAccessToken to token', function () {
array_merge( $this->token_mock
[ ->shouldReceive('getAttribute')
'logging' => [ ->once()
'enabled' => true, ->with('hasValidAccessToken')
'location' => '/some/valid/path', ->andReturnTrue();
],
],
$this->configs,
),
);
$this->token_mock expect($this->client->hasValidAccessToken())->toBeTrue();
->shouldReceive('getAttribute') });
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock it('delegates hasValidRefreshToken to token', function () {
->shouldReceive('getAttribute') $this->token_mock
->once() ->shouldReceive('getAttribute')
->with('hasValidRefreshToken') ->once()
->andReturnFalse(); ->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertTrue( expect($this->client->hasValidRefreshToken())->toBeFalse();
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;
}
+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());
}
}
+131 -209
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,242 +7,166 @@
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 $this->data_service_mock = Mockery::mock(DataService::class);
*/ $this->quickbooks_mock = Mockery::mock(QuickBooks::class);
class ControllerTest extends TestCase $this->redirector_mock = Mockery::mock(Redirector::class);
{ $this->request_mock = Mockery::mock(Request::class);
/** $this->session_mock = Mockery::mock(Store::class);
* @var Controller $this->url_generator_mock = Mockery::mock(UrlGenerator::class);
*/ $this->view_factory_mock = Mockery::mock(ViewFactory::class);
protected $controller; $this->view_mock = Mockery::mock(View::class);
/** $this->controller = new Controller;
* @var Mock });
*/
protected $data_service_mock;
/** it('can be constructed', function () {
* @var Mock expect($this->controller)->toBeInstanceOf(Controller::class);
*/ });
protected $quickbooks_mock;
/** it('shows view to disconnect if account linked', function () {
* @var Mock $this->data_service_mock
*/ ->shouldReceive('getCompanyInfo')
protected $redirector_mock; ->once()
->withNoArgs()
->andReturn(['name' => 'Company']);
/** $this->quickbooks_mock
* @var Mock ->shouldReceive('hasValidRefreshToken')
*/ ->once()
protected $request_mock; ->withNoArgs()
->andReturnTrue();
/** $this->quickbooks_mock
* @var Mock ->shouldReceive('getDataService')
*/ ->once()
protected $session_mock; ->withNoArgs()
->andReturn($this->data_service_mock);
/** $this->view_factory_mock
* @var Mock ->shouldReceive('make')
*/ ->once()
protected $view_factory_mock; ->with('quickbooks::disconnect')
->andReturn($this->view_mock);
/** $this->view_mock
* @var Mock ->shouldReceive('with')
*/ ->once()
protected $view_mock; ->withArgs(['company', ['name' => 'Company']])
->andReturnSelf();
protected function setUp(): void $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
{ });
$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(); it('shows view to connect if account not linked', function () {
} $this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
/** $this->quickbooks_mock
* @test ->shouldReceive('authorizationUri')
*/ ->once()
public function it_can_be_constructed() ->withNoArgs()
{ ->andReturn('http://uri');
$this->assertInstanceOf(Controller::class, $this->controller);
}
/** $this->view_factory_mock
* @test ->shouldReceive('make')
*/ ->once()
public function it_shows_view_to_connect_if_account_not_linked() ->with('quickbooks::connect')
{ ->andReturn($this->view_mock);
$this->data_service_mock
->shouldReceive('getCompanyInfo')
->once()
->withNoArgs()
->andReturn([
'name' => 'Company',
]);
$this->quickbooks_mock $this->view_mock
->shouldReceive('hasValidRefreshToken') ->shouldReceive('with')
->once() ->once()
->withNoArgs() ->withArgs(['authorization_uri', 'http://uri'])
->andReturnTrue(); ->andReturnSelf();
$this->quickbooks_mock $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
->shouldReceive('getDataService') });
->once()
->withNoArgs()
->andReturn($this->data_service_mock);
$this->view_factory_mock it('disconnects from quickbooks when requested', function () {
->shouldReceive('make') $this->request_mock
->once() ->shouldReceive('session')
->with('quickbooks::disconnect') ->once()
->andReturn($this->view_mock); ->andReturn($this->session_mock);
$this->view_mock $this->session_mock
->shouldReceive('with') ->shouldReceive('flash')
->once() ->once()
->withArgs([ ->withAnyArgs();
'company',
[
'name' => 'Company',
],
])
->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->redirector_mock
} ->shouldReceive('back')
->once()
->andReturn(new RedirectResponse('/test', 302));
/** $this->quickbooks_mock
* @test ->shouldReceive('deleteToken')
*/ ->once()
public function it_shows_view_to_disconnect_if_account_linked() ->withNoArgs();
{
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
$this->quickbooks_mock $result = $this->controller->disconnect(
->shouldReceive('authorizationUri') $this->redirector_mock,
->once() $this->request_mock,
->withNoArgs() $this->quickbooks_mock,
->andReturn('http://uri'); );
$this->view_factory_mock expect($result)->toBeInstanceOf(RedirectResponse::class);
->shouldReceive('make') });
->once()
->with('quickbooks::connect')
->andReturn($this->view_mock);
$this->view_mock it('finishes connecting to quickbooks when given a valid token', function () {
->shouldReceive('with') $realmId = random_int(1, 9999);
->once()
->withArgs(['authorization_uri', 'http://uri'])
->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->quickbooks_mock
} ->shouldReceive('exchangeCodeForToken')
->once()
->withArgs(['code', $realmId]);
/** $this->request_mock
* @test ->shouldReceive('get')
*/ ->once()
public function it_disconnects_from_quickbooks_when_requested() ->withArgs(['code'])
{ ->andReturn('code');
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->session_mock $this->request_mock
->shouldReceive('flash') ->shouldReceive('get')
->once() ->once()
->withAnyArgs(); ->withArgs(['realmId'])
->andReturn($realmId);
$this->redirector_mock $this->request_mock
->shouldReceive('back') ->shouldReceive('session')
->once() ->once()
->andReturn(new RedirectResponse('/test', 302)); ->andReturn($this->session_mock);
$this->quickbooks_mock $this->session_mock
->shouldReceive('deleteToken') ->shouldReceive('flash')
->once() ->once()
->withNoArgs(); ->withAnyArgs();
$result = $this->controller->disconnect( $this->redirector_mock
$this->redirector_mock, ->shouldReceive('intended')
$this->request_mock, ->once()
$this->quickbooks_mock, ->withAnyArgs()
); ->andReturn(new RedirectResponse('/test', 302));
$this->assertInstanceOf(RedirectResponse::class, $result); $this->url_generator_mock
} ->shouldReceive('route')
->withArgs(['quickbooks.connect'])
->once();
/** $result = $this->controller->token(
* @test $this->redirector_mock,
*/ $this->request_mock,
public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by_quickbooks() $this->quickbooks_mock,
{ $this->url_generator_mock,
$this->url_generator_mock = Mockery::mock(UrlGenerator::class); );
$realmId = random_int(1, 9999);
$this->quickbooks_mock expect($result)->toBeInstanceOf(RedirectResponse::class);
->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);
}
}
+59 -113
View File
@@ -1,133 +1,79 @@
<?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 $this->quickbooks_mock = Mockery::mock(QuickBooks::class);
*/ $this->redirector_mock = Mockery::mock(Redirector::class);
class FilterTest extends TestCase $this->request_mock = Mockery::mock(Request::class);
{ $this->session_mock = Mockery::mock(Session::class);
/** $this->url_generator_mock = Mockery::mock(UrlGenerator::class);
* @var Filter
*/
protected $filter;
/** $this->filter = new Filter(
* @var Mock $this->quickbooks_mock,
*/ $this->redirector_mock,
protected $quickbooks_mock; $this->session_mock,
$this->url_generator_mock,
);
});
/** it('can be constructed', function () {
* @var Mock expect($this->filter)->toBeInstanceOf(Filter::class);
*/ });
protected $redirector_mock;
/** it('passes the request to the next middleware if account linked to quickbooks', function () {
* @var Mock $next_middleware = function ($request) {
*/ expect($request)->toBe($this->request_mock);
protected $request_mock; };
/** $this->quickbooks_mock
* @var Mock ->shouldReceive('hasValidRefreshToken')
*/ ->once()
protected $session_mock; ->withNoArgs()
->andReturnTrue();
/** $this->filter->handle($this->request_mock, $next_middleware);
* @var Mock });
*/
protected $url_generator_mock;
protected function setUp(): void it('redirects to quickbooks connect route after setting intended session if account not linked', function () {
{ $this->request_mock
$this->quickbooks_mock = Mockery::mock(QuickBooks::class); ->shouldReceive('path')
$this->redirector_mock = Mockery::mock(Redirector::class); ->once()
$this->request_mock = Mockery::mock(Request::class); ->withNoArgs()
$this->session_mock = Mockery::mock(Session::class); ->andReturn('path');
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$this->filter = new Filter( $this->url_generator_mock
$this->quickbooks_mock, ->shouldReceive('to')
$this->redirector_mock, ->once()
$this->session_mock, ->with('path')
$this->url_generator_mock, ->andReturn('http://to/path');
);
}
/** $this->session_mock
* @test ->shouldReceive('put')
*/ ->once()
public function it_can_be_constructed() ->withArgs(['url.intended', 'http://to/path'])
{ ->andReturnNull();
$this->assertInstanceOf(Filter::class, $this->filter);
}
/** $this->redirector_mock
* @test ->shouldReceive('route')
*/ ->once()
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks() ->with('quickbooks.connect')
{ ->andReturnSelf();
$next_middleware = function ($request) {
$this->assertEquals($this->request_mock, $request);
};
$this->quickbooks_mock $next_middleware = function ($request) {
->shouldReceive('hasValidRefreshToken') throw new RuntimeException('Next middleware should not be called');
->once() };
->withNoArgs()
->andReturnTrue();
$this->filter->handle($this->request_mock, $next_middleware); $this->quickbooks_mock
} ->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
/** $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);
}
}
+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]);
});
});
+74 -177
View File
@@ -1,212 +1,109 @@
<?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());
}
/** $this->token->access_token_expires_at = Carbon::now()->addSecond();
* @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(); expect($this->token->getHasValidAccessTokenAttribute())->toBeTrue();
});
$this->assertTrue($this->token->getHasValidAccessTokenAttribute()); it('knows if refresh token expires at is less than now it is not expired', function () {
} Carbon::setTestNow(Carbon::now());
/** $this->token->refresh_token_expires_at = Carbon::now()->addSecond();
* @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(); expect($this->token->getHasValidRefreshTokenAttribute())->toBeTrue();
});
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute()); it('knows if access token expires at is greater than or equal to now it is expired', function () {
} Carbon::setTestNow(Carbon::now());
/** $this->token->access_token_expires_at = Carbon::now();
* @test expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
*/
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->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());
$this->token->access_token_expires_at->subSecond(); $this->token->refresh_token_expires_at = Carbon::now();
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
$this->assertFalse($this->token->getHasValidAccessTokenAttribute()); $this->token->refresh_token_expires_at->subSecond();
} expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
});
/** it('stores the oauth token parts in expected properties', function () {
* @test $oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class);
*/
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(); $oauth_token_mock->shouldReceive('getAccessToken')->once()->withNoArgs()->andReturn('access_token');
$oauth_token_mock->shouldReceive('getAccessTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-06-15 12:00:00');
$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('2025-09-24 12:00:00');
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute()); $result = $this->token->parseOauthToken($oauth_token_mock);
$this->token->refresh_token_expires_at->subSecond(); expect($result)->toBeInstanceOf(Token::class)
->and($this->token->access_token)->toBe('access_token')
->and($this->token->realm_id)->toBe('realm_id')
->and($this->token->refresh_token)->toBe('refresh_token')
->and($this->token->access_token_expires_at)->toBeInstanceOf(Carbon::class)
->and($this->token->refresh_token_expires_at)->toBeInstanceOf(Carbon::class);
});
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute()); it('allows itself to be deleted and returns new token', function () {
} $this->token->user = Mockery::mock(User::class);
/** $has_one_mock = Mockery::mock(HasOne::class);
* @test $token_mock = Mockery::mock(Token::class);
*/
public function it_stores_the_oauth_token_parts_in_expected_properties()
{
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
$oauth_token_mock $has_one_mock->shouldReceive('make')->once()->withNoArgs()->andReturn($token_mock);
->shouldReceive('getAccessToken')
->once()
->withNoArgs()
->andReturn('access_token');
$oauth_token_mock $this->token->user
->shouldReceive('getAccessTokenExpiresAt') ->shouldReceive('quickBooksToken')
->once() ->once()
->withNoArgs() ->withNoArgs()
->andReturn('now'); ->andReturn($has_one_mock);
$oauth_token_mock $this->token->user->id = 1;
->shouldReceive('getRealmID')
->once()
->withNoArgs()
->andReturn('realm_id');
$oauth_token_mock expect($this->token->remove())->toBe($token_mock);
->shouldReceive('getRefreshToken') });
->once()
->withNoArgs()
->andReturn('refresh_token');
$oauth_token_mock it('gets related user model from configuration', function () {
->shouldReceive('getRefreshTokenExpiresAt') expect($this->token->user()->getModel())->toBeInstanceOf(User::class);
->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,
];
}