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
composer.lock
/.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 Unstable Version](https://poser.pugx.org/wearepixel/laravel-quickbooks/v/unstable)](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://poser.pugx.org/wearepixel/laravel-quickbooks/license)](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)
[![Total Downloads](https://img.shields.io/packagist/dt/wearepixel/laravel-quickbooks.svg)](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)
[![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
| Branch | Status | Coverage | Code Quality |
| ------ | :----: | :------: | :----------: |
| 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) |
| Laravel | PHP | Package |
|---------|------|---------|
| 12.x | ^8.3 | ^2.0 |
| 11.x | ^8.3 | ^2.0 |
| 10.x | ^8.3 | ^1.0 |
## Installation
1. Install QuickBooks PHP Client:
1. Install the package:
```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
$ 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
1. You will need a ```quickBooksToken``` relationship on your ```User``` model. There is a trait named ```Wearepixel\QuickBooks\HasQuickBooksToken```, which you can include on your ```User``` model, which will setup the relationship. To do this implement the following:
Add ```use Wearepixel\QuickBooks\HasQuickBooksToken;``` to your service container at the top of User.php
and also add the trait within the class. For example:
### 1. Add the trait to your User model
```php
use Wearepixel\QuickBooks\HasQuickBooksToken;
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
QUICKBOOKS_CLIENT_ID=<client id given by QuickBooks>
QUICKBOOKS_CLIENT_SECRET=<client secret>
```
```bash
QUICKBOOKS_CLIENT_ID=<your client id>
QUICKBOOKS_CLIENT_SECRET=<your client secret>
```
#### Optional Keys
```bash
QUICKBOOKS_API_URL=<Development|Production> # Defaults to App's env value
QUICKBOOKS_DEBUG=<true|false> # Defaults to App's debug value
```
Optional:
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
A configuration file named ```quickbooks.php``` can be published to ```config/``` by running...
### 3. Publish config and views (optional)
```bash
php artisan vendor:publish --tag=quickbooks-config
```
#### Views
View files can be published by running...
```bash
php artisan vendor:publish --tag=quickbooks-views
```
```bash
php artisan vendor:publish --tag=quickbooks-config
php artisan vendor:publish --tag=quickbooks-views
```
## Usage
Here is an example of getting the company information from QuickBooks:
### NOTE: Before doing these commands, go to your connect route (default: /quickbooks/connect) to get a QuickBooks token for your user
First, direct users to `/quickbooks/connect` to authorize their QuickBooks account. Once connected, you can access the API:
```php
php artisan tinker
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
>>> Auth::logInUsingId(1)
=> App\Models\User {#1668
id: 1,
// Other keys removed for example
}
>>> $quickbooks = app('Wearepixel\QuickBooks\Client') // or app('QuickBooks')
=> Wearepixel\QuickBooks\Client {#1613}
>>> $quickbooks->getDataService()->getCompanyInfo();
=> QuickBooksOnline\API\Data\IPPCompanyInfo {#1673
+CompanyName: "Sandbox Company_US_1",
+LegalName: "Sandbox Company_US_1",
// Other properties removed for example
}
>>>
$quickbooks = app(\Wearepixel\QuickBooks\Client::class);
// Get company info
$company = $quickbooks->getDataService()->getCompanyInfo();
// Access reports
$reportService = $quickbooks->getReportService();
```
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
If you have routes that will be dependent on the user's account having a usable QuickBooks OAuth token, there is an included middleware ```Wearepixel\QuickBooks\Laravel\Filter``` that gets registered as ```quickbooks``` that will ensure the account is linked and redirect them to the `connect` route if needed.
Here is an example route definition:
Protect routes that require a QuickBooks connection using the `quickbooks` middleware. Users without a valid token will be redirected to the connect page.
```php
Route::view('some/route/needing/quickbooks/token/before/using', 'some.view')
Route::get('quickbooks/invoices', InvoiceController::class)
->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",
"version": "1.0.0",
"description": "Wearepixel's Laravel Client for QuickBooks.",
"keywords": [
"quickbooks",
"client",
"laravel",
"wearepixel"
],
"license": "MIT",
"authors": [
{
"name": "Pixel",
"email": "hello@wearepixel.com.au"
}
],
"require": {
"php": "^8.0.2",
"illuminate/database": "^10.0|^11.0",
"illuminate/http": "^10.0|^11.0",
"illuminate/routing": "^10.0|^11.0",
"illuminate/support": "^10.0|^11.0",
"nesbot/carbon": "^2.62.1",
"quickbooks/v3-php-sdk": "^5.4.6"
},
"require-dev": {
"laravel/pint": "^1.2",
"mockery/mockery": "^1.5.1",
"phpunit/phpunit": "^10.0",
"psy/psysh": "^0.11",
"scrutinizer/ocular": "^1.9"
},
"autoload": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"prefer-stable": true
"name": "wearepixel/laravel-quickbooks",
"version": "2.0.1",
"description": "Laravel Client for QuickBooks.",
"keywords": [
"quickbooks",
"client",
"laravel",
"wearepixel"
],
"license": "MIT",
"authors": [
{
"name": "Pixel",
"email": "hello@wearepixel.com.au"
}
],
"require": {
"php": "^8.3",
"illuminate/database": "^10.0|^11.0|^12.0",
"illuminate/http": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.62.1|^3.0",
"quickbooks/v3-php-sdk": "^5.4.6"
},
"require-dev": {
"laravel/pint": "^1.2",
"mockery/mockery": "^1.6",
"pestphp/pest": "^3.0",
"psy/psysh": "^0.12"
},
"autoload": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Wearepixel\\QuickBooks\\": "tests/"
},
"files": [
"tests/Overrides.php"
]
},
"extra": {
"laravel": {
"providers": [
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
]
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
},
"prefer-stable": true
}
@@ -15,10 +15,9 @@
public function up()
{
Schema::create('quickbooks_tokens', function (Blueprint $table) {
$user_id_type =
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
? 'unsignedBigInteger'
: 'unsignedInteger';
$user_id_type = str_contains(DB::getSchemaBuilder()->getColumnType('users', 'id'), 'bigint')
? 'unsignedBigInteger'
: 'unsignedInteger';
$table->bigIncrements('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"
backupStaticAttributes="false"
backupStaticProperties="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
verbose="true">
stopOnFailure="false">
<testsuites>
<testsuite name="Wearepixel's Laravel QuickBooks Client">
@@ -28,26 +24,13 @@
<!-- <env name="VARIABLE" value="value"/> -->
</php>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<source>
<include>
<directory suffix=".php">./src</directory>
<exclude>
<directory>./src/Http/Exceptions</directory>
<directory>./src/Providers</directory>
</exclude>
</whitelist>
</filter>
<logging>
<log type="coverage-html"
target="./build/phpunit/coverage"
lowUpperBound="35"
highLowerBound="70"/>
<log type="coverage-text"
target="php://stdout"
showOnlySummary="true"
showUncoveredFiles="false"/>
<log type="coverage-clover" target="build/phpunit/logs/clover.xml"/>
<log type="junit" target="./build/phpunit/logs/junit.xml"/>
</logging>
</include>
<exclude>
<directory>./src/Http/Exceptions</directory>
<directory>./src/Providers</directory>
</exclude>
</source>
</phpunit>
+9 -3
View File
@@ -181,6 +181,12 @@ public function hasValidRefreshToken(): bool
* 2) Have valid refresh token, so renew access token & then use
* 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 ServiceException
*/
@@ -203,7 +209,7 @@ protected function makeDataService(): DataService
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
)->useJson();
}
// Have refresh, so update access & allow app to run
@@ -214,7 +220,7 @@ protected function makeDataService(): DataService
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
)->useJson();
$oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken();
@@ -226,7 +232,7 @@ protected function makeDataService(): DataService
}
// 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 Session $session,
protected UrlGenerator $url_generator,
) {
}
) {}
/**
* Handle an incoming request.
+2 -6
View File
@@ -3,19 +3,15 @@
namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Client;
/**
* 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.
*/
+9 -8
View File
@@ -2,8 +2,8 @@
namespace Wearepixel\QuickBooks\Providers;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Wearepixel\QuickBooks\Http\Controllers\Controller;
use Wearepixel\QuickBooks\Http\Middleware\Filter;
/**
@@ -89,19 +89,20 @@ protected function registerRoutes(): void
->prefix($config['prefix'])
->as('quickbooks.')
->middleware($config['middleware']['default'])
->namespace('Wearepixel\QuickBooks\Http\Controllers')
->group(function (Router $router) use ($config) {
$router
->get($config['paths']['connect'], 'Controller@connect')
->group(function () use ($config) {
$this->app->router
->get($config['paths']['connect'], [Controller::class, 'connect'])
->middleware($config['middleware']['authenticated'])
->name('connect');
$router
->delete($config['paths']['disconnect'], 'Controller@disconnect')
$this->app->router
->delete($config['paths']['disconnect'], [Controller::class, 'disconnect'])
->middleware($config['middleware']['authenticated'])
->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';
/**
* 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.
+175 -254
View File
@@ -1,299 +1,220 @@
<?php
namespace Wearepixel\QuickBooks;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\Core\Http\Serialization\SerializationFormat;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Exception\ServiceException;
use Wearepixel\QuickBooks\Client;
use Wearepixel\QuickBooks\Token;
/**
* Class ClientTest
*/
class ClientTest extends TestCase
{
/**
* @var Client
*/
protected $client;
beforeEach(function () {
$this->configs = [
'data_service' => [
'auth_mode' => 'oauth2',
'base_url' => 'Development',
'client_id' => 'QUICKBOOKS_CLIENT_ID',
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
'scope' => 'com.intuit.quickbooks.accounting',
],
];
/**
* @var array
*/
protected $configs = [];
$this->token_mock = Mockery::mock(Token::class);
$this->client = new Client($this->configs, $this->token_mock);
});
/**
* @var Mock
*/
protected $token_mock;
it('can be constructed', function () {
expect($this->client)->toBeInstanceOf(Client::class);
});
protected function setUp(): void
{
$this->configs = [
'data_service' => [
'auth_mode' => 'oauth2',
'base_url' => 'Development',
'client_id' => 'QUICKBOOKS_CLIENT_ID',
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
'scope' => 'com.intuit.quickbooks.accounting',
],
];
it('returns a data service configured to request oauth token when token is empty', function () {
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$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)
{
if (! is_null($configs)) {
return new Client($configs, $this->token_mock);
}
it('caches the data service once it is made', function () {
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
return new Client($this->configs, $this->token_mock);
}
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Client::class, $this->client);
}
expect($this->client->getDataService())->toBeInstanceOf(DataService::class);
});
/**
* @test
*/
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
it('returns a data service with oauth token when valid access token exists', function () {
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('access_token')
->andReturn('access_token');
$this->assertFalse(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
/**
* @test
*/
public function it_caches_the_data_service_once_it_is_made()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
expect(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
)->toBeTrue();
});
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received
$this->assertEquals(DataService::class, get_class($this->client->getDataService()));
}
it('returns a data service with refreshed token when access token expired but refresh token valid', function () {
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
/**
* @test
*/
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
{
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('access_token')
->andReturn('access_token');
$this->token_mock
->shouldReceive('getAttribute')
->never()
->with('access_token');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
$this->assertTrue(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
$this->client->getDataService();
})->throws(ServiceException::class);
/**
* @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 report service using the data service')
->skip('Once we figure out how to test around the static DataService::Configure');
$this->token_mock
->shouldReceive('getAttribute')
->twice()
->with('hasValidAccessToken')
->andReturnFalse();
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')
->andReturnTrue();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->never()
->with('access_token');
$format = $this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Message->Request->SerializationFormat;
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('realm_id')
->andReturn('realm_id');
expect($format)->toBe(SerializationFormat::Json);
});
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('refresh_token')
->andReturn('refresh_token');
it('has logging off by default')
->skip('Have to figure out how to test this with the new code in 5.x');
// TODO: This really needs to test that things are getting called correctly on the DataService class
$this->assertFalse(
$this->client
->getDataService()
->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(),
);
}
it('allows logging turned on and pointed to expected file')
->skip('Have to figure out how to test this with the new code in 5.x');
/**
* @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('returns self after deleting token', function () {
$this->token_mock
->shouldReceive('remove')
->once()
->withNoArgs()
->andReturnSelf();
/**
* @test
*/
public function it_has_logging_off_by_default()
{
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
expect($this->client->deleteToken())->toBeInstanceOf(Client::class);
});
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnFalse();
it('clears cached data service when token is changed', function () {
$this->token_mock
->shouldReceive('getAttribute')
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertFalse(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
}
$first = $this->client->getDataService();
/**
* @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');
$new_token = Mockery::mock(Token::class);
$new_token
->shouldReceive('getAttribute')
->with('hasValidAccessToken')
->andReturnFalse();
$this->client = $this->makeClient(
array_merge(
[
'logging' => [
'enabled' => true,
'location' => '/some/valid/path',
],
],
$this->configs,
),
);
$new_token
->shouldReceive('getAttribute')
->with('hasValidRefreshToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->client->setToken($new_token);
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$second = $this->client->getDataService();
$this->assertTrue(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
expect($second)->not->toBe($first);
});
$this->assertEquals(
'/some/valid/path',
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->ServiceRequestLoggingLocation,
);
}
it('delegates hasValidAccessToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnTrue();
/**
* @test
*/
public function it_returns_self_after_deleting_token()
{
$this->token_mock
->shouldReceive('remove')
->once()
->withNoArgs()
->andReturnSelf();
expect($this->client->hasValidAccessToken())->toBeTrue();
});
$this->assertInstanceOf(Client::class, $this->client->deleteToken());
}
}
it('delegates hasValidRefreshToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
function dir($path)
{
return $path === '/some/valid/path';
}
function route($name)
{
return $name;
}
expect($this->client->hasValidRefreshToken())->toBeFalse();
});
+10 -33
View File
@@ -1,39 +1,16 @@
<?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/**
* Class HasQuickBooksTokenTest
*/
class HasQuickBooksTokenTest extends TestCase
{
/**
* @var User
*/
protected $user;
beforeEach(function () {
$this->user = new User;
});
protected function setUp(): void
{
parent::setUp();
it('can be constructed', function () {
expect($this->user)->toBeInstanceOf(User::class);
});
$this->user = new User();
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(User::class, $this->user);
}
/**
* @test
*/
public function it_has_a_hasOne_relationship_to_token()
{
$this->assertInstanceOf(Token::class, $this->user->quickBooksToken()->getModel());
}
}
it('has a hasOne relationship to token', function () {
expect($this->user->quickBooksToken()->getModel())->toBeInstanceOf(Token::class);
});
+131 -209
View File
@@ -1,7 +1,5 @@
<?php
namespace Wearepixel\QuickBooks\Http\Controllers;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View;
@@ -9,242 +7,166 @@
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Session\Store;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService;
use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase;
use Wearepixel\QuickBooks\Http\Controllers\Controller;
/**
* Class ControllerTest
*/
class ControllerTest extends TestCase
{
/**
* @var Controller
*/
protected $controller;
beforeEach(function () {
$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->url_generator_mock = Mockery::mock(UrlGenerator::class);
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
$this->view_mock = Mockery::mock(View::class);
/**
* @var Mock
*/
protected $data_service_mock;
$this->controller = new Controller;
});
/**
* @var Mock
*/
protected $quickbooks_mock;
it('can be constructed', function () {
expect($this->controller)->toBeInstanceOf(Controller::class);
});
/**
* @var Mock
*/
protected $redirector_mock;
it('shows view to disconnect if account linked', function () {
$this->data_service_mock
->shouldReceive('getCompanyInfo')
->once()
->withNoArgs()
->andReturn(['name' => 'Company']);
/**
* @var Mock
*/
protected $request_mock;
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
/**
* @var Mock
*/
protected $session_mock;
$this->quickbooks_mock
->shouldReceive('getDataService')
->once()
->withNoArgs()
->andReturn($this->data_service_mock);
/**
* @var Mock
*/
protected $view_factory_mock;
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::disconnect')
->andReturn($this->view_mock);
/**
* @var Mock
*/
protected $view_mock;
$this->view_mock
->shouldReceive('with')
->once()
->withArgs(['company', ['name' => 'Company']])
->andReturnSelf();
protected function setUp(): void
{
$this->data_service_mock = Mockery::mock(DataService::class);
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Store::class);
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
$this->view_mock = Mockery::mock(View::class);
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
});
$this->controller = new Controller();
}
it('shows view to connect if account not linked', function () {
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Controller::class, $this->controller);
}
$this->quickbooks_mock
->shouldReceive('authorizationUri')
->once()
->withNoArgs()
->andReturn('http://uri');
/**
* @test
*/
public function it_shows_view_to_connect_if_account_not_linked()
{
$this->data_service_mock
->shouldReceive('getCompanyInfo')
->once()
->withNoArgs()
->andReturn([
'name' => 'Company',
]);
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::connect')
->andReturn($this->view_mock);
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
$this->view_mock
->shouldReceive('with')
->once()
->withArgs(['authorization_uri', 'http://uri'])
->andReturnSelf();
$this->quickbooks_mock
->shouldReceive('getDataService')
->once()
->withNoArgs()
->andReturn($this->data_service_mock);
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
});
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::disconnect')
->andReturn($this->view_mock);
it('disconnects from quickbooks when requested', function () {
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->view_mock
->shouldReceive('with')
->once()
->withArgs([
'company',
[
'name' => 'Company',
],
])
->andReturnSelf();
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
}
$this->redirector_mock
->shouldReceive('back')
->once()
->andReturn(new RedirectResponse('/test', 302));
/**
* @test
*/
public function it_shows_view_to_disconnect_if_account_linked()
{
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
$this->quickbooks_mock
->shouldReceive('deleteToken')
->once()
->withNoArgs();
$this->quickbooks_mock
->shouldReceive('authorizationUri')
->once()
->withNoArgs()
->andReturn('http://uri');
$result = $this->controller->disconnect(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
);
$this->view_factory_mock
->shouldReceive('make')
->once()
->with('quickbooks::connect')
->andReturn($this->view_mock);
expect($result)->toBeInstanceOf(RedirectResponse::class);
});
$this->view_mock
->shouldReceive('with')
->once()
->withArgs(['authorization_uri', 'http://uri'])
->andReturnSelf();
it('finishes connecting to quickbooks when given a valid token', function () {
$realmId = random_int(1, 9999);
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
}
$this->quickbooks_mock
->shouldReceive('exchangeCodeForToken')
->once()
->withArgs(['code', $realmId]);
/**
* @test
*/
public function it_disconnects_from_quickbooks_when_requested()
{
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['code'])
->andReturn('code');
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['realmId'])
->andReturn($realmId);
$this->redirector_mock
->shouldReceive('back')
->once()
->andReturn(new RedirectResponse('/test', 302));
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->quickbooks_mock
->shouldReceive('deleteToken')
->once()
->withNoArgs();
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$result = $this->controller->disconnect(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
);
$this->redirector_mock
->shouldReceive('intended')
->once()
->withAnyArgs()
->andReturn(new RedirectResponse('/test', 302));
$this->assertInstanceOf(RedirectResponse::class, $result);
}
$this->url_generator_mock
->shouldReceive('route')
->withArgs(['quickbooks.connect'])
->once();
/**
* @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);
$result = $this->controller->token(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
$this->url_generator_mock,
);
$this->quickbooks_mock
->shouldReceive('exchangeCodeForToken')
->once()
->withArgs(['code', $realmId]);
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['code'])
->andReturn('code');
$this->request_mock
->shouldReceive('get')
->once()
->withArgs(['realmId'])
->andReturn($realmId);
$this->request_mock
->shouldReceive('session')
->once()
->andReturn($this->session_mock);
$this->session_mock
->shouldReceive('flash')
->once()
->withAnyArgs();
$this->redirector_mock
->shouldReceive('intended')
->once()
->withAnyArgs()
->andReturn(new RedirectResponse('/test', 302));
$this->url_generator_mock
->shouldReceive('route')
->withArgs(['quickbooks.connect'])
->once();
$result = $this->controller->token(
$this->redirector_mock,
$this->request_mock,
$this->quickbooks_mock,
$this->url_generator_mock,
);
$this->assertInstanceOf(RedirectResponse::class, $result);
}
}
expect($result)->toBeInstanceOf(RedirectResponse::class);
});
+59 -113
View File
@@ -1,133 +1,79 @@
<?php
namespace Wearepixel\QuickBooks\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Mockery;
use Mockery\Mock;
use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase;
use Wearepixel\QuickBooks\Http\Middleware\Filter;
/**
* Class FilterTest
*/
class FilterTest extends TestCase
{
/**
* @var Filter
*/
protected $filter;
beforeEach(function () {
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Session::class);
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
/**
* @var Mock
*/
protected $quickbooks_mock;
$this->filter = new Filter(
$this->quickbooks_mock,
$this->redirector_mock,
$this->session_mock,
$this->url_generator_mock,
);
});
/**
* @var Mock
*/
protected $redirector_mock;
it('can be constructed', function () {
expect($this->filter)->toBeInstanceOf(Filter::class);
});
/**
* @var Mock
*/
protected $request_mock;
it('passes the request to the next middleware if account linked to quickbooks', function () {
$next_middleware = function ($request) {
expect($request)->toBe($this->request_mock);
};
/**
* @var Mock
*/
protected $session_mock;
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
/**
* @var Mock
*/
protected $url_generator_mock;
$this->filter->handle($this->request_mock, $next_middleware);
});
protected function setUp(): void
{
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Session::class);
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
it('redirects to quickbooks connect route after setting intended session if account not linked', function () {
$this->request_mock
->shouldReceive('path')
->once()
->withNoArgs()
->andReturn('path');
$this->filter = new Filter(
$this->quickbooks_mock,
$this->redirector_mock,
$this->session_mock,
$this->url_generator_mock,
);
}
$this->url_generator_mock
->shouldReceive('to')
->once()
->with('path')
->andReturn('http://to/path');
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Filter::class, $this->filter);
}
$this->session_mock
->shouldReceive('put')
->once()
->withArgs(['url.intended', 'http://to/path'])
->andReturnNull();
/**
* @test
*/
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
{
$next_middleware = function ($request) {
$this->assertEquals($this->request_mock, $request);
};
$this->redirector_mock
->shouldReceive('route')
->once()
->with('quickbooks.connect')
->andReturnSelf();
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnTrue();
$next_middleware = function ($request) {
throw new RuntimeException('Next middleware should not be called');
};
$this->filter->handle($this->request_mock, $next_middleware);
}
$this->quickbooks_mock
->shouldReceive('hasValidRefreshToken')
->once()
->withNoArgs()
->andReturnFalse();
/**
* @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);
}
}
$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
namespace Wearepixel\QuickBooks;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Mockery;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use Wearepixel\QuickBooks\Stubs\TokenStub;
use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/**
* Class TokenTest
*/
class TokenTest extends TestCase
{
/**
* @var Token
*/
protected $token;
beforeEach(function () {
$this->token = new TokenStub;
});
protected function setUp(): void
{
$this->token = new TokenStub();
}
it('can be constructed', function () {
expect($this->token)->toBeInstanceOf(Token::class);
});
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Token::class, $this->token);
}
it('has accessor for valid access token', function () {
expect($this->token->getHasValidAccessTokenAttribute())->not->toBeNull();
});
/**
* @test
*/
public function it_has_accessor_for_valid_access_token()
{
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
}
it('has accessor for valid refresh token', function () {
expect($this->token->getHasValidRefreshTokenAttribute())->not->toBeNull();
});
/**
* @test
*/
public function it_has_accessor_for_valid_refresh_token()
{
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
}
it('knows that the access token expires at has to be valid', function () {
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
});
/**
* @test
*/
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
}
it('knows that the refresh token expires at has to be valid', function () {
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
});
/**
* @test
*/
public function it_knows_that_the_refresh_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
}
it('knows if access token expires at is less than now it is not expired', function () {
Carbon::setTestNow(Carbon::now());
/**
* @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();
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());
/**
* @test
*/
public function it_know_if_refresh_token_expires_at_is_less_than_now_it_is_not_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->refresh_token_expires_at = Carbon::now()->addSecond();
$this->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());
/**
* @test
*/
public function it_know_if_access_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->access_token_expires_at = Carbon::now();
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
$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();
});
/**
* @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);
it('stores the oauth token parts in expected properties', function () {
$oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class);
$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);
/**
* @test
*/
public function it_stores_the_oauth_token_parts_in_expected_properties()
{
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
$has_one_mock = Mockery::mock(HasOne::class);
$token_mock = Mockery::mock(Token::class);
$oauth_token_mock
->shouldReceive('getAccessToken')
->once()
->withNoArgs()
->andReturn('access_token');
$has_one_mock->shouldReceive('make')->once()->withNoArgs()->andReturn($token_mock);
$oauth_token_mock
->shouldReceive('getAccessTokenExpiresAt')
->once()
->withNoArgs()
->andReturn('now');
$this->token->user
->shouldReceive('quickBooksToken')
->once()
->withNoArgs()
->andReturn($has_one_mock);
$oauth_token_mock
->shouldReceive('getRealmID')
->once()
->withNoArgs()
->andReturn('realm_id');
$this->token->user->id = 1;
$oauth_token_mock
->shouldReceive('getRefreshToken')
->once()
->withNoArgs()
->andReturn('refresh_token');
expect($this->token->remove())->toBe($token_mock);
});
$oauth_token_mock
->shouldReceive('getRefreshTokenExpiresAt')
->once()
->withNoArgs()
->andReturn('now');
$this->token->parseOauthToken($oauth_token_mock);
}
/**
* @test
*/
public function it_allows_itself_to_be_deleted_and_returns_new_token()
{
$this->token->user = Mockery::mock(User::class);
$has_one_mock = Mockery::mock(HasOne::class);
$token_mock = Mockery::mock(Token::class);
$has_one_mock
->shouldReceive('make')
->once()
->withNoArgs()
->andReturn($token_mock);
$this->token->user
->shouldReceive('quickBooksToken')
->once()
->withNoArgs()
->andReturn($has_one_mock);
$this->token->user->id = 1;
$this->assertEquals($token_mock, $this->token->remove());
}
/**
* @test
*/
public function it_get_related_user_model_from_configuration()
{
$this->assertInstanceOf(User::class, $this->token->user()->getModel());
}
}
function config($key)
{
return [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
];
}
it('gets related user model from configuration', function () {
expect($this->token->user()->getModel())->toBeInstanceOf(User::class);
});