|
| 1 | +import { describe, expect, test } from "vitest" |
| 2 | +import { parseOriginError } from "../src/exception" |
| 3 | +import { mapToChannelError } from "../src/lib/error-mapper" |
| 4 | + |
| 5 | +/** |
| 6 | + * The Graph body Meta returns when a Page is temporarily restricted from |
| 7 | + * messaging a thread. Every piece of it has to survive into `ErrorLog.detail`: |
| 8 | + * code 10 alone covers a dozen unrelated permission failures, and the subcode |
| 9 | + * is the only thing that names this one. |
| 10 | + */ |
| 11 | +const RESTRICTED_THREAD_ERROR = { |
| 12 | + message: "Application does not have permission for this action", |
| 13 | + type: "OAuthException", |
| 14 | + code: 10, |
| 15 | + error_subcode: 1_893_063, |
| 16 | + error_user_title: "Bạn không thể gửi tin nhắn đến cuộc trò chuyện này", |
| 17 | + error_user_msg: |
| 18 | + "Bạn tạm thời bị hạn chế gửi tin nhắn. Tìm hiểu thêm về thời gian và lý do chúng tôi hạn chế khả năng nhắn tin.", |
| 19 | + fbtrace_id: "Ae1wFHiG1XvSn1TtWwjWxPZ", |
| 20 | +} |
| 21 | + |
| 22 | +const EXPECTED_DETAIL = |
| 23 | + "#(10 - 1893063) Application does not have permission for this action. Bạn tạm thời bị hạn chế gửi tin nhắn. Tìm hiểu thêm về thời gian và lý do chúng tôi hạn chế khả năng nhắn tin." |
| 24 | + |
| 25 | +describe("parseOriginError message composition", () => { |
| 26 | + test("keeps the code pair and both sentences of a restricted-thread failure", () => { |
| 27 | + expect( |
| 28 | + parseOriginError({ |
| 29 | + httpStatus: 403, |
| 30 | + errorBody: { error: RESTRICTED_THREAD_ERROR }, |
| 31 | + }), |
| 32 | + ).toMatchObject({ |
| 33 | + httpStatusCode: 403, |
| 34 | + code: 10, |
| 35 | + subCode: 1_893_063, |
| 36 | + type: "OAuthException", |
| 37 | + message: EXPECTED_DETAIL, |
| 38 | + }) |
| 39 | + }) |
| 40 | + |
| 41 | + test("composes the same string from the explicit response shape", () => { |
| 42 | + expect( |
| 43 | + parseOriginError({ response: { error: RESTRICTED_THREAD_ERROR } }), |
| 44 | + ).toMatchObject({ message: EXPECTED_DETAIL }) |
| 45 | + }) |
| 46 | + |
| 47 | + test("falls back to the user title when Meta sent no user message", () => { |
| 48 | + expect( |
| 49 | + parseOriginError({ |
| 50 | + httpStatus: 400, |
| 51 | + errorBody: { |
| 52 | + error: { |
| 53 | + message: "Invalid parameter", |
| 54 | + code: 100, |
| 55 | + error_subcode: 2_018_001, |
| 56 | + error_user_title: "Message not sent", |
| 57 | + }, |
| 58 | + }, |
| 59 | + }), |
| 60 | + ).toMatchObject({ |
| 61 | + message: "#(100 - 2018001) Invalid parameter. Message not sent", |
| 62 | + }) |
| 63 | + }) |
| 64 | + |
| 65 | + test("adds no prefix to a failure that never reached Graph", () => { |
| 66 | + expect(parseOriginError(new Error("socket hang up"))).toMatchObject({ |
| 67 | + httpStatusCode: 400, |
| 68 | + message: "socket hang up", |
| 69 | + }) |
| 70 | + }) |
| 71 | +}) |
| 72 | + |
| 73 | +describe("the string that reaches ErrorLog.detail", () => { |
| 74 | + /** |
| 75 | + * `toEntry` in `@chatbotx.io/business` writes `error.message` verbatim into |
| 76 | + * `ErrorLog.detail`, and `parseSdkError` is what hands it over. Nothing |
| 77 | + * between here and the row re-derives the message, so pinning it at |
| 78 | + * `getErrorData()` pins the column. |
| 79 | + */ |
| 80 | + test("survives mapToChannelError and getErrorData unchanged", async () => { |
| 81 | + const channelError = mapToChannelError({ |
| 82 | + httpStatus: 403, |
| 83 | + errorBody: { error: RESTRICTED_THREAD_ERROR }, |
| 84 | + }) |
| 85 | + |
| 86 | + expect(channelError.message).toBe(EXPECTED_DETAIL) |
| 87 | + |
| 88 | + const errorData = await channelError.getErrorData() |
| 89 | + expect(errorData).toMatchObject({ |
| 90 | + message: EXPECTED_DETAIL, |
| 91 | + code: 10, |
| 92 | + subcode: 1_893_063, |
| 93 | + statusCode: 403, |
| 94 | + // Terminal, so `recordProviderErrorLog` writes the row on this emission. |
| 95 | + isRetryable: false, |
| 96 | + }) |
| 97 | + }) |
| 98 | +}) |
0 commit comments