datocms-plugin-record-bin 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -11
- package/build/assets/index-BnrW9Ts8.js +15 -0
- package/build/assets/index-aWCW2c0n.css +1 -0
- package/build/index.html +13 -1
- package/index.html +12 -0
- package/package.json +24 -18
- package/src/entrypoints/BinOutlet.tsx +262 -37
- package/src/entrypoints/ConfigScreen.tsx +939 -38
- package/src/entrypoints/ErrorModal.tsx +86 -2
- package/src/index.tsx +73 -28
- package/src/react-app-env.d.ts +1 -1
- package/src/types/types.ts +36 -8
- package/src/utils/binCleanup.test.ts +107 -0
- package/src/utils/binCleanup.ts +71 -23
- package/src/utils/debugLogger.ts +27 -0
- package/src/utils/deployProviders.test.ts +33 -0
- package/src/utils/deployProviders.ts +28 -0
- package/src/utils/getDeploymentUrlFromParameters.test.ts +26 -0
- package/src/utils/getDeploymentUrlFromParameters.ts +21 -0
- package/src/utils/getRuntimeMode.test.ts +57 -0
- package/src/utils/getRuntimeMode.ts +23 -0
- package/src/utils/lambdaLessCapture.test.ts +218 -0
- package/src/utils/lambdaLessCapture.ts +160 -0
- package/src/utils/lambdaLessCleanup.test.ts +125 -0
- package/src/utils/lambdaLessCleanup.ts +69 -0
- package/src/utils/lambdaLessRestore.test.ts +248 -0
- package/src/utils/lambdaLessRestore.ts +159 -0
- package/src/utils/recordBinModel.ts +108 -0
- package/src/utils/recordBinPayload.test.ts +103 -0
- package/src/utils/recordBinPayload.ts +136 -0
- package/src/utils/recordBinWebhook.test.ts +253 -0
- package/src/utils/recordBinWebhook.ts +305 -0
- package/src/utils/render.tsx +17 -8
- package/src/utils/restoreError.test.ts +112 -0
- package/src/utils/restoreError.ts +221 -0
- package/src/utils/verifyLambdaHealth.test.ts +248 -0
- package/src/utils/verifyLambdaHealth.ts +422 -0
- package/vite.config.ts +11 -0
- package/build/asset-manifest.json +0 -13
- package/build/static/css/main.10f29737.css +0 -2
- package/build/static/css/main.10f29737.css.map +0 -1
- package/build/static/js/main.53795e3b.js +0 -3
- package/build/static/js/main.53795e3b.js.LICENSE.txt +0 -47
- package/build/static/js/main.53795e3b.js.map +0 -1
- package/src/entrypoints/InstallationModal.tsx +0 -107
- package/src/entrypoints/PreInstallConfig.tsx +0 -28
- package/src/utils/attemptVercelInitialization.ts +0 -16
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { errorObject } from "../types/types";
|
|
3
|
+
import {
|
|
4
|
+
buildRestoreErrorPayload,
|
|
5
|
+
isRestoreSuccessResponse,
|
|
6
|
+
parseJsonStringSafely,
|
|
7
|
+
} from "./restoreError";
|
|
8
|
+
|
|
9
|
+
describe("buildRestoreErrorPayload", () => {
|
|
10
|
+
it("returns an existing errorObject as-is", () => {
|
|
11
|
+
const existingError: errorObject = {
|
|
12
|
+
simplifiedError: {
|
|
13
|
+
code: "VALIDATION_INVALID",
|
|
14
|
+
details: {
|
|
15
|
+
code: "INVALID_FIELD",
|
|
16
|
+
field: "title",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
fullErrorPayload: "raw-payload",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
expect(buildRestoreErrorPayload(existingError)).toEqual(existingError);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("extracts simplified error from Dato errors array payload", () => {
|
|
26
|
+
const result = buildRestoreErrorPayload({
|
|
27
|
+
errors: [
|
|
28
|
+
{
|
|
29
|
+
attributes: {
|
|
30
|
+
code: "VALIDATION_INVALID",
|
|
31
|
+
details: {
|
|
32
|
+
code: "INVALID_FIELD",
|
|
33
|
+
field: "title",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.simplifiedError.code).toBe("VALIDATION_INVALID");
|
|
41
|
+
expect(result.simplifiedError.details.code).toBe("INVALID_FIELD");
|
|
42
|
+
expect(result.simplifiedError.details.field).toBe("title");
|
|
43
|
+
expect(result.fullErrorPayload).toContain("VALIDATION_INVALID");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("extracts simplified error from { error: ... } wrapper payload", () => {
|
|
47
|
+
const result = buildRestoreErrorPayload(
|
|
48
|
+
{
|
|
49
|
+
error: {
|
|
50
|
+
code: "INVALID_LINK",
|
|
51
|
+
details: {
|
|
52
|
+
code: "MISSING_RELATION",
|
|
53
|
+
message: "Missing linked record",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
fullErrorPayload: "raw-response-body",
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(result.simplifiedError.code).toBe("INVALID_LINK");
|
|
63
|
+
expect(result.simplifiedError.details.code).toBe("MISSING_RELATION");
|
|
64
|
+
expect(result.simplifiedError.details.message).toBe("Missing linked record");
|
|
65
|
+
expect(result.fullErrorPayload).toBe("raw-response-body");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("falls back to UNKNOWN when error payload is unstructured", () => {
|
|
69
|
+
const result = buildRestoreErrorPayload(
|
|
70
|
+
{ error: {} },
|
|
71
|
+
{ fallbackMessage: "Custom fallback message" }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.simplifiedError.code).toBe("UNKNOWN");
|
|
75
|
+
expect(result.simplifiedError.details.code).toBe("UNKNOWN");
|
|
76
|
+
expect(result.simplifiedError.details.message).toBe("Custom fallback message");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses plain string payload as fallback message", () => {
|
|
80
|
+
const result = buildRestoreErrorPayload("Gateway timeout");
|
|
81
|
+
|
|
82
|
+
expect(result.simplifiedError.code).toBe("UNKNOWN");
|
|
83
|
+
expect(result.simplifiedError.details.message).toBe("Gateway timeout");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("parseJsonStringSafely", () => {
|
|
88
|
+
it("returns undefined for invalid JSON", () => {
|
|
89
|
+
expect(parseJsonStringSafely("not-json")).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("parses valid JSON text", () => {
|
|
93
|
+
expect(parseJsonStringSafely('{"ok": true}')).toEqual({ ok: true });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("isRestoreSuccessResponse", () => {
|
|
98
|
+
it("returns true for valid restore success payload", () => {
|
|
99
|
+
expect(
|
|
100
|
+
isRestoreSuccessResponse({
|
|
101
|
+
restoredRecord: { id: "record-id", modelID: "model-id" },
|
|
102
|
+
})
|
|
103
|
+
).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns false when restoredRecord shape is missing", () => {
|
|
107
|
+
expect(isRestoreSuccessResponse({ restoredRecord: { id: "record-id" } })).toBe(
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
expect(isRestoreSuccessResponse({})).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { errorObject } from "../types/types";
|
|
2
|
+
|
|
3
|
+
const UNKNOWN_ERROR_CODE = "UNKNOWN";
|
|
4
|
+
const DEFAULT_FALLBACK_MESSAGE = "Could not parse restoration error payload.";
|
|
5
|
+
|
|
6
|
+
type BuildRestoreErrorPayloadOptions = {
|
|
7
|
+
fullErrorPayload?: string;
|
|
8
|
+
fallbackMessage?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
const isNonEmptyString = (value: unknown): value is string =>
|
|
15
|
+
typeof value === "string" && value.trim().length > 0;
|
|
16
|
+
|
|
17
|
+
const serializeUnknownError = (error: unknown): string => {
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
return JSON.stringify(error, Object.getOwnPropertyNames(error), 2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(error, null, 2);
|
|
24
|
+
} catch {
|
|
25
|
+
return String(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const isErrorObject = (value: unknown): value is errorObject => {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof value.fullErrorPayload !== "string") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isRecord(value.simplifiedError)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return isRecord(value.simplifiedError.details);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildUnknownSimplifiedError = (
|
|
46
|
+
error: unknown,
|
|
47
|
+
fallbackMessage: string
|
|
48
|
+
): errorObject["simplifiedError"] => {
|
|
49
|
+
const message = isNonEmptyString(error)
|
|
50
|
+
? error
|
|
51
|
+
: error instanceof Error && isNonEmptyString(error.message)
|
|
52
|
+
? error.message
|
|
53
|
+
: fallbackMessage;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
code: UNKNOWN_ERROR_CODE,
|
|
57
|
+
details: {
|
|
58
|
+
code: UNKNOWN_ERROR_CODE,
|
|
59
|
+
message,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const normalizeSimplifiedErrorRecord = (
|
|
65
|
+
candidate: Record<string, unknown>,
|
|
66
|
+
fallbackMessage: string
|
|
67
|
+
): errorObject["simplifiedError"] | undefined => {
|
|
68
|
+
const candidateDetails = isRecord(candidate.details) ? { ...candidate.details } : {};
|
|
69
|
+
const hasStructuredShape =
|
|
70
|
+
isNonEmptyString(candidate.code) || Object.keys(candidateDetails).length > 0;
|
|
71
|
+
|
|
72
|
+
if (!hasStructuredShape) {
|
|
73
|
+
if (isNonEmptyString(candidate.message)) {
|
|
74
|
+
return {
|
|
75
|
+
code: UNKNOWN_ERROR_CODE,
|
|
76
|
+
details: {
|
|
77
|
+
code: UNKNOWN_ERROR_CODE,
|
|
78
|
+
message: candidate.message,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const code = isNonEmptyString(candidate.code)
|
|
87
|
+
? candidate.code
|
|
88
|
+
: UNKNOWN_ERROR_CODE;
|
|
89
|
+
const details: Record<string, unknown> = { ...candidateDetails };
|
|
90
|
+
|
|
91
|
+
if (!isNonEmptyString(details.code)) {
|
|
92
|
+
details.code = code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isNonEmptyString(details.message) && isNonEmptyString(candidate.message)) {
|
|
96
|
+
details.message = candidate.message;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
Object.keys(details).length === 1 &&
|
|
101
|
+
details.code === UNKNOWN_ERROR_CODE &&
|
|
102
|
+
!isNonEmptyString(details.message)
|
|
103
|
+
) {
|
|
104
|
+
details.message = fallbackMessage;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...candidate,
|
|
109
|
+
code,
|
|
110
|
+
details,
|
|
111
|
+
} as errorObject["simplifiedError"];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const unwrapErrorWrapper = (error: unknown): unknown => {
|
|
115
|
+
let current = error;
|
|
116
|
+
|
|
117
|
+
while (isRecord(current) && "error" in current) {
|
|
118
|
+
const nestedError = current.error;
|
|
119
|
+
if (nestedError === undefined || nestedError === current) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
current = nestedError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return current;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const extractSimplifiedError = (
|
|
130
|
+
error: unknown,
|
|
131
|
+
fallbackMessage: string
|
|
132
|
+
): errorObject["simplifiedError"] => {
|
|
133
|
+
if (isErrorObject(error)) {
|
|
134
|
+
return error.simplifiedError;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const unwrappedError = unwrapErrorWrapper(error);
|
|
138
|
+
|
|
139
|
+
if (isErrorObject(unwrappedError)) {
|
|
140
|
+
return unwrappedError.simplifiedError;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isRecord(unwrappedError) && Array.isArray(unwrappedError.errors)) {
|
|
144
|
+
const firstError = unwrappedError.errors[0];
|
|
145
|
+
if (isRecord(firstError) && isRecord(firstError.attributes)) {
|
|
146
|
+
const normalizedAttributes = normalizeSimplifiedErrorRecord(
|
|
147
|
+
firstError.attributes,
|
|
148
|
+
fallbackMessage
|
|
149
|
+
);
|
|
150
|
+
if (normalizedAttributes) {
|
|
151
|
+
return normalizedAttributes;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isRecord(unwrappedError)) {
|
|
157
|
+
const normalizedError = normalizeSimplifiedErrorRecord(
|
|
158
|
+
unwrappedError,
|
|
159
|
+
fallbackMessage
|
|
160
|
+
);
|
|
161
|
+
if (normalizedError) {
|
|
162
|
+
return normalizedError;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return buildUnknownSimplifiedError(unwrappedError, fallbackMessage);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const parseJsonStringSafely = (
|
|
170
|
+
rawResponseText: string
|
|
171
|
+
): unknown | undefined => {
|
|
172
|
+
if (!isNonEmptyString(rawResponseText)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(rawResponseText);
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const buildRestoreErrorPayload = (
|
|
184
|
+
error: unknown,
|
|
185
|
+
options: BuildRestoreErrorPayloadOptions = {}
|
|
186
|
+
): errorObject => {
|
|
187
|
+
const { fullErrorPayload, fallbackMessage = DEFAULT_FALLBACK_MESSAGE } = options;
|
|
188
|
+
|
|
189
|
+
if (isErrorObject(error) && !isNonEmptyString(fullErrorPayload)) {
|
|
190
|
+
return error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
simplifiedError: extractSimplifiedError(error, fallbackMessage),
|
|
195
|
+
fullErrorPayload: fullErrorPayload ?? serializeUnknownError(error),
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export type RestoreSuccessResponse = {
|
|
200
|
+
restoredRecord: {
|
|
201
|
+
id: string;
|
|
202
|
+
modelID: string;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const isRestoreSuccessResponse = (
|
|
207
|
+
payload: unknown
|
|
208
|
+
): payload is RestoreSuccessResponse => {
|
|
209
|
+
if (!isRecord(payload)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!isRecord(payload.restoredRecord)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
isNonEmptyString(payload.restoredRecord.id) &&
|
|
219
|
+
isNonEmptyString(payload.restoredRecord.modelID)
|
|
220
|
+
);
|
|
221
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
LambdaHealthCheckError,
|
|
4
|
+
verifyLambdaHealth,
|
|
5
|
+
} from "./verifyLambdaHealth";
|
|
6
|
+
|
|
7
|
+
const expectRejected = async (
|
|
8
|
+
promise: Promise<unknown>
|
|
9
|
+
): Promise<LambdaHealthCheckError> => {
|
|
10
|
+
try {
|
|
11
|
+
await promise;
|
|
12
|
+
throw new Error("Expected promise to reject");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return error as LambdaHealthCheckError;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
vi.unstubAllGlobals();
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("verifyLambdaHealth", () => {
|
|
25
|
+
it("accepts a valid health handshake response", async () => {
|
|
26
|
+
const fetchMock = vi.fn(async () =>
|
|
27
|
+
new Response(
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
ok: true,
|
|
30
|
+
mpi: {
|
|
31
|
+
message: "DATOCMS_RECORD_BIN_LAMBDA_PONG",
|
|
32
|
+
version: "2026-02-25",
|
|
33
|
+
},
|
|
34
|
+
service: "record-bin-lambda-function",
|
|
35
|
+
status: "ready",
|
|
36
|
+
}),
|
|
37
|
+
{ status: 200 }
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
41
|
+
|
|
42
|
+
const result = await verifyLambdaHealth({
|
|
43
|
+
baseUrl: "https://record-bin.vercel.app/",
|
|
44
|
+
environment: "main",
|
|
45
|
+
phase: "finish_installation",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.endpoint).toBe(
|
|
49
|
+
"https://record-bin.vercel.app/api/datocms/plugin-health"
|
|
50
|
+
);
|
|
51
|
+
expect(result.normalizedBaseUrl).toBe("https://record-bin.vercel.app");
|
|
52
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
54
|
+
"https://record-bin.vercel.app/api/datocms/plugin-health",
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Accept: "*/*",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const requestInit = fetchMock.mock.calls[0][1] as RequestInit;
|
|
65
|
+
const payload = JSON.parse(requestInit.body as string) as {
|
|
66
|
+
mpi: { phase: string };
|
|
67
|
+
};
|
|
68
|
+
expect(payload.mpi.phase).toBe("finish_installation");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("accepts a Netlify deployment URL", async () => {
|
|
72
|
+
const fetchMock = vi.fn(async () =>
|
|
73
|
+
new Response(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
ok: true,
|
|
76
|
+
mpi: {
|
|
77
|
+
message: "DATOCMS_RECORD_BIN_LAMBDA_PONG",
|
|
78
|
+
version: "2026-02-25",
|
|
79
|
+
},
|
|
80
|
+
service: "record-bin-lambda-function",
|
|
81
|
+
status: "ready",
|
|
82
|
+
}),
|
|
83
|
+
{ status: 200 }
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
87
|
+
|
|
88
|
+
const result = await verifyLambdaHealth({
|
|
89
|
+
baseUrl: "https://record-bin.netlify.app/",
|
|
90
|
+
environment: "main",
|
|
91
|
+
phase: "finish_installation",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.endpoint).toBe(
|
|
95
|
+
"https://record-bin.netlify.app/api/datocms/plugin-health"
|
|
96
|
+
);
|
|
97
|
+
expect(result.normalizedBaseUrl).toBe("https://record-bin.netlify.app");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("accepts a hostname-only URL by prepending https", async () => {
|
|
101
|
+
const fetchMock = vi.fn(async () =>
|
|
102
|
+
new Response(
|
|
103
|
+
JSON.stringify({
|
|
104
|
+
ok: true,
|
|
105
|
+
mpi: {
|
|
106
|
+
message: "DATOCMS_RECORD_BIN_LAMBDA_PONG",
|
|
107
|
+
version: "2026-02-25",
|
|
108
|
+
},
|
|
109
|
+
service: "record-bin-lambda-function",
|
|
110
|
+
status: "ready",
|
|
111
|
+
}),
|
|
112
|
+
{ status: 200 }
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
116
|
+
|
|
117
|
+
const result = await verifyLambdaHealth({
|
|
118
|
+
baseUrl: "melodious-chebakia-9da33e.netlify.app",
|
|
119
|
+
environment: "main",
|
|
120
|
+
phase: "finish_installation",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.endpoint).toBe(
|
|
124
|
+
"https://melodious-chebakia-9da33e.netlify.app/api/datocms/plugin-health"
|
|
125
|
+
);
|
|
126
|
+
expect(result.normalizedBaseUrl).toBe(
|
|
127
|
+
"https://melodious-chebakia-9da33e.netlify.app"
|
|
128
|
+
);
|
|
129
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
130
|
+
"https://melodious-chebakia-9da33e.netlify.app/api/datocms/plugin-health",
|
|
131
|
+
expect.any(Object)
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("fails when URL is invalid", async () => {
|
|
136
|
+
const error = await expectRejected(
|
|
137
|
+
verifyLambdaHealth({
|
|
138
|
+
baseUrl: "not-a-valid-url",
|
|
139
|
+
environment: "main",
|
|
140
|
+
phase: "finish_installation",
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(error).toBeInstanceOf(LambdaHealthCheckError);
|
|
145
|
+
expect(error.code).toBe("INVALID_URL");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("fails when request times out", async () => {
|
|
149
|
+
vi.useFakeTimers();
|
|
150
|
+
const fetchMock = vi.fn(
|
|
151
|
+
(_input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
|
|
152
|
+
new Promise((_resolve, reject) => {
|
|
153
|
+
init?.signal?.addEventListener("abort", () => {
|
|
154
|
+
const abortError = new Error("aborted");
|
|
155
|
+
abortError.name = "AbortError";
|
|
156
|
+
reject(abortError);
|
|
157
|
+
});
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
161
|
+
|
|
162
|
+
const promise = expectRejected(
|
|
163
|
+
verifyLambdaHealth({
|
|
164
|
+
baseUrl: "https://record-bin.vercel.app",
|
|
165
|
+
environment: "main",
|
|
166
|
+
phase: "config_mount",
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await vi.advanceTimersByTimeAsync(8000);
|
|
171
|
+
const error = await promise;
|
|
172
|
+
|
|
173
|
+
expect(error.code).toBe("TIMEOUT");
|
|
174
|
+
expect(error.message).toContain("timed out");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("fails when endpoint returns non-200 status", async () => {
|
|
178
|
+
const fetchMock = vi.fn(async () =>
|
|
179
|
+
new Response(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
code: "INVALID_MPI_MESSAGE",
|
|
184
|
+
message: "Expected DATOCMS_RECORD_BIN_PLUGIN_PING",
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
{ status: 400 }
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
191
|
+
|
|
192
|
+
const error = await expectRejected(
|
|
193
|
+
verifyLambdaHealth({
|
|
194
|
+
baseUrl: "https://record-bin.vercel.app",
|
|
195
|
+
environment: "main",
|
|
196
|
+
phase: "config_mount",
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(error.code).toBe("HTTP");
|
|
201
|
+
expect(error.httpStatus).toBe(400);
|
|
202
|
+
expect(error.message).toContain("INVALID_MPI_MESSAGE");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("fails when endpoint returns invalid JSON", async () => {
|
|
206
|
+
const fetchMock = vi.fn(async () => new Response("not-json", { status: 200 }));
|
|
207
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
208
|
+
|
|
209
|
+
const error = await expectRejected(
|
|
210
|
+
verifyLambdaHealth({
|
|
211
|
+
baseUrl: "https://record-bin.vercel.app",
|
|
212
|
+
environment: "main",
|
|
213
|
+
phase: "config_mount",
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(error.code).toBe("INVALID_JSON");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("fails when JSON does not match expected MPI response", async () => {
|
|
221
|
+
const fetchMock = vi.fn(async () =>
|
|
222
|
+
new Response(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
ok: true,
|
|
225
|
+
mpi: {
|
|
226
|
+
message: "WRONG_MESSAGE",
|
|
227
|
+
version: "2026-02-25",
|
|
228
|
+
},
|
|
229
|
+
service: "record-bin-lambda-function",
|
|
230
|
+
status: "ready",
|
|
231
|
+
}),
|
|
232
|
+
{ status: 200 }
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
236
|
+
|
|
237
|
+
const error = await expectRejected(
|
|
238
|
+
verifyLambdaHealth({
|
|
239
|
+
baseUrl: "https://record-bin.vercel.app",
|
|
240
|
+
environment: "main",
|
|
241
|
+
phase: "config_mount",
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(error.code).toBe("UNEXPECTED_RESPONSE");
|
|
246
|
+
expect(error.message).toContain("MPI PONG");
|
|
247
|
+
});
|
|
248
|
+
});
|