|
1 | 1 | import { describe, it, expect, beforeEach, vi } from "vitest"; |
| 2 | +import pkg from "../package.json"; |
2 | 3 | import { NylasConnect } from "./connect-client"; |
3 | 4 | import { logger } from "./utils/logger"; |
4 | 5 | import { LogLevel } from "./types"; |
@@ -108,6 +109,40 @@ describe("NylasConnect (fundamentals)", () => { |
108 | 109 | expect(localStorage.getItem("@nylas/connect:token_default")).toBeTruthy(); |
109 | 110 | }); |
110 | 111 |
|
| 112 | + it("sends x-nylas-connect header on token exchange", async () => { |
| 113 | + const auth = new NylasConnect({ |
| 114 | + clientId, |
| 115 | + redirectUri, |
| 116 | + apiUrl: "https://api.us.nylas.com", |
| 117 | + }); |
| 118 | + |
| 119 | + await auth.connect(); |
| 120 | + |
| 121 | + // Minimal successful token response |
| 122 | + const header = base64url({ alg: "none", typ: "JWT" }); |
| 123 | + const payload = base64url({ sub: "u" }); |
| 124 | + const idToken = `${header}.${payload}.sig`; |
| 125 | + |
| 126 | + const mockFetch = vi.fn().mockResolvedValue({ |
| 127 | + ok: true, |
| 128 | + json: async () => ({ |
| 129 | + access_token: "a", |
| 130 | + id_token: idToken, |
| 131 | + grant_id: "g", |
| 132 | + expires_in: 3600, |
| 133 | + scope: "s", |
| 134 | + }), |
| 135 | + }); |
| 136 | + vi.stubGlobal("fetch", mockFetch); |
| 137 | + |
| 138 | + await auth.handleRedirectCallback( |
| 139 | + `${redirectUri}?code=auth_code_1&state=stateXYZ`, |
| 140 | + ); |
| 141 | + |
| 142 | + const lastCall = mockFetch.mock.calls.at(-1); |
| 143 | + expect(lastCall[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 144 | + }); |
| 145 | + |
111 | 146 | it("logout(grantId) removes the specific session and emits SIGNED_OUT", async () => { |
112 | 147 | const auth = new NylasConnect({ |
113 | 148 | clientId, |
@@ -718,6 +753,9 @@ describe("NylasConnect (sessions, validation, and events)", () => { |
718 | 753 |
|
719 | 754 | const status = await auth.getConnectionStatus(); |
720 | 755 | expect(status).toBe("connected"); |
| 756 | + const lastCall = (fetch as any).mock.calls.at(-1); |
| 757 | + expect(lastCall[1]).toBeDefined(); |
| 758 | + expect(lastCall[1].headers["x-nylas-connect"]).toBe(pkg.version); |
721 | 759 | const emitted = spy.mock.calls.map((c) => c[0]); |
722 | 760 | expect(emitted).not.toContain("CONNECTION_STATUS_CHANGED"); |
723 | 761 | }); |
@@ -754,6 +792,8 @@ describe("NylasConnect (sessions, validation, and events)", () => { |
754 | 792 |
|
755 | 793 | const status = await auth.getConnectionStatus(); |
756 | 794 | expect(status).toBe("invalid"); |
| 795 | + const lastCall = (fetch as any).mock.calls.at(-1); |
| 796 | + expect(lastCall[1].headers["x-nylas-connect"]).toBe(pkg.version); |
757 | 797 | const emitted = spy.mock.calls.map((c) => c[0]); |
758 | 798 | expect(emitted).not.toContain("CONNECTION_STATUS_CHANGED"); |
759 | 799 | }); |
@@ -835,6 +875,13 @@ describe("NylasConnect (custom code exchange)", () => { |
835 | 875 | expect(result.accessToken).toBe("custom_access_token"); |
836 | 876 | expect(result.grantId).toBe("custom_grant_123"); |
837 | 877 |
|
| 878 | + // Trigger a token validation request so we can assert headers |
| 879 | + await auth.getConnectionStatus(); |
| 880 | + |
| 881 | + // Verify header present on token validation call |
| 882 | + const lastCallCustom = (fetch as any).mock.calls.at(-1); |
| 883 | + expect(lastCallCustom[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 884 | + |
838 | 885 | // Verify events were emitted |
839 | 886 | const events = spy.mock.calls.map((call) => call[0]); |
840 | 887 | expect(events).toContain("CONNECT_SUCCESS"); |
@@ -1163,13 +1210,11 @@ describe("NylasConnect (custom code exchange)", () => { |
1163 | 1210 | // Verify built-in exchange was used |
1164 | 1211 | expect(result.accessToken).toBe("builtin_access_token"); |
1165 | 1212 | expect(result.grantId).toBe("builtin_grant_123"); |
1166 | | - expect(fetch).toHaveBeenCalledWith( |
1167 | | - "https://api.us.nylas.com/v3/connect/token", |
1168 | | - expect.objectContaining({ |
1169 | | - method: "POST", |
1170 | | - headers: { "Content-Type": "application/json" }, |
1171 | | - }), |
1172 | | - ); |
| 1213 | + const lastCall = (fetch as any).mock.calls.at(-1); |
| 1214 | + expect(lastCall[0]).toBe("https://api.us.nylas.com/v3/connect/token"); |
| 1215 | + expect(lastCall[1].method).toBe("POST"); |
| 1216 | + expect(lastCall[1].headers["Content-Type"]).toBe("application/json"); |
| 1217 | + expect(lastCall[1].headers["x-nylas-connect"]).toBe(pkg.version); |
1173 | 1218 | }); |
1174 | 1219 |
|
1175 | 1220 | function createValidIdToken(): string { |
@@ -1245,21 +1290,21 @@ describe("NylasConnect (Identity Provider Token)", () => { |
1245 | 1290 | expect(mockIdentityProviderToken).toHaveBeenCalledTimes(1); |
1246 | 1291 |
|
1247 | 1292 | // Verify the fetch was called with JSON content type and idp_claims |
1248 | | - expect(mockFetch).toHaveBeenCalledWith( |
| 1293 | + const lastCallInclude = mockFetch.mock.calls.at(-1); |
| 1294 | + expect(lastCallInclude[0]).toBe( |
1249 | 1295 | "https://api.us.nylas.com/v3/connect/token", |
1250 | | - expect.objectContaining({ |
1251 | | - method: "POST", |
1252 | | - headers: { |
1253 | | - "Content-Type": "application/json", |
1254 | | - }, |
1255 | | - body: JSON.stringify({ |
1256 | | - client_id: clientId, |
1257 | | - redirect_uri: redirectUri, |
1258 | | - code: "auth_code_1", |
1259 | | - grant_type: "authorization_code", |
1260 | | - code_verifier: "verifier123", |
1261 | | - idp_claims: mockIdpToken, |
1262 | | - }), |
| 1296 | + ); |
| 1297 | + expect(lastCallInclude[1].method).toBe("POST"); |
| 1298 | + expect(lastCallInclude[1].headers["Content-Type"]).toBe("application/json"); |
| 1299 | + expect(lastCallInclude[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 1300 | + expect(lastCallInclude[1].body).toBe( |
| 1301 | + JSON.stringify({ |
| 1302 | + client_id: clientId, |
| 1303 | + redirect_uri: redirectUri, |
| 1304 | + code: "auth_code_1", |
| 1305 | + grant_type: "authorization_code", |
| 1306 | + code_verifier: "verifier123", |
| 1307 | + idp_claims: mockIdpToken, |
1263 | 1308 | }), |
1264 | 1309 | ); |
1265 | 1310 |
|
@@ -1310,21 +1355,19 @@ describe("NylasConnect (Identity Provider Token)", () => { |
1310 | 1355 | ); |
1311 | 1356 |
|
1312 | 1357 | // Verify the fetch was called with JSON format but no idp_claims |
1313 | | - expect(mockFetch).toHaveBeenCalledWith( |
1314 | | - "https://api.us.nylas.com/v3/connect/token", |
1315 | | - expect.objectContaining({ |
1316 | | - method: "POST", |
1317 | | - headers: { |
1318 | | - "Content-Type": "application/json", |
1319 | | - }, |
1320 | | - body: JSON.stringify({ |
1321 | | - client_id: clientId, |
1322 | | - redirect_uri: redirectUri, |
1323 | | - code: "auth_code_1", |
1324 | | - grant_type: "authorization_code", |
1325 | | - code_verifier: "verifier123", |
1326 | | - // No idp_claims field |
1327 | | - }), |
| 1358 | + const lastCallNull = mockFetch.mock.calls.at(-1); |
| 1359 | + expect(lastCallNull[0]).toBe("https://api.us.nylas.com/v3/connect/token"); |
| 1360 | + expect(lastCallNull[1].method).toBe("POST"); |
| 1361 | + expect(lastCallNull[1].headers["Content-Type"]).toBe("application/json"); |
| 1362 | + expect(lastCallNull[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 1363 | + expect(lastCallNull[1].body).toBe( |
| 1364 | + JSON.stringify({ |
| 1365 | + client_id: clientId, |
| 1366 | + redirect_uri: redirectUri, |
| 1367 | + code: "auth_code_1", |
| 1368 | + grant_type: "authorization_code", |
| 1369 | + code_verifier: "verifier123", |
| 1370 | + // No idp_claims field |
1328 | 1371 | }), |
1329 | 1372 | ); |
1330 | 1373 |
|
@@ -1378,17 +1421,17 @@ describe("NylasConnect (Identity Provider Token)", () => { |
1378 | 1421 | expect(mockIdentityProviderToken).toHaveBeenCalledTimes(1); |
1379 | 1422 |
|
1380 | 1423 | // Verify the fetch was called without idp_claims (empty string is falsy) |
1381 | | - expect(mockFetch).toHaveBeenCalledWith( |
1382 | | - "https://api.us.nylas.com/v3/connect/token", |
1383 | | - expect.objectContaining({ |
1384 | | - body: JSON.stringify({ |
1385 | | - client_id: clientId, |
1386 | | - redirect_uri: redirectUri, |
1387 | | - code: "auth_code_1", |
1388 | | - grant_type: "authorization_code", |
1389 | | - code_verifier: "verifier123", |
1390 | | - // No idp_claims field should be present for empty string |
1391 | | - }), |
| 1424 | + const lastCallEmpty = mockFetch.mock.calls.at(-1); |
| 1425 | + expect(lastCallEmpty[0]).toBe("https://api.us.nylas.com/v3/connect/token"); |
| 1426 | + expect(lastCallEmpty[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 1427 | + expect(lastCallEmpty[1].body).toBe( |
| 1428 | + JSON.stringify({ |
| 1429 | + client_id: clientId, |
| 1430 | + redirect_uri: redirectUri, |
| 1431 | + code: "auth_code_1", |
| 1432 | + grant_type: "authorization_code", |
| 1433 | + code_verifier: "verifier123", |
| 1434 | + // No idp_claims field should be present for empty string |
1392 | 1435 | }), |
1393 | 1436 | ); |
1394 | 1437 | }); |
@@ -1433,21 +1476,19 @@ describe("NylasConnect (Identity Provider Token)", () => { |
1433 | 1476 | ); |
1434 | 1477 |
|
1435 | 1478 | // Verify the fetch was called with JSON format but no idp_claims |
1436 | | - expect(mockFetch).toHaveBeenCalledWith( |
1437 | | - "https://api.us.nylas.com/v3/connect/token", |
1438 | | - expect.objectContaining({ |
1439 | | - method: "POST", |
1440 | | - headers: { |
1441 | | - "Content-Type": "application/json", |
1442 | | - }, |
1443 | | - body: JSON.stringify({ |
1444 | | - client_id: clientId, |
1445 | | - redirect_uri: redirectUri, |
1446 | | - code: "auth_code_1", |
1447 | | - grant_type: "authorization_code", |
1448 | | - code_verifier: "verifier123", |
1449 | | - // No idp_claims field |
1450 | | - }), |
| 1479 | + const lastCallNoCb = mockFetch.mock.calls.at(-1); |
| 1480 | + expect(lastCallNoCb[0]).toBe("https://api.us.nylas.com/v3/connect/token"); |
| 1481 | + expect(lastCallNoCb[1].method).toBe("POST"); |
| 1482 | + expect(lastCallNoCb[1].headers["Content-Type"]).toBe("application/json"); |
| 1483 | + expect(lastCallNoCb[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 1484 | + expect(lastCallNoCb[1].body).toBe( |
| 1485 | + JSON.stringify({ |
| 1486 | + client_id: clientId, |
| 1487 | + redirect_uri: redirectUri, |
| 1488 | + code: "auth_code_1", |
| 1489 | + grant_type: "authorization_code", |
| 1490 | + code_verifier: "verifier123", |
| 1491 | + // No idp_claims field |
1451 | 1492 | }), |
1452 | 1493 | ); |
1453 | 1494 |
|
@@ -1502,17 +1543,17 @@ describe("NylasConnect (Identity Provider Token)", () => { |
1502 | 1543 | expect(mockIdentityProviderToken).toHaveBeenCalledTimes(1); |
1503 | 1544 |
|
1504 | 1545 | // Verify the fetch was called with the sync token |
1505 | | - expect(mockFetch).toHaveBeenCalledWith( |
1506 | | - "https://api.us.nylas.com/v3/connect/token", |
1507 | | - expect.objectContaining({ |
1508 | | - body: JSON.stringify({ |
1509 | | - client_id: clientId, |
1510 | | - redirect_uri: redirectUri, |
1511 | | - code: "auth_code_1", |
1512 | | - grant_type: "authorization_code", |
1513 | | - code_verifier: "verifier123", |
1514 | | - idp_claims: mockIdpToken, |
1515 | | - }), |
| 1546 | + const lastCallSync = mockFetch.mock.calls.at(-1); |
| 1547 | + expect(lastCallSync[0]).toBe("https://api.us.nylas.com/v3/connect/token"); |
| 1548 | + expect(lastCallSync[1].headers["x-nylas-connect"]).toBe(pkg.version); |
| 1549 | + expect(lastCallSync[1].body).toBe( |
| 1550 | + JSON.stringify({ |
| 1551 | + client_id: clientId, |
| 1552 | + redirect_uri: redirectUri, |
| 1553 | + code: "auth_code_1", |
| 1554 | + grant_type: "authorization_code", |
| 1555 | + code_verifier: "verifier123", |
| 1556 | + idp_claims: mockIdpToken, |
1516 | 1557 | }), |
1517 | 1558 | ); |
1518 | 1559 | }); |
|
0 commit comments