| @@ -99,6 +99,14 @@ const tokenResponse = · 98 unmodified lines |
| 99 | 99 | () => |
| 100 | 100 | json(200, body); |
| 101 | 101 | |
| 102 | +const tokenResponseFetch = |
| 103 | + (body: unknown): typeof globalThis.fetch => |
| 104 | + async () => |
| 105 | + new Response(JSON.stringify(body), { |
| 106 | + status: 200, |
| 107 | + headers: { "content-type": "application/json" }, |
| 108 | + }); |
| 109 | + |
| 102 | 110 | // --------------------------------------------------------------------------- |
| 103 | 111 | // PKCE |
| 104 | 112 | // --------------------------------------------------------------------------- |
| @@ -575,6 +583,46 @@ describe("exchangeAuthorizationCode", () => { · 470 unmodified lines |
| 575 | 583 | ), |
| 576 | 584 | ); |
| 577 | 585 | |
| 586 | + it.effect("normalizes Slack's comma-delimited top-level scopes", () => |
| 587 | + Effect.gen(function* () { |
| 588 | + const result = yield* exchangeAuthorizationCode({ |
| 589 | + tokenUrl: "https://slack.com/api/oauth.v2.user.access", |
| 590 | + clientId: "cid", |
| 591 | + clientSecret: "csecret", |
| 592 | + redirectUrl: "https://app.example.com/cb", |
| 593 | + codeVerifier: "verifier", |
| 594 | + code: "abc", |
| 595 | + fetch: tokenResponseFetch({ |
| 596 | + access_token: "xoxp-user-token", |
| 597 | + token_type: "Bearer", |
| 598 | + scope: "channels:read,chat:write,reactions:read", |
| 599 | + }), |
| 600 | + }); |
| 601 | + |
| 602 | + expect(result.scope).toBe("channels:read chat:write reactions:read"); |
| 603 | + }), |
| 604 | + ); |
| 605 | + |
| 606 | + it.effect("preserves commas in scope tokens from non-Slack providers", () => |
| 607 | + Effect.gen(function* () { |
| 608 | + const result = yield* exchangeAuthorizationCode({ |
| 609 | + tokenUrl: "https://oauth.example.com/token", |
| 610 | + clientId: "cid", |
| 611 | + clientSecret: "csecret", |
| 612 | + redirectUrl: "https://app.example.com/cb", |
| 613 | + codeVerifier: "verifier", |
| 614 | + code: "abc", |
| 615 | + fetch: tokenResponseFetch({ |
| 616 | + access_token: "provider-token", |
| 617 | + token_type: "Bearer", |
| 618 | + scope: "scope,with-comma other.scope", |
| 619 | + }), |
| 620 | + }); |
| 621 | + |
| 622 | + expect(result.scope).toBe("scope,with-comma other.scope"); |
| 623 | + }), |
| 624 | + ); |
| 625 | + |
| 578 | 626 | it.effect("keeps a standard top-level scope ahead of nested provider metadata", () => |
| 579 | 627 | withTokenEndpoint( |
| 580 | 628 | tokenResponse({ |
| @@ -856,6 +904,24 @@ describe("exchangeClientCredentials", () => { · 275 unmodified lines |
| 856 | 904 | }); |
| 857 | 905 | |
| 858 | 906 | describe("refreshAccessToken", () => { |
| 907 | + it.effect("normalizes Slack's comma-delimited scopes on refresh", () => |
| 908 | + Effect.gen(function* () { |
| 909 | + const result = yield* refreshAccessToken({ |
| 910 | + tokenUrl: "https://slack.com/api/oauth.v2.user.access", |
| 911 | + clientId: "cid", |
| 912 | + clientSecret: "csecret", |
| 913 | + refreshToken: "refresh-token", |
| 914 | + fetch: tokenResponseFetch({ |
| 915 | + access_token: "xoxp-refreshed-token", |
| 916 | + token_type: "Bearer", |
| 917 | + scope: "channels:read,chat:write,reactions:read", |
| 918 | + }), |
| 919 | + }); |
| 920 | + |
| 921 | + expect(result.scope).toBe("channels:read chat:write reactions:read"); |
| 922 | + }), |
| 923 | + ); |
| 924 | + |
| 859 | 925 | it.effect("posts grant_type=refresh_token with the refresh token", () => |
| 860 | 926 | withTokenEndpoint(tokenResponse(validRefreshBody), ({ tokenUrl, calls }) => |
| 861 | 927 | Effect.gen(function* () { |