Setting the file. One moment.
Donations Test · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 46.10
Post Detail
references/donations/tests/ donations.test.mjs
JavaScript · 258 lines · 15 KB
{ pathToFileURL }
from
'node:url'
;
9 import { runInNewContext } from 'node:vm' ;
10 import { createHash } from 'node:crypto' ;
11
12 const transportSource = await readFile ( new URL ( '../../shared/app/rest/wix-client.js' , import . meta .url), 'utf8' );
13 const donationSource = await readFile ( new URL ( '../app/rest/wix-donations.js' , import . meta .url), 'utf8' );
14 const legacyTransportSource = transportSource
15 . replace ( 'export const WIX_API_SUPPORTS_STRICT_PAYMENT_ERRORS = true; \n ' , '' )
16 . replace ( ', throwOnPaymentRequired?: boolean' , '' )
17 . replace ( ', throwOnPaymentRequired = false' , '' )
18 . replace ( 'res.status === 402 && !throwOnPaymentRequired' , 'res.status === 402' );
19
20 const moduleUrl = source => `data:text/javascript;base64,${ Buffer . from ( source ). toString ( 'base64' ) }` ;
21 function helperWithTransport ( source ) {
22 const standalone = source
23 . replace ( 'export { WIX_CLIENT_ID } from "./wix-config.js";' , '' )
24 . replace ( 'import { WIX_CLIENT_ID } from "./wix-config.js";' , 'const WIX_CLIENT_ID = "public-test-client";' );
25 return import ( moduleUrl (donationSource. replace ( "'./wix-client.js'" , JSON . stringify ( moduleUrl (standalone)))));
26 }
27
28 async function deployFixture ( existingTransport , verticals = [ 'donations' ]) {
29 const script = await readFile ( new URL ( '../../../install/deploy.cjs' , import . meta .url), 'utf8' );
30 const ref = '/app/.agents/skills/wix-vibe-headless/references' ;
31 const files = new Map ([
32 [ `${ ref }/shared/app/rest/wix-client.js` , transportSource],
33 [ `${ ref }/shared/app/rest/wix-config.js` , 'export const WIX_CLIENT_ID = "<YOUR-CLIENT-ID>"; \n export const WIX_METASITE_ID = "<YOUR-SITE-ID>";' ],
34 [ `${ ref }/donations/app/rest/wix-donations.js` , donationSource],
35 [ `${ ref }/cms/app/rest/wix-cms.js` , 'export const fixture = true;' ],
36 ]);
37 if (existingTransport !== undefined ) files. set ( '/app/src/rest/wix-client.js' , existingTransport);
38 const copies = [];
39 const writes = [];
40 let result;
41 const fs = {
42 existsSync : path => files. has (path) || [ ... files. keys ()]. some ( file => file. startsWith ( `${ path }/` )),
43 cpSync : ( from , to , options = {}) => {
44 copies. push ({ from, to, options });
45 const entries = [ ... files]. filter (([ path ]) => path === from || path. startsWith ( `${ from }/` ));
46 assert. ok (entries. length , `Missing copy source: ${ from }` );
47 for ( const [ path , source ] of entries) {
48 const destination = `${ to }${ path . slice ( from . length ) }` ;
49 if ( ! files. has (destination) || options.force !== false ) files. set (destination, source);
50 }
51 },
52 mkdirSync : () => {},
53 writeFileSync : ( path , source ) => { writes. push (path); files. set (path, source); },
54 readFileSync : path => { assert. ok (files. has (path), `Missing fixture file: ${ path }` ); return files. get (path); },
55 };
56 const process = { argv: [ 'node' , 'deploy.cjs' , ... verticals, '--client-id' , 'test-client' , '--metasite-id' , 'test-site' ], exitCode: 0 };
57 runInNewContext (script, {
58 require : name => {
59 if (name === 'fs' ) return fs;
60 assert. equal (name, 'crypto' );
61 return { createHash };
62 },
63 process,
64 console: { log : value => { result = JSON . parse (value); } },
65 });
66 return { result, files, copies, writes, exitCode: process.exitCode };
67 }
68
69 const fixture = await mkdtemp ( join ( tmpdir (), 'wix-donation-contract-' ));
70 await cp ( new URL ( '../../shared/app/' , import . meta .url), fixture, { recursive: true });
71 await cp ( new URL ( '../app/' , import . meta .url), fixture, { recursive: true });
72 await writeFile ( join (fixture, 'package.json' ), '{"type":"module"}' );
73 await writeFile ( join (fixture, 'rest/wix-config.js' ), 'export const WIX_CLIENT_ID = "public-test-client";' );
74 const api = await import ( pathToFileURL ( join (fixture, 'rest/wix-donations.js' )));
75 const transport = await import ( pathToFileURL ( join (fixture, 'rest/wix-client.js' )));
76 const originalFetch = globalThis.fetch;
77 const originalWindow = globalThis.window;
78 const storage = new Map ();
79 globalThis.window = { location: { origin: 'https://donor.example.test' }, localStorage: {
80 getItem : key => storage. get (key) ?? null , setItem : ( key , value ) => storage. set (key, value), removeItem : key => storage. delete (key),
81 } };
82 after ( async () => { globalThis.fetch = originalFetch; globalThis.window = originalWindow; await rm (fixture, { recursive: true , force: true }); });
83
84 const campaign = {
85 id: 'campaign-id' , status: 'COLLECTING' , donationFrequencies: [ 'ONE_TIME' , 'MONTH' ],
86 predefinedDonationAmounts: [{ id: 'preset-id' , price: { amount: '25.00' } }],
87 customAmountEnabled: true , customAmountOptions: { minimum: { amount: '5.00' }, maximum: { amount: '1000' } },
88 askDonorCoverFee: true , commentsEnabled: true ,
89 };
90 const json = ( body , status = 200 ) => new Response ( JSON . stringify (body), { status, headers: { 'Content-Type' : 'application/json' } });
91 function mockRequests ( override = () => undefined ) {
92 const calls = [];
93 globalThis. fetch = async ( url , options ) => {
94 const path = new URL (url).pathname;
95 const body = options.body ? JSON . parse (options.body) : undefined ;
96 calls. push ({ path, body, options });
97 const overridden = override (path, body);
98 if (overridden) return overridden;
99 if (path === '/oauth2/token' ) return json ({ access_token: 'visitor-test-token' , refresh_token: 'test-refresh' , expires_in: 3600 });
100 if (path. endsWith ( '/campaign-id' )) return json ({ donationCampaign: campaign });
101 if (path === '/ecom/v2/carts' ) return json ({ cart: { id: 'cart-id' } });
102 if (path === '/_api/redirects-api/v1/redirect-session' ) return json ({ redirectSession: { fullUrl: 'https://checkout.example.test/session' } });
103 throw new Error ( `Unexpected test request: ${ path }` );
104 };
105 return calls;
106 }
107
108 test ( 'one-time flow uses one visitor, one standalone cart and one redirect; no V1 checkout' , async () => {
109 const calls = mockRequests ();
110 assert. equal ( await api. donationCheckoutUrl ( 'campaign-id' , { amount: '25' , frequency: 'ONE_TIME' }), 'https://checkout.example.test/session' );
111 const businessCalls = calls. filter ( call => call.path !== '/oauth2/token' );
112 assert. deepEqual (businessCalls. map ( call => call.path), [
113 '/donation-campaigns/v2/donation-campaigns/campaign-id' , '/ecom/v2/carts' , '/_api/redirects-api/v1/redirect-session' ,
114 ]);
115 assert. ok (businessCalls. every ( call => call.options.headers.Authorization === 'visitor-test-token' ));
116 assert. deepEqual (businessCalls[ 1 ].body.catalogItems[ 0 ].catalogReference.options, { amount: 25 });
117 assert. deepEqual (businessCalls[ 2 ].body, { ecomCheckout: { checkoutId: 'cart-id' }, callbacks: {
118 postFlowUrl: 'https://donor.example.test/donate/campaign-id' , thankYouPageUrl: 'https://donor.example.test/donate/thank-you' ,
119 } });
120 });
121
122 test ( 'recurrence, fees and note preserve donor choices without double-charging the fee' , () => {
123 const body = api. donationCartBody (campaign, { amount: '37.50' , frequency: 'MONTH' , coverFee: true , note: ' A message ' });
124 assert. deepEqual (body.catalogItems[ 0 ].catalogReference.options, { amount: 37.5 , frequency: 'MONTH' , donorCoveringFees: true });
125 assert. equal (body.cart.note, 'A message' );
126 });
127
128 test ( 'selection validation respects campaign settings and permits correction' , () => {
129 for ( const amount of [ '' , 'oops' , 0 , - 1 , Infinity , true , null , {}, 1 , 1001 ]) {
130 assert. throws (() => api. donationCartBody (campaign, { amount }));
131 }
132 assert. equal (api. donationCartBody (campaign, { amount: 10 }).catalogItems[ 0 ].catalogReference.options.amount, 10 );
133 assert. throws (() => api. donationCartBody (campaign, { amount: 25 , frequency: 'YEAR' }), /offered/ );
134 assert. throws (() => api. donationCartBody ({ ... campaign, customAmountEnabled: false }, { amount: 10 }), /suggested/ );
135 assert. throws (() => api. donationCartBody ({ ... campaign, archived: true }, { amount: 25 }), /not accepting/ );
136 assert. throws (() => api. donationCartBody ({ ... campaign, commentsEnabled: false }, { amount: 25 , note: 'A note' }), /not enabled/ );
137 assert. throws (() => api. donationCartBody ({ ... campaign, askDonorCoverFee: false }, { amount: 25 , coverFee: true }), /not offered/ );
138 });
139
140 test ( 'bad cart options preserve the HTTP error and never attempt a redirect' , async () => {
141 const errorBody = { details: { applicationError: { code: 'ITEM_NOT_FOUND_IN_CATALOG' } } };
142 const calls = mockRequests ( path => path === '/ecom/v2/carts' ? json (errorBody, 400 ) : undefined );
143 await assert. rejects (api. donationCheckoutUrl ( 'campaign-id' , { amount: 25 }), error => {
144 assert. equal (error.status, 400 ); assert. deepEqual (error.body, errorBody); assert. equal (error.stage, 'cart' ); return true ;
145 });
146 assert. equal (calls. filter ( call => call.path. includes ( 'redirect-session' )). length , 0 );
147 });
148
149 test ( 'redirect failure retains the known cart and does not silently create a second cart' , async () => {
150 const calls = mockRequests ( path => path. includes ( 'redirect-session' ) ? json ({ message: 'Origin not allowed' }, 403 ) : undefined );
151 await assert. rejects (api. donationCheckoutUrl ( 'campaign-id' , { amount: 25 }), error => {
152 assert. equal (error.status, 403 ); assert. equal (error.stage, 'redirect' ); assert. equal (error.cartId, 'cart-id' ); return true ;
153 });
154 assert. equal (calls. filter ( call => call.path === '/ecom/v2/carts' ). length , 1 );
155 });
156
157 test ( 'Donations retains a payment-required error body instead of returning undefined' , async () => {
158 const errorBody = { message: 'Payment setup required' , code: 'PAYMENT_REQUIRED' };
159 mockRequests (() => json (errorBody, 402 ));
160 await assert. rejects (transport. wixApiRequest ( '/a-test-read' , { throwOnPaymentRequired: true }), error => {
161 assert. equal (error.status, 402 ); assert. deepEqual (error.body, errorBody); return true ;
162 });
163 });
164
165 test ( 'the shared transport keeps its existing 402 behavior for other verticals' , async () => {
166 mockRequests (() => json ({ message: 'Payment setup required' }, 402 ));
167 const originalWarn = console.warn;
168 const warnings = [];
169 console. warn = message => warnings. push (message);
170 try {
171 assert. equal ( await transport. wixApiRequest ( '/a-test-read' ), undefined );
172 assert. equal (warnings. length , 1 );
173 } finally {
174 console.warn = originalWarn;
175 }
176 });
177
178 test ( 'query errors do not become empty campaigns; nextCursor is returned on success' , async () => {
179 mockRequests ( path => path. endsWith ( '/query' ) ? json ({ message: 'Permission denied' }, 403 ) : undefined );
180 await assert. rejects (api. queryDonationCampaigns (), error => error.status === 403 );
181 mockRequests ( path => path. endsWith ( '/query' ) ? json ({ donationCampaigns: [campaign], pagingMetadata: { cursors: { next: 'next-page' } } }) : undefined );
182 assert. deepEqual ( await api. queryDonationCampaigns (), { campaigns: [campaign], nextCursor: 'next-page' });
183 });
184
185 test ( 'absent campaigns differ from denied access; unavailable metrics stay errors' , async () => {
186 mockRequests (() => json ({ message: 'Missing' }, 404 ));
187 assert. equal ( await api. getDonationCampaign ( 'missing-id' ), null );
188 mockRequests (() => json ({ message: 'Permission denied' }, 403 ));
189 await assert. rejects (api. getDonationCampaign ( 'campaign-id' ), error => error.status === 403 );
190 await assert. rejects (api. getDonationCampaignMetrics ( 'campaign-id' ), error => error.status === 403 );
191 });
192
193 test ( 'platform deploy copies the new donation helper alongside the shared transport' , async () => {
194 const { result , files , copies , exitCode } = await deployFixture ();
195 assert. deepEqual (result.verticals, [ 'donations' ]);
196 assert. equal (result.error, undefined );
197 assert. equal (exitCode, 0 );
198 assert. equal (result.donationsTransport, 'from_shared_tree' );
199 assert. ok (copies. some ( copy => copy.from. endsWith ( '/shared/app' ) && copy.to === '/app/src' ));
200 assert. ok (copies. some ( copy => copy.from. endsWith ( '/donations/app' ) && copy.to === '/app/src' ));
201 assert. equal (files. get ( '/app/src/rest/wix-client.js' ), transportSource);
202 assert. equal (files. get ( '/app/src/rest/wix-donations.js' ), donationSource);
203 assert. equal (result.wixConfig, 'written' );
204 });
205
206 test ( 'incremental deployment upgrades only the exact shipped legacy transport and preserves 402 details' , async () => {
207 assert. equal ( createHash ( 'sha256' ). update (legacyTransportSource). digest ( 'hex' ), '10d53e5e06ac3da62f93d41cfd618b6169c5bb1a5ba07fb58ec7700aad42889e' );
208 const { result , files , copies , exitCode } = await deployFixture (legacyTransportSource);
209 assert. equal (exitCode, 0 );
210 assert. equal (result.donationsTransport, 'upgraded_unmodified_legacy' );
211 assert. equal (files. get ( '/app/src/rest/wix-client.js' ), transportSource);
212 assert. equal (copies. filter ( copy => copy.to === '/app/src/rest/wix-client.js' && copy.options.force === true ). length , 1 );
213 const upgraded = await helperWithTransport (files. get ( '/app/src/rest/wix-client.js' ));
214 const errorBody = { code: 'PAYMENT_REQUIRED' , message: 'Payment setup required' };
215 mockRequests (() => json (errorBody, 402 ));
216 await assert. rejects (upgraded. queryDonationCampaigns (), error => error.status === 402 && error.body.code === errorBody.code);
217 });
218
219 test ( 'customized incompatible transport fails deployment preflight without any writes' , async () => {
220 const customized = `${ legacyTransportSource } \n // User-owned customization. \n ` ;
221 const { result , files , copies , writes , exitCode } = await deployFixture (customized);
222 assert. equal (exitCode, 1 );
223 assert. deepEqual (result.verticals, []);
224 assert. match (result.error, /customized or incompatible . * left unchanged . * Merge strict payment-error support/ );
225 assert. equal (files. get ( '/app/src/rest/wix-client.js' ), customized);
226 assert. equal (files. has ( '/app/src/rest/wix-donations.js' ), false );
227 assert. equal (copies. length , 0 );
228 assert. equal (writes. length , 0 );
229 });
230
231 test ( 'compatible customized transport is kept byte-for-byte on a Donations deploy' , async () => {
232 const customized = `${ transportSource } \n // User-owned customization. \n ` ;
233 const { result , files , copies , exitCode } = await deployFixture (customized);
234 assert. equal (exitCode, 0 );
235 assert. equal (result.donationsTransport, 'already_supported' );
236 assert. equal (files. get ( '/app/src/rest/wix-client.js' ), customized);
237 assert. equal (copies. some ( copy => copy.to === '/app/src/rest/wix-client.js' ), false );
238 });
239
240 test ( 'non-Donations deployment leaves the legacy transport untouched' , async () => {
241 const { result , files , copies , exitCode } = await deployFixture (legacyTransportSource, [ 'cms' ]);
242 assert. equal (exitCode, 0 );
243 assert. deepEqual (result.verticals, [ 'cms' ]);
244 assert. equal (result.donationsTransport, undefined );
245 assert. equal (files. get ( '/app/src/rest/wix-client.js' ), legacyTransportSource);
246 assert. equal (copies. some ( copy => copy.to === '/app/src/rest/wix-client.js' ), false );
247 });
248
249 test ( 'manual copying with a legacy transport fails actionably before making a network request' , async () => {
250 const incompatible = await helperWithTransport (legacyTransportSource);
251 const calls = mockRequests ();
252 await assert. rejects (incompatible. queryDonationCampaigns (), error => {
253 assert. equal (error.code, 'DONATIONS_TRANSPORT_UPGRADE_REQUIRED' );
254 assert. match (error.message, /Re-run the Donations deploy step . * customized src \/ rest \/ wix-client \. js/ );
255 return true ;
256 });
257 assert. equal (calls. length , 0 );
258 });