Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 508 lines 13 kB view raw
1import { 2 Source, 3 pipe, 4 fromValue, 5 toPromise, 6 take, 7 makeSubject, 8 share, 9 publish, 10 scan, 11 tap, 12 map, 13} from 'wonka'; 14 15import { 16 makeOperation, 17 CombinedError, 18 Client, 19 Operation, 20 OperationResult, 21} from '@urql/core'; 22 23import { vi, expect, it } from 'vitest'; 24import { print } from 'graphql'; 25import { 26 queryResponse, 27 queryOperation, 28} from '../../../packages/core/src/test-utils'; 29import { authExchange } from './authExchange'; 30 31const makeExchangeArgs = () => { 32 const operations: Operation[] = []; 33 const result = vi.fn( 34 (operation: Operation): OperationResult => ({ ...queryResponse, operation }) 35 ); 36 37 return { 38 operations, 39 result, 40 exchangeArgs: { 41 forward: (op$: Source<Operation>) => 42 pipe( 43 op$, 44 tap(op => operations.push(op)), 45 map(result), 46 share 47 ), 48 client: new Client({ 49 url: '/api', 50 exchanges: [], 51 }), 52 } as any, 53 }; 54}; 55 56it('adds the auth header correctly', async () => { 57 const { exchangeArgs } = makeExchangeArgs(); 58 59 const res = await pipe( 60 fromValue(queryOperation), 61 authExchange(async utils => { 62 const token = 'my-token'; 63 return { 64 addAuthToOperation(operation) { 65 return utils.appendHeaders(operation, { 66 Authorization: token, 67 }); 68 }, 69 didAuthError: () => false, 70 async refreshAuth() { 71 /*noop*/ 72 }, 73 }; 74 })(exchangeArgs), 75 take(1), 76 toPromise 77 ); 78 79 expect(res.operation.context.authAttempt).toBe(false); 80 expect(res.operation.context.fetchOptions).toEqual({ 81 ...(queryOperation.context.fetchOptions || {}), 82 headers: { 83 Authorization: 'my-token', 84 }, 85 }); 86}); 87 88it('adds the auth header correctly when intialized asynchronously', async () => { 89 const { exchangeArgs } = makeExchangeArgs(); 90 91 const res = await pipe( 92 fromValue(queryOperation), 93 authExchange(async utils => { 94 // delayed initial auth 95 await Promise.resolve(); 96 const token = 'async-token'; 97 98 return { 99 addAuthToOperation(operation) { 100 return utils.appendHeaders(operation, { 101 Authorization: token, 102 }); 103 }, 104 didAuthError: () => false, 105 async refreshAuth() { 106 /*noop*/ 107 }, 108 }; 109 })(exchangeArgs), 110 take(1), 111 toPromise 112 ); 113 114 expect(res.operation.context.authAttempt).toBe(false); 115 expect(res.operation.context.fetchOptions).toEqual({ 116 ...(queryOperation.context.fetchOptions || {}), 117 headers: { 118 Authorization: 'async-token', 119 }, 120 }); 121}); 122 123it('supports calls to the mutate() method in refreshAuth()', async () => { 124 const { exchangeArgs } = makeExchangeArgs(); 125 126 const willAuthError = vi 127 .fn() 128 .mockReturnValueOnce(true) 129 .mockReturnValue(false); 130 131 const [mutateRes, res] = await pipe( 132 fromValue(queryOperation), 133 authExchange(async utils => { 134 const token = 'async-token'; 135 136 return { 137 addAuthToOperation(operation) { 138 return utils.appendHeaders(operation, { 139 Authorization: token, 140 }); 141 }, 142 willAuthError, 143 didAuthError: () => false, 144 async refreshAuth() { 145 const result = await utils.mutate('mutation { auth }', undefined); 146 expect(print(result.operation.query)).toBe('mutation {\n auth\n}'); 147 }, 148 }; 149 })(exchangeArgs), 150 take(2), 151 scan((acc, res) => [...acc, res], [] as OperationResult[]), 152 toPromise 153 ); 154 155 expect(mutateRes.operation.context.fetchOptions).toEqual({ 156 headers: { 157 Authorization: 'async-token', 158 }, 159 }); 160 161 expect(res.operation.context.authAttempt).toBe(true); 162 expect(res.operation.context.fetchOptions).toEqual({ 163 method: 'POST', 164 headers: { 165 Authorization: 'async-token', 166 }, 167 }); 168}); 169 170it('adds the same token to subsequent operations', async () => { 171 const { exchangeArgs } = makeExchangeArgs(); 172 const { source, next } = makeSubject<any>(); 173 174 const result = vi.fn(); 175 const auth$ = pipe( 176 source, 177 authExchange(async utils => { 178 const token = 'my-token'; 179 return { 180 addAuthToOperation(operation) { 181 return utils.appendHeaders(operation, { 182 Authorization: token, 183 }); 184 }, 185 didAuthError: () => false, 186 async refreshAuth() { 187 /*noop*/ 188 }, 189 }; 190 })(exchangeArgs), 191 tap(result), 192 take(2), 193 toPromise 194 ); 195 196 await new Promise(resolve => setTimeout(resolve)); 197 198 next(queryOperation); 199 200 next( 201 makeOperation('query', queryOperation, { 202 ...queryOperation.context, 203 foo: 'bar', 204 }) 205 ); 206 207 await auth$; 208 expect(result).toHaveBeenCalledTimes(2); 209 210 expect(result.mock.calls[0][0].operation.context.authAttempt).toBe(false); 211 expect(result.mock.calls[0][0].operation.context.fetchOptions).toEqual({ 212 ...(queryOperation.context.fetchOptions || {}), 213 headers: { 214 Authorization: 'my-token', 215 }, 216 }); 217 218 expect(result.mock.calls[1][0].operation.context.authAttempt).toBe(false); 219 expect(result.mock.calls[1][0].operation.context.fetchOptions).toEqual({ 220 ...(queryOperation.context.fetchOptions || {}), 221 headers: { 222 Authorization: 'my-token', 223 }, 224 }); 225}); 226 227it('triggers authentication when an operation did error', async () => { 228 const { exchangeArgs, result, operations } = makeExchangeArgs(); 229 const { source, next } = makeSubject<any>(); 230 231 const didAuthError = vi.fn().mockReturnValueOnce(true); 232 233 pipe( 234 source, 235 authExchange(async utils => { 236 let token = 'initial-token'; 237 return { 238 addAuthToOperation(operation) { 239 return utils.appendHeaders(operation, { 240 Authorization: token, 241 }); 242 }, 243 didAuthError, 244 async refreshAuth() { 245 token = 'final-token'; 246 }, 247 }; 248 })(exchangeArgs), 249 publish 250 ); 251 252 await new Promise(resolve => setTimeout(resolve)); 253 254 result.mockReturnValueOnce({ 255 ...queryResponse, 256 operation: queryOperation, 257 data: undefined, 258 error: new CombinedError({ 259 graphQLErrors: [{ message: 'Oops' }], 260 }), 261 }); 262 263 next(queryOperation); 264 expect(result).toHaveBeenCalledTimes(1); 265 expect(didAuthError).toHaveBeenCalledTimes(1); 266 267 await new Promise(resolve => setTimeout(resolve)); 268 269 expect(result).toHaveBeenCalledTimes(2); 270 expect(operations.length).toBe(2); 271 expect(operations[0]).toHaveProperty( 272 'context.fetchOptions.headers.Authorization', 273 'initial-token' 274 ); 275 expect(operations[1]).toHaveProperty( 276 'context.fetchOptions.headers.Authorization', 277 'final-token' 278 ); 279}); 280 281it('triggers authentication when an operation will error', async () => { 282 const { exchangeArgs, result, operations } = makeExchangeArgs(); 283 const { source, next } = makeSubject<any>(); 284 285 const willAuthError = vi 286 .fn() 287 .mockReturnValueOnce(true) 288 .mockReturnValue(false); 289 290 pipe( 291 source, 292 authExchange(async utils => { 293 let token = 'initial-token'; 294 return { 295 addAuthToOperation(operation) { 296 return utils.appendHeaders(operation, { 297 Authorization: token, 298 }); 299 }, 300 willAuthError, 301 didAuthError: () => false, 302 async refreshAuth() { 303 token = 'final-token'; 304 }, 305 }; 306 })(exchangeArgs), 307 publish 308 ); 309 310 await new Promise(resolve => setTimeout(resolve)); 311 312 next(queryOperation); 313 expect(result).toHaveBeenCalledTimes(0); 314 expect(willAuthError).toHaveBeenCalledTimes(1); 315 316 await new Promise(resolve => setTimeout(resolve)); 317 318 expect(result).toHaveBeenCalledTimes(1); 319 expect(operations.length).toBe(1); 320 expect(operations[0]).toHaveProperty( 321 'context.fetchOptions.headers.Authorization', 322 'final-token' 323 ); 324}); 325 326it('calls willAuthError on queued operations', async () => { 327 const { exchangeArgs, result, operations } = makeExchangeArgs(); 328 const { source, next } = makeSubject<any>(); 329 330 let initialAuthResolve: ((_?: any) => void) | undefined; 331 332 const willAuthError = vi 333 .fn() 334 .mockReturnValueOnce(true) 335 .mockReturnValue(false); 336 337 pipe( 338 source, 339 authExchange(async utils => { 340 await new Promise(resolve => { 341 initialAuthResolve = resolve; 342 }); 343 344 let token = 'token'; 345 return { 346 willAuthError, 347 didAuthError: () => false, 348 addAuthToOperation(operation) { 349 return utils.appendHeaders(operation, { 350 Authorization: token, 351 }); 352 }, 353 async refreshAuth() { 354 token = 'final-token'; 355 }, 356 }; 357 })(exchangeArgs), 358 publish 359 ); 360 361 await Promise.resolve(); 362 363 next({ ...queryOperation, key: 1 }); 364 next({ ...queryOperation, key: 2 }); 365 366 expect(result).toHaveBeenCalledTimes(0); 367 expect(willAuthError).toHaveBeenCalledTimes(0); 368 369 expect(initialAuthResolve).toBeDefined(); 370 initialAuthResolve!(); 371 372 await new Promise(resolve => setTimeout(resolve)); 373 374 expect(willAuthError).toHaveBeenCalledTimes(2); 375 expect(result).toHaveBeenCalledTimes(2); 376 377 expect(operations.length).toBe(2); 378 expect(operations[0]).toHaveProperty( 379 'context.fetchOptions.headers.Authorization', 380 'final-token' 381 ); 382 383 expect(operations[1]).toHaveProperty( 384 'context.fetchOptions.headers.Authorization', 385 'final-token' 386 ); 387}); 388 389it('does not infinitely retry authentication when an operation did error', async () => { 390 const { exchangeArgs, result, operations } = makeExchangeArgs(); 391 const { source, next } = makeSubject<any>(); 392 393 const didAuthError = vi.fn().mockReturnValue(true); 394 395 pipe( 396 source, 397 authExchange(async utils => { 398 let token = 'initial-token'; 399 return { 400 addAuthToOperation(operation) { 401 return utils.appendHeaders(operation, { 402 Authorization: token, 403 }); 404 }, 405 didAuthError, 406 async refreshAuth() { 407 token = 'final-token'; 408 }, 409 }; 410 })(exchangeArgs), 411 publish 412 ); 413 414 await new Promise(resolve => setTimeout(resolve)); 415 416 result.mockImplementation(x => ({ 417 ...queryResponse, 418 operation: { 419 ...queryResponse.operation, 420 ...x, 421 }, 422 data: undefined, 423 error: new CombinedError({ 424 graphQLErrors: [{ message: 'Oops' }], 425 }), 426 })); 427 428 next(queryOperation); 429 expect(result).toHaveBeenCalledTimes(1); 430 expect(didAuthError).toHaveBeenCalledTimes(1); 431 432 await new Promise(resolve => setTimeout(resolve)); 433 434 expect(result).toHaveBeenCalledTimes(2); 435 expect(operations.length).toBe(2); 436 expect(operations[0]).toHaveProperty( 437 'context.fetchOptions.headers.Authorization', 438 'initial-token' 439 ); 440 expect(operations[1]).toHaveProperty( 441 'context.fetchOptions.headers.Authorization', 442 'final-token' 443 ); 444}); 445 446it('passes on failing refreshAuth() errors to results', async () => { 447 const { exchangeArgs, result } = makeExchangeArgs(); 448 449 const didAuthError = vi.fn().mockReturnValue(true); 450 const willAuthError = vi.fn().mockReturnValue(true); 451 452 const res = await pipe( 453 fromValue(queryOperation), 454 authExchange(async utils => { 455 const token = 'initial-token'; 456 return { 457 addAuthToOperation(operation) { 458 return utils.appendHeaders(operation, { 459 Authorization: token, 460 }); 461 }, 462 didAuthError, 463 willAuthError, 464 async refreshAuth() { 465 throw new Error('test'); 466 }, 467 }; 468 })(exchangeArgs), 469 take(1), 470 toPromise 471 ); 472 473 expect(result).toHaveBeenCalledTimes(0); 474 expect(didAuthError).toHaveBeenCalledTimes(0); 475 expect(willAuthError).toHaveBeenCalledTimes(1); 476 477 expect(res.error).toMatchInlineSnapshot('[CombinedError: [Network] test]'); 478}); 479 480it('passes on errors during initialization', async () => { 481 const { source, next } = makeSubject<any>(); 482 const { exchangeArgs, result } = makeExchangeArgs(); 483 const init = vi.fn().mockRejectedValue(new Error('oops!')); 484 const output = vi.fn(); 485 486 pipe(source, authExchange(init)(exchangeArgs), tap(output), publish); 487 488 expect(result).toHaveBeenCalledTimes(0); 489 expect(output).toHaveBeenCalledTimes(0); 490 491 next(queryOperation); 492 await new Promise(resolve => setTimeout(resolve)); 493 expect(result).toHaveBeenCalledTimes(0); 494 expect(output).toHaveBeenCalledTimes(1); 495 expect(init).toHaveBeenCalledTimes(1); 496 expect(output.mock.calls[0][0].error).toMatchInlineSnapshot( 497 '[CombinedError: [Network] oops!]' 498 ); 499 500 next(queryOperation); 501 await new Promise(resolve => setTimeout(resolve)); 502 expect(result).toHaveBeenCalledTimes(0); 503 expect(output).toHaveBeenCalledTimes(2); 504 expect(init).toHaveBeenCalledTimes(2); 505 expect(output.mock.calls[1][0].error).toMatchInlineSnapshot( 506 '[CombinedError: [Network] oops!]' 507 ); 508});