From 861a08e083d6189cec8e9b8a23bf68316bed1816 Mon Sep 17 00:00:00 2001 From: Joel Male Date: Wed, 18 Feb 2026 10:43:14 +1000 Subject: [PATCH] test: add tests for upgrade-sensitive areas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- tests/ClientTest.php | 51 ++++++++++++++++++++++++++++++++++++++++ tests/TokenModelTest.php | 49 ++++++++++++++++++++++++++++++++++++++ tests/TokenTest.php | 13 +++++++--- 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 tests/TokenModelTest.php diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6ff3083..ebd30b4 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -145,3 +145,54 @@ expect($this->client->deleteToken())->toBeInstanceOf(Client::class); }); + +it('clears cached data service when token is changed', function () { + $this->token_mock + ->shouldReceive('getAttribute') + ->with('hasValidAccessToken') + ->andReturnFalse(); + + $this->token_mock + ->shouldReceive('getAttribute') + ->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(); +}); diff --git a/tests/TokenModelTest.php b/tests/TokenModelTest.php new file mode 100644 index 0000000..01f5c8d --- /dev/null +++ b/tests/TokenModelTest.php @@ -0,0 +1,49 @@ +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]); + }); +}); diff --git a/tests/TokenTest.php b/tests/TokenTest.php index 66ca2e6..da4da8b 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -70,12 +70,19 @@ $oauth_token_mock = Mockery::mock(QuickBooksOnline\API\Core\OAuth\OAuth2\OAuth2AccessToken::class); $oauth_token_mock->shouldReceive('getAccessToken')->once()->withNoArgs()->andReturn('access_token'); - $oauth_token_mock->shouldReceive('getAccessTokenExpiresAt')->once()->withNoArgs()->andReturn('now'); + $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('now'); + $oauth_token_mock->shouldReceive('getRefreshTokenExpiresAt')->once()->withNoArgs()->andReturn('2025-09-24 12:00:00'); - $this->token->parseOauthToken($oauth_token_mock); + $result = $this->token->parseOauthToken($oauth_token_mock); + + 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); }); it('allows itself to be deleted and returns new token', function () {