Skip to content

Commit 5e1caaf

Browse files
authored
Fix/evault idempotency (#422)
* chore: fix the issue with evault core sending back the same w3id * tests: add unit tests for evault eName idempotency * fix: axios args thing
1 parent 9d2c182 commit 5e1caaf

File tree

2 files changed

+276
-2
lines changed

2 files changed

+276
-2
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from "vitest";
2+
import axios from "axios";
3+
import {
4+
setupE2ETestServer,
5+
teardownE2ETestServer,
6+
provisionTestEVault,
7+
makeGraphQLRequest,
8+
type E2ETestServer,
9+
type ProvisionedEVault,
10+
} from "../../test-utils/e2e-setup";
11+
12+
// Store original axios functions before any spying happens
13+
const originalAxiosGet = axios.get;
14+
const originalAxiosPost = axios.post;
15+
16+
describe("GraphQLServer Webhook Payload W3ID", () => {
17+
let server: E2ETestServer;
18+
let evault1: ProvisionedEVault;
19+
let evault2: ProvisionedEVault;
20+
const evaultW3ID = "evault-w3id-123";
21+
let axiosGetSpy: any;
22+
let axiosPostSpy: any;
23+
24+
beforeAll(async () => {
25+
server = await setupE2ETestServer();
26+
evault1 = await provisionTestEVault(server);
27+
evault2 = await provisionTestEVault(server);
28+
}, 120000);
29+
30+
afterAll(async () => {
31+
await teardownE2ETestServer(server);
32+
// Restore original implementations
33+
if (axiosGetSpy) {
34+
axiosGetSpy.mockRestore();
35+
}
36+
if (axiosPostSpy) {
37+
axiosPostSpy.mockRestore();
38+
}
39+
});
40+
41+
beforeEach(() => {
42+
// Restore any existing spies first
43+
if (axiosGetSpy) {
44+
axiosGetSpy.mockRestore();
45+
}
46+
if (axiosPostSpy) {
47+
axiosPostSpy.mockRestore();
48+
}
49+
50+
vi.clearAllMocks();
51+
52+
// Mock axios.get for platforms endpoint only
53+
axiosGetSpy = vi.spyOn(axios, "get").mockImplementation((...args: any[]) => {
54+
const url = args[0];
55+
if (typeof url === "string" && url.includes("/platforms")) {
56+
return Promise.resolve({
57+
data: ["http://localhost:9999"], // Mock platform URL
58+
}) as any;
59+
}
60+
// For other GET requests, call through to original with all arguments preserved
61+
return (originalAxiosGet as any).apply(axios, args);
62+
});
63+
64+
// Spy on axios.post to capture webhook payloads
65+
axiosPostSpy = vi.spyOn(axios, "post").mockImplementation((url: string | any, data?: any, config?: any) => {
66+
// If it's a webhook call, capture it and return success
67+
// Note: axios.post(url, data, config) - data is the second parameter
68+
if (typeof url === "string" && url.includes("/api/webhook")) {
69+
// Log for debugging
70+
console.log("Webhook intercepted:", { url, data });
71+
return Promise.resolve({ status: 200, data: {} }) as any;
72+
}
73+
// For GraphQL and other requests, call through to original (stored before spying)
74+
return originalAxiosPost.call(axios, url, data, config);
75+
});
76+
});
77+
78+
describe("storeMetaEnvelope webhook payload", () => {
79+
it("should include X-ENAME in webhook payload", async () => {
80+
const testData = { field: "value", test: "store-test" };
81+
const testOntology = "WebhookTestOntology";
82+
83+
// Make GraphQL mutation with user's W3ID in X-ENAME header
84+
const mutation = `
85+
mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) {
86+
storeMetaEnvelope(input: $input) {
87+
metaEnvelope {
88+
id
89+
ontology
90+
}
91+
}
92+
}
93+
`;
94+
95+
await makeGraphQLRequest(server, mutation, {
96+
input: {
97+
ontology: testOntology,
98+
payload: testData,
99+
acl: ["*"],
100+
},
101+
}, {
102+
"X-ENAME": evault1.w3id,
103+
});
104+
105+
// Wait for the setTimeout delay (3 seconds) in the actual code
106+
await new Promise(resolve => setTimeout(resolve, 3500));
107+
108+
// Verify axios.post was called (webhook delivery)
109+
expect(axios.post).toHaveBeenCalled();
110+
111+
// Get the webhook payload from the axios.post call
112+
const webhookCalls = (axios.post as any).mock.calls;
113+
const webhookCall = webhookCalls.find((call: any[]) =>
114+
typeof call[0] === "string" && call[0].includes("/api/webhook")
115+
);
116+
117+
expect(webhookCall).toBeDefined();
118+
const webhookPayload = webhookCall[1]; // Second argument is the payload
119+
120+
console.log("Webhook payload:", JSON.stringify(webhookPayload, null, 2));
121+
console.log("Expected w3id:", evault1.w3id);
122+
123+
// Verify the webhook payload contains the user's W3ID, not the eVault's W3ID
124+
expect(webhookPayload).toBeDefined();
125+
expect(webhookPayload.w3id).toBe(evault1.w3id);
126+
expect(webhookPayload.w3id).not.toBe(evaultW3ID);
127+
expect(webhookPayload.data).toEqual(testData);
128+
expect(webhookPayload.schemaId).toBe(testOntology);
129+
});
130+
131+
it("should use different W3IDs for different users in webhook payloads", async () => {
132+
const testData1 = { user: "1", data: "test1" };
133+
const testData2 = { user: "2", data: "test2" };
134+
const testOntology = "MultiUserWebhookTest";
135+
136+
const mutation = `
137+
mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) {
138+
storeMetaEnvelope(input: $input) {
139+
metaEnvelope {
140+
id
141+
ontology
142+
}
143+
}
144+
}
145+
`;
146+
147+
// Store for user1
148+
await makeGraphQLRequest(server, mutation, {
149+
input: {
150+
ontology: testOntology,
151+
payload: testData1,
152+
acl: ["*"],
153+
},
154+
}, {
155+
"X-ENAME": evault1.w3id,
156+
});
157+
158+
// Store for user2
159+
await makeGraphQLRequest(server, mutation, {
160+
input: {
161+
ontology: testOntology,
162+
payload: testData2,
163+
acl: ["*"],
164+
},
165+
}, {
166+
"X-ENAME": evault2.w3id,
167+
});
168+
169+
// Wait for setTimeout delays
170+
await new Promise(resolve => setTimeout(resolve, 3500));
171+
172+
// Get all webhook calls
173+
const webhookCalls = (axios.post as any).mock.calls.filter((call: any[]) =>
174+
typeof call[0] === "string" && call[0].includes("/api/webhook")
175+
);
176+
177+
expect(webhookCalls.length).toBeGreaterThanOrEqual(2);
178+
179+
// Find payloads by their data
180+
const payload1 = webhookCalls.find((call: any[]) =>
181+
call[1]?.data?.user === "1"
182+
)?.[1];
183+
const payload2 = webhookCalls.find((call: any[]) =>
184+
call[1]?.data?.user === "2"
185+
)?.[1];
186+
187+
expect(payload1).toBeDefined();
188+
expect(payload1.w3id).toBe(evault1.w3id);
189+
expect(payload2).toBeDefined();
190+
expect(payload2.w3id).toBe(evault2.w3id);
191+
expect(payload1.w3id).not.toBe(payload2.w3id);
192+
});
193+
});
194+
195+
describe("updateMetaEnvelopeById webhook payload", () => {
196+
it("should include user's W3ID (eName) in webhook payload, not eVault's W3ID", async () => {
197+
const testData = { field: "updated-value", test: "update-test" };
198+
const testOntology = "UpdateWebhookTestOntology";
199+
200+
// First, create an envelope
201+
const createMutation = `
202+
mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) {
203+
storeMetaEnvelope(input: $input) {
204+
metaEnvelope {
205+
id
206+
ontology
207+
}
208+
}
209+
}
210+
`;
211+
212+
const createResult = await makeGraphQLRequest(server, createMutation, {
213+
input: {
214+
ontology: testOntology,
215+
payload: { field: "initial-value" },
216+
acl: ["*"],
217+
},
218+
}, {
219+
"X-ENAME": evault1.w3id,
220+
});
221+
222+
const envelopeId = createResult.storeMetaEnvelope.metaEnvelope.id;
223+
224+
// Clear previous webhook calls
225+
(axios.post as any).mockClear();
226+
227+
// Now update the envelope
228+
const updateMutation = `
229+
mutation UpdateMetaEnvelopeById($id: String!, $input: MetaEnvelopeInput!) {
230+
updateMetaEnvelopeById(id: $id, input: $input) {
231+
metaEnvelope {
232+
id
233+
ontology
234+
}
235+
}
236+
}
237+
`;
238+
239+
await makeGraphQLRequest(server, updateMutation, {
240+
id: envelopeId,
241+
input: {
242+
ontology: testOntology,
243+
payload: testData,
244+
acl: ["*"],
245+
},
246+
}, {
247+
"X-ENAME": evault1.w3id,
248+
});
249+
250+
// Wait a bit for webhook delivery (update doesn't have setTimeout delay)
251+
await new Promise(resolve => setTimeout(resolve, 500));
252+
253+
// Verify axios.post was called (webhook delivery)
254+
expect(axios.post).toHaveBeenCalled();
255+
256+
// Get the webhook payload
257+
const webhookCalls = (axios.post as any).mock.calls.filter((call: any[]) =>
258+
typeof call[0] === "string" && call[0].includes("/api/webhook")
259+
);
260+
261+
expect(webhookCalls.length).toBeGreaterThan(0);
262+
const webhookPayload = webhookCalls[0][1];
263+
264+
// Verify the webhook payload contains the user's W3ID, not the eVault's W3ID
265+
expect(webhookPayload).toBeDefined();
266+
expect(webhookPayload.w3id).toBe(evault1.w3id);
267+
expect(webhookPayload.w3id).not.toBe(evaultW3ID);
268+
expect(webhookPayload.id).toBe(envelopeId);
269+
expect(webhookPayload.data).toEqual(testData);
270+
expect(webhookPayload.schemaId).toBe(testOntology);
271+
});
272+
});
273+
});
274+

infrastructure/evault-core/src/core/protocol/graphql-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class GraphQLServer {
207207
context.tokenPayload?.platform || null;
208208
const webhookPayload = {
209209
id: result.metaEnvelope.id,
210-
w3id: this.getCurrentEvaultW3ID(),
210+
w3id: context.eName,
211211
evaultPublicKey: this.evaultPublicKey,
212212
data: input.payload,
213213
schemaId: input.ontology,
@@ -272,7 +272,7 @@ export class GraphQLServer {
272272
context.tokenPayload?.platform || null;
273273
const webhookPayload = {
274274
id: id,
275-
w3id: this.getCurrentEvaultW3ID(),
275+
w3id: context.eName,
276276
evaultPublicKey: this.evaultPublicKey,
277277
data: input.payload,
278278
schemaId: input.ontology,

0 commit comments

Comments
 (0)