17 Commits
Author SHA1 Message Date
pixel-workflows[bot] c94788fdb8 chore: update dependabot configuration 2026-07-27 23:27:56 +00:00
pixel-workflows[bot] 3a5c51d3d0 chore: sync files from wearepixel/pixel-workflows - synced local 'pint.json' with remote 'files/pint.json' 2026-07-27 08:21:02 +00:00
pixel-workflows[bot] f7d07f45e5 chore: sync files from wearepixel/pixel-workflows - created local '.gitattributes' from remote 'files/.gitattributes' 2026-07-27 08:20:59 +00:00
pixel-workflows[bot] b4a5d00439 chore: sync files from wearepixel/pixel-workflows - synced local '.editorconfig' with remote 'files/.editorconfig' 2026-07-27 08:20:56 +00:00
pixel-workflows[bot] 524a09d545 chore: sync files from wearepixel/pixel-workflows - created local '.github/workflows/dependabot-slack-alert.yml' from remote 'files/.github/workflows/dependabot-slack-alert.yml' 2026-07-27 08:20:53 +00:00
pixel-workflows[bot] c75ab43346 chore: sync files from wearepixel/pixel-workflows - created local '.github/workflows/dependabot-auto-merge.yml' from remote 'files/.github/workflows/dependabot-auto-merge.yml' 2026-07-27 08:20:50 +00:00
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
27 changed files with 893 additions and 1063 deletions
+6 -4
View File
@@ -3,14 +3,16 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4 indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.yml] [*.{yml,yaml}]
indent_style = space
indent_size = 2 indent_size = 2
[docker-compose.yml]
indent_size = 4
+10 -8
View File
@@ -1,9 +1,11 @@
* text=auto * text=auto eol=lf
/build export-ignore *.blade.php diff=html
/tests export-ignore *.css diff=css
.gitattributes export-ignore *.html diff=html
.gitignore export-ignore *.md diff=markdown
.travis.yml export-ignore *.php diff=php
nitpick.json export-ignore
phpunit.xml.dist export-ignore /.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+38
View File
@@ -0,0 +1,38 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
groups:
actions:
applies-to: version-updates
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:
applies-to: version-updates
update-types:
- "minor"
- "patch"
php-security:
applies-to: security-updates
patterns: ["*"]
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
@@ -0,0 +1,59 @@
name: Dependabot Auto-merge
on:
workflow_run:
workflows: ["Tests"]
types: [completed]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.actor.login == 'dependabot[bot]'
steps:
- name: Find PR and check update type
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
PR_DATA=$(gh pr list --repo "${{ github.repository }}" --head "$HEAD_BRANCH" --json number,title,url,author --jq '.[0]')
PR_URL=$(echo "$PR_DATA" | jq -r '.url')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
if [ "$PR_AUTHOR" != "app/dependabot" ]; then
echo "is_safe=false" >> $GITHUB_OUTPUT
echo "Not a Dependabot PR, skipping."
exit 0
fi
# Parse major version numbers from title: "bump X from A[.B.C] to D[.E.F]"
# Works for both "from 4.0.0 to 4.1.0" and "from 4 to 6" (GitHub Actions)
FROM_MAJOR=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+')
TO_MAJOR=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+')
echo "PR: $PR_TITLE (major: $FROM_MAJOR -> $TO_MAJOR)"
if [ "$FROM_MAJOR" = "$TO_MAJOR" ]; then
echo "is_safe=true" >> $GITHUB_OUTPUT
else
echo "is_safe=false" >> $GITHUB_OUTPUT
echo "Major version bump, skipping auto-merge."
fi
- name: Merge minor and patch updates
if: steps.check.outputs.is_safe == 'true'
run: gh pr merge --merge "${{ steps.check.outputs.pr_url }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,98 @@
name: Dependabot Slack Alert
on:
pull_request_target:
types: [opened]
jobs:
notify:
name: Notify Slack of major update
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Check if major version bump
id: check
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
from_major=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+' | head -1)
to_major=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+' | head -1)
if [ -n "$from_major" ] && [ -n "$to_major" ] && [ "$from_major" != "$to_major" ]; then
echo "is_major=true" >> $GITHUB_OUTPUT
echo "Major bump detected: $from_major -> $to_major"
else
echo "is_major=false" >> $GITHUB_OUTPUT
echo "Minor/patch bump, skipping"
fi
- name: Send Slack alert
if: steps.check.outputs.is_major == 'true'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "SLACK_WEBHOOK_URL not configured for this repo, skipping"
exit 0
fi
payload=$(jq -n \
--arg repo "$REPO" \
--arg title "$PR_TITLE" \
--arg url "$PR_URL" \
--arg pr "#$PR_NUMBER" \
'{
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ":dependabot: *Major dependency update requires review*"
}
},
{
type: "section",
fields: [
{type: "mrkdwn", text: ("*Repository*\n`" + $repo + "`")},
{type: "mrkdwn", text: ("*Pull Request*\n<" + $url + "|" + $pr + ">") }
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: $title
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Major version bumps are not auto-merged as they may contain breaking changes."
}
]
},
{
type: "actions",
elements: [
{
type: "button",
text: {type: "plain_text", text: "Review PR"},
url: $url,
style: "primary"
}
]
}
]
}')
curl -s -o /dev/null -w "%{http_code}" -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-type: application/json" \
--data "$payload" | grep -q "^200$" \
&& echo "Slack notification sent" \
|| echo "Slack notification failed"
+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
+52 -64
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=<client id given by QuickBooks> QUICKBOOKS_CLIENT_ID=<your client id>
QUICKBOOKS_CLIENT_SECRET=<client secret> QUICKBOOKS_CLIENT_SECRET=<your client secret>
``` ```
#### Optional Keys Optional:
```bash ```bash
QUICKBOOKS_API_URL=<Development|Production> # Defaults to App's env value QUICKBOOKS_API_URL=<Development|Production> # Defaults based on APP_ENV
QUICKBOOKS_DEBUG=<true|false> # Defaults to App's debug value QUICKBOOKS_DEBUG=<true|false> # Defaults to APP_DEBUG
``` ```
3. _[Optional]_ Publish configs & views ### 3. Publish config and views (optional)
#### Config
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
```
#### Views
View files can be published by running...
```bash
php artisan vendor:publish --tag=quickbooks-views 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
+19 -14
View File
@@ -1,7 +1,7 @@
{ {
"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",
@@ -16,20 +16,19 @@
} }
], ],
"require": { "require": {
"php": "^8.0.2", "php": "^8.3",
"illuminate/database": "^10.0|^11.0", "illuminate/database": "^10.0|^11.0|^12.0",
"illuminate/http": "^10.0|^11.0", "illuminate/http": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0", "illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.62.1", "nesbot/carbon": "^2.62.1|^3.0",
"quickbooks/v3-php-sdk": "^5.4.6" "quickbooks/v3-php-sdk": "^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": {
@@ -39,7 +38,10 @@
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Wearepixel\\QuickBooks\\": "tests/" "Wearepixel\\QuickBooks\\": "tests/"
} },
"files": [
"tests/Overrides.php"
]
}, },
"extra": { "extra": {
"laravel": { "laravel": {
@@ -50,6 +52,9 @@
} }
}, },
"config": { "config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true "sort-packages": true
}, },
"prefer-stable": true "prefer-stable": true
@@ -15,8 +15,7 @@
public function up() public function up()
{ {
Schema::create('quickbooks_tokens', function (Blueprint $table) { Schema::create('quickbooks_tokens', function (Blueprint $table) {
$user_id_type = $user_id_type = str_contains(DB::getSchemaBuilder()->getColumnType('users', 'id'), 'bigint')
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
? 'unsignedBigInteger' ? 'unsignedBigInteger'
: 'unsignedInteger'; : 'unsignedInteger';
-9
View File
@@ -1,9 +0,0 @@
* text=auto
/build export-ignore
/tests export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
nitpick.json export-ignore
phpunit.xml.dist export-ignore
+6 -23
View File
@@ -8,15 +8,11 @@
--> -->
<phpunit backupGlobals="false" <phpunit backupGlobals="false"
backupStaticAttributes="false" backupStaticProperties="false"
bootstrap="vendor/autoload.php" bootstrap="vendor/autoload.php"
colors="true" colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false" processIsolation="false"
stopOnFailure="false" stopOnFailure="false">
verbose="true">
<testsuites> <testsuites>
<testsuite name="Wearepixel's Laravel QuickBooks Client"> <testsuite name="Wearepixel's Laravel QuickBooks Client">
@@ -28,26 +24,13 @@
<!-- <env name="VARIABLE" value="value"/> --> <!-- <env name="VARIABLE" value="value"/> -->
</php> </php>
<filter> <source>
<whitelist processUncoveredFilesFromWhitelist="true"> <include>
<directory suffix=".php">./src</directory> <directory suffix=".php">./src</directory>
</include>
<exclude> <exclude>
<directory>./src/Http/Exceptions</directory> <directory>./src/Http/Exceptions</directory>
<directory>./src/Providers</directory> <directory>./src/Providers</directory>
</exclude> </exclude>
</whitelist> </source>
</filter>
<logging>
<log type="coverage-html"
target="./build/phpunit/coverage"
lowUpperBound="35"
highLowerBound="70"/>
<log type="coverage-text"
target="php://stdout"
showOnlySummary="true"
showUncoveredFiles="false"/>
<log type="coverage-clover" target="build/phpunit/logs/clover.xml"/>
<log type="junit" target="./build/phpunit/logs/junit.xml"/>
</logging>
</phpunit> </phpunit>
+8 -1
View File
@@ -1,6 +1,13 @@
{ {
"preset": "laravel", "preset": "laravel",
"rules": { "rules": {
"no_superfluous_phpdoc_tags": true "concat_space": {
"spacing": "one"
},
"class_attributes_separation": false,
"ordered_imports": {
"sort_algorithm": "length"
},
"not_operator_with_space": false
} }
} }
+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.
+80 -181
View File
@@ -1,34 +1,11 @@
<?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
*/
class ClientTest extends TestCase
{
/**
* @var Client
*/
protected $client;
/**
* @var array
*/
protected $configs = [];
/**
* @var Mock
*/
protected $token_mock;
protected function setUp(): void
{
$this->configs = [ $this->configs = [
'data_service' => [ 'data_service' => [
'auth_mode' => 'oauth2', 'auth_mode' => 'oauth2',
@@ -40,32 +17,14 @@ protected function setUp(): void
]; ];
$this->token_mock = Mockery::mock(Token::class); $this->token_mock = Mockery::mock(Token::class);
$this->client = new Client($this->configs, $this->token_mock);
});
$this->client = $this->makeClient(); it('can be constructed', function () {
} expect($this->client)->toBeInstanceOf(Client::class);
});
private function makeClient($configs = null) it('returns a data service configured to request oauth token when token is empty', function () {
{
if (! is_null($configs)) {
return new Client($configs, $this->token_mock);
}
return new Client($this->configs, $this->token_mock);
}
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(Client::class, $this->client);
}
/**
* @test
*/
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty()
{
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -78,19 +37,15 @@ public function it_returns_a_data_service_configured_to_request_oauth_token_when
->with('hasValidRefreshToken') ->with('hasValidRefreshToken')
->andReturnFalse(); ->andReturnFalse();
$this->assertFalse( expect(
$this->client $this->client
->getDataService() ->getDataService()
->getServiceContext() ->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(), ->IppConfiguration->Security->isAccessTokenSet(),
); )->toBeFalse();
} });
/** it('caches the data service once it is made', function () {
* @test
*/
public function it_caches_the_data_service_once_it_is_made()
{
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -103,15 +58,10 @@ public function it_caches_the_data_service_once_it_is_made()
->with('hasValidRefreshToken') ->with('hasValidRefreshToken')
->andReturnFalse(); ->andReturnFalse();
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received expect($this->client->getDataService())->toBeInstanceOf(DataService::class);
$this->assertEquals(DataService::class, get_class($this->client->getDataService())); });
}
/** it('returns a data service with oauth token when valid access token exists', function () {
* @test
*/
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
{
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -136,21 +86,15 @@ public function it_returns_a_data_service_with_oauth_token_when_valid_access_tok
->with('refresh_token') ->with('refresh_token')
->andReturn('refresh_token'); ->andReturn('refresh_token');
$this->assertTrue( expect(
$this->client $this->client
->getDataService() ->getDataService()
->getServiceContext() ->getServiceContext()
->IppConfiguration->Security->isAccessTokenSet(), ->IppConfiguration->Security->isAccessTokenSet(),
); )->toBeTrue();
} });
/**
* @test
*/
public function it_returns_a_data_service_with_refreshed_token_when_access_token_expired_but_refresh_token_valid()
{
$this->expectException(ServiceException::class);
it('returns a data service with refreshed token when access token expired but refresh token valid', function () {
$this->token_mock $this->token_mock
->shouldReceive('getAttribute') ->shouldReceive('getAttribute')
->twice() ->twice()
@@ -180,120 +124,75 @@ public function it_returns_a_data_service_with_refreshed_token_when_access_token
->with('refresh_token') ->with('refresh_token')
->andReturn('refresh_token'); ->andReturn('refresh_token');
// TODO: This really needs to test that things are getting called correctly on the DataService class $this->client->getDataService();
$this->assertFalse( })->throws(ServiceException::class);
$this->client
->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_report_service_using_the_data_service()
{
$this->markTestSkipped(
'Once we figure out how to test around the static DataService::Configure',
);
}
/** it('has logging off by default')
* @test ->skip('Have to figure out how to test this with the new code in 5.x');
*/
public function it_has_logging_off_by_default()
{
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
$this->token_mock it('allows logging turned on and pointed to expected file')
->shouldReceive('getAttribute') ->skip('Have to figure out how to test this with the new code in 5.x');
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock it('returns self after deleting token', function () {
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertFalse(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
}
/**
* @test
*/
public function it_allows_logging_turned_on_and_pointed_to_expected_file()
{
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
$this->client = $this->makeClient(
array_merge(
[
'logging' => [
'enabled' => true,
'location' => '/some/valid/path',
],
],
$this->configs,
),
);
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnFalse();
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
$this->assertTrue(
filter_var(
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->EnableRequestResponseLogging,
FILTER_VALIDATE_BOOLEAN,
),
);
$this->assertEquals(
'/some/valid/path',
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
->RequestLog->ServiceRequestLoggingLocation,
);
}
/**
* @test
*/
public function it_returns_self_after_deleting_token()
{
$this->token_mock $this->token_mock
->shouldReceive('remove') ->shouldReceive('remove')
->once() ->once()
->withNoArgs() ->withNoArgs()
->andReturnSelf(); ->andReturnSelf();
$this->assertInstanceOf(Client::class, $this->client->deleteToken()); expect($this->client->deleteToken())->toBeInstanceOf(Client::class);
} });
}
function dir($path) it('clears cached data service when token is changed', function () {
{ $this->token_mock
return $path === '/some/valid/path'; ->shouldReceive('getAttribute')
} ->with('hasValidAccessToken')
->andReturnFalse();
function route($name) $this->token_mock
{ ->shouldReceive('getAttribute')
return $name; ->with('hasValidRefreshToken')
} ->andReturnFalse();
$first = $this->client->getDataService();
$new_token = Mockery::mock(Token::class);
$new_token
->shouldReceive('getAttribute')
->with('hasValidAccessToken')
->andReturnFalse();
$new_token
->shouldReceive('getAttribute')
->with('hasValidRefreshToken')
->andReturnFalse();
$this->client->setToken($new_token);
$second = $this->client->getDataService();
expect($second)->not->toBe($first);
});
it('delegates hasValidAccessToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidAccessToken')
->andReturnTrue();
expect($this->client->hasValidAccessToken())->toBeTrue();
});
it('delegates hasValidRefreshToken to token', function () {
$this->token_mock
->shouldReceive('getAttribute')
->once()
->with('hasValidRefreshToken')
->andReturnFalse();
expect($this->client->hasValidRefreshToken())->toBeFalse();
});
+10 -33
View File
@@ -1,39 +1,16 @@
<?php <?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User; use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/** beforeEach(function () {
* Class HasQuickBooksTokenTest $this->user = new User;
*/ });
class HasQuickBooksTokenTest extends TestCase
{
/**
* @var User
*/
protected $user;
protected function setUp(): void it('can be constructed', function () {
{ expect($this->user)->toBeInstanceOf(User::class);
parent::setUp(); });
$this->user = new User(); it('has a hasOne relationship to token', function () {
} expect($this->user->quickBooksToken()->getModel())->toBeInstanceOf(Token::class);
});
/**
* @test
*/
public function it_can_be_constructed()
{
$this->assertInstanceOf(User::class, $this->user);
}
/**
* @test
*/
public function it_has_a_hasOne_relationship_to_token()
{
$this->assertInstanceOf(Token::class, $this->user->quickBooksToken()->getModel());
}
}
+20 -98
View File
@@ -1,7 +1,5 @@
<?php <?php
namespace Wearepixel\QuickBooks\Http\Controllers;
use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@@ -9,90 +7,33 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
use Illuminate\Session\Store; use Illuminate\Session\Store;
use Mockery;
use Mockery\Mock;
use QuickBooksOnline\API\DataService\DataService; use QuickBooksOnline\API\DataService\DataService;
use Wearepixel\QuickBooks\Client as QuickBooks; use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase; use Wearepixel\QuickBooks\Http\Controllers\Controller;
/** beforeEach(function () {
* Class ControllerTest
*/
class ControllerTest extends TestCase
{
/**
* @var Controller
*/
protected $controller;
/**
* @var Mock
*/
protected $data_service_mock;
/**
* @var Mock
*/
protected $quickbooks_mock;
/**
* @var Mock
*/
protected $redirector_mock;
/**
* @var Mock
*/
protected $request_mock;
/**
* @var Mock
*/
protected $session_mock;
/**
* @var Mock
*/
protected $view_factory_mock;
/**
* @var Mock
*/
protected $view_mock;
protected function setUp(): void
{
$this->data_service_mock = Mockery::mock(DataService::class); $this->data_service_mock = Mockery::mock(DataService::class);
$this->quickbooks_mock = Mockery::mock(QuickBooks::class); $this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class); $this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class); $this->request_mock = Mockery::mock(Request::class);
$this->session_mock = Mockery::mock(Store::class); $this->session_mock = Mockery::mock(Store::class);
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$this->view_factory_mock = Mockery::mock(ViewFactory::class); $this->view_factory_mock = Mockery::mock(ViewFactory::class);
$this->view_mock = Mockery::mock(View::class); $this->view_mock = Mockery::mock(View::class);
$this->controller = new Controller(); $this->controller = new Controller;
} });
/** it('can be constructed', function () {
* @test expect($this->controller)->toBeInstanceOf(Controller::class);
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Controller::class, $this->controller);
}
/** it('shows view to disconnect if account linked', function () {
* @test
*/
public function it_shows_view_to_connect_if_account_not_linked()
{
$this->data_service_mock $this->data_service_mock
->shouldReceive('getCompanyInfo') ->shouldReceive('getCompanyInfo')
->once() ->once()
->withNoArgs() ->withNoArgs()
->andReturn([ ->andReturn(['name' => 'Company']);
'name' => 'Company',
]);
$this->quickbooks_mock $this->quickbooks_mock
->shouldReceive('hasValidRefreshToken') ->shouldReceive('hasValidRefreshToken')
@@ -115,22 +56,13 @@ public function it_shows_view_to_connect_if_account_not_linked()
$this->view_mock $this->view_mock
->shouldReceive('with') ->shouldReceive('with')
->once() ->once()
->withArgs([ ->withArgs(['company', ['name' => 'Company']])
'company',
[
'name' => 'Company',
],
])
->andReturnSelf(); ->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
} });
/** it('shows view to connect if account not linked', function () {
* @test
*/
public function it_shows_view_to_disconnect_if_account_linked()
{
$this->quickbooks_mock $this->quickbooks_mock
->shouldReceive('hasValidRefreshToken') ->shouldReceive('hasValidRefreshToken')
->once() ->once()
@@ -156,13 +88,9 @@ public function it_shows_view_to_disconnect_if_account_linked()
->andReturnSelf(); ->andReturnSelf();
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock); $this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
} });
/** it('disconnects from quickbooks when requested', function () {
* @test
*/
public function it_disconnects_from_quickbooks_when_requested()
{
$this->request_mock $this->request_mock
->shouldReceive('session') ->shouldReceive('session')
->once() ->once()
@@ -189,15 +117,10 @@ public function it_disconnects_from_quickbooks_when_requested()
$this->quickbooks_mock, $this->quickbooks_mock,
); );
$this->assertInstanceOf(RedirectResponse::class, $result); expect($result)->toBeInstanceOf(RedirectResponse::class);
} });
/** it('finishes connecting to quickbooks when given a valid token', function () {
* @test
*/
public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by_quickbooks()
{
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
$realmId = random_int(1, 9999); $realmId = random_int(1, 9999);
$this->quickbooks_mock $this->quickbooks_mock
@@ -245,6 +168,5 @@ public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by
$this->url_generator_mock, $this->url_generator_mock,
); );
$this->assertInstanceOf(RedirectResponse::class, $result); expect($result)->toBeInstanceOf(RedirectResponse::class);
} });
}
+12 -66
View File
@@ -1,53 +1,13 @@
<?php <?php
namespace Wearepixel\QuickBooks\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Contracts\Routing\UrlGenerator;
use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
use Mockery;
use Mockery\Mock;
use Wearepixel\QuickBooks\Client as QuickBooks; use Wearepixel\QuickBooks\Client as QuickBooks;
use Wearepixel\QuickBooks\TestCase; use Wearepixel\QuickBooks\Http\Middleware\Filter;
/** beforeEach(function () {
* Class FilterTest
*/
class FilterTest extends TestCase
{
/**
* @var Filter
*/
protected $filter;
/**
* @var Mock
*/
protected $quickbooks_mock;
/**
* @var Mock
*/
protected $redirector_mock;
/**
* @var Mock
*/
protected $request_mock;
/**
* @var Mock
*/
protected $session_mock;
/**
* @var Mock
*/
protected $url_generator_mock;
protected function setUp(): void
{
$this->quickbooks_mock = Mockery::mock(QuickBooks::class); $this->quickbooks_mock = Mockery::mock(QuickBooks::class);
$this->redirector_mock = Mockery::mock(Redirector::class); $this->redirector_mock = Mockery::mock(Redirector::class);
$this->request_mock = Mockery::mock(Request::class); $this->request_mock = Mockery::mock(Request::class);
@@ -60,23 +20,15 @@ protected function setUp(): void
$this->session_mock, $this->session_mock,
$this->url_generator_mock, $this->url_generator_mock,
); );
} });
/** it('can be constructed', function () {
* @test expect($this->filter)->toBeInstanceOf(Filter::class);
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Filter::class, $this->filter);
}
/** it('passes the request to the next middleware if account linked to quickbooks', function () {
* @test
*/
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
{
$next_middleware = function ($request) { $next_middleware = function ($request) {
$this->assertEquals($this->request_mock, $request); expect($request)->toBe($this->request_mock);
}; };
$this->quickbooks_mock $this->quickbooks_mock
@@ -86,13 +38,9 @@ public function it_just_passes_the_request_to_the_next_middleware_if_account_lin
->andReturnTrue(); ->andReturnTrue();
$this->filter->handle($this->request_mock, $next_middleware); $this->filter->handle($this->request_mock, $next_middleware);
} });
/** it('redirects to quickbooks connect route after setting intended session if account not linked', function () {
* @test
*/
public function it_redirects_to_quickbooks_connect_route_after_setting_intended_session_if_account_not_linked()
{
$this->request_mock $this->request_mock
->shouldReceive('path') ->shouldReceive('path')
->once() ->once()
@@ -118,8 +66,7 @@ public function it_redirects_to_quickbooks_connect_route_after_setting_intended_
->andReturnSelf(); ->andReturnSelf();
$next_middleware = function ($request) { $next_middleware = function ($request) {
// If this is called, then fail test throw new RuntimeException('Next middleware should not be called');
$this->assertTrue(false);
}; };
$this->quickbooks_mock $this->quickbooks_mock
@@ -129,5 +76,4 @@ public function it_redirects_to_quickbooks_connect_route_after_setting_intended_
->andReturnFalse(); ->andReturnFalse();
$this->filter->handle($this->request_mock, $next_middleware); $this->filter->handle($this->request_mock, $next_middleware);
} });
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace Wearepixel\QuickBooks;
use Wearepixel\QuickBooks\Stubs\User;
function config(string $key): array
{
return [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
];
}
function route(string $name): string
{
return $name;
}
function dir(string $path): bool
{
return $path === '/some/valid/path';
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
pest()->use(MockeryPHPUnitIntegration::class);
-75
View File
@@ -1,75 +0,0 @@
<?php
namespace Wearepixel\QuickBooks;
use ArrayAccess;
use Countable;
use Iterator;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
/**
* Class TestCase
*/
abstract class TestCase extends PHPUnitTestCase
{
use MockeryPHPUnitIntegration;
/**
* Helper to allow mocking of iterator classes.
*
* @link https://gist.github.com/VladaHejda/8299871
*
* @return void
*/
protected function mockArrayIterator(MockInterface $mock, array $items)
{
if ($mock instanceof ArrayAccess) {
foreach ($items as $key => $val) {
$mock
->shouldReceive('offsetGet')
->with($key)
->andReturn($val);
$mock
->shouldReceive('offsetExists')
->with($key)
->andReturn(true);
}
$mock->shouldReceive('offsetExists')->andReturn(false);
}
if ($mock instanceof Iterator) {
$counter = 0;
$mock->shouldReceive('rewind')->andReturnUsing(function () use (&$counter) {
$counter = 0;
});
$vals = array_values($items);
$keys = array_values(array_keys($items));
$mock->shouldReceive('valid')->andReturnUsing(function () use (&$counter, $vals) {
return isset($vals[$counter]);
});
$mock->shouldReceive('current')->andReturnUsing(function () use (&$counter, $vals) {
return $vals[$counter];
});
$mock->shouldReceive('key')->andReturnUsing(function () use (&$counter, $keys) {
return $keys[$counter];
});
$mock->shouldReceive('next')->andReturnUsing(function () use (&$counter) {
$counter++;
});
}
if ($mock instanceof Countable) {
$mock->shouldReceive('count')->andReturn(count($items));
}
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
use Illuminate\Contracts\Support\DeferrableProvider;
use Wearepixel\QuickBooks\Client;
use Wearepixel\QuickBooks\Providers\ClientServiceProvider;
use Wearepixel\QuickBooks\Token;
describe('Token model structure', function () {
it('uses the quickbooks_tokens table', function () {
expect((new Token)->getTable())->toBe('quickbooks_tokens');
});
it('casts expiration fields to datetime', function () {
$casts = (new Token)->getCasts();
expect($casts)
->toHaveKey('access_token_expires_at', 'datetime')
->toHaveKey('refresh_token_expires_at', 'datetime');
});
it('has all expected fillable attributes', function () {
$fillable = (new Token)->getFillable();
expect($fillable)->toEqualCanonicalizing([
'access_token',
'access_token_expires_at',
'realm_id',
'refresh_token',
'refresh_token_expires_at',
'user_id',
]);
});
});
describe('ClientServiceProvider structure', function () {
it('implements DeferrableProvider', function () {
expect(ClientServiceProvider::class)
->toImplement(DeferrableProvider::class);
});
it('declares Client in provides array', function () {
$app = Mockery::mock(\Illuminate\Contracts\Foundation\Application::class);
$app->shouldIgnoreMissing();
$provider = new ClientServiceProvider($app);
expect($provider->provides())->toBe([Client::class]);
});
});
+59 -162
View File
@@ -1,184 +1,97 @@
<?php <?php
namespace Wearepixel\QuickBooks;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Mockery;
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
use Wearepixel\QuickBooks\Stubs\TokenStub; use Wearepixel\QuickBooks\Stubs\TokenStub;
use Wearepixel\QuickBooks\Stubs\User; use Wearepixel\QuickBooks\Stubs\User;
use Wearepixel\QuickBooks\Token;
/** beforeEach(function () {
* Class TokenTest $this->token = new TokenStub;
*/ });
class TokenTest extends TestCase
{
/**
* @var Token
*/
protected $token;
protected function setUp(): void it('can be constructed', function () {
{ expect($this->token)->toBeInstanceOf(Token::class);
$this->token = new TokenStub(); });
}
/** it('has accessor for valid access token', function () {
* @test expect($this->token->getHasValidAccessTokenAttribute())->not->toBeNull();
*/ });
public function it_can_be_constructed()
{
$this->assertInstanceOf(Token::class, $this->token);
}
/** it('has accessor for valid refresh token', function () {
* @test expect($this->token->getHasValidRefreshTokenAttribute())->not->toBeNull();
*/ });
public function it_has_accessor_for_valid_access_token()
{
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
}
/** it('knows that the access token expires at has to be valid', function () {
* @test expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
*/ });
public function it_has_accessor_for_valid_refresh_token()
{
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
}
/** it('knows that the refresh token expires at has to be valid', function () {
* @test expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
*/ });
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
}
/** it('knows if access token expires at is less than now it is not expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
public function it_knows_that_the_refresh_token_expires_at_has_to_be_valid()
{
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
}
/**
* @test
*/
public function it_know_if_access_token_expires_at_is_less_than_now_it_is_not_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->access_token_expires_at = Carbon::now()->addSecond(); $this->token->access_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidAccessTokenAttribute()); expect($this->token->getHasValidAccessTokenAttribute())->toBeTrue();
} });
/** it('knows if refresh token expires at is less than now it is not expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
public function it_know_if_refresh_token_expires_at_is_less_than_now_it_is_not_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->refresh_token_expires_at = Carbon::now()->addSecond(); $this->token->refresh_token_expires_at = Carbon::now()->addSecond();
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute()); expect($this->token->getHasValidRefreshTokenAttribute())->toBeTrue();
} });
/** it('knows if access token expires at is greater than or equal to now it is expired', function () {
* @test Carbon::setTestNow(Carbon::now());
*/
public function it_know_if_access_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->access_token_expires_at = Carbon::now(); $this->token->access_token_expires_at = Carbon::now();
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
$this->token->access_token_expires_at->subSecond(); $this->token->access_token_expires_at->subSecond();
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
});
$this->assertFalse($this->token->getHasValidAccessTokenAttribute()); it('knows if refresh token expires at is greater than or equal to now it is expired', function () {
} Carbon::setTestNow(Carbon::now());
/**
* @test
*/
public function it_know_if_refresh_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
{
$now = Carbon::now();
Carbon::setTestNow($now);
$this->token->refresh_token_expires_at = Carbon::now(); $this->token->refresh_token_expires_at = Carbon::now();
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
$this->token->refresh_token_expires_at->subSecond(); $this->token->refresh_token_expires_at->subSecond();
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
});
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute()); it('stores the oauth token parts in expected properties', function () {
} $oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class);
/** $oauth_token_mock->shouldReceive('getAccessToken')->once()->withNoArgs()->andReturn('access_token');
* @test $oauth_token_mock->shouldReceive('getAccessTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-06-15 12:00:00');
*/ $oauth_token_mock->shouldReceive('getRealmID')->once()->withNoArgs()->andReturn('realm_id');
public function it_stores_the_oauth_token_parts_in_expected_properties() $oauth_token_mock->shouldReceive('getRefreshToken')->once()->withNoArgs()->andReturn('refresh_token');
{ $oauth_token_mock->shouldReceive('getRefreshTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-09-24 12:00:00');
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
$oauth_token_mock $result = $this->token->parseOauthToken($oauth_token_mock);
->shouldReceive('getAccessToken')
->once()
->withNoArgs()
->andReturn('access_token');
$oauth_token_mock expect($result)->toBeInstanceOf(Token::class)
->shouldReceive('getAccessTokenExpiresAt') ->and($this->token->access_token)->toBe('access_token')
->once() ->and($this->token->realm_id)->toBe('realm_id')
->withNoArgs() ->and($this->token->refresh_token)->toBe('refresh_token')
->andReturn('now'); ->and($this->token->access_token_expires_at)->toBeInstanceOf(Carbon::class)
->and($this->token->refresh_token_expires_at)->toBeInstanceOf(Carbon::class);
});
$oauth_token_mock it('allows itself to be deleted and returns new token', function () {
->shouldReceive('getRealmID')
->once()
->withNoArgs()
->andReturn('realm_id');
$oauth_token_mock
->shouldReceive('getRefreshToken')
->once()
->withNoArgs()
->andReturn('refresh_token');
$oauth_token_mock
->shouldReceive('getRefreshTokenExpiresAt')
->once()
->withNoArgs()
->andReturn('now');
$this->token->parseOauthToken($oauth_token_mock);
}
/**
* @test
*/
public function it_allows_itself_to_be_deleted_and_returns_new_token()
{
$this->token->user = Mockery::mock(User::class); $this->token->user = Mockery::mock(User::class);
$has_one_mock = Mockery::mock(HasOne::class); $has_one_mock = Mockery::mock(HasOne::class);
$token_mock = Mockery::mock(Token::class); $token_mock = Mockery::mock(Token::class);
$has_one_mock $has_one_mock->shouldReceive('make')->once()->withNoArgs()->andReturn($token_mock);
->shouldReceive('make')
->once()
->withNoArgs()
->andReturn($token_mock);
$this->token->user $this->token->user
->shouldReceive('quickBooksToken') ->shouldReceive('quickBooksToken')
@@ -188,25 +101,9 @@ public function it_allows_itself_to_be_deleted_and_returns_new_token()
$this->token->user->id = 1; $this->token->user->id = 1;
$this->assertEquals($token_mock, $this->token->remove()); expect($this->token->remove())->toBe($token_mock);
} });
/** it('gets related user model from configuration', function () {
* @test expect($this->token->user()->getModel())->toBeInstanceOf(User::class);
*/ });
public function it_get_related_user_model_from_configuration()
{
$this->assertInstanceOf(User::class, $this->token->user()->getModel());
}
}
function config($key)
{
return [
'keys' => [
'foreign' => 'user_id',
'owner' => 'id',
],
'model' => User::class,
];
}