codex.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. parseJwtClaims,
  4. extractAccountIdFromClaims,
  5. extractAccountId,
  6. type IdTokenClaims,
  7. } from "../../src/plugin/codex"
  8. function createTestJwt(payload: object): string {
  9. const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")
  10. const body = Buffer.from(JSON.stringify(payload)).toString("base64url")
  11. return `${header}.${body}.sig`
  12. }
  13. describe("plugin.codex", () => {
  14. describe("parseJwtClaims", () => {
  15. test("parses valid JWT with claims", () => {
  16. const payload = { email: "[email protected]", chatgpt_account_id: "acc-123" }
  17. const jwt = createTestJwt(payload)
  18. const claims = parseJwtClaims(jwt)
  19. expect(claims).toEqual(payload)
  20. })
  21. test("returns undefined for JWT with less than 3 parts", () => {
  22. expect(parseJwtClaims("invalid")).toBeUndefined()
  23. expect(parseJwtClaims("only.two")).toBeUndefined()
  24. })
  25. test("returns undefined for invalid base64", () => {
  26. expect(parseJwtClaims("a.!!!invalid!!!.b")).toBeUndefined()
  27. })
  28. test("returns undefined for invalid JSON payload", () => {
  29. const header = Buffer.from("{}").toString("base64url")
  30. const invalidJson = Buffer.from("not json").toString("base64url")
  31. expect(parseJwtClaims(`${header}.${invalidJson}.sig`)).toBeUndefined()
  32. })
  33. })
  34. describe("extractAccountIdFromClaims", () => {
  35. test("extracts chatgpt_account_id from root", () => {
  36. const claims: IdTokenClaims = { chatgpt_account_id: "acc-root" }
  37. expect(extractAccountIdFromClaims(claims)).toBe("acc-root")
  38. })
  39. test("extracts chatgpt_account_id from nested https://api.openai.com/auth", () => {
  40. const claims: IdTokenClaims = {
  41. "https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" },
  42. }
  43. expect(extractAccountIdFromClaims(claims)).toBe("acc-nested")
  44. })
  45. test("prefers root over nested", () => {
  46. const claims: IdTokenClaims = {
  47. chatgpt_account_id: "acc-root",
  48. "https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" },
  49. }
  50. expect(extractAccountIdFromClaims(claims)).toBe("acc-root")
  51. })
  52. test("extracts from organizations array as fallback", () => {
  53. const claims: IdTokenClaims = {
  54. organizations: [{ id: "org-123" }, { id: "org-456" }],
  55. }
  56. expect(extractAccountIdFromClaims(claims)).toBe("org-123")
  57. })
  58. test("returns undefined when no accountId found", () => {
  59. const claims: IdTokenClaims = { email: "[email protected]" }
  60. expect(extractAccountIdFromClaims(claims)).toBeUndefined()
  61. })
  62. })
  63. describe("extractAccountId", () => {
  64. test("extracts from id_token first", () => {
  65. const idToken = createTestJwt({ chatgpt_account_id: "from-id-token" })
  66. const accessToken = createTestJwt({ chatgpt_account_id: "from-access-token" })
  67. expect(
  68. extractAccountId({
  69. id_token: idToken,
  70. access_token: accessToken,
  71. refresh_token: "rt",
  72. }),
  73. ).toBe("from-id-token")
  74. })
  75. test("falls back to access_token when id_token has no accountId", () => {
  76. const idToken = createTestJwt({ email: "[email protected]" })
  77. const accessToken = createTestJwt({
  78. "https://api.openai.com/auth": { chatgpt_account_id: "from-access" },
  79. })
  80. expect(
  81. extractAccountId({
  82. id_token: idToken,
  83. access_token: accessToken,
  84. refresh_token: "rt",
  85. }),
  86. ).toBe("from-access")
  87. })
  88. test("returns undefined when no tokens have accountId", () => {
  89. const token = createTestJwt({ email: "[email protected]" })
  90. expect(
  91. extractAccountId({
  92. id_token: token,
  93. access_token: token,
  94. refresh_token: "rt",
  95. }),
  96. ).toBeUndefined()
  97. })
  98. test("handles missing id_token", () => {
  99. const accessToken = createTestJwt({ chatgpt_account_id: "acc-123" })
  100. expect(
  101. extractAccountId({
  102. id_token: "",
  103. access_token: accessToken,
  104. refresh_token: "rt",
  105. }),
  106. ).toBe("acc-123")
  107. })
  108. })
  109. })