mirror of
https://github.com/wearepixel/laravel-quickbooks.git
synced 2026-11-14 22:11:50 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ba7e82e2 | ||
|
|
c94788fdb8 | ||
|
|
3a5c51d3d0 | ||
|
|
f7d07f45e5 | ||
|
|
b4a5d00439 | ||
|
|
524a09d545 | ||
|
|
c75ab43346 | ||
|
|
32266f6dd5 | ||
|
|
9002e0ebca | ||
|
|
473103c243 | ||
|
|
f59295773d | ||
|
|
446f6788d2 | ||
|
|
831cd49719 | ||
|
|
bb2d375450 | ||
|
|
861a08e083 | ||
|
|
97c04ec693 | ||
|
|
8ec2c3cc38 | ||
|
|
32f5c22846 |
+6
-4
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -5,3 +5,6 @@ phpunit.xml
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
composer.lock
|
composer.lock
|
||||||
/.idea/
|
/.idea/
|
||||||
|
.cursor
|
||||||
|
.claude
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -1,115 +1,103 @@
|
|||||||
# Wearepixel's Laravel QuickBooks Client
|
# Laravel QuickBooks Client
|
||||||
|
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-quickbooks)
|
[](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 | [](https://github.com/wearepixel/laravel-quickbooks/workflows/CI/badge.svg?branch=master) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/?branch=master) | [](https://scrutinizer-ci.com/g/wearepixel/laravel-quickbooks/?branch=main) |
|
| 10.x | ^8.3 | ^1.0 |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install QuickBooks PHP Client:
|
1. Install the package:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ composer require wearepixel/laravel-quickbooks
|
composer require wearepixel/laravel-quickbooks
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run our migration to install the `quickbooks_tokens` table:
|
2. Run the migration to create the `quickbooks_tokens` table:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ php artisan migrate --package=wearepixel/laravel-quickbooks
|
php artisan migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
The package uses the [auto registration feature](https://laravel.com/docs/packages#package-discovery) of Laravel.
|
The package uses Laravel's [auto-discovery](https://laravel.com/docs/packages#package-discovery), so no manual provider registration is needed.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
1. You will need a ```quickBooksToken``` relationship on your ```User``` model. There is a trait named ```Wearepixel\QuickBooks\HasQuickBooksToken```, which you can include on your ```User``` model, which will setup the relationship. To do this implement the following:
|
### 1. Add the trait to your User model
|
||||||
|
|
||||||
Add ```use Wearepixel\QuickBooks\HasQuickBooksToken;``` to your service container at the top of User.php
|
|
||||||
and also add the trait within the class. For example:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
use Wearepixel\QuickBooks\HasQuickBooksToken;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
use Notifiable, HasQuickBooksToken;
|
use HasQuickBooksToken;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE: If your ```User``` model is not ```App\Models\User```, then you will need to configure the path in the ```configs/quickbooks.php```.**
|
If your User model is not `App\Models\User`, publish the config and update the `user.model` value.
|
||||||
|
|
||||||
2. Add the appropriate values to your ```.env```
|
### 2. Set your environment variables
|
||||||
|
|
||||||
#### Minimal Keys
|
```bash
|
||||||
```bash
|
QUICKBOOKS_CLIENT_ID=<your client id>
|
||||||
QUICKBOOKS_CLIENT_ID=<client id given by QuickBooks>
|
QUICKBOOKS_CLIENT_SECRET=<your client secret>
|
||||||
QUICKBOOKS_CLIENT_SECRET=<client secret>
|
```
|
||||||
```
|
|
||||||
|
|
||||||
#### Optional Keys
|
Optional:
|
||||||
```bash
|
|
||||||
QUICKBOOKS_API_URL=<Development|Production> # Defaults to App's env value
|
|
||||||
QUICKBOOKS_DEBUG=<true|false> # Defaults to App's debug value
|
|
||||||
```
|
|
||||||
|
|
||||||
3. _[Optional]_ Publish configs & views
|
```bash
|
||||||
|
QUICKBOOKS_API_URL=<Development|Production> # Defaults based on APP_ENV
|
||||||
|
QUICKBOOKS_DEBUG=<true|false> # Defaults to APP_DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
#### Config
|
### 3. Publish config and views (optional)
|
||||||
A configuration file named ```quickbooks.php``` can be published to ```config/``` by running...
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan vendor:publish --tag=quickbooks-config
|
php artisan vendor:publish --tag=quickbooks-config
|
||||||
```
|
php artisan vendor:publish --tag=quickbooks-views
|
||||||
|
```
|
||||||
#### Views
|
|
||||||
View files can be published by running...
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan vendor:publish --tag=quickbooks-views
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Here is an example of getting the company information from QuickBooks:
|
First, direct users to `/quickbooks/connect` to authorize their QuickBooks account. Once connected, you can access the API:
|
||||||
|
|
||||||
### NOTE: Before doing these commands, go to your connect route (default: /quickbooks/connect) to get a QuickBooks token for your user
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
php artisan tinker
|
$quickbooks = app(\Wearepixel\QuickBooks\Client::class);
|
||||||
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
|
|
||||||
>>> Auth::logInUsingId(1)
|
// Get company info
|
||||||
=> App\Models\User {#1668
|
$company = $quickbooks->getDataService()->getCompanyInfo();
|
||||||
id: 1,
|
|
||||||
// Other keys removed for example
|
// Access reports
|
||||||
}
|
$reportService = $quickbooks->getReportService();
|
||||||
>>> $quickbooks = app('Wearepixel\QuickBooks\Client') // or app('QuickBooks')
|
|
||||||
=> Wearepixel\QuickBooks\Client {#1613}
|
|
||||||
>>> $quickbooks->getDataService()->getCompanyInfo();
|
|
||||||
=> QuickBooksOnline\API\Data\IPPCompanyInfo {#1673
|
|
||||||
+CompanyName: "Sandbox Company_US_1",
|
|
||||||
+LegalName: "Sandbox Company_US_1",
|
|
||||||
// Other properties removed for example
|
|
||||||
}
|
|
||||||
>>>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can call any of the resources as documented [in the SDK](https://intuit.github.io/QuickBooks-V3-PHP-SDK/quickstart.html).
|
You can call any of the resources documented in the [QuickBooks PHP SDK](https://intuit.github.io/QuickBooks-V3-PHP-SDK/quickstart.html).
|
||||||
|
|
||||||
## Middleware
|
## Middleware
|
||||||
|
|
||||||
If you have routes that will be dependent on the user's account having a usable QuickBooks OAuth token, there is an included middleware ```Wearepixel\QuickBooks\Laravel\Filter``` that gets registered as ```quickbooks``` that will ensure the account is linked and redirect them to the `connect` route if needed.
|
Protect routes that require a QuickBooks connection using the `quickbooks` middleware. Users without a valid token will be redirected to the connect page.
|
||||||
|
|
||||||
Here is an example route definition:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Route::view('some/route/needing/quickbooks/token/before/using', 'some.view')
|
Route::get('quickbooks/invoices', InvoiceController::class)
|
||||||
->middleware('quickbooks');
|
->middleware('quickbooks');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vendor/bin/pest
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
+11
@@ -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.
|
||||||
+59
-54
@@ -1,56 +1,61 @@
|
|||||||
{
|
{
|
||||||
"name": "wearepixel/laravel-quickbooks",
|
"name": "wearepixel/laravel-quickbooks",
|
||||||
"version": "1.0.0",
|
"version": "2.0.2",
|
||||||
"description": "Wearepixel's Laravel Client for QuickBooks.",
|
"description": "Laravel Client for QuickBooks.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"quickbooks",
|
"quickbooks",
|
||||||
"client",
|
"client",
|
||||||
"laravel",
|
"laravel",
|
||||||
"wearepixel"
|
"wearepixel"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Pixel",
|
"name": "Pixel",
|
||||||
"email": "hello@wearepixel.com.au"
|
"email": "hello@wearepixel.com.au"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.0.2",
|
"php": "^8.3",
|
||||||
"illuminate/database": "^10.0|^11.0",
|
"illuminate/database": "^10.0|^11.0|^12.0",
|
||||||
"illuminate/http": "^10.0|^11.0",
|
"illuminate/http": "^10.0|^11.0|^12.0",
|
||||||
"illuminate/routing": "^10.0|^11.0",
|
"illuminate/routing": "^10.0|^11.0|^12.0",
|
||||||
"illuminate/support": "^10.0|^11.0",
|
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||||
"nesbot/carbon": "^2.62.1",
|
"nesbot/carbon": "^2.62.1|^3.0",
|
||||||
"quickbooks/v3-php-sdk": "^5.4.6"
|
"quickbooks/v3-php-sdk": "^6.3.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/pint": "^1.2",
|
"laravel/pint": "^1.2",
|
||||||
"mockery/mockery": "^1.5.1",
|
"mockery/mockery": "^1.6",
|
||||||
"phpunit/phpunit": "^10.0",
|
"pestphp/pest": "^5.3",
|
||||||
"psy/psysh": "^0.11",
|
"psy/psysh": "^0.12"
|
||||||
"scrutinizer/ocular": "^1.9"
|
},
|
||||||
},
|
"autoload": {
|
||||||
"autoload": {
|
"psr-4": {
|
||||||
"psr-4": {
|
"Wearepixel\\QuickBooks\\": "src/"
|
||||||
"Wearepixel\\QuickBooks\\": "src/"
|
}
|
||||||
}
|
},
|
||||||
},
|
"autoload-dev": {
|
||||||
"autoload-dev": {
|
"psr-4": {
|
||||||
"psr-4": {
|
"Wearepixel\\QuickBooks\\": "tests/"
|
||||||
"Wearepixel\\QuickBooks\\": "tests/"
|
},
|
||||||
}
|
"files": [
|
||||||
},
|
"tests/Overrides.php"
|
||||||
"extra": {
|
]
|
||||||
"laravel": {
|
},
|
||||||
"providers": [
|
"extra": {
|
||||||
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
|
"laravel": {
|
||||||
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
|
"providers": [
|
||||||
]
|
"Wearepixel\\QuickBooks\\Providers\\ServiceProvider",
|
||||||
}
|
"Wearepixel\\QuickBooks\\Providers\\ClientServiceProvider"
|
||||||
},
|
]
|
||||||
"config": {
|
}
|
||||||
"sort-packages": true
|
},
|
||||||
},
|
"config": {
|
||||||
"prefer-stable": true
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true
|
||||||
|
},
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,9 @@
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
Schema::create('quickbooks_tokens', function (Blueprint $table) {
|
Schema::create('quickbooks_tokens', function (Blueprint $table) {
|
||||||
$user_id_type =
|
$user_id_type = str_contains(DB::getSchemaBuilder()->getColumnType('users', 'id'), 'bigint')
|
||||||
DB::getSchemaBuilder()->getColumnType('users', 'id') === 'bigint'
|
? 'unsignedBigInteger'
|
||||||
? 'unsignedBigInteger'
|
: 'unsignedInteger';
|
||||||
: 'unsignedInteger';
|
|
||||||
|
|
||||||
$table->bigIncrements('id');
|
$table->bigIncrements('id');
|
||||||
$table->{$user_id_type}('user_id');
|
$table->{$user_id_type}('user_id');
|
||||||
|
|||||||
@@ -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
@@ -8,15 +8,11 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<phpunit backupGlobals="false"
|
<phpunit backupGlobals="false"
|
||||||
backupStaticAttributes="false"
|
backupStaticProperties="false"
|
||||||
bootstrap="vendor/autoload.php"
|
bootstrap="vendor/autoload.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
convertErrorsToExceptions="true"
|
|
||||||
convertNoticesToExceptions="true"
|
|
||||||
convertWarningsToExceptions="true"
|
|
||||||
processIsolation="false"
|
processIsolation="false"
|
||||||
stopOnFailure="false"
|
stopOnFailure="false">
|
||||||
verbose="true">
|
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Wearepixel's Laravel QuickBooks Client">
|
<testsuite name="Wearepixel's Laravel QuickBooks Client">
|
||||||
@@ -28,26 +24,13 @@
|
|||||||
<!-- <env name="VARIABLE" value="value"/> -->
|
<!-- <env name="VARIABLE" value="value"/> -->
|
||||||
</php>
|
</php>
|
||||||
|
|
||||||
<filter>
|
<source>
|
||||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
<include>
|
||||||
<directory suffix=".php">./src</directory>
|
<directory suffix=".php">./src</directory>
|
||||||
<exclude>
|
</include>
|
||||||
<directory>./src/Http/Exceptions</directory>
|
<exclude>
|
||||||
<directory>./src/Providers</directory>
|
<directory>./src/Http/Exceptions</directory>
|
||||||
</exclude>
|
<directory>./src/Providers</directory>
|
||||||
</whitelist>
|
</exclude>
|
||||||
</filter>
|
</source>
|
||||||
|
|
||||||
<logging>
|
|
||||||
<log type="coverage-html"
|
|
||||||
target="./build/phpunit/coverage"
|
|
||||||
lowUpperBound="35"
|
|
||||||
highLowerBound="70"/>
|
|
||||||
<log type="coverage-text"
|
|
||||||
target="php://stdout"
|
|
||||||
showOnlySummary="true"
|
|
||||||
showUncoveredFiles="false"/>
|
|
||||||
<log type="coverage-clover" target="build/phpunit/logs/clover.xml"/>
|
|
||||||
<log type="junit" target="./build/phpunit/logs/junit.xml"/>
|
|
||||||
</logging>
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -1,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
@@ -34,11 +34,14 @@ class Token extends Model
|
|||||||
protected $table = 'quickbooks_tokens';
|
protected $table = 'quickbooks_tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be mutated to dates.
|
* The attributes that should be cast.
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
protected $dates = ['access_token_expires_at', 'refresh_token_expires_at'];
|
protected $casts = [
|
||||||
|
'access_token_expires_at' => 'datetime',
|
||||||
|
'refresh_token_expires_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|||||||
+157
-258
@@ -1,299 +1,198 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Wearepixel\QuickBooks;
|
|
||||||
|
|
||||||
use Mockery;
|
|
||||||
use Mockery\Mock;
|
|
||||||
use QuickBooksOnline\API\DataService\DataService;
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
use QuickBooksOnline\API\Exception\ServiceException;
|
use QuickBooksOnline\API\Exception\ServiceException;
|
||||||
|
use Wearepixel\QuickBooks\Client;
|
||||||
|
use Wearepixel\QuickBooks\Token;
|
||||||
|
|
||||||
/**
|
beforeEach(function () {
|
||||||
* Class ClientTest
|
$this->configs = [
|
||||||
*/
|
'data_service' => [
|
||||||
class ClientTest extends TestCase
|
'auth_mode' => 'oauth2',
|
||||||
{
|
'base_url' => 'Development',
|
||||||
/**
|
'client_id' => 'QUICKBOOKS_CLIENT_ID',
|
||||||
* @var Client
|
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
|
||||||
*/
|
'scope' => 'com.intuit.quickbooks.accounting',
|
||||||
protected $client;
|
],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
$this->token_mock = Mockery::mock(Token::class);
|
||||||
* @var array
|
$this->client = new Client($this->configs, $this->token_mock);
|
||||||
*/
|
});
|
||||||
protected $configs = [];
|
|
||||||
|
|
||||||
/**
|
it('can be constructed', function () {
|
||||||
* @var Mock
|
expect($this->client)->toBeInstanceOf(Client::class);
|
||||||
*/
|
});
|
||||||
protected $token_mock;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
it('returns a data service configured to request oauth token when token is empty', function () {
|
||||||
{
|
$this->token_mock
|
||||||
$this->configs = [
|
->shouldReceive('getAttribute')
|
||||||
'data_service' => [
|
->twice()
|
||||||
'auth_mode' => 'oauth2',
|
->with('hasValidAccessToken')
|
||||||
'base_url' => 'Development',
|
->andReturnFalse();
|
||||||
'client_id' => 'QUICKBOOKS_CLIENT_ID',
|
|
||||||
'client_secret' => 'QUICKBOOKS_CLIENT_SECRET',
|
|
||||||
'scope' => 'com.intuit.quickbooks.accounting',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->token_mock = Mockery::mock(Token::class);
|
$this->token_mock
|
||||||
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
$this->client = $this->makeClient();
|
expect(
|
||||||
}
|
$this->client
|
||||||
|
->getDataService()
|
||||||
|
->getServiceContext()
|
||||||
|
->IppConfiguration->Security->isAccessTokenSet(),
|
||||||
|
)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
private function makeClient($configs = null)
|
it('caches the data service once it is made', function () {
|
||||||
{
|
$this->token_mock
|
||||||
if (! is_null($configs)) {
|
->shouldReceive('getAttribute')
|
||||||
return new Client($configs, $this->token_mock);
|
->twice()
|
||||||
}
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
return new Client($this->configs, $this->token_mock);
|
$this->token_mock
|
||||||
}
|
->shouldReceive('getAttribute')
|
||||||
|
->once()
|
||||||
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
/**
|
expect($this->client->getDataService())->toBeInstanceOf(DataService::class);
|
||||||
* @test
|
});
|
||||||
*/
|
|
||||||
public function it_can_be_constructed()
|
|
||||||
{
|
|
||||||
$this->assertInstanceOf(Client::class, $this->client);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('returns a data service with oauth token when valid access token exists', function () {
|
||||||
* @test
|
$this->token_mock
|
||||||
*/
|
->shouldReceive('getAttribute')
|
||||||
public function it_returns_a_data_service_configured_to_request_oauth_token_when_token_is_empty()
|
->twice()
|
||||||
{
|
->with('hasValidAccessToken')
|
||||||
$this->token_mock
|
->andReturnTrue();
|
||||||
->shouldReceive('getAttribute')
|
|
||||||
->twice()
|
|
||||||
->with('hasValidAccessToken')
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->token_mock
|
$this->token_mock
|
||||||
->shouldReceive('getAttribute')
|
->shouldReceive('getAttribute')
|
||||||
->once()
|
->once()
|
||||||
->with('hasValidRefreshToken')
|
->with('access_token')
|
||||||
->andReturnFalse();
|
->andReturn('access_token');
|
||||||
|
|
||||||
$this->assertFalse(
|
$this->token_mock
|
||||||
$this->client
|
->shouldReceive('getAttribute')
|
||||||
->getDataService()
|
->once()
|
||||||
->getServiceContext()
|
->with('realm_id')
|
||||||
->IppConfiguration->Security->isAccessTokenSet(),
|
->andReturn('realm_id');
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->token_mock
|
||||||
* @test
|
->shouldReceive('getAttribute')
|
||||||
*/
|
->once()
|
||||||
public function it_caches_the_data_service_once_it_is_made()
|
->with('refresh_token')
|
||||||
{
|
->andReturn('refresh_token');
|
||||||
$this->token_mock
|
|
||||||
->shouldReceive('getAttribute')
|
|
||||||
->twice()
|
|
||||||
->with('hasValidAccessToken')
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->token_mock
|
expect(
|
||||||
->shouldReceive('getAttribute')
|
$this->client
|
||||||
->once()
|
->getDataService()
|
||||||
->with('hasValidRefreshToken')
|
->getServiceContext()
|
||||||
->andReturnFalse();
|
->IppConfiguration->Security->isAccessTokenSet(),
|
||||||
|
)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Find out if this is the actual goal of this test, to make sure a DataService is received
|
it('returns a data service with refreshed token when access token expired but refresh token valid', function () {
|
||||||
$this->assertEquals(DataService::class, get_class($this->client->getDataService()));
|
$this->token_mock
|
||||||
}
|
->shouldReceive('getAttribute')
|
||||||
|
->twice()
|
||||||
|
->with('hasValidAccessToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
/**
|
$this->token_mock
|
||||||
* @test
|
->shouldReceive('getAttribute')
|
||||||
*/
|
->once()
|
||||||
public function it_returns_a_data_service_with_oauth_token_when_valid_access_token_exist()
|
->with('hasValidRefreshToken')
|
||||||
{
|
->andReturnTrue();
|
||||||
$this->token_mock
|
|
||||||
->shouldReceive('getAttribute')
|
|
||||||
->twice()
|
|
||||||
->with('hasValidAccessToken')
|
|
||||||
->andReturnTrue();
|
|
||||||
|
|
||||||
$this->token_mock
|
$this->token_mock
|
||||||
->shouldReceive('getAttribute')
|
->shouldReceive('getAttribute')
|
||||||
->once()
|
->never()
|
||||||
->with('access_token')
|
->with('access_token');
|
||||||
->andReturn('access_token');
|
|
||||||
|
|
||||||
$this->token_mock
|
$this->token_mock
|
||||||
->shouldReceive('getAttribute')
|
->shouldReceive('getAttribute')
|
||||||
->once()
|
->once()
|
||||||
->with('realm_id')
|
->with('realm_id')
|
||||||
->andReturn('realm_id');
|
->andReturn('realm_id');
|
||||||
|
|
||||||
$this->token_mock
|
$this->token_mock
|
||||||
->shouldReceive('getAttribute')
|
->shouldReceive('getAttribute')
|
||||||
->once()
|
->once()
|
||||||
->with('refresh_token')
|
->with('refresh_token')
|
||||||
->andReturn('refresh_token');
|
->andReturn('refresh_token');
|
||||||
|
|
||||||
$this->assertTrue(
|
$this->client->getDataService();
|
||||||
$this->client
|
})->throws(ServiceException::class);
|
||||||
->getDataService()
|
|
||||||
->getServiceContext()
|
|
||||||
->IppConfiguration->Security->isAccessTokenSet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('returns a report service using the data service')
|
||||||
* @test
|
->skip('Once we figure out how to test around the static DataService::Configure');
|
||||||
*/
|
|
||||||
public function it_returns_a_data_service_with_refreshed_token_when_access_token_expired_but_refresh_token_valid()
|
|
||||||
{
|
|
||||||
$this->expectException(ServiceException::class);
|
|
||||||
|
|
||||||
$this->token_mock
|
it('has logging off by default')
|
||||||
->shouldReceive('getAttribute')
|
->skip('Have to figure out how to test this with the new code in 5.x');
|
||||||
->twice()
|
|
||||||
->with('hasValidAccessToken')
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->token_mock
|
it('allows logging turned on and pointed to expected file')
|
||||||
->shouldReceive('getAttribute')
|
->skip('Have to figure out how to test this with the new code in 5.x');
|
||||||
->once()
|
|
||||||
->with('hasValidRefreshToken')
|
|
||||||
->andReturnTrue();
|
|
||||||
|
|
||||||
$this->token_mock
|
it('returns self after deleting token', function () {
|
||||||
->shouldReceive('getAttribute')
|
$this->token_mock
|
||||||
->never()
|
->shouldReceive('remove')
|
||||||
->with('access_token');
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
$this->token_mock
|
expect($this->client->deleteToken())->toBeInstanceOf(Client::class);
|
||||||
->shouldReceive('getAttribute')
|
});
|
||||||
->once()
|
|
||||||
->with('realm_id')
|
|
||||||
->andReturn('realm_id');
|
|
||||||
|
|
||||||
$this->token_mock
|
it('clears cached data service when token is changed', function () {
|
||||||
->shouldReceive('getAttribute')
|
$this->token_mock
|
||||||
->once()
|
->shouldReceive('getAttribute')
|
||||||
->with('refresh_token')
|
->with('hasValidAccessToken')
|
||||||
->andReturn('refresh_token');
|
->andReturnFalse();
|
||||||
|
|
||||||
// TODO: This really needs to test that things are getting called correctly on the DataService class
|
$this->token_mock
|
||||||
$this->assertFalse(
|
->shouldReceive('getAttribute')
|
||||||
$this->client
|
->with('hasValidRefreshToken')
|
||||||
->getDataService()
|
->andReturnFalse();
|
||||||
->getServiceContext()
|
|
||||||
->IppConfiguration->Security->isAccessTokenSet(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$first = $this->client->getDataService();
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_returns_a_report_service_using_the_data_service()
|
|
||||||
{
|
|
||||||
$this->markTestSkipped(
|
|
||||||
'Once we figure out how to test around the static DataService::Configure',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$new_token = Mockery::mock(Token::class);
|
||||||
* @test
|
$new_token
|
||||||
*/
|
->shouldReceive('getAttribute')
|
||||||
public function it_has_logging_off_by_default()
|
->with('hasValidAccessToken')
|
||||||
{
|
->andReturnFalse();
|
||||||
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
|
|
||||||
|
|
||||||
$this->token_mock
|
$new_token
|
||||||
->shouldReceive('getAttribute')
|
->shouldReceive('getAttribute')
|
||||||
->once()
|
->with('hasValidRefreshToken')
|
||||||
->with('hasValidAccessToken')
|
->andReturnFalse();
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->token_mock
|
$this->client->setToken($new_token);
|
||||||
->shouldReceive('getAttribute')
|
|
||||||
->once()
|
|
||||||
->with('hasValidRefreshToken')
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->assertFalse(
|
$second = $this->client->getDataService();
|
||||||
filter_var(
|
|
||||||
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
|
||||||
->RequestLog->EnableRequestResponseLogging,
|
|
||||||
FILTER_VALIDATE_BOOLEAN,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
expect($second)->not->toBe($first);
|
||||||
* @test
|
});
|
||||||
*/
|
|
||||||
public function it_allows_logging_turned_on_and_pointed_to_expected_file()
|
|
||||||
{
|
|
||||||
$this->markTestSkipped('Have to figure out how to test this with the new code in 5.x');
|
|
||||||
|
|
||||||
$this->client = $this->makeClient(
|
it('delegates hasValidAccessToken to token', function () {
|
||||||
array_merge(
|
$this->token_mock
|
||||||
[
|
->shouldReceive('getAttribute')
|
||||||
'logging' => [
|
->once()
|
||||||
'enabled' => true,
|
->with('hasValidAccessToken')
|
||||||
'location' => '/some/valid/path',
|
->andReturnTrue();
|
||||||
],
|
|
||||||
],
|
|
||||||
$this->configs,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->token_mock
|
expect($this->client->hasValidAccessToken())->toBeTrue();
|
||||||
->shouldReceive('getAttribute')
|
});
|
||||||
->once()
|
|
||||||
->with('hasValidAccessToken')
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->token_mock
|
it('delegates hasValidRefreshToken to token', function () {
|
||||||
->shouldReceive('getAttribute')
|
$this->token_mock
|
||||||
->once()
|
->shouldReceive('getAttribute')
|
||||||
->with('hasValidRefreshToken')
|
->once()
|
||||||
->andReturnFalse();
|
->with('hasValidRefreshToken')
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
$this->assertTrue(
|
expect($this->client->hasValidRefreshToken())->toBeFalse();
|
||||||
filter_var(
|
});
|
||||||
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
|
||||||
->RequestLog->EnableRequestResponseLogging,
|
|
||||||
FILTER_VALIDATE_BOOLEAN,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
'/some/valid/path',
|
|
||||||
$this->client->getDataService()->getServiceContext()->IppConfiguration->Logger
|
|
||||||
->RequestLog->ServiceRequestLoggingLocation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_returns_self_after_deleting_token()
|
|
||||||
{
|
|
||||||
$this->token_mock
|
|
||||||
->shouldReceive('remove')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturnSelf();
|
|
||||||
|
|
||||||
$this->assertInstanceOf(Client::class, $this->client->deleteToken());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dir($path)
|
|
||||||
{
|
|
||||||
return $path === '/some/valid/path';
|
|
||||||
}
|
|
||||||
|
|
||||||
function route($name)
|
|
||||||
{
|
|
||||||
return $name;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Wearepixel\QuickBooks\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||||
use Illuminate\Contracts\View\Factory as ViewFactory;
|
use Illuminate\Contracts\View\Factory as ViewFactory;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
@@ -9,242 +7,166 @@
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Redirector;
|
use Illuminate\Routing\Redirector;
|
||||||
use Illuminate\Session\Store;
|
use Illuminate\Session\Store;
|
||||||
use Mockery;
|
|
||||||
use Mockery\Mock;
|
|
||||||
use QuickBooksOnline\API\DataService\DataService;
|
use QuickBooksOnline\API\DataService\DataService;
|
||||||
use Wearepixel\QuickBooks\Client as QuickBooks;
|
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||||
use Wearepixel\QuickBooks\TestCase;
|
use Wearepixel\QuickBooks\Http\Controllers\Controller;
|
||||||
|
|
||||||
/**
|
beforeEach(function () {
|
||||||
* Class ControllerTest
|
$this->data_service_mock = Mockery::mock(DataService::class);
|
||||||
*/
|
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
||||||
class ControllerTest extends TestCase
|
$this->redirector_mock = Mockery::mock(Redirector::class);
|
||||||
{
|
$this->request_mock = Mockery::mock(Request::class);
|
||||||
/**
|
$this->session_mock = Mockery::mock(Store::class);
|
||||||
* @var Controller
|
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
||||||
*/
|
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
|
||||||
protected $controller;
|
$this->view_mock = Mockery::mock(View::class);
|
||||||
|
|
||||||
/**
|
$this->controller = new Controller;
|
||||||
* @var Mock
|
});
|
||||||
*/
|
|
||||||
protected $data_service_mock;
|
|
||||||
|
|
||||||
/**
|
it('can be constructed', function () {
|
||||||
* @var Mock
|
expect($this->controller)->toBeInstanceOf(Controller::class);
|
||||||
*/
|
});
|
||||||
protected $quickbooks_mock;
|
|
||||||
|
|
||||||
/**
|
it('shows view to disconnect if account linked', function () {
|
||||||
* @var Mock
|
$this->data_service_mock
|
||||||
*/
|
->shouldReceive('getCompanyInfo')
|
||||||
protected $redirector_mock;
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturn(['name' => 'Company']);
|
||||||
|
|
||||||
/**
|
$this->quickbooks_mock
|
||||||
* @var Mock
|
->shouldReceive('hasValidRefreshToken')
|
||||||
*/
|
->once()
|
||||||
protected $request_mock;
|
->withNoArgs()
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
/**
|
$this->quickbooks_mock
|
||||||
* @var Mock
|
->shouldReceive('getDataService')
|
||||||
*/
|
->once()
|
||||||
protected $session_mock;
|
->withNoArgs()
|
||||||
|
->andReturn($this->data_service_mock);
|
||||||
|
|
||||||
/**
|
$this->view_factory_mock
|
||||||
* @var Mock
|
->shouldReceive('make')
|
||||||
*/
|
->once()
|
||||||
protected $view_factory_mock;
|
->with('quickbooks::disconnect')
|
||||||
|
->andReturn($this->view_mock);
|
||||||
|
|
||||||
/**
|
$this->view_mock
|
||||||
* @var Mock
|
->shouldReceive('with')
|
||||||
*/
|
->once()
|
||||||
protected $view_mock;
|
->withArgs(['company', ['name' => 'Company']])
|
||||||
|
->andReturnSelf();
|
||||||
|
|
||||||
protected function setUp(): void
|
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
||||||
{
|
});
|
||||||
$this->data_service_mock = Mockery::mock(DataService::class);
|
|
||||||
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
|
||||||
$this->redirector_mock = Mockery::mock(Redirector::class);
|
|
||||||
$this->request_mock = Mockery::mock(Request::class);
|
|
||||||
$this->session_mock = Mockery::mock(Store::class);
|
|
||||||
$this->view_factory_mock = Mockery::mock(ViewFactory::class);
|
|
||||||
$this->view_mock = Mockery::mock(View::class);
|
|
||||||
|
|
||||||
$this->controller = new Controller();
|
it('shows view to connect if account not linked', function () {
|
||||||
}
|
$this->quickbooks_mock
|
||||||
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
/**
|
$this->quickbooks_mock
|
||||||
* @test
|
->shouldReceive('authorizationUri')
|
||||||
*/
|
->once()
|
||||||
public function it_can_be_constructed()
|
->withNoArgs()
|
||||||
{
|
->andReturn('http://uri');
|
||||||
$this->assertInstanceOf(Controller::class, $this->controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->view_factory_mock
|
||||||
* @test
|
->shouldReceive('make')
|
||||||
*/
|
->once()
|
||||||
public function it_shows_view_to_connect_if_account_not_linked()
|
->with('quickbooks::connect')
|
||||||
{
|
->andReturn($this->view_mock);
|
||||||
$this->data_service_mock
|
|
||||||
->shouldReceive('getCompanyInfo')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn([
|
|
||||||
'name' => 'Company',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->quickbooks_mock
|
$this->view_mock
|
||||||
->shouldReceive('hasValidRefreshToken')
|
->shouldReceive('with')
|
||||||
->once()
|
->once()
|
||||||
->withNoArgs()
|
->withArgs(['authorization_uri', 'http://uri'])
|
||||||
->andReturnTrue();
|
->andReturnSelf();
|
||||||
|
|
||||||
$this->quickbooks_mock
|
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
||||||
->shouldReceive('getDataService')
|
});
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn($this->data_service_mock);
|
|
||||||
|
|
||||||
$this->view_factory_mock
|
it('disconnects from quickbooks when requested', function () {
|
||||||
->shouldReceive('make')
|
$this->request_mock
|
||||||
->once()
|
->shouldReceive('session')
|
||||||
->with('quickbooks::disconnect')
|
->once()
|
||||||
->andReturn($this->view_mock);
|
->andReturn($this->session_mock);
|
||||||
|
|
||||||
$this->view_mock
|
$this->session_mock
|
||||||
->shouldReceive('with')
|
->shouldReceive('flash')
|
||||||
->once()
|
->once()
|
||||||
->withArgs([
|
->withAnyArgs();
|
||||||
'company',
|
|
||||||
[
|
|
||||||
'name' => 'Company',
|
|
||||||
],
|
|
||||||
])
|
|
||||||
->andReturnSelf();
|
|
||||||
|
|
||||||
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
$this->redirector_mock
|
||||||
}
|
->shouldReceive('back')
|
||||||
|
->once()
|
||||||
|
->andReturn(new RedirectResponse('/test', 302));
|
||||||
|
|
||||||
/**
|
$this->quickbooks_mock
|
||||||
* @test
|
->shouldReceive('deleteToken')
|
||||||
*/
|
->once()
|
||||||
public function it_shows_view_to_disconnect_if_account_linked()
|
->withNoArgs();
|
||||||
{
|
|
||||||
$this->quickbooks_mock
|
|
||||||
->shouldReceive('hasValidRefreshToken')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->quickbooks_mock
|
$result = $this->controller->disconnect(
|
||||||
->shouldReceive('authorizationUri')
|
$this->redirector_mock,
|
||||||
->once()
|
$this->request_mock,
|
||||||
->withNoArgs()
|
$this->quickbooks_mock,
|
||||||
->andReturn('http://uri');
|
);
|
||||||
|
|
||||||
$this->view_factory_mock
|
expect($result)->toBeInstanceOf(RedirectResponse::class);
|
||||||
->shouldReceive('make')
|
});
|
||||||
->once()
|
|
||||||
->with('quickbooks::connect')
|
|
||||||
->andReturn($this->view_mock);
|
|
||||||
|
|
||||||
$this->view_mock
|
it('finishes connecting to quickbooks when given a valid token', function () {
|
||||||
->shouldReceive('with')
|
$realmId = random_int(1, 9999);
|
||||||
->once()
|
|
||||||
->withArgs(['authorization_uri', 'http://uri'])
|
|
||||||
->andReturnSelf();
|
|
||||||
|
|
||||||
$this->controller->connect($this->quickbooks_mock, $this->view_factory_mock);
|
$this->quickbooks_mock
|
||||||
}
|
->shouldReceive('exchangeCodeForToken')
|
||||||
|
->once()
|
||||||
|
->withArgs(['code', $realmId]);
|
||||||
|
|
||||||
/**
|
$this->request_mock
|
||||||
* @test
|
->shouldReceive('get')
|
||||||
*/
|
->once()
|
||||||
public function it_disconnects_from_quickbooks_when_requested()
|
->withArgs(['code'])
|
||||||
{
|
->andReturn('code');
|
||||||
$this->request_mock
|
|
||||||
->shouldReceive('session')
|
|
||||||
->once()
|
|
||||||
->andReturn($this->session_mock);
|
|
||||||
|
|
||||||
$this->session_mock
|
$this->request_mock
|
||||||
->shouldReceive('flash')
|
->shouldReceive('get')
|
||||||
->once()
|
->once()
|
||||||
->withAnyArgs();
|
->withArgs(['realmId'])
|
||||||
|
->andReturn($realmId);
|
||||||
|
|
||||||
$this->redirector_mock
|
$this->request_mock
|
||||||
->shouldReceive('back')
|
->shouldReceive('session')
|
||||||
->once()
|
->once()
|
||||||
->andReturn(new RedirectResponse('/test', 302));
|
->andReturn($this->session_mock);
|
||||||
|
|
||||||
$this->quickbooks_mock
|
$this->session_mock
|
||||||
->shouldReceive('deleteToken')
|
->shouldReceive('flash')
|
||||||
->once()
|
->once()
|
||||||
->withNoArgs();
|
->withAnyArgs();
|
||||||
|
|
||||||
$result = $this->controller->disconnect(
|
$this->redirector_mock
|
||||||
$this->redirector_mock,
|
->shouldReceive('intended')
|
||||||
$this->request_mock,
|
->once()
|
||||||
$this->quickbooks_mock,
|
->withAnyArgs()
|
||||||
);
|
->andReturn(new RedirectResponse('/test', 302));
|
||||||
|
|
||||||
$this->assertInstanceOf(RedirectResponse::class, $result);
|
$this->url_generator_mock
|
||||||
}
|
->shouldReceive('route')
|
||||||
|
->withArgs(['quickbooks.connect'])
|
||||||
|
->once();
|
||||||
|
|
||||||
/**
|
$result = $this->controller->token(
|
||||||
* @test
|
$this->redirector_mock,
|
||||||
*/
|
$this->request_mock,
|
||||||
public function it_finishes_connecting_to_quickbooks_when_given_a_valid_token_by_quickbooks()
|
$this->quickbooks_mock,
|
||||||
{
|
$this->url_generator_mock,
|
||||||
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
);
|
||||||
$realmId = random_int(1, 9999);
|
|
||||||
|
|
||||||
$this->quickbooks_mock
|
expect($result)->toBeInstanceOf(RedirectResponse::class);
|
||||||
->shouldReceive('exchangeCodeForToken')
|
});
|
||||||
->once()
|
|
||||||
->withArgs(['code', $realmId]);
|
|
||||||
|
|
||||||
$this->request_mock
|
|
||||||
->shouldReceive('get')
|
|
||||||
->once()
|
|
||||||
->withArgs(['code'])
|
|
||||||
->andReturn('code');
|
|
||||||
|
|
||||||
$this->request_mock
|
|
||||||
->shouldReceive('get')
|
|
||||||
->once()
|
|
||||||
->withArgs(['realmId'])
|
|
||||||
->andReturn($realmId);
|
|
||||||
|
|
||||||
$this->request_mock
|
|
||||||
->shouldReceive('session')
|
|
||||||
->once()
|
|
||||||
->andReturn($this->session_mock);
|
|
||||||
|
|
||||||
$this->session_mock
|
|
||||||
->shouldReceive('flash')
|
|
||||||
->once()
|
|
||||||
->withAnyArgs();
|
|
||||||
|
|
||||||
$this->redirector_mock
|
|
||||||
->shouldReceive('intended')
|
|
||||||
->once()
|
|
||||||
->withAnyArgs()
|
|
||||||
->andReturn(new RedirectResponse('/test', 302));
|
|
||||||
|
|
||||||
$this->url_generator_mock
|
|
||||||
->shouldReceive('route')
|
|
||||||
->withArgs(['quickbooks.connect'])
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$result = $this->controller->token(
|
|
||||||
$this->redirector_mock,
|
|
||||||
$this->request_mock,
|
|
||||||
$this->quickbooks_mock,
|
|
||||||
$this->url_generator_mock,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(RedirectResponse::class, $result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,133 +1,79 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Wearepixel\QuickBooks\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||||
use Illuminate\Contracts\Session\Session;
|
use Illuminate\Contracts\Session\Session;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Redirector;
|
use Illuminate\Routing\Redirector;
|
||||||
use Mockery;
|
|
||||||
use Mockery\Mock;
|
|
||||||
use Wearepixel\QuickBooks\Client as QuickBooks;
|
use Wearepixel\QuickBooks\Client as QuickBooks;
|
||||||
use Wearepixel\QuickBooks\TestCase;
|
use Wearepixel\QuickBooks\Http\Middleware\Filter;
|
||||||
|
|
||||||
/**
|
beforeEach(function () {
|
||||||
* Class FilterTest
|
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
||||||
*/
|
$this->redirector_mock = Mockery::mock(Redirector::class);
|
||||||
class FilterTest extends TestCase
|
$this->request_mock = Mockery::mock(Request::class);
|
||||||
{
|
$this->session_mock = Mockery::mock(Session::class);
|
||||||
/**
|
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
||||||
* @var Filter
|
|
||||||
*/
|
|
||||||
protected $filter;
|
|
||||||
|
|
||||||
/**
|
$this->filter = new Filter(
|
||||||
* @var Mock
|
$this->quickbooks_mock,
|
||||||
*/
|
$this->redirector_mock,
|
||||||
protected $quickbooks_mock;
|
$this->session_mock,
|
||||||
|
$this->url_generator_mock,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
it('can be constructed', function () {
|
||||||
* @var Mock
|
expect($this->filter)->toBeInstanceOf(Filter::class);
|
||||||
*/
|
});
|
||||||
protected $redirector_mock;
|
|
||||||
|
|
||||||
/**
|
it('passes the request to the next middleware if account linked to quickbooks', function () {
|
||||||
* @var Mock
|
$next_middleware = function ($request) {
|
||||||
*/
|
expect($request)->toBe($this->request_mock);
|
||||||
protected $request_mock;
|
};
|
||||||
|
|
||||||
/**
|
$this->quickbooks_mock
|
||||||
* @var Mock
|
->shouldReceive('hasValidRefreshToken')
|
||||||
*/
|
->once()
|
||||||
protected $session_mock;
|
->withNoArgs()
|
||||||
|
->andReturnTrue();
|
||||||
|
|
||||||
/**
|
$this->filter->handle($this->request_mock, $next_middleware);
|
||||||
* @var Mock
|
});
|
||||||
*/
|
|
||||||
protected $url_generator_mock;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
it('redirects to quickbooks connect route after setting intended session if account not linked', function () {
|
||||||
{
|
$this->request_mock
|
||||||
$this->quickbooks_mock = Mockery::mock(QuickBooks::class);
|
->shouldReceive('path')
|
||||||
$this->redirector_mock = Mockery::mock(Redirector::class);
|
->once()
|
||||||
$this->request_mock = Mockery::mock(Request::class);
|
->withNoArgs()
|
||||||
$this->session_mock = Mockery::mock(Session::class);
|
->andReturn('path');
|
||||||
$this->url_generator_mock = Mockery::mock(UrlGenerator::class);
|
|
||||||
|
|
||||||
$this->filter = new Filter(
|
$this->url_generator_mock
|
||||||
$this->quickbooks_mock,
|
->shouldReceive('to')
|
||||||
$this->redirector_mock,
|
->once()
|
||||||
$this->session_mock,
|
->with('path')
|
||||||
$this->url_generator_mock,
|
->andReturn('http://to/path');
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->session_mock
|
||||||
* @test
|
->shouldReceive('put')
|
||||||
*/
|
->once()
|
||||||
public function it_can_be_constructed()
|
->withArgs(['url.intended', 'http://to/path'])
|
||||||
{
|
->andReturnNull();
|
||||||
$this->assertInstanceOf(Filter::class, $this->filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->redirector_mock
|
||||||
* @test
|
->shouldReceive('route')
|
||||||
*/
|
->once()
|
||||||
public function it_just_passes_the_request_to_the_next_middleware_if_account_linked_to_quickbooks()
|
->with('quickbooks.connect')
|
||||||
{
|
->andReturnSelf();
|
||||||
$next_middleware = function ($request) {
|
|
||||||
$this->assertEquals($this->request_mock, $request);
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->quickbooks_mock
|
$next_middleware = function ($request) {
|
||||||
->shouldReceive('hasValidRefreshToken')
|
throw new RuntimeException('Next middleware should not be called');
|
||||||
->once()
|
};
|
||||||
->withNoArgs()
|
|
||||||
->andReturnTrue();
|
|
||||||
|
|
||||||
$this->filter->handle($this->request_mock, $next_middleware);
|
$this->quickbooks_mock
|
||||||
}
|
->shouldReceive('hasValidRefreshToken')
|
||||||
|
->once()
|
||||||
|
->withNoArgs()
|
||||||
|
->andReturnFalse();
|
||||||
|
|
||||||
/**
|
$this->filter->handle($this->request_mock, $next_middleware);
|
||||||
* @test
|
});
|
||||||
*/
|
|
||||||
public function it_redirects_to_quickbooks_connect_route_after_setting_intended_session_if_account_not_linked()
|
|
||||||
{
|
|
||||||
$this->request_mock
|
|
||||||
->shouldReceive('path')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn('path');
|
|
||||||
|
|
||||||
$this->url_generator_mock
|
|
||||||
->shouldReceive('to')
|
|
||||||
->once()
|
|
||||||
->with('path')
|
|
||||||
->andReturn('http://to/path');
|
|
||||||
|
|
||||||
$this->session_mock
|
|
||||||
->shouldReceive('put')
|
|
||||||
->once()
|
|
||||||
->withArgs(['url.intended', 'http://to/path'])
|
|
||||||
->andReturnNull();
|
|
||||||
|
|
||||||
$this->redirector_mock
|
|
||||||
->shouldReceive('route')
|
|
||||||
->once()
|
|
||||||
->with('quickbooks.connect')
|
|
||||||
->andReturnSelf();
|
|
||||||
|
|
||||||
$next_middleware = function ($request) {
|
|
||||||
// If this is called, then fail test
|
|
||||||
$this->assertTrue(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->quickbooks_mock
|
|
||||||
->shouldReceive('hasValidRefreshToken')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturnFalse();
|
|
||||||
|
|
||||||
$this->filter->handle($this->request_mock, $next_middleware);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||||
|
|
||||||
|
pest()->use(MockeryPHPUnitIntegration::class);
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -1,212 +1,109 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Wearepixel\QuickBooks;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Mockery;
|
|
||||||
use QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken;
|
|
||||||
use Wearepixel\QuickBooks\Stubs\TokenStub;
|
use Wearepixel\QuickBooks\Stubs\TokenStub;
|
||||||
use Wearepixel\QuickBooks\Stubs\User;
|
use Wearepixel\QuickBooks\Stubs\User;
|
||||||
|
use Wearepixel\QuickBooks\Token;
|
||||||
|
|
||||||
/**
|
beforeEach(function () {
|
||||||
* Class TokenTest
|
$this->token = new TokenStub;
|
||||||
*/
|
});
|
||||||
class TokenTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var Token
|
|
||||||
*/
|
|
||||||
protected $token;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
it('can be constructed', function () {
|
||||||
{
|
expect($this->token)->toBeInstanceOf(Token::class);
|
||||||
$this->token = new TokenStub();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('has accessor for valid access token', function () {
|
||||||
* @test
|
expect($this->token->getHasValidAccessTokenAttribute())->not->toBeNull();
|
||||||
*/
|
});
|
||||||
public function it_can_be_constructed()
|
|
||||||
{
|
|
||||||
$this->assertInstanceOf(Token::class, $this->token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('has accessor for valid refresh token', function () {
|
||||||
* @test
|
expect($this->token->getHasValidRefreshTokenAttribute())->not->toBeNull();
|
||||||
*/
|
});
|
||||||
public function it_has_accessor_for_valid_access_token()
|
|
||||||
{
|
|
||||||
$this->assertNotNull($this->token->getHasValidAccessTokenAttribute());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('knows that the access token expires at has to be valid', function () {
|
||||||
* @test
|
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
|
||||||
*/
|
});
|
||||||
public function it_has_accessor_for_valid_refresh_token()
|
|
||||||
{
|
|
||||||
$this->assertNotNull($this->token->getHasValidRefreshTokenAttribute());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('knows that the refresh token expires at has to be valid', function () {
|
||||||
* @test
|
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
|
||||||
*/
|
});
|
||||||
public function it_knows_that_the_access_token_expires_at_has_to_be_valid()
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
it('knows if access token expires at is less than now it is not expired', function () {
|
||||||
* @test
|
Carbon::setTestNow(Carbon::now());
|
||||||
*/
|
|
||||||
public function it_knows_that_the_refresh_token_expires_at_has_to_be_valid()
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$this->token->access_token_expires_at = Carbon::now()->addSecond();
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_know_if_access_token_expires_at_is_less_than_now_it_is_not_expired()
|
|
||||||
{
|
|
||||||
$now = Carbon::now();
|
|
||||||
Carbon::setTestNow($now);
|
|
||||||
|
|
||||||
$this->token->access_token_expires_at = Carbon::now()->addSecond();
|
expect($this->token->getHasValidAccessTokenAttribute())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
$this->assertTrue($this->token->getHasValidAccessTokenAttribute());
|
it('knows if refresh token expires at is less than now it is not expired', function () {
|
||||||
}
|
Carbon::setTestNow(Carbon::now());
|
||||||
|
|
||||||
/**
|
$this->token->refresh_token_expires_at = Carbon::now()->addSecond();
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_know_if_refresh_token_expires_at_is_less_than_now_it_is_not_expired()
|
|
||||||
{
|
|
||||||
$now = Carbon::now();
|
|
||||||
Carbon::setTestNow($now);
|
|
||||||
|
|
||||||
$this->token->refresh_token_expires_at = Carbon::now()->addSecond();
|
expect($this->token->getHasValidRefreshTokenAttribute())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
$this->assertTrue($this->token->getHasValidRefreshTokenAttribute());
|
it('knows if access token expires at is greater than or equal to now it is expired', function () {
|
||||||
}
|
Carbon::setTestNow(Carbon::now());
|
||||||
|
|
||||||
/**
|
$this->token->access_token_expires_at = Carbon::now();
|
||||||
* @test
|
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
|
||||||
*/
|
|
||||||
public function it_know_if_access_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
|
|
||||||
{
|
|
||||||
$now = Carbon::now();
|
|
||||||
Carbon::setTestNow($now);
|
|
||||||
|
|
||||||
$this->token->access_token_expires_at = Carbon::now();
|
$this->token->access_token_expires_at->subSecond();
|
||||||
|
expect($this->token->getHasValidAccessTokenAttribute())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
it('knows if refresh token expires at is greater than or equal to now it is expired', function () {
|
||||||
|
Carbon::setTestNow(Carbon::now());
|
||||||
|
|
||||||
$this->token->access_token_expires_at->subSecond();
|
$this->token->refresh_token_expires_at = Carbon::now();
|
||||||
|
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
|
||||||
|
|
||||||
$this->assertFalse($this->token->getHasValidAccessTokenAttribute());
|
$this->token->refresh_token_expires_at->subSecond();
|
||||||
}
|
expect($this->token->getHasValidRefreshTokenAttribute())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
it('stores the oauth token parts in expected properties', function () {
|
||||||
* @test
|
$oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class);
|
||||||
*/
|
|
||||||
public function it_know_if_refresh_token_expires_at_is_greater_than_or_equal_to_now_it_is_expired()
|
|
||||||
{
|
|
||||||
$now = Carbon::now();
|
|
||||||
Carbon::setTestNow($now);
|
|
||||||
|
|
||||||
$this->token->refresh_token_expires_at = Carbon::now();
|
$oauth_token_mock->shouldReceive('getAccessToken')->once()->withNoArgs()->andReturn('access_token');
|
||||||
|
$oauth_token_mock->shouldReceive('getAccessTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-06-15 12:00:00');
|
||||||
|
$oauth_token_mock->shouldReceive('getRealmID')->once()->withNoArgs()->andReturn('realm_id');
|
||||||
|
$oauth_token_mock->shouldReceive('getRefreshToken')->once()->withNoArgs()->andReturn('refresh_token');
|
||||||
|
$oauth_token_mock->shouldReceive('getRefreshTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-09-24 12:00:00');
|
||||||
|
|
||||||
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
$result = $this->token->parseOauthToken($oauth_token_mock);
|
||||||
|
|
||||||
$this->token->refresh_token_expires_at->subSecond();
|
expect($result)->toBeInstanceOf(Token::class)
|
||||||
|
->and($this->token->access_token)->toBe('access_token')
|
||||||
|
->and($this->token->realm_id)->toBe('realm_id')
|
||||||
|
->and($this->token->refresh_token)->toBe('refresh_token')
|
||||||
|
->and($this->token->access_token_expires_at)->toBeInstanceOf(Carbon::class)
|
||||||
|
->and($this->token->refresh_token_expires_at)->toBeInstanceOf(Carbon::class);
|
||||||
|
});
|
||||||
|
|
||||||
$this->assertFalse($this->token->getHasValidRefreshTokenAttribute());
|
it('allows itself to be deleted and returns new token', function () {
|
||||||
}
|
$this->token->user = Mockery::mock(User::class);
|
||||||
|
|
||||||
/**
|
$has_one_mock = Mockery::mock(HasOne::class);
|
||||||
* @test
|
$token_mock = Mockery::mock(Token::class);
|
||||||
*/
|
|
||||||
public function it_stores_the_oauth_token_parts_in_expected_properties()
|
|
||||||
{
|
|
||||||
$oauth_token_mock = Mockery::mock(OAuth2AccessToken::class);
|
|
||||||
|
|
||||||
$oauth_token_mock
|
$has_one_mock->shouldReceive('make')->once()->withNoArgs()->andReturn($token_mock);
|
||||||
->shouldReceive('getAccessToken')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn('access_token');
|
|
||||||
|
|
||||||
$oauth_token_mock
|
$this->token->user
|
||||||
->shouldReceive('getAccessTokenExpiresAt')
|
->shouldReceive('quickBooksToken')
|
||||||
->once()
|
->once()
|
||||||
->withNoArgs()
|
->withNoArgs()
|
||||||
->andReturn('now');
|
->andReturn($has_one_mock);
|
||||||
|
|
||||||
$oauth_token_mock
|
$this->token->user->id = 1;
|
||||||
->shouldReceive('getRealmID')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn('realm_id');
|
|
||||||
|
|
||||||
$oauth_token_mock
|
expect($this->token->remove())->toBe($token_mock);
|
||||||
->shouldReceive('getRefreshToken')
|
});
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn('refresh_token');
|
|
||||||
|
|
||||||
$oauth_token_mock
|
it('gets related user model from configuration', function () {
|
||||||
->shouldReceive('getRefreshTokenExpiresAt')
|
expect($this->token->user()->getModel())->toBeInstanceOf(User::class);
|
||||||
->once()
|
});
|
||||||
->withNoArgs()
|
|
||||||
->andReturn('now');
|
|
||||||
|
|
||||||
$this->token->parseOauthToken($oauth_token_mock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_allows_itself_to_be_deleted_and_returns_new_token()
|
|
||||||
{
|
|
||||||
$this->token->user = Mockery::mock(User::class);
|
|
||||||
|
|
||||||
$has_one_mock = Mockery::mock(HasOne::class);
|
|
||||||
$token_mock = Mockery::mock(Token::class);
|
|
||||||
|
|
||||||
$has_one_mock
|
|
||||||
->shouldReceive('make')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn($token_mock);
|
|
||||||
|
|
||||||
$this->token->user
|
|
||||||
->shouldReceive('quickBooksToken')
|
|
||||||
->once()
|
|
||||||
->withNoArgs()
|
|
||||||
->andReturn($has_one_mock);
|
|
||||||
|
|
||||||
$this->token->user->id = 1;
|
|
||||||
|
|
||||||
$this->assertEquals($token_mock, $this->token->remove());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function it_get_related_user_model_from_configuration()
|
|
||||||
{
|
|
||||||
$this->assertInstanceOf(User::class, $this->token->user()->getModel());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function config($key)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'keys' => [
|
|
||||||
'foreign' => 'user_id',
|
|
||||||
'owner' => 'id',
|
|
||||||
],
|
|
||||||
'model' => User::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user