@@ -12,7 +12,14 @@ import {
1212} from '@sim/testing'
1313import { beforeEach , describe , expect , it , vi } from 'vitest'
1414
15+ const { mockRevokeQuickBooksToken } = vi . hoisted ( ( ) => ( {
16+ mockRevokeQuickBooksToken : vi . fn ( ) ,
17+ } ) )
18+
1519vi . mock ( '@sim/audit' , ( ) => auditMock )
20+ vi . mock ( '@/lib/oauth/quickbooks' , ( ) => ( {
21+ revokeQuickBooksToken : mockRevokeQuickBooksToken ,
22+ } ) )
1623
1724import { POST } from '@/app/api/auth/oauth/disconnect/route'
1825
@@ -21,6 +28,7 @@ describe('OAuth Disconnect API Route', () => {
2128 vi . clearAllMocks ( )
2229 resetDbChainMock ( )
2330 dbChainMockFns . where . mockResolvedValue ( [ ] )
31+ mockRevokeQuickBooksToken . mockResolvedValue ( undefined )
2432 } )
2533
2634 it ( 'should disconnect provider successfully' , async ( ) => {
@@ -56,6 +64,148 @@ describe('OAuth Disconnect API Route', () => {
5664 expect ( data . success ) . toBe ( true )
5765 } )
5866
67+ it ( 'revokes the QuickBooks refresh token before deleting the local account' , async ( ) => {
68+ authMockFns . mockGetSession . mockResolvedValueOnce ( {
69+ user : { id : 'user-123' } ,
70+ } )
71+ dbChainMockFns . where
72+ . mockResolvedValueOnce ( [
73+ {
74+ id : 'account-1' ,
75+ providerId : 'quickbooks' ,
76+ accessToken : 'access-token' ,
77+ refreshToken : 'refresh-token' ,
78+ } ,
79+ ] )
80+ . mockResolvedValueOnce ( [ ] )
81+
82+ const response = await POST (
83+ createMockRequest ( 'POST' , {
84+ provider : 'quickbooks' ,
85+ providerId : 'quickbooks' ,
86+ accountId : 'account-1' ,
87+ } )
88+ )
89+
90+ expect ( response . status ) . toBe ( 200 )
91+ expect ( mockRevokeQuickBooksToken ) . toHaveBeenCalledWith ( 'refresh-token' )
92+ expect ( dbChainMockFns . delete ) . toHaveBeenCalled ( )
93+ expect ( mockRevokeQuickBooksToken . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
94+ dbChainMockFns . delete . mock . invocationCallOrder [ 0 ]
95+ )
96+ } )
97+
98+ it ( 'falls back to the QuickBooks access token when no refresh token is stored' , async ( ) => {
99+ authMockFns . mockGetSession . mockResolvedValueOnce ( {
100+ user : { id : 'user-123' } ,
101+ } )
102+ dbChainMockFns . where
103+ . mockResolvedValueOnce ( [
104+ {
105+ id : 'account-1' ,
106+ providerId : 'quickbooks' ,
107+ accessToken : 'access-token' ,
108+ refreshToken : null ,
109+ } ,
110+ ] )
111+ . mockResolvedValueOnce ( [ ] )
112+
113+ const response = await POST (
114+ createMockRequest ( 'POST' , {
115+ provider : 'quickbooks' ,
116+ providerId : 'quickbooks' ,
117+ accountId : 'account-1' ,
118+ } )
119+ )
120+
121+ expect ( response . status ) . toBe ( 200 )
122+ expect ( mockRevokeQuickBooksToken ) . toHaveBeenCalledWith ( 'access-token' )
123+ } )
124+
125+ it ( 'keeps QuickBooks credentials locally when Intuit revocation fails' , async ( ) => {
126+ authMockFns . mockGetSession . mockResolvedValueOnce ( {
127+ user : { id : 'user-123' } ,
128+ } )
129+ dbChainMockFns . where . mockResolvedValueOnce ( [
130+ {
131+ id : 'account-1' ,
132+ providerId : 'quickbooks' ,
133+ accessToken : 'access-token' ,
134+ refreshToken : 'refresh-token' ,
135+ } ,
136+ ] )
137+ mockRevokeQuickBooksToken . mockRejectedValueOnce ( new Error ( 'Intuit unavailable' ) )
138+
139+ const response = await POST (
140+ createMockRequest ( 'POST' , {
141+ provider : 'quickbooks' ,
142+ providerId : 'quickbooks' ,
143+ accountId : 'account-1' ,
144+ } )
145+ )
146+ const data = await response . json ( )
147+
148+ expect ( response . status ) . toBe ( 502 )
149+ expect ( data . error ) . toBe ( 'Unable to revoke QuickBooks access. Please try again.' )
150+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
151+ } )
152+
153+ it ( 'removes a tokenless QuickBooks account without calling Intuit' , async ( ) => {
154+ authMockFns . mockGetSession . mockResolvedValueOnce ( {
155+ user : { id : 'user-123' } ,
156+ } )
157+ dbChainMockFns . where
158+ . mockResolvedValueOnce ( [
159+ {
160+ id : 'account-1' ,
161+ providerId : 'quickbooks' ,
162+ accessToken : null ,
163+ refreshToken : null ,
164+ } ,
165+ ] )
166+ . mockResolvedValueOnce ( [ ] )
167+
168+ const response = await POST (
169+ createMockRequest ( 'POST' , {
170+ provider : 'quickbooks' ,
171+ providerId : 'quickbooks' ,
172+ accountId : 'account-1' ,
173+ } )
174+ )
175+
176+ expect ( response . status ) . toBe ( 200 )
177+ expect ( mockRevokeQuickBooksToken ) . not . toHaveBeenCalled ( )
178+ expect ( dbChainMockFns . delete ) . toHaveBeenCalled ( )
179+ } )
180+
181+ it ( 'does not revoke tokens for non-QuickBooks providers' , async ( ) => {
182+ authMockFns . mockGetSession . mockResolvedValueOnce ( {
183+ user : { id : 'user-123' } ,
184+ } )
185+ dbChainMockFns . where
186+ . mockResolvedValueOnce ( [
187+ {
188+ id : 'account-1' ,
189+ providerId : 'google-email' ,
190+ accessToken : 'access-token' ,
191+ refreshToken : 'refresh-token' ,
192+ } ,
193+ ] )
194+ . mockResolvedValueOnce ( [ ] )
195+
196+ const response = await POST (
197+ createMockRequest ( 'POST' , {
198+ provider : 'google' ,
199+ providerId : 'google-email' ,
200+ accountId : 'account-1' ,
201+ } )
202+ )
203+
204+ expect ( response . status ) . toBe ( 200 )
205+ expect ( mockRevokeQuickBooksToken ) . not . toHaveBeenCalled ( )
206+ expect ( dbChainMockFns . delete ) . toHaveBeenCalled ( )
207+ } )
208+
59209 it ( 'should handle unauthenticated user' , async ( ) => {
60210 authMockFns . mockGetSession . mockResolvedValueOnce ( null )
61211
0 commit comments