This is page 17 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /src/clients/logs/log-analyzer.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Log analysis, summarization, and pattern detection 3 | */ 4 | 5 | import { Logger } from '../../utils/logger.js'; 6 | import { LogProcessor } from './log-processor.js'; 7 | import { LogFormatter } from './log-formatter.js'; 8 | import type { LogSummary, LogFileMetadata, ProcessedLogEntry } from './log-types.js'; 9 | 10 | export class LogAnalyzer { 11 | private logger: Logger; 12 | private processor: LogProcessor; 13 | 14 | constructor(logger: Logger) { 15 | this.logger = logger; 16 | this.processor = new LogProcessor(logger); 17 | } 18 | 19 | /** 20 | * Analyze log files and generate comprehensive summary 21 | */ 22 | async analyzeLogs( 23 | files: LogFileMetadata[], 24 | fileContents: Map<string, string>, 25 | date: string, 26 | ): Promise<LogSummary> { 27 | const summary: LogSummary = { 28 | date, 29 | errorCount: 0, 30 | warningCount: 0, 31 | infoCount: 0, 32 | debugCount: 0, 33 | keyIssues: [], 34 | files: files.map((f: LogFileMetadata) => f.filename), 35 | }; 36 | 37 | // Analyze each log file for counts and patterns 38 | for (const file of files) { 39 | const content = fileContents.get(file.filename); 40 | if (!content) { 41 | this.logger.warn(`No content found for analysis: ${file.filename}`); 42 | continue; 43 | } 44 | 45 | try { 46 | // Count different log levels 47 | const counts = this.processor.countLogLevels(content); 48 | summary.errorCount += counts.errorCount; 49 | summary.warningCount += counts.warningCount; 50 | summary.infoCount += counts.infoCount; 51 | summary.debugCount += counts.debugCount; 52 | 53 | // Extract key issues from error files 54 | if (this.isErrorFile(file.filename)) { 55 | const issues = this.processor.extractKeyIssues(content); 56 | summary.keyIssues.push(...issues); 57 | } 58 | } catch (error) { 59 | this.logger.error(`Error analyzing file ${file.filename}:`, error); 60 | } 61 | } 62 | 63 | // Remove duplicate key issues 64 | summary.keyIssues = [...new Set(summary.keyIssues)]; 65 | 66 | return summary; 67 | } 68 | 69 | /** 70 | * Detect patterns and anomalies in logs 71 | */ 72 | detectPatterns(entries: ProcessedLogEntry[]): { 73 | frequentErrors: Map<string, number>; 74 | timePatterns: Map<string, number>; 75 | sourcePatterns: Map<string, number>; 76 | } { 77 | const frequentErrors = new Map<string, number>(); 78 | const timePatterns = new Map<string, number>(); 79 | const sourcePatterns = new Map<string, number>(); 80 | 81 | for (const entry of entries) { 82 | // Count error patterns 83 | if (entry.level === 'error') { 84 | const errorPattern = this.extractErrorPattern(entry.content); 85 | frequentErrors.set(errorPattern, (frequentErrors.get(errorPattern) ?? 0) + 1); 86 | } 87 | 88 | // Count time patterns (hour-based) 89 | if (entry.timestamp) { 90 | const hour = new Date(entry.timestamp).getHours(); 91 | const hourKey = `${hour}:00-${hour + 1}:00`; 92 | timePatterns.set(hourKey, (timePatterns.get(hourKey) ?? 0) + 1); 93 | } 94 | 95 | // Count source patterns 96 | if (entry.source) { 97 | sourcePatterns.set(entry.source, (sourcePatterns.get(entry.source) ?? 0) + 1); 98 | } 99 | } 100 | 101 | return { 102 | frequentErrors, 103 | timePatterns, 104 | sourcePatterns, 105 | }; 106 | } 107 | 108 | /** 109 | * Generate health score based on log analysis 110 | */ 111 | calculateHealthScore(summary: LogSummary): { 112 | score: number; 113 | level: 'excellent' | 'good' | 'warning' | 'critical'; 114 | factors: string[]; 115 | } { 116 | const factors: string[] = []; 117 | let score = 100; 118 | 119 | // Deduct points for errors 120 | if (summary.errorCount > 0) { 121 | const errorPenalty = Math.min(summary.errorCount * 2, 30); 122 | score -= errorPenalty; 123 | factors.push(`Errors detected: -${errorPenalty} points`); 124 | } 125 | 126 | // Deduct points for warnings 127 | if (summary.warningCount > 10) { 128 | const warningPenalty = Math.min((summary.warningCount - 10) * 0.5, 15); 129 | score -= warningPenalty; 130 | factors.push(`High warning count: -${warningPenalty} points`); 131 | } 132 | 133 | // Deduct points for key issues 134 | if (summary.keyIssues.length > 0) { 135 | const issuePenalty = Math.min(summary.keyIssues.length * 5, 25); 136 | score -= issuePenalty; 137 | factors.push(`Key issues: -${issuePenalty} points`); 138 | } 139 | 140 | // Determine level 141 | let level: 'excellent' | 'good' | 'warning' | 'critical'; 142 | if (score >= 90) { 143 | level = 'excellent'; 144 | } else if (score >= 75) { 145 | level = 'good'; 146 | } else if (score >= 50) { 147 | level = 'warning'; 148 | } else { 149 | level = 'critical'; 150 | } 151 | 152 | return { score: Math.max(0, score), level, factors }; 153 | } 154 | 155 | /** 156 | * Find trending issues across time periods 157 | */ 158 | findTrendingIssues( 159 | currentSummary: LogSummary, 160 | previousSummaries: LogSummary[], 161 | ): { 162 | increasing: string[]; 163 | decreasing: string[]; 164 | new: string[]; 165 | } { 166 | const trending = { 167 | increasing: [] as string[], 168 | decreasing: [] as string[], 169 | new: [] as string[], 170 | }; 171 | 172 | // Compare current issues with previous periods 173 | const previousIssues = new Set( 174 | previousSummaries.flatMap(summary => summary.keyIssues), 175 | ); 176 | 177 | for (const issue of currentSummary.keyIssues) { 178 | if (!previousIssues.has(issue)) { 179 | trending.new.push(issue); 180 | } 181 | } 182 | 183 | // For increasing/decreasing, we'd need more sophisticated tracking 184 | // This is a simplified version 185 | const currentErrorCount = currentSummary.errorCount; 186 | const avgPreviousErrors = previousSummaries.length > 0 187 | ? previousSummaries.reduce((sum, s) => sum + s.errorCount, 0) / previousSummaries.length 188 | : 0; 189 | 190 | if (currentErrorCount > avgPreviousErrors * 1.5) { 191 | trending.increasing.push('Overall error rate'); 192 | } else if (currentErrorCount < avgPreviousErrors * 0.5) { 193 | trending.decreasing.push('Overall error rate'); 194 | } 195 | 196 | return trending; 197 | } 198 | 199 | /** 200 | * Generate recommendations based on analysis 201 | */ 202 | generateRecommendations(summary: LogSummary, patterns: ReturnType<typeof this.detectPatterns>): string[] { 203 | const recommendations: string[] = []; 204 | 205 | if (summary.errorCount > 10) { 206 | recommendations.push('High error count detected. Review error logs for critical issues.'); 207 | } 208 | 209 | if (summary.warningCount > 50) { 210 | recommendations.push('High warning count. Consider addressing warnings to prevent future errors.'); 211 | } 212 | 213 | if (patterns.frequentErrors.size > 0) { 214 | const topError = Array.from(patterns.frequentErrors.entries()) 215 | .sort((a, b) => b[1] - a[1])[0]; 216 | recommendations.push(`Most frequent error: "${topError[0]}" (${topError[1]} occurrences)`); 217 | } 218 | 219 | if (patterns.timePatterns.size > 0) { 220 | const peakHour = Array.from(patterns.timePatterns.entries()) 221 | .sort((a, b) => b[1] - a[1])[0]; 222 | recommendations.push(`Peak activity time: ${peakHour[0]} (${peakHour[1]} events)`); 223 | } 224 | 225 | if (summary.keyIssues.length === 0 && summary.errorCount === 0) { 226 | recommendations.push('System appears to be running smoothly with no critical issues detected.'); 227 | } 228 | 229 | return recommendations; 230 | } 231 | 232 | /** 233 | * Extract error pattern for categorization 234 | */ 235 | private extractErrorPattern(errorContent: string): string { 236 | // Extract the core error message, removing dynamic parts 237 | const patterns = [ 238 | /Exception: (.+?)(\s+at\s|$)/, 239 | /Error: (.+?)(\s+at\s|$)/, 240 | /Failed to (.+?)(\s+\(|$)/, 241 | /Cannot (.+?)(\s+\(|$)/, 242 | ]; 243 | 244 | for (const pattern of patterns) { 245 | const match = errorContent.match(pattern); 246 | if (match) { 247 | return match[1].trim(); 248 | } 249 | } 250 | 251 | // Fallback: use first 50 characters 252 | return LogFormatter.truncateText(errorContent, 50); 253 | } 254 | 255 | /** 256 | * Check if filename indicates an error log file 257 | */ 258 | private isErrorFile(filename: string): boolean { 259 | const normalizedName = filename.toLowerCase(); 260 | return normalizedName.includes('error-') || normalizedName.includes('customerror-'); 261 | } 262 | 263 | /** 264 | * Format analysis results for display 265 | */ 266 | formatAnalysisResults( 267 | summary: LogSummary, 268 | patterns: ReturnType<typeof this.detectPatterns>, 269 | healthScore: ReturnType<typeof this.calculateHealthScore>, 270 | recommendations: string[], 271 | ): string { 272 | const sections = [ 273 | LogFormatter.formatLogSummary(summary), 274 | '', 275 | `🏥 Health Score: ${healthScore.score}/100 (${healthScore.level})`, 276 | healthScore.factors.length > 0 ? `Factors: ${healthScore.factors.join(', ')}` : '', 277 | '', 278 | '🔍 Pattern Analysis:', 279 | `- Unique error patterns: ${patterns.frequentErrors.size}`, 280 | `- Active time periods: ${patterns.timePatterns.size}`, 281 | `- Different sources: ${patterns.sourcePatterns.size}`, 282 | '', 283 | '💡 Recommendations:', 284 | ...recommendations.map(rec => `- ${rec}`), 285 | ].filter(Boolean); 286 | 287 | return sections.join('\n'); 288 | } 289 | } 290 | ``` -------------------------------------------------------------------------------- /tests/best-practices-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BestPracticesToolHandler } from '../src/core/handlers/best-practices-handler.js'; 2 | import { HandlerContext } from '../src/core/handlers/base-handler.js'; 3 | import { Logger } from '../src/utils/logger.js'; 4 | 5 | // Mock the SFCCBestPracticesClient 6 | const mockBestPracticesClient = { 7 | getAvailableGuides: jest.fn(), 8 | getBestPracticeGuide: jest.fn(), 9 | searchBestPractices: jest.fn(), 10 | getHookReference: jest.fn(), 11 | }; 12 | 13 | jest.mock('../src/clients/best-practices-client.js', () => ({ 14 | SFCCBestPracticesClient: jest.fn(() => mockBestPracticesClient), 15 | })); 16 | 17 | describe('BestPracticesToolHandler', () => { 18 | let mockLogger: jest.Mocked<Logger>; 19 | let mockClient: typeof mockBestPracticesClient; 20 | let context: HandlerContext; 21 | let handler: BestPracticesToolHandler; 22 | 23 | beforeEach(() => { 24 | mockLogger = { 25 | debug: jest.fn(), 26 | log: jest.fn(), 27 | error: jest.fn(), 28 | timing: jest.fn(), 29 | methodEntry: jest.fn(), 30 | methodExit: jest.fn(), 31 | } as any; 32 | 33 | // Reset mocks 34 | jest.clearAllMocks(); 35 | 36 | // Use the mock client directly 37 | mockClient = mockBestPracticesClient; 38 | 39 | jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); 40 | 41 | context = { 42 | logger: mockLogger, 43 | config: null as any, 44 | capabilities: { canAccessLogs: false, canAccessOCAPI: false }, 45 | }; 46 | 47 | handler = new BestPracticesToolHandler(context, 'BestPractices'); 48 | }); 49 | 50 | afterEach(() => { 51 | jest.restoreAllMocks(); 52 | }); 53 | 54 | // Helper function to initialize handler for tests that need it 55 | const initializeHandler = async () => { 56 | await (handler as any).initialize(); 57 | }; 58 | 59 | describe('canHandle', () => { 60 | it('should handle best practices tools', () => { 61 | expect(handler.canHandle('get_available_best_practice_guides')).toBe(true); 62 | expect(handler.canHandle('get_best_practice_guide')).toBe(true); 63 | expect(handler.canHandle('search_best_practices')).toBe(true); 64 | expect(handler.canHandle('get_hook_reference')).toBe(true); 65 | }); 66 | 67 | it('should not handle non-best-practices tools', () => { 68 | expect(handler.canHandle('get_latest_error')).toBe(false); 69 | expect(handler.canHandle('unknown_tool')).toBe(false); 70 | }); 71 | }); 72 | 73 | describe('initialization', () => { 74 | it('should initialize best practices client', async () => { 75 | await initializeHandler(); 76 | 77 | const MockedConstructor = jest.requireMock('../src/clients/best-practices-client.js').SFCCBestPracticesClient; 78 | expect(MockedConstructor).toHaveBeenCalled(); 79 | expect(mockLogger.debug).toHaveBeenCalledWith('Best practices client initialized'); 80 | }); 81 | }); 82 | 83 | describe('disposal', () => { 84 | it('should dispose best practices client properly', async () => { 85 | await initializeHandler(); 86 | await (handler as any).dispose(); 87 | 88 | expect(mockLogger.debug).toHaveBeenCalledWith('Best practices client disposed'); 89 | }); 90 | }); 91 | 92 | describe('get_available_best_practice_guides tool', () => { 93 | beforeEach(async () => { 94 | await initializeHandler(); 95 | mockClient.getAvailableGuides.mockResolvedValue([ 96 | { name: 'cartridge_creation', title: 'Cartridge Creation', description: 'Best practices for cartridge creation' }, 97 | { name: 'isml_templates', title: 'ISML Templates', description: 'Best practices for ISML templates' }, 98 | ]); 99 | }); 100 | 101 | it('should handle get_available_best_practice_guides', async () => { 102 | const result = await handler.handle('get_available_best_practice_guides', {}, Date.now()); 103 | 104 | expect(mockClient.getAvailableGuides).toHaveBeenCalled(); 105 | expect(result.content[0].text).toContain('cartridge_creation'); 106 | expect(result.content[0].text).toContain('isml_templates'); 107 | }); 108 | }); 109 | 110 | describe('get_best_practice_guide tool', () => { 111 | beforeEach(async () => { 112 | await initializeHandler(); 113 | mockClient.getBestPracticeGuide.mockResolvedValue({ 114 | title: 'Cartridge Creation Best Practices', 115 | description: 'Complete guide for creating custom cartridges', 116 | sections: ['Overview', 'Setup', 'Configuration'], 117 | content: 'Detailed content about cartridge creation...', 118 | }); 119 | }); 120 | 121 | it('should handle get_best_practice_guide with guideName', async () => { 122 | const args = { guideName: 'cartridge_creation' }; 123 | const result = await handler.handle('get_best_practice_guide', args, Date.now()); 124 | 125 | expect(mockClient.getBestPracticeGuide).toHaveBeenCalledWith('cartridge_creation'); 126 | expect(result.content[0].text).toContain('Cartridge Creation Best Practices'); 127 | }); 128 | 129 | it('should throw error when guideName is missing', async () => { 130 | const result = await handler.handle('get_best_practice_guide', {}, Date.now()); 131 | expect(result.isError).toBe(true); 132 | expect(result.content[0].text).toContain('guideName must be a non-empty string'); 133 | }); 134 | }); 135 | 136 | describe('search_best_practices tool', () => { 137 | beforeEach(async () => { 138 | await initializeHandler(); 139 | mockClient.searchBestPractices.mockResolvedValue([ 140 | { guide: 'cartridge_creation', section: 'Setup', content: 'Cartridge setup instructions...' }, 141 | { guide: 'security', section: 'Authentication', content: 'Security best practices...' }, 142 | ]); 143 | }); 144 | 145 | it('should handle search_best_practices with query', async () => { 146 | const args = { query: 'validation' }; 147 | const result = await handler.handle('search_best_practices', args, Date.now()); 148 | 149 | expect(mockClient.searchBestPractices).toHaveBeenCalledWith('validation'); 150 | expect(result.content[0].text).toContain('cartridge_creation'); 151 | expect(result.content[0].text).toContain('security'); 152 | }); 153 | 154 | it('should throw error when query is missing', async () => { 155 | const result = await handler.handle('search_best_practices', {}, Date.now()); 156 | expect(result.isError).toBe(true); 157 | expect(result.content[0].text).toContain('query must be a non-empty string'); 158 | }); 159 | 160 | it('should throw error when query is empty', async () => { 161 | const result = await handler.handle('search_best_practices', { query: '' }, Date.now()); 162 | expect(result.isError).toBe(true); 163 | expect(result.content[0].text).toContain('query must be a non-empty string'); 164 | }); 165 | }); 166 | 167 | describe('get_hook_reference tool', () => { 168 | beforeEach(async () => { 169 | await initializeHandler(); 170 | mockClient.getHookReference.mockResolvedValue({ 171 | type: 'OCAPI Hooks', 172 | hooks: [ 173 | { endpoint: 'customers.post', description: 'Customer creation hook' }, 174 | { endpoint: 'orders.get', description: 'Order retrieval hook' }, 175 | ], 176 | }); 177 | }); 178 | 179 | it('should handle get_hook_reference with guideName', async () => { 180 | const args = { guideName: 'ocapi_hooks' }; 181 | const result = await handler.handle('get_hook_reference', args, Date.now()); 182 | 183 | expect(mockClient.getHookReference).toHaveBeenCalledWith('ocapi_hooks'); 184 | expect(result.content[0].text).toContain('OCAPI Hooks'); 185 | expect(result.content[0].text).toContain('customers.post'); 186 | }); 187 | 188 | it('should throw error when guideName is missing', async () => { 189 | const result = await handler.handle('get_hook_reference', {}, Date.now()); 190 | expect(result.isError).toBe(true); 191 | expect(result.content[0].text).toContain('guideName must be a non-empty string'); 192 | }); 193 | }); 194 | 195 | describe('error handling', () => { 196 | beforeEach(async () => { 197 | await initializeHandler(); 198 | }); 199 | 200 | it('should handle client errors gracefully', async () => { 201 | mockClient.getBestPracticeGuide.mockRejectedValue(new Error('Guide not found')); 202 | 203 | const result = await handler.handle('get_best_practice_guide', { guideName: 'unknown_guide' }, Date.now()); 204 | expect(result.isError).toBe(true); 205 | expect(result.content[0].text).toContain('Guide not found'); 206 | }); 207 | 208 | it('should throw error for unsupported tools', async () => { 209 | await expect(handler.handle('unsupported_tool', {}, Date.now())) 210 | .rejects.toThrow('Unsupported tool'); 211 | }); 212 | }); 213 | 214 | describe('timing and logging', () => { 215 | beforeEach(async () => { 216 | await initializeHandler(); 217 | mockClient.getAvailableGuides.mockResolvedValue([]); 218 | }); 219 | 220 | it('should log timing information', async () => { 221 | const startTime = Date.now(); 222 | await handler.handle('get_available_best_practice_guides', {}, startTime); 223 | 224 | expect(mockLogger.timing).toHaveBeenCalledWith('get_available_best_practice_guides', startTime); 225 | }); 226 | 227 | it('should log execution details', async () => { 228 | await handler.handle('get_available_best_practice_guides', {}, Date.now()); 229 | 230 | expect(mockLogger.debug).toHaveBeenCalledWith( 231 | 'get_available_best_practice_guides completed successfully', 232 | expect.any(Object), 233 | ); 234 | }); 235 | }); 236 | }); 237 | ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/mock-data/ocapi/site-preferences-ccv.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_v": "23.2", 3 | "_type": "preference_value_search_result", 4 | "count": 5, 5 | "hits": [ 6 | { 7 | "_type": "preference_value", 8 | "attribute_definition": { 9 | "_type": "object_attribute_definition", 10 | "_resource_state": "250db50d76a5e4bf869062a6ed5aab18d77aed015a880e4f0f5b8e86cc8e071d", 11 | "creation_date": "2025-03-06T07:14:22.000Z", 12 | "description": { 13 | "default": "Enable or disable CCV card authorization" 14 | }, 15 | "display_name": { 16 | "default": "CCV Cards Authorise Enabled" 17 | }, 18 | "effective_id": "c_ccvCardsAuthoriseEnabled", 19 | "externally_defined": false, 20 | "externally_managed": false, 21 | "id": "ccvCardsAuthoriseEnabled", 22 | "key": false, 23 | "last_modified": "2025-03-06T07:14:22.000Z", 24 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvCardsAuthoriseEnabled", 25 | "localizable": false, 26 | "mandatory": false, 27 | "multi_value_type": false, 28 | "order_required": false, 29 | "queryable": false, 30 | "read_only": false, 31 | "requires_encoding": false, 32 | "searchable": false, 33 | "set_value_type": false, 34 | "site_specific": false, 35 | "system": false, 36 | "value_type": "boolean", 37 | "visible": true 38 | }, 39 | "description": { 40 | "default": "Enable or disable CCV card authorization" 41 | }, 42 | "display_name": { 43 | "default": "CCV Cards Authorise Enabled" 44 | }, 45 | "id": "ccvCardsAuthoriseEnabled", 46 | "site_values": { 47 | "RefArch": true, 48 | "RefArchGlobal": true, 49 | "pxl_1": null, 50 | "pxl_2": null, 51 | "pxl_3": null, 52 | "pxl_4": null, 53 | "pxl_5": null, 54 | "pxl_6": null 55 | }, 56 | "value_type": "boolean" 57 | }, 58 | { 59 | "_type": "preference_value", 60 | "attribute_definition": { 61 | "_type": "object_attribute_definition", 62 | "_resource_state": "34dce900d8eb50d95d97fcc0fd74449e78720a0da5a6804e561f347d8fdb5227", 63 | "creation_date": "2025-03-06T07:14:22.000Z", 64 | "description": { 65 | "default": "Enable storing cards in vault for CCV" 66 | }, 67 | "display_name": { 68 | "default": "CCV Store Cards In Vault Enabled" 69 | }, 70 | "effective_id": "c_ccvStoreCardsInVaultEnabled", 71 | "externally_defined": false, 72 | "externally_managed": false, 73 | "id": "ccvStoreCardsInVaultEnabled", 74 | "key": false, 75 | "last_modified": "2025-03-06T07:14:22.000Z", 76 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvStoreCardsInVaultEnabled", 77 | "localizable": false, 78 | "mandatory": false, 79 | "multi_value_type": false, 80 | "order_required": false, 81 | "queryable": false, 82 | "read_only": false, 83 | "requires_encoding": false, 84 | "searchable": false, 85 | "set_value_type": false, 86 | "site_specific": false, 87 | "system": false, 88 | "value_type": "boolean", 89 | "visible": true 90 | }, 91 | "description": { 92 | "default": "Enable storing cards in vault for CCV" 93 | }, 94 | "display_name": { 95 | "default": "CCV Store Cards In Vault Enabled" 96 | }, 97 | "id": "ccvStoreCardsInVaultEnabled", 98 | "site_values": { 99 | "RefArch": false, 100 | "RefArchGlobal": true, 101 | "pxl_1": null, 102 | "pxl_2": null, 103 | "pxl_3": null, 104 | "pxl_4": null, 105 | "pxl_5": null, 106 | "pxl_6": null 107 | }, 108 | "value_type": "boolean" 109 | }, 110 | { 111 | "_type": "preference_value", 112 | "attribute_definition": { 113 | "_type": "object_attribute_definition", 114 | "_resource_state": "bbcd6304068fcf391c849bb37952598bebed1bd55bafbe211e2a6539b4b522a9", 115 | "creation_date": "2025-03-06T07:14:22.000Z", 116 | "description": { 117 | "default": "Enable automatic refunds for CCV payments" 118 | }, 119 | "display_name": { 120 | "default": "CCV Auto Refund Enabled" 121 | }, 122 | "effective_id": "c_ccvAutoRefundEnabled", 123 | "externally_defined": false, 124 | "externally_managed": false, 125 | "id": "ccvAutoRefundEnabled", 126 | "key": false, 127 | "last_modified": "2025-03-06T07:14:22.000Z", 128 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvAutoRefundEnabled", 129 | "localizable": false, 130 | "mandatory": false, 131 | "multi_value_type": false, 132 | "order_required": false, 133 | "queryable": false, 134 | "read_only": false, 135 | "requires_encoding": false, 136 | "searchable": false, 137 | "set_value_type": false, 138 | "site_specific": false, 139 | "system": false, 140 | "value_type": "boolean", 141 | "visible": true 142 | }, 143 | "description": { 144 | "default": "Enable automatic refunds for CCV payments" 145 | }, 146 | "display_name": { 147 | "default": "CCV Auto Refund Enabled" 148 | }, 149 | "id": "ccvAutoRefundEnabled", 150 | "site_values": { 151 | "RefArch": false, 152 | "RefArchGlobal": false, 153 | "pxl_1": null, 154 | "pxl_2": null, 155 | "pxl_3": null, 156 | "pxl_4": null, 157 | "pxl_5": null, 158 | "pxl_6": null 159 | }, 160 | "value_type": "boolean" 161 | }, 162 | { 163 | "_type": "preference_value", 164 | "attribute_definition": { 165 | "_type": "object_attribute_definition", 166 | "_resource_state": "efb6bd1843b9484576c8ffaf9447bc7d0bdee5bb6b0e9231d54804dbff5a11fa", 167 | "creation_date": "2025-03-06T07:14:22.000Z", 168 | "description": { 169 | "default": "Enable SCA ready processing for CCV" 170 | }, 171 | "display_name": { 172 | "default": "CCV SCA Ready Enabled" 173 | }, 174 | "effective_id": "c_ccvScaReadyEnabled", 175 | "externally_defined": false, 176 | "externally_managed": false, 177 | "id": "ccvScaReadyEnabled", 178 | "key": false, 179 | "last_modified": "2025-03-06T07:14:22.000Z", 180 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvScaReadyEnabled", 181 | "localizable": false, 182 | "mandatory": false, 183 | "multi_value_type": false, 184 | "order_required": false, 185 | "queryable": false, 186 | "read_only": false, 187 | "requires_encoding": false, 188 | "searchable": false, 189 | "set_value_type": false, 190 | "site_specific": false, 191 | "system": false, 192 | "value_type": "boolean", 193 | "visible": true 194 | }, 195 | "description": { 196 | "default": "Enable SCA ready processing for CCV" 197 | }, 198 | "display_name": { 199 | "default": "CCV SCA Ready Enabled" 200 | }, 201 | "id": "ccvScaReadyEnabled", 202 | "site_values": { 203 | "RefArch": true, 204 | "RefArchGlobal": true, 205 | "pxl_1": null, 206 | "pxl_2": null, 207 | "pxl_3": null, 208 | "pxl_4": null, 209 | "pxl_5": null, 210 | "pxl_6": null 211 | }, 212 | "value_type": "boolean" 213 | }, 214 | { 215 | "_type": "preference_value", 216 | "attribute_definition": { 217 | "_type": "object_attribute_definition", 218 | "_resource_state": "ed156d5f2de1fbbea7fa8350da7b2424feb7e54ac4e338d50fd6fec992df2c6f", 219 | "creation_date": "2025-03-06T07:14:22.000Z", 220 | "description": { 221 | "default": "Configuration for CCV 3DS exemption handling" 222 | }, 223 | "display_name": { 224 | "default": "CCV 3DS Exemption" 225 | }, 226 | "effective_id": "c_ccv3DSExemption", 227 | "externally_defined": false, 228 | "externally_managed": false, 229 | "id": "ccv3DSExemption", 230 | "key": false, 231 | "last_modified": "2025-03-06T07:14:22.000Z", 232 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccv3DSExemption", 233 | "localizable": false, 234 | "mandatory": false, 235 | "multi_value_type": false, 236 | "order_required": false, 237 | "queryable": false, 238 | "read_only": false, 239 | "requires_encoding": false, 240 | "searchable": false, 241 | "set_value_type": false, 242 | "site_specific": false, 243 | "system": false, 244 | "value_type": "string", 245 | "visible": true 246 | }, 247 | "description": { 248 | "default": "Configuration for CCV 3DS exemption handling" 249 | }, 250 | "display_name": { 251 | "default": "CCV 3DS Exemption" 252 | }, 253 | "id": "ccv3DSExemption", 254 | "site_values": { 255 | "RefArch": "none", 256 | "RefArchGlobal": "low_value", 257 | "pxl_1": null, 258 | "pxl_2": null, 259 | "pxl_3": null, 260 | "pxl_4": null, 261 | "pxl_5": null, 262 | "pxl_6": null 263 | }, 264 | "value_type": "string" 265 | } 266 | ], 267 | "query": { 268 | "match_all_query": { 269 | "_type": "match_all_query" 270 | } 271 | }, 272 | "select": "(**)", 273 | "start": 0, 274 | "total": 5 275 | } 276 | ``` -------------------------------------------------------------------------------- /tests/base-http-client.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for BaseHttpClient 3 | * Tests the foundation HTTP client functionality 4 | */ 5 | 6 | import { BaseHttpClient } from '../src/clients/base/http-client.js'; 7 | import { Logger } from '../src/utils/logger.js'; 8 | 9 | // Mock fetch globally 10 | global.fetch = jest.fn(); 11 | 12 | // Mock Logger 13 | jest.mock('../src/utils/logger.js', () => ({ 14 | Logger: { 15 | initialize: jest.fn(), 16 | getInstance: jest.fn(() => ({ 17 | methodEntry: jest.fn(), 18 | methodExit: jest.fn(), 19 | debug: jest.fn(), 20 | warn: jest.fn(), 21 | error: jest.fn(), 22 | timing: jest.fn(), 23 | log: jest.fn(), 24 | info: jest.fn(), 25 | })), 26 | getChildLogger: jest.fn(() => ({ 27 | methodEntry: jest.fn(), 28 | methodExit: jest.fn(), 29 | debug: jest.fn(), 30 | warn: jest.fn(), 31 | error: jest.fn(), 32 | timing: jest.fn(), 33 | log: jest.fn(), 34 | info: jest.fn(), 35 | })), 36 | }, 37 | })); 38 | 39 | // Concrete implementation for testing abstract class 40 | class TestHttpClient extends BaseHttpClient { 41 | private authHeaders: Record<string, string> = {}; 42 | private shouldFailAuth = false; 43 | 44 | constructor(baseUrl: string = 'https://test-api.example.com') { 45 | super(baseUrl, 'TestHttpClient'); 46 | } 47 | 48 | // Implementation of abstract method 49 | protected async getAuthHeaders(): Promise<Record<string, string>> { 50 | if (this.shouldFailAuth) { 51 | throw new Error('Auth failed'); 52 | } 53 | return this.authHeaders; 54 | } 55 | 56 | // Test helpers 57 | setAuthHeaders(headers: Record<string, string>) { 58 | this.authHeaders = headers; 59 | } 60 | 61 | setAuthFailure(shouldFail: boolean) { 62 | this.shouldFailAuth = shouldFail; 63 | } 64 | 65 | // Expose protected methods for testing 66 | public async testMakeRequest<T>(endpoint: string, options?: any): Promise<T> { 67 | return this.makeRequest<T>(endpoint, options); 68 | } 69 | 70 | public async testGet<T>(endpoint: string): Promise<T> { 71 | return this.get<T>(endpoint); 72 | } 73 | 74 | public async testPost<T>(endpoint: string, data?: any): Promise<T> { 75 | return this.post<T>(endpoint, data); 76 | } 77 | 78 | public async testPut<T>(endpoint: string, data?: any): Promise<T> { 79 | return this.put<T>(endpoint, data); 80 | } 81 | 82 | public async testPatch<T>(endpoint: string, data?: any): Promise<T> { 83 | return this.patch<T>(endpoint, data); 84 | } 85 | 86 | public async testDelete<T>(endpoint: string): Promise<T> { 87 | return this.delete<T>(endpoint); 88 | } 89 | } 90 | 91 | describe('BaseHttpClient', () => { 92 | let client: TestHttpClient; 93 | let mockFetch: jest.MockedFunction<typeof fetch>; 94 | 95 | beforeEach(() => { 96 | jest.clearAllMocks(); 97 | mockFetch = fetch as jest.MockedFunction<typeof fetch>; 98 | client = new TestHttpClient(); 99 | }); 100 | 101 | describe('constructor', () => { 102 | it('should initialize with base URL and logger context', () => { 103 | const customClient = new TestHttpClient('https://custom.api.com'); 104 | expect(customClient).toBeInstanceOf(BaseHttpClient); 105 | expect(Logger.getChildLogger).toHaveBeenCalledWith('TestHttpClient'); 106 | }); 107 | }); 108 | 109 | describe('makeRequest', () => { 110 | it('should make successful GET request with auth headers', async () => { 111 | const mockResponse = { data: 'test-data' }; 112 | client.setAuthHeaders({ 'Authorization': 'Bearer token123' }); 113 | 114 | mockFetch.mockResolvedValue({ 115 | ok: true, 116 | json: async () => mockResponse, 117 | } as Response); 118 | 119 | const result = await client.testMakeRequest('/test-endpoint'); 120 | 121 | expect(mockFetch).toHaveBeenCalledWith( 122 | 'https://test-api.example.com/test-endpoint', 123 | { 124 | headers: { 125 | 'Content-Type': 'application/json', 126 | 'Authorization': 'Bearer token123', 127 | }, 128 | }, 129 | ); 130 | expect(result).toEqual(mockResponse); 131 | }); 132 | 133 | it('should handle 401 errors with retry logic', async () => { 134 | const mockResponse = { data: 'success-after-retry' }; 135 | client.setAuthHeaders({ 'Authorization': 'Bearer old-token' }); 136 | 137 | // First call returns 401 138 | mockFetch 139 | .mockResolvedValueOnce({ 140 | ok: false, 141 | status: 401, 142 | } as Response) 143 | .mockResolvedValueOnce({ 144 | ok: true, 145 | json: async () => mockResponse, 146 | } as Response); 147 | 148 | const result = await client.testMakeRequest('/test-endpoint'); 149 | 150 | expect(mockFetch).toHaveBeenCalledTimes(2); 151 | expect(result).toEqual(mockResponse); 152 | }); 153 | 154 | it('should throw error for non-401 HTTP errors', async () => { 155 | mockFetch.mockResolvedValue({ 156 | ok: false, 157 | status: 500, 158 | statusText: 'Internal Server Error', 159 | text: async () => 'Server error details', 160 | } as Response); 161 | 162 | await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow( 163 | 'Request failed: 500 Internal Server Error - Server error details', 164 | ); 165 | }); 166 | 167 | it('should handle network errors', async () => { 168 | mockFetch.mockRejectedValue(new Error('Network error')); 169 | 170 | await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow( 171 | 'Network error', 172 | ); 173 | }); 174 | 175 | it('should handle auth header failures', async () => { 176 | client.setAuthFailure(true); 177 | 178 | await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow( 179 | 'Auth failed', 180 | ); 181 | }); 182 | 183 | it('should merge custom headers with auth headers', async () => { 184 | const mockResponse = { data: 'test' }; 185 | client.setAuthHeaders({ 'Authorization': 'Bearer token' }); 186 | 187 | mockFetch.mockResolvedValue({ 188 | ok: true, 189 | json: async () => mockResponse, 190 | } as Response); 191 | 192 | await client.testMakeRequest('/test', { 193 | headers: { 'Custom-Header': 'custom-value' }, 194 | }); 195 | 196 | expect(mockFetch).toHaveBeenCalledWith( 197 | 'https://test-api.example.com/test', 198 | { 199 | headers: { 200 | 'Content-Type': 'application/json', 201 | 'Authorization': 'Bearer token', 202 | 'Custom-Header': 'custom-value', 203 | }, 204 | }, 205 | ); 206 | }); 207 | }); 208 | 209 | describe('HTTP method wrappers', () => { 210 | beforeEach(() => { 211 | client.setAuthHeaders({ 'Authorization': 'Bearer token' }); 212 | mockFetch.mockResolvedValue({ 213 | ok: true, 214 | json: async () => ({ success: true }), 215 | } as Response); 216 | }); 217 | 218 | it('should make GET request', async () => { 219 | await client.testGet('/test'); 220 | 221 | expect(mockFetch).toHaveBeenCalledWith( 222 | 'https://test-api.example.com/test', 223 | expect.objectContaining({ method: 'GET' }), // GET method is explicitly set 224 | ); 225 | }); 226 | 227 | it('should make POST request with data', async () => { 228 | const postData = { name: 'test' }; 229 | await client.testPost('/test', postData); 230 | 231 | expect(mockFetch).toHaveBeenCalledWith( 232 | 'https://test-api.example.com/test', 233 | expect.objectContaining({ 234 | method: 'POST', 235 | body: JSON.stringify(postData), 236 | }), 237 | ); 238 | }); 239 | 240 | it('should make POST request without data', async () => { 241 | await client.testPost('/test'); 242 | 243 | expect(mockFetch).toHaveBeenCalledWith( 244 | 'https://test-api.example.com/test', 245 | expect.objectContaining({ 246 | method: 'POST', 247 | }), 248 | ); 249 | expect(mockFetch.mock.calls[0][1]).not.toHaveProperty('body'); 250 | }); 251 | 252 | it('should make PUT request with data', async () => { 253 | const putData = { id: 1, name: 'updated' }; 254 | await client.testPut('/test/1', putData); 255 | 256 | expect(mockFetch).toHaveBeenCalledWith( 257 | 'https://test-api.example.com/test/1', 258 | expect.objectContaining({ 259 | method: 'PUT', 260 | body: JSON.stringify(putData), 261 | }), 262 | ); 263 | }); 264 | 265 | it('should make PATCH request with data', async () => { 266 | const patchData = { name: 'patched' }; 267 | await client.testPatch('/test/1', patchData); 268 | 269 | expect(mockFetch).toHaveBeenCalledWith( 270 | 'https://test-api.example.com/test/1', 271 | expect.objectContaining({ 272 | method: 'PATCH', 273 | body: JSON.stringify(patchData), 274 | }), 275 | ); 276 | }); 277 | 278 | it('should make DELETE request', async () => { 279 | await client.testDelete('/test/1'); 280 | 281 | expect(mockFetch).toHaveBeenCalledWith( 282 | 'https://test-api.example.com/test/1', 283 | expect.objectContaining({ 284 | method: 'DELETE', 285 | }), 286 | ); 287 | }); 288 | }); 289 | 290 | describe('error handling during retry', () => { 291 | it('should throw error if retry also fails', async () => { 292 | client.setAuthHeaders({ 'Authorization': 'Bearer token' }); 293 | 294 | mockFetch 295 | .mockResolvedValueOnce({ 296 | ok: false, 297 | status: 401, 298 | } as Response) 299 | .mockResolvedValueOnce({ 300 | ok: false, 301 | status: 500, 302 | statusText: 'Internal Server Error', 303 | text: async () => 'Retry failed', 304 | } as Response); 305 | 306 | await expect(client.testMakeRequest('/test')).rejects.toThrow( 307 | 'Request failed after retry: 500 Internal Server Error - Retry failed', 308 | ); 309 | }); 310 | }); 311 | }); 312 | ``` -------------------------------------------------------------------------------- /docs/dw_web/PagingModel.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.web 2 | 3 | # Class PagingModel 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.web.PagingModel 9 | 10 | ## Description 11 | 12 | A page model is a helper class to apply a pages to a collection of elements or an iterator of elements and supports creating URLs for continued paging through the elements. The page model is intended to be initialized with the collection or iterator, than the paging position is applyed and than the elements are extracted with getPageElements(). In case the page model is initialized with a collection the page model can be reused multiple times. 13 | 14 | ## Constants 15 | 16 | ### DEFAULT_PAGE_SIZE 17 | 18 | **Type:** Number = 10 19 | 20 | The default page size. 21 | 22 | ### MAX_PAGE_SIZE 23 | 24 | **Type:** Number = 2000 25 | 26 | The maximum supported page size. 27 | 28 | ### PAGING_SIZE_PARAMETER 29 | 30 | **Type:** String = "sz" 31 | 32 | The URL Parameter used for the page size. 33 | 34 | ### PAGING_START_PARAMETER 35 | 36 | **Type:** String = "start" 37 | 38 | The URL parameter used for the start position. 39 | 40 | ## Properties 41 | 42 | ### count 43 | 44 | **Type:** Number (Read Only) 45 | 46 | The count of the number of items in the model. 47 | 48 | ### currentPage 49 | 50 | **Type:** Number (Read Only) 51 | 52 | The index number of the current page. The page 53 | counting starts with 0. The method also works with a miss-aligned 54 | start. In that case the start is always treated as the start of 55 | a page. 56 | 57 | ### empty 58 | 59 | **Type:** boolean (Read Only) 60 | 61 | Identifies if the model is empty. 62 | 63 | ### end 64 | 65 | **Type:** Number (Read Only) 66 | 67 | The index of the last element on the current page. 68 | 69 | ### maxPage 70 | 71 | **Type:** Number (Read Only) 72 | 73 | The maximum possible page number. Counting for pages starts 74 | with 0. The method also works with a miss-aligned start. In that case 75 | the returned number might be higher than ((count-1) / pageSize). 76 | 77 | ### pageCount 78 | 79 | **Type:** Number (Read Only) 80 | 81 | The total page count. The method also works 82 | with a miss-aligned start. In that case the returned number might 83 | be higher than (count / pageSize). 84 | 85 | ### pageElements 86 | 87 | **Type:** Iterator (Read Only) 88 | 89 | An iterator that can be used to iterate through the elements of 90 | the current page. 91 | 92 | In case of a collection as the page models source, the method can be 93 | called multiple times. Each time a fresh iterator is returned. 94 | 95 | In case of an iterator as the page models source, the method must be 96 | called only once. The method will always return the same iterator, 97 | which means the method amy return an exhausted iterator. 98 | 99 | ### pageSize 100 | 101 | **Type:** Number 102 | 103 | The size of the page. 104 | 105 | ### start 106 | 107 | **Type:** Number 108 | 109 | The current start position from which iteration will start. 110 | 111 | ## Constructor Summary 112 | 113 | PagingModel(elements : Iterator, count : Number) Constructs the PagingModel using the specified iterator and count value. 114 | 115 | PagingModel(elements : Collection) Constructs the PagingModel using the specified collection. 116 | 117 | ## Method Summary 118 | 119 | ### appendPageSize 120 | 121 | **Signature:** `static appendPageSize(url : URL, pageSize : Number) : URL` 122 | 123 | Returns an URL containing the page size parameter appended to the specified url. 124 | 125 | ### appendPaging 126 | 127 | **Signature:** `appendPaging(url : URL) : URL` 128 | 129 | Returns an URL by appending the current page start position and the current page size to the URL. 130 | 131 | ### appendPaging 132 | 133 | **Signature:** `appendPaging(url : URL, position : Number) : URL` 134 | 135 | Returns an URL by appending the paging parameters for a desired page start position and the current page size to the specified url. 136 | 137 | ### getCount 138 | 139 | **Signature:** `getCount() : Number` 140 | 141 | Returns the count of the number of items in the model. 142 | 143 | ### getCurrentPage 144 | 145 | **Signature:** `getCurrentPage() : Number` 146 | 147 | Returns the index number of the current page. 148 | 149 | ### getEnd 150 | 151 | **Signature:** `getEnd() : Number` 152 | 153 | Returns the index of the last element on the current page. 154 | 155 | ### getMaxPage 156 | 157 | **Signature:** `getMaxPage() : Number` 158 | 159 | Returns the maximum possible page number. 160 | 161 | ### getPageCount 162 | 163 | **Signature:** `getPageCount() : Number` 164 | 165 | Returns the total page count. 166 | 167 | ### getPageElements 168 | 169 | **Signature:** `getPageElements() : Iterator` 170 | 171 | Returns an iterator that can be used to iterate through the elements of the current page. 172 | 173 | ### getPageSize 174 | 175 | **Signature:** `getPageSize() : Number` 176 | 177 | Returns the size of the page. 178 | 179 | ### getStart 180 | 181 | **Signature:** `getStart() : Number` 182 | 183 | Returns the current start position from which iteration will start. 184 | 185 | ### isEmpty 186 | 187 | **Signature:** `isEmpty() : boolean` 188 | 189 | Identifies if the model is empty. 190 | 191 | ### setPageSize 192 | 193 | **Signature:** `setPageSize(pageSize : Number) : void` 194 | 195 | Sets the size of the page. 196 | 197 | ### setStart 198 | 199 | **Signature:** `setStart(start : Number) : void` 200 | 201 | Sets the current start position from which iteration will start. 202 | 203 | ## Constructor Detail 204 | 205 | ## Method Detail 206 | 207 | ## Method Details 208 | 209 | ### appendPageSize 210 | 211 | **Signature:** `static appendPageSize(url : URL, pageSize : Number) : URL` 212 | 213 | **Description:** Returns an URL containing the page size parameter appended to the specified url. The name of the page size parameter is 'sz' (see PAGE_SIZE_PARAMETER). The start position parameter is not appended to the returned URL. 214 | 215 | **Parameters:** 216 | 217 | - `url`: the URL to append the page size parameter to. 218 | - `pageSize`: the page size 219 | 220 | **Returns:** 221 | 222 | an URL that contains the page size parameter. 223 | 224 | --- 225 | 226 | ### appendPaging 227 | 228 | **Signature:** `appendPaging(url : URL) : URL` 229 | 230 | **Description:** Returns an URL by appending the current page start position and the current page size to the URL. 231 | 232 | **Parameters:** 233 | 234 | - `url`: the URL to append the current paging position to. 235 | 236 | **Returns:** 237 | 238 | an URL containing the current paging position. 239 | 240 | --- 241 | 242 | ### appendPaging 243 | 244 | **Signature:** `appendPaging(url : URL, position : Number) : URL` 245 | 246 | **Description:** Returns an URL by appending the paging parameters for a desired page start position and the current page size to the specified url. The name of the page start position parameter is 'start' (see PAGING_START_PARAMETER) and the page size parameter is 'sz' (see PAGE_SIZE_PARAMETER). 247 | 248 | **Parameters:** 249 | 250 | - `url`: the URL to append the paging parameter to. 251 | - `position`: the start position. 252 | 253 | **Returns:** 254 | 255 | an URL that contains the paging parameters. 256 | 257 | --- 258 | 259 | ### getCount 260 | 261 | **Signature:** `getCount() : Number` 262 | 263 | **Description:** Returns the count of the number of items in the model. 264 | 265 | **Returns:** 266 | 267 | the count of the number of items in the model. 268 | 269 | --- 270 | 271 | ### getCurrentPage 272 | 273 | **Signature:** `getCurrentPage() : Number` 274 | 275 | **Description:** Returns the index number of the current page. The page counting starts with 0. The method also works with a miss-aligned start. In that case the start is always treated as the start of a page. 276 | 277 | **Returns:** 278 | 279 | the index number of the current page. 280 | 281 | --- 282 | 283 | ### getEnd 284 | 285 | **Signature:** `getEnd() : Number` 286 | 287 | **Description:** Returns the index of the last element on the current page. 288 | 289 | **Returns:** 290 | 291 | the index of the last element on the current page. 292 | 293 | --- 294 | 295 | ### getMaxPage 296 | 297 | **Signature:** `getMaxPage() : Number` 298 | 299 | **Description:** Returns the maximum possible page number. Counting for pages starts with 0. The method also works with a miss-aligned start. In that case the returned number might be higher than ((count-1) / pageSize). 300 | 301 | **Returns:** 302 | 303 | the maximum possible page number. 304 | 305 | --- 306 | 307 | ### getPageCount 308 | 309 | **Signature:** `getPageCount() : Number` 310 | 311 | **Description:** Returns the total page count. The method also works with a miss-aligned start. In that case the returned number might be higher than (count / pageSize). 312 | 313 | **Returns:** 314 | 315 | the total page count. 316 | 317 | --- 318 | 319 | ### getPageElements 320 | 321 | **Signature:** `getPageElements() : Iterator` 322 | 323 | **Description:** Returns an iterator that can be used to iterate through the elements of the current page. In case of a collection as the page models source, the method can be called multiple times. Each time a fresh iterator is returned. In case of an iterator as the page models source, the method must be called only once. The method will always return the same iterator, which means the method amy return an exhausted iterator. 324 | 325 | **Returns:** 326 | 327 | an iterator that you use to iterate through the elements of the current page. 328 | 329 | --- 330 | 331 | ### getPageSize 332 | 333 | **Signature:** `getPageSize() : Number` 334 | 335 | **Description:** Returns the size of the page. 336 | 337 | **Returns:** 338 | 339 | the size of the page. 340 | 341 | --- 342 | 343 | ### getStart 344 | 345 | **Signature:** `getStart() : Number` 346 | 347 | **Description:** Returns the current start position from which iteration will start. 348 | 349 | **Returns:** 350 | 351 | the current start position from which iteration will start. 352 | 353 | --- 354 | 355 | ### isEmpty 356 | 357 | **Signature:** `isEmpty() : boolean` 358 | 359 | **Description:** Identifies if the model is empty. 360 | 361 | **Returns:** 362 | 363 | true if the model is empty, false otherwise. 364 | 365 | --- 366 | 367 | ### setPageSize 368 | 369 | **Signature:** `setPageSize(pageSize : Number) : void` 370 | 371 | **Description:** Sets the size of the page. The page size must be greater or equal to 1. 372 | 373 | **Parameters:** 374 | 375 | - `pageSize`: the size of the page. 376 | 377 | --- 378 | 379 | ### setStart 380 | 381 | **Signature:** `setStart(start : Number) : void` 382 | 383 | **Description:** Sets the current start position from which iteration will start. 384 | 385 | **Parameters:** 386 | 387 | - `start`: the current start position from which iteration will start. 388 | 389 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/get-hook-reference.docs-only.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "get_hook_reference docs-only tests" 2 | 3 | # Chosen as next untested docs-only tool (no existing YAML test file). Covers success (ocapi_hooks, scapi_hooks), empty result (invalid guideName), structure validation, field extraction, and performance. 4 | 5 | tests: 6 | - it: "should list tools include get_hook_reference" 7 | request: 8 | jsonrpc: "2.0" 9 | id: "list-hooks-tool" 10 | method: "tools/list" 11 | params: {} 12 | expect: 13 | response: 14 | jsonrpc: "2.0" 15 | id: "list-hooks-tool" 16 | result: 17 | tools: "match:type:array" 18 | match:extractField: "tools.*.name" 19 | value: "match:arrayContains:get_hook_reference" 20 | stderr: "toBeEmpty" 21 | 22 | - it: "should retrieve OCAPI hook reference with categories" 23 | request: 24 | jsonrpc: "2.0" 25 | id: "ocapi-hooks-1" 26 | method: "tools/call" 27 | params: 28 | name: "get_hook_reference" 29 | arguments: 30 | guideName: "ocapi_hooks" 31 | expect: 32 | response: 33 | jsonrpc: "2.0" 34 | id: "ocapi-hooks-1" 35 | result: 36 | content: 37 | match:arrayElements: 38 | match:partial: 39 | type: "text" 40 | text: "match:contains:Shop API Hooks" 41 | isError: false 42 | performance: 43 | maxResponseTime: "1200ms" 44 | stderr: "toBeEmpty" 45 | 46 | - it: "should retrieve SCAPI hook reference including signatures" 47 | request: 48 | jsonrpc: "2.0" 49 | id: "scapi-hooks-1" 50 | method: "tools/call" 51 | params: 52 | name: "get_hook_reference" 53 | arguments: 54 | guideName: "scapi_hooks" 55 | expect: 56 | response: 57 | jsonrpc: "2.0" 58 | id: "scapi-hooks-1" 59 | result: 60 | content: 61 | match:arrayElements: 62 | match:partial: 63 | type: "text" 64 | text: "match:contains:Shopper Baskets API Hooks" 65 | isError: false 66 | performance: 67 | maxResponseTime: "1200ms" 68 | stderr: "toBeEmpty" 69 | 70 | - it: "should return empty array content for invalid guideName without error" 71 | request: 72 | jsonrpc: "2.0" 73 | id: "invalid-hooks-1" 74 | method: "tools/call" 75 | params: 76 | name: "get_hook_reference" 77 | arguments: 78 | guideName: "invalid_hooks" 79 | expect: 80 | response: 81 | jsonrpc: "2.0" 82 | id: "invalid-hooks-1" 83 | result: 84 | content: 85 | - type: "text" 86 | text: "match:regex:^\\[\\s*\\]$" 87 | isError: false 88 | performance: 89 | maxResponseTime: "800ms" 90 | stderr: "toBeEmpty" 91 | 92 | - it: "should contain at least one hook endpoint pattern in OCAPI response" 93 | request: 94 | jsonrpc: "2.0" 95 | id: "ocapi-hooks-endpoint-pattern" 96 | method: "tools/call" 97 | params: 98 | name: "get_hook_reference" 99 | arguments: 100 | guideName: "ocapi_hooks" 101 | expect: 102 | response: 103 | jsonrpc: "2.0" 104 | id: "ocapi-hooks-endpoint-pattern" 105 | result: 106 | content: 107 | match:arrayElements: 108 | match:partial: 109 | text: "match:regex:[\\s\\S]*GET /products/\\{id\\}[\\s\\S]*" 110 | isError: false 111 | performance: 112 | maxResponseTime: "1200ms" 113 | stderr: "toBeEmpty" 114 | 115 | - it: "should contain at least one SCAPI signature pattern in response" 116 | request: 117 | jsonrpc: "2.0" 118 | id: "scapi-hooks-signature-pattern" 119 | method: "tools/call" 120 | params: 121 | name: "get_hook_reference" 122 | arguments: 123 | guideName: "scapi_hooks" 124 | expect: 125 | response: 126 | jsonrpc: "2.0" 127 | id: "scapi-hooks-signature-pattern" 128 | result: 129 | content: 130 | match:arrayElements: 131 | match:partial: 132 | text: "match:contains:modifyPOSTResponse(basket : dw.order.Basket" 133 | isError: false 134 | performance: 135 | maxResponseTime: "1200ms" 136 | stderr: "toBeEmpty" 137 | 138 | - it: "should reject missing guideName with error flag and message" 139 | request: 140 | jsonrpc: "2.0" 141 | id: "missing-param-1" 142 | method: "tools/call" 143 | params: 144 | name: "get_hook_reference" 145 | arguments: {} 146 | expect: 147 | response: 148 | jsonrpc: "2.0" 149 | id: "missing-param-1" 150 | result: 151 | content: 152 | match:arrayElements: 153 | match:partial: 154 | type: "text" 155 | text: "match:contains:guideName must be a non-empty string" 156 | isError: true 157 | performance: 158 | maxResponseTime: "600ms" 159 | stderr: "toBeEmpty" 160 | 161 | - it: "should reject empty guideName with same validation error" 162 | request: 163 | jsonrpc: "2.0" 164 | id: "empty-param-1" 165 | method: "tools/call" 166 | params: 167 | name: "get_hook_reference" 168 | arguments: 169 | guideName: "" 170 | expect: 171 | response: 172 | jsonrpc: "2.0" 173 | id: "empty-param-1" 174 | result: 175 | content: 176 | match:arrayElements: 177 | match:partial: 178 | type: "text" 179 | text: "match:contains:guideName must be a non-empty string" 180 | isError: true 181 | performance: 182 | maxResponseTime: "600ms" 183 | stderr: "toBeEmpty" 184 | 185 | - it: "OCAPI response should include multiple categories" 186 | request: 187 | jsonrpc: "2.0" 188 | id: "ocapi-categories-1" 189 | method: "tools/call" 190 | params: 191 | name: "get_hook_reference" 192 | arguments: 193 | guideName: "ocapi_hooks" 194 | expect: 195 | response: 196 | jsonrpc: "2.0" 197 | id: "ocapi-categories-1" 198 | result: 199 | content: 200 | match:arrayElements: 201 | match:partial: 202 | text: "match:contains:Data API Hooks" 203 | isError: false 204 | performance: 205 | maxResponseTime: "1200ms" 206 | stderr: "toBeEmpty" 207 | 208 | - it: "SCAPI response should include multiple categories" 209 | request: 210 | jsonrpc: "2.0" 211 | id: "scapi-categories-1" 212 | method: "tools/call" 213 | params: 214 | name: "get_hook_reference" 215 | arguments: 216 | guideName: "scapi_hooks" 217 | expect: 218 | response: 219 | jsonrpc: "2.0" 220 | id: "scapi-categories-1" 221 | result: 222 | content: 223 | match:arrayElements: 224 | match:partial: 225 | text: "match:contains:Shopper Orders API Hooks" 226 | isError: false 227 | performance: 228 | maxResponseTime: "1200ms" 229 | stderr: "toBeEmpty" 230 | 231 | - it: "SCAPI response should contain multiple distinct hook signatures" 232 | request: 233 | jsonrpc: "2.0" 234 | id: "scapi-signatures-1" 235 | method: "tools/call" 236 | params: 237 | name: "get_hook_reference" 238 | arguments: 239 | guideName: "scapi_hooks" 240 | expect: 241 | response: 242 | jsonrpc: "2.0" 243 | id: "scapi-signatures-1" 244 | result: 245 | content: 246 | match:arrayElements: 247 | match:partial: 248 | text: "match:regex:[\\s\\S]*beforePOST\\(basket : dw.order.Basket[\\s\\S]*afterPOST\\(basket : dw.order.Basket[\\s\\S]*" 249 | isError: false 250 | performance: 251 | maxResponseTime: "1500ms" 252 | stderr: "toBeEmpty" 253 | 254 | - it: "OCAPI response should have multiple hookPoints for basket POST" 255 | request: 256 | jsonrpc: "2.0" 257 | id: "ocapi-multiple-hookpoints-1" 258 | method: "tools/call" 259 | params: 260 | name: "get_hook_reference" 261 | arguments: 262 | guideName: "ocapi_hooks" 263 | expect: 264 | response: 265 | jsonrpc: "2.0" 266 | id: "ocapi-multiple-hookpoints-1" 267 | result: 268 | content: 269 | match:arrayElements: 270 | match:partial: 271 | text: "match:regex:[\\s\\S]*POST /baskets[\\s\\S]*modifyPOSTResponse[\\s\\S]*validateBasket[\\s\\S]*" 272 | isError: false 273 | performance: 274 | maxResponseTime: "1500ms" 275 | stderr: "toBeEmpty" 276 | 277 | - it: "SCAPI response should NOT contain unrelated error text" 278 | request: 279 | jsonrpc: "2.0" 280 | id: "scapi-negative-1" 281 | method: "tools/call" 282 | params: 283 | name: "get_hook_reference" 284 | arguments: 285 | guideName: "scapi_hooks" 286 | expect: 287 | response: 288 | jsonrpc: "2.0" 289 | id: "scapi-negative-1" 290 | result: 291 | content: 292 | match:arrayElements: 293 | match:partial: 294 | text: "match:not:contains:guideName must be a non-empty string" 295 | isError: false 296 | performance: 297 | maxResponseTime: "1200ms" 298 | stderr: "toBeEmpty" 299 | 300 | - it: "SCAPI content payload should be reasonably large (length > 500 chars)" 301 | request: 302 | jsonrpc: "2.0" 303 | id: "scapi-size-1" 304 | method: "tools/call" 305 | params: 306 | name: "get_hook_reference" 307 | arguments: 308 | guideName: "scapi_hooks" 309 | expect: 310 | response: 311 | jsonrpc: "2.0" 312 | id: "scapi-size-1" 313 | result: 314 | content: 315 | match:arrayElements: 316 | match:partial: 317 | text: "match:regex:^[\\s\\S]{500,}$" 318 | isError: false 319 | performance: 320 | maxResponseTime: "1500ms" 321 | stderr: "toBeEmpty" 322 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/get-hook-reference.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "get_hook_reference full-mode tests" 2 | 3 | # Chosen as next untested full-mode tool (no existing YAML test file). Covers success (ocapi_hooks, scapi_hooks), empty result (invalid guideName), structure validation, field extraction, and performance. 4 | 5 | tests: 6 | - it: "should list tools include get_hook_reference" 7 | request: 8 | jsonrpc: "2.0" 9 | id: "list-hooks-tool" 10 | method: "tools/list" 11 | params: {} 12 | expect: 13 | response: 14 | jsonrpc: "2.0" 15 | id: "list-hooks-tool" 16 | result: 17 | tools: "match:type:array" 18 | match:extractField: "tools.*.name" 19 | value: "match:arrayContains:get_hook_reference" 20 | stderr: "toBeEmpty" 21 | 22 | - it: "should retrieve OCAPI hook reference with categories" 23 | request: 24 | jsonrpc: "2.0" 25 | id: "ocapi-hooks-1" 26 | method: "tools/call" 27 | params: 28 | name: "get_hook_reference" 29 | arguments: 30 | guideName: "ocapi_hooks" 31 | expect: 32 | response: 33 | jsonrpc: "2.0" 34 | id: "ocapi-hooks-1" 35 | result: 36 | content: 37 | match:arrayElements: 38 | match:partial: 39 | type: "text" 40 | text: "match:contains:Shop API Hooks" 41 | isError: false 42 | performance: 43 | maxResponseTime: "1200ms" 44 | stderr: "toBeEmpty" 45 | 46 | - it: "should retrieve SCAPI hook reference including signatures" 47 | request: 48 | jsonrpc: "2.0" 49 | id: "scapi-hooks-1" 50 | method: "tools/call" 51 | params: 52 | name: "get_hook_reference" 53 | arguments: 54 | guideName: "scapi_hooks" 55 | expect: 56 | response: 57 | jsonrpc: "2.0" 58 | id: "scapi-hooks-1" 59 | result: 60 | content: 61 | match:arrayElements: 62 | match:partial: 63 | type: "text" 64 | text: "match:contains:Shopper Baskets API Hooks" 65 | isError: false 66 | performance: 67 | maxResponseTime: "1200ms" 68 | stderr: "toBeEmpty" 69 | 70 | - it: "should return empty array content for invalid guideName without error" 71 | request: 72 | jsonrpc: "2.0" 73 | id: "invalid-hooks-1" 74 | method: "tools/call" 75 | params: 76 | name: "get_hook_reference" 77 | arguments: 78 | guideName: "invalid_hooks" 79 | expect: 80 | response: 81 | jsonrpc: "2.0" 82 | id: "invalid-hooks-1" 83 | result: 84 | content: 85 | - type: "text" 86 | text: "match:regex:^\\[\\s*\\]$" 87 | isError: false 88 | performance: 89 | maxResponseTime: "800ms" 90 | stderr: "toBeEmpty" 91 | 92 | - it: "should contain at least one hook endpoint pattern in OCAPI response" 93 | request: 94 | jsonrpc: "2.0" 95 | id: "ocapi-hooks-endpoint-pattern" 96 | method: "tools/call" 97 | params: 98 | name: "get_hook_reference" 99 | arguments: 100 | guideName: "ocapi_hooks" 101 | expect: 102 | response: 103 | jsonrpc: "2.0" 104 | id: "ocapi-hooks-endpoint-pattern" 105 | result: 106 | content: 107 | match:arrayElements: 108 | match:partial: 109 | text: "match:regex:[\\s\\S]*GET /products/\\{id\\}[\\s\\S]*" 110 | isError: false 111 | performance: 112 | maxResponseTime: "1200ms" 113 | stderr: "toBeEmpty" 114 | 115 | - it: "should contain at least one SCAPI signature pattern in response" 116 | request: 117 | jsonrpc: "2.0" 118 | id: "scapi-hooks-signature-pattern" 119 | method: "tools/call" 120 | params: 121 | name: "get_hook_reference" 122 | arguments: 123 | guideName: "scapi_hooks" 124 | expect: 125 | response: 126 | jsonrpc: "2.0" 127 | id: "scapi-hooks-signature-pattern" 128 | result: 129 | content: 130 | match:arrayElements: 131 | match:partial: 132 | text: "match:contains:modifyPOSTResponse(basket : dw.order.Basket" 133 | isError: false 134 | performance: 135 | maxResponseTime: "1200ms" 136 | stderr: "toBeEmpty" 137 | 138 | - it: "should reject missing guideName with error flag and message" 139 | request: 140 | jsonrpc: "2.0" 141 | id: "missing-param-1" 142 | method: "tools/call" 143 | params: 144 | name: "get_hook_reference" 145 | arguments: {} 146 | expect: 147 | response: 148 | jsonrpc: "2.0" 149 | id: "missing-param-1" 150 | result: 151 | content: 152 | match:arrayElements: 153 | match:partial: 154 | type: "text" 155 | text: "match:contains:guideName must be a non-empty string" 156 | isError: true 157 | performance: 158 | maxResponseTime: "600ms" 159 | stderr: "toBeEmpty" 160 | 161 | - it: "should reject empty guideName with same validation error" 162 | request: 163 | jsonrpc: "2.0" 164 | id: "empty-param-1" 165 | method: "tools/call" 166 | params: 167 | name: "get_hook_reference" 168 | arguments: 169 | guideName: "" 170 | expect: 171 | response: 172 | jsonrpc: "2.0" 173 | id: "empty-param-1" 174 | result: 175 | content: 176 | match:arrayElements: 177 | match:partial: 178 | type: "text" 179 | text: "match:contains:guideName must be a non-empty string" 180 | isError: true 181 | performance: 182 | maxResponseTime: "600ms" 183 | stderr: "toBeEmpty" 184 | 185 | - it: "OCAPI response should include multiple categories" 186 | request: 187 | jsonrpc: "2.0" 188 | id: "ocapi-categories-1" 189 | method: "tools/call" 190 | params: 191 | name: "get_hook_reference" 192 | arguments: 193 | guideName: "ocapi_hooks" 194 | expect: 195 | response: 196 | jsonrpc: "2.0" 197 | id: "ocapi-categories-1" 198 | result: 199 | content: 200 | match:arrayElements: 201 | match:partial: 202 | text: "match:contains:Data API Hooks" 203 | isError: false 204 | performance: 205 | maxResponseTime: "1200ms" 206 | stderr: "toBeEmpty" 207 | 208 | - it: "SCAPI response should include multiple categories" 209 | request: 210 | jsonrpc: "2.0" 211 | id: "scapi-categories-1" 212 | method: "tools/call" 213 | params: 214 | name: "get_hook_reference" 215 | arguments: 216 | guideName: "scapi_hooks" 217 | expect: 218 | response: 219 | jsonrpc: "2.0" 220 | id: "scapi-categories-1" 221 | result: 222 | content: 223 | match:arrayElements: 224 | match:partial: 225 | text: "match:contains:Shopper Orders API Hooks" 226 | isError: false 227 | performance: 228 | maxResponseTime: "1200ms" 229 | stderr: "toBeEmpty" 230 | 231 | - it: "SCAPI response should contain multiple distinct hook signatures" 232 | request: 233 | jsonrpc: "2.0" 234 | id: "scapi-signatures-1" 235 | method: "tools/call" 236 | params: 237 | name: "get_hook_reference" 238 | arguments: 239 | guideName: "scapi_hooks" 240 | expect: 241 | response: 242 | jsonrpc: "2.0" 243 | id: "scapi-signatures-1" 244 | result: 245 | content: 246 | match:arrayElements: 247 | match:partial: 248 | text: "match:regex:[\\s\\S]*beforePOST\\(basket : dw.order.Basket[\\s\\S]*afterPOST\\(basket : dw.order.Basket[\\s\\S]*" 249 | isError: false 250 | performance: 251 | maxResponseTime: "1500ms" 252 | stderr: "toBeEmpty" 253 | 254 | - it: "OCAPI response should have multiple hookPoints for basket POST" 255 | request: 256 | jsonrpc: "2.0" 257 | id: "ocapi-multiple-hookpoints-1" 258 | method: "tools/call" 259 | params: 260 | name: "get_hook_reference" 261 | arguments: 262 | guideName: "ocapi_hooks" 263 | expect: 264 | response: 265 | jsonrpc: "2.0" 266 | id: "ocapi-multiple-hookpoints-1" 267 | result: 268 | content: 269 | match:arrayElements: 270 | match:partial: 271 | text: "match:regex:[\\s\\S]*POST /baskets[\\s\\S]*modifyPOSTResponse[\\s\\S]*validateBasket[\\s\\S]*" 272 | isError: false 273 | performance: 274 | maxResponseTime: "1500ms" 275 | stderr: "toBeEmpty" 276 | 277 | - it: "SCAPI response should NOT contain unrelated error text" 278 | request: 279 | jsonrpc: "2.0" 280 | id: "scapi-negative-1" 281 | method: "tools/call" 282 | params: 283 | name: "get_hook_reference" 284 | arguments: 285 | guideName: "scapi_hooks" 286 | expect: 287 | response: 288 | jsonrpc: "2.0" 289 | id: "scapi-negative-1" 290 | result: 291 | content: 292 | match:arrayElements: 293 | match:partial: 294 | text: "match:not:contains:guideName must be a non-empty string" 295 | isError: false 296 | performance: 297 | maxResponseTime: "1200ms" 298 | stderr: "toBeEmpty" 299 | 300 | - it: "SCAPI content payload should be reasonably large (length > 500 chars)" 301 | request: 302 | jsonrpc: "2.0" 303 | id: "scapi-size-1" 304 | method: "tools/call" 305 | params: 306 | name: "get_hook_reference" 307 | arguments: 308 | guideName: "scapi_hooks" 309 | expect: 310 | response: 311 | jsonrpc: "2.0" 312 | id: "scapi-size-1" 313 | result: 314 | content: 315 | match:arrayElements: 316 | match:partial: 317 | text: "match:regex:^[\\s\\S]{500,}$" 318 | isError: false 319 | performance: 320 | maxResponseTime: "1500ms" 321 | stderr: "toBeEmpty" 322 | ``` -------------------------------------------------------------------------------- /docs/TopLevel/DataView.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: TopLevel 2 | 3 | # Class DataView 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - DataView 9 | 10 | ## Description 11 | 12 | The DataView provides low level access to ArrayBuffer. 13 | 14 | ## Properties 15 | 16 | ### buffer 17 | 18 | **Type:** ArrayBuffer 19 | 20 | The array buffer referenced by this view. 21 | 22 | ### byteLength 23 | 24 | **Type:** Number 25 | 26 | The number of bytes in the array buffer used by this view. 27 | 28 | ### byteOffset 29 | 30 | **Type:** Number 31 | 32 | The start offset for this view within the array buffer. 33 | 34 | ## Constructor Summary 35 | 36 | DataView(buffer : ArrayBuffer, byteOffset : Number, byteLength : Number) Creates a data view on the given ArrayBuffer. 37 | 38 | ## Method Summary 39 | 40 | ### getFloat32 41 | 42 | **Signature:** `getFloat32(byteOffset : Number, littleEndian : boolean) : Number` 43 | 44 | Returns the 32-bit floating point number at the given offset. 45 | 46 | ### getFloat64 47 | 48 | **Signature:** `getFloat64(byteOffset : Number, littleEndian : boolean) : Number` 49 | 50 | Returns the 64-bit floating point number at the given offset. 51 | 52 | ### getInt16 53 | 54 | **Signature:** `getInt16(byteOffset : Number, littleEndian : boolean) : Number` 55 | 56 | Returns the 16-bit signed integer number at the given offset. 57 | 58 | ### getInt32 59 | 60 | **Signature:** `getInt32(byteOffset : Number, littleEndian : boolean) : Number` 61 | 62 | Returns the 32-bit signed integer number at the given offset. 63 | 64 | ### getInt8 65 | 66 | **Signature:** `getInt8(byteOffset : Number) : Number` 67 | 68 | Returns the 8-bit signed integer number at the given offset. 69 | 70 | ### getUint16 71 | 72 | **Signature:** `getUint16(byteOffset : Number, littleEndian : boolean) : Number` 73 | 74 | Returns the 16-bit unsigned integer number at the given offset. 75 | 76 | ### getUint32 77 | 78 | **Signature:** `getUint32(byteOffset : Number, littleEndian : boolean) : Number` 79 | 80 | Returns the 32-bit unsigned integer number at the given offset. 81 | 82 | ### getUint8 83 | 84 | **Signature:** `getUint8(byteOffset : Number) : Number` 85 | 86 | Returns the 8-bit unsigned integer number at the given offset. 87 | 88 | ### setFloat32 89 | 90 | **Signature:** `setFloat32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 91 | 92 | Writes a 32-bit floating point number into the byte array at the given offset. 93 | 94 | ### setFloat64 95 | 96 | **Signature:** `setFloat64(byteOffset : Number, value : Number, littleEndian : boolean) : void` 97 | 98 | Writes a 64-bit floating point number into the byte array at the given offset. 99 | 100 | ### setInt16 101 | 102 | **Signature:** `setInt16(byteOffset : Number, value : Number, littleEndian : boolean) : void` 103 | 104 | Writes a 16-bit signed integer number into the byte array at the given offset. 105 | 106 | ### setInt32 107 | 108 | **Signature:** `setInt32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 109 | 110 | Writes a 32-bit signed integer number into the byte array at the given offset. 111 | 112 | ### setInt8 113 | 114 | **Signature:** `setInt8(byteOffset : Number, value : Number) : void` 115 | 116 | Writes an 8-bit signed integer number into the byte array at the given offset. 117 | 118 | ### setUint16 119 | 120 | **Signature:** `setUint16(byteOffset : Number, value : Number, littleEndian : boolean) : void` 121 | 122 | Writes a 16-bit unsigned integer number into the byte array at the given offset. 123 | 124 | ### setUint32 125 | 126 | **Signature:** `setUint32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 127 | 128 | Writes a 32-bit unsigned integer number into the byte array at the given offset. 129 | 130 | ### setUint8 131 | 132 | **Signature:** `setUint8(byteOffset : Number, value : Number) : void` 133 | 134 | Writes an 8-bit unsigned integer number into the byte array at the given offset. 135 | 136 | ## Constructor Detail 137 | 138 | ## Method Detail 139 | 140 | ## Method Details 141 | 142 | ### getFloat32 143 | 144 | **Signature:** `getFloat32(byteOffset : Number, littleEndian : boolean) : Number` 145 | 146 | **Description:** Returns the 32-bit floating point number at the given offset. 147 | 148 | **Parameters:** 149 | 150 | - `byteOffset`: The offset within the view. 151 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 152 | 153 | --- 154 | 155 | ### getFloat64 156 | 157 | **Signature:** `getFloat64(byteOffset : Number, littleEndian : boolean) : Number` 158 | 159 | **Description:** Returns the 64-bit floating point number at the given offset. 160 | 161 | **Parameters:** 162 | 163 | - `byteOffset`: The offset within the view. 164 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 165 | 166 | --- 167 | 168 | ### getInt16 169 | 170 | **Signature:** `getInt16(byteOffset : Number, littleEndian : boolean) : Number` 171 | 172 | **Description:** Returns the 16-bit signed integer number at the given offset. 173 | 174 | **Parameters:** 175 | 176 | - `byteOffset`: The offset within the view. 177 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 178 | 179 | --- 180 | 181 | ### getInt32 182 | 183 | **Signature:** `getInt32(byteOffset : Number, littleEndian : boolean) : Number` 184 | 185 | **Description:** Returns the 32-bit signed integer number at the given offset. 186 | 187 | **Parameters:** 188 | 189 | - `byteOffset`: The offset within the view. 190 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 191 | 192 | --- 193 | 194 | ### getInt8 195 | 196 | **Signature:** `getInt8(byteOffset : Number) : Number` 197 | 198 | **Description:** Returns the 8-bit signed integer number at the given offset. 199 | 200 | **Parameters:** 201 | 202 | - `byteOffset`: The offset within the view. 203 | 204 | --- 205 | 206 | ### getUint16 207 | 208 | **Signature:** `getUint16(byteOffset : Number, littleEndian : boolean) : Number` 209 | 210 | **Description:** Returns the 16-bit unsigned integer number at the given offset. 211 | 212 | **Parameters:** 213 | 214 | - `byteOffset`: The offset within the view. 215 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 216 | 217 | --- 218 | 219 | ### getUint32 220 | 221 | **Signature:** `getUint32(byteOffset : Number, littleEndian : boolean) : Number` 222 | 223 | **Description:** Returns the 32-bit unsigned integer number at the given offset. 224 | 225 | **Parameters:** 226 | 227 | - `byteOffset`: The offset within the view. 228 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format. 229 | 230 | --- 231 | 232 | ### getUint8 233 | 234 | **Signature:** `getUint8(byteOffset : Number) : Number` 235 | 236 | **Description:** Returns the 8-bit unsigned integer number at the given offset. 237 | 238 | **Parameters:** 239 | 240 | - `byteOffset`: The offset within the view. 241 | 242 | --- 243 | 244 | ### setFloat32 245 | 246 | **Signature:** `setFloat32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 247 | 248 | **Description:** Writes a 32-bit floating point number into the byte array at the given offset. 249 | 250 | **Parameters:** 251 | 252 | - `byteOffset`: The offset within the view. 253 | - `value`: The value to be written. 254 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 255 | 256 | --- 257 | 258 | ### setFloat64 259 | 260 | **Signature:** `setFloat64(byteOffset : Number, value : Number, littleEndian : boolean) : void` 261 | 262 | **Description:** Writes a 64-bit floating point number into the byte array at the given offset. 263 | 264 | **Parameters:** 265 | 266 | - `byteOffset`: The offset within the view. 267 | - `value`: The value to be written. 268 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 269 | 270 | --- 271 | 272 | ### setInt16 273 | 274 | **Signature:** `setInt16(byteOffset : Number, value : Number, littleEndian : boolean) : void` 275 | 276 | **Description:** Writes a 16-bit signed integer number into the byte array at the given offset. 277 | 278 | **Parameters:** 279 | 280 | - `byteOffset`: The offset within the view. 281 | - `value`: The value to be written. 282 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 283 | 284 | --- 285 | 286 | ### setInt32 287 | 288 | **Signature:** `setInt32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 289 | 290 | **Description:** Writes a 32-bit signed integer number into the byte array at the given offset. 291 | 292 | **Parameters:** 293 | 294 | - `byteOffset`: The offset within the view. 295 | - `value`: The value to be written. 296 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 297 | 298 | --- 299 | 300 | ### setInt8 301 | 302 | **Signature:** `setInt8(byteOffset : Number, value : Number) : void` 303 | 304 | **Description:** Writes an 8-bit signed integer number into the byte array at the given offset. 305 | 306 | **Parameters:** 307 | 308 | - `byteOffset`: The offset within the view. 309 | - `value`: The value to be written. 310 | 311 | --- 312 | 313 | ### setUint16 314 | 315 | **Signature:** `setUint16(byteOffset : Number, value : Number, littleEndian : boolean) : void` 316 | 317 | **Description:** Writes a 16-bit unsigned integer number into the byte array at the given offset. 318 | 319 | **Parameters:** 320 | 321 | - `byteOffset`: The offset within the view. 322 | - `value`: The value to be written. 323 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 324 | 325 | --- 326 | 327 | ### setUint32 328 | 329 | **Signature:** `setUint32(byteOffset : Number, value : Number, littleEndian : boolean) : void` 330 | 331 | **Description:** Writes a 32-bit unsigned integer number into the byte array at the given offset. 332 | 333 | **Parameters:** 334 | 335 | - `byteOffset`: The offset within the view. 336 | - `value`: The value to be written. 337 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used. 338 | 339 | --- 340 | 341 | ### setUint8 342 | 343 | **Signature:** `setUint8(byteOffset : Number, value : Number) : void` 344 | 345 | **Description:** Writes an 8-bit unsigned integer number into the byte array at the given offset. 346 | 347 | **Parameters:** 348 | 349 | - `byteOffset`: The offset within the view. 350 | - `value`: The value to be written. 351 | 352 | --- ``` -------------------------------------------------------------------------------- /tests/client-factory.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ClientFactory } from '../src/core/handlers/client-factory.js'; 2 | import { HandlerContext } from '../src/core/handlers/base-handler.js'; 3 | import { Logger } from '../src/utils/logger.js'; 4 | import { SFCCConfig } from '../src/types/types.js'; 5 | 6 | // Mock the clients 7 | jest.mock('../src/clients/log-client.js'); 8 | jest.mock('../src/clients/ocapi-client.js'); 9 | jest.mock('../src/clients/ocapi/code-versions-client.js'); 10 | jest.mock('../src/clients/cartridge-generation-client.js'); 11 | 12 | describe('ClientFactory', () => { 13 | let mockLogger: jest.Mocked<Logger>; 14 | let factory: ClientFactory; 15 | 16 | beforeEach(() => { 17 | mockLogger = { 18 | debug: jest.fn(), 19 | info: jest.fn(), 20 | warn: jest.fn(), 21 | error: jest.fn(), 22 | timing: jest.fn(), 23 | } as any; 24 | }); 25 | 26 | describe('createLogClient', () => { 27 | it('should create log client when capabilities and config are available', () => { 28 | const context: HandlerContext = { 29 | logger: mockLogger, 30 | config: { hostname: 'test.com' } as SFCCConfig, 31 | capabilities: { 32 | canAccessLogs: true, 33 | canAccessOCAPI: false, 34 | }, 35 | }; 36 | factory = new ClientFactory(context, mockLogger); 37 | 38 | const client = factory.createLogClient(); 39 | 40 | expect(client).toBeDefined(); 41 | expect(mockLogger.debug).toHaveBeenCalledWith('Creating SFCC Log Client'); 42 | }); 43 | 44 | it('should return null when log access capability is missing', () => { 45 | const context: HandlerContext = { 46 | logger: mockLogger, 47 | config: { hostname: 'test.com' } as SFCCConfig, 48 | capabilities: { 49 | canAccessLogs: false, 50 | canAccessOCAPI: false, 51 | }, 52 | }; 53 | factory = new ClientFactory(context, mockLogger); 54 | 55 | const client = factory.createLogClient(); 56 | 57 | expect(client).toBeNull(); 58 | expect(mockLogger.debug).toHaveBeenCalledWith('Log client not created: missing log access capability or config'); 59 | }); 60 | 61 | it('should return null when config is missing', () => { 62 | const context: HandlerContext = { 63 | logger: mockLogger, 64 | config: undefined as any, 65 | capabilities: { 66 | canAccessLogs: true, 67 | canAccessOCAPI: false, 68 | }, 69 | }; 70 | factory = new ClientFactory(context, mockLogger); 71 | 72 | const client = factory.createLogClient(); 73 | 74 | expect(client).toBeNull(); 75 | expect(mockLogger.debug).toHaveBeenCalledWith('Log client not created: missing log access capability or config'); 76 | }); 77 | }); 78 | 79 | describe('createOCAPIClient', () => { 80 | it('should create OCAPI client when all credentials are available', () => { 81 | const context: HandlerContext = { 82 | logger: mockLogger, 83 | config: { 84 | hostname: 'test.com', 85 | clientId: 'client123', 86 | clientSecret: 'secret123', 87 | } as SFCCConfig, 88 | capabilities: { 89 | canAccessLogs: false, 90 | canAccessOCAPI: true, 91 | }, 92 | }; 93 | factory = new ClientFactory(context, mockLogger); 94 | 95 | const client = factory.createOCAPIClient(); 96 | 97 | expect(client).toBeDefined(); 98 | expect(mockLogger.debug).toHaveBeenCalledWith('Creating OCAPI Client'); 99 | }); 100 | 101 | it('should return null when OCAPI capability is missing', () => { 102 | const context: HandlerContext = { 103 | logger: mockLogger, 104 | config: { 105 | hostname: 'test.com', 106 | clientId: 'client123', 107 | clientSecret: 'secret123', 108 | } as SFCCConfig, 109 | capabilities: { 110 | canAccessLogs: false, 111 | canAccessOCAPI: false, 112 | }, 113 | }; 114 | factory = new ClientFactory(context, mockLogger); 115 | 116 | const client = factory.createOCAPIClient(); 117 | 118 | expect(client).toBeNull(); 119 | expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability'); 120 | }); 121 | 122 | it('should return null when hostname is missing', () => { 123 | const context: HandlerContext = { 124 | logger: mockLogger, 125 | config: { 126 | clientId: 'client123', 127 | clientSecret: 'secret123', 128 | } as SFCCConfig, 129 | capabilities: { 130 | canAccessLogs: false, 131 | canAccessOCAPI: true, 132 | }, 133 | }; 134 | factory = new ClientFactory(context, mockLogger); 135 | 136 | const client = factory.createOCAPIClient(); 137 | 138 | expect(client).toBeNull(); 139 | expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability'); 140 | }); 141 | 142 | it('should return null when clientId is missing', () => { 143 | const context: HandlerContext = { 144 | logger: mockLogger, 145 | config: { 146 | hostname: 'test.com', 147 | clientSecret: 'secret123', 148 | } as SFCCConfig, 149 | capabilities: { 150 | canAccessLogs: false, 151 | canAccessOCAPI: true, 152 | }, 153 | }; 154 | factory = new ClientFactory(context, mockLogger); 155 | 156 | const client = factory.createOCAPIClient(); 157 | 158 | expect(client).toBeNull(); 159 | expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability'); 160 | }); 161 | 162 | it('should return null when clientSecret is missing', () => { 163 | const context: HandlerContext = { 164 | logger: mockLogger, 165 | config: { 166 | hostname: 'test.com', 167 | clientId: 'client123', 168 | } as SFCCConfig, 169 | capabilities: { 170 | canAccessLogs: false, 171 | canAccessOCAPI: true, 172 | }, 173 | }; 174 | factory = new ClientFactory(context, mockLogger); 175 | 176 | const client = factory.createOCAPIClient(); 177 | 178 | expect(client).toBeNull(); 179 | expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability'); 180 | }); 181 | }); 182 | 183 | describe('createCodeVersionsClient', () => { 184 | it('should create code versions client when all credentials are available', () => { 185 | const context: HandlerContext = { 186 | logger: mockLogger, 187 | config: { 188 | hostname: 'test.com', 189 | clientId: 'client123', 190 | clientSecret: 'secret123', 191 | } as SFCCConfig, 192 | capabilities: { 193 | canAccessLogs: false, 194 | canAccessOCAPI: true, 195 | }, 196 | }; 197 | factory = new ClientFactory(context, mockLogger); 198 | 199 | const client = factory.createCodeVersionsClient(); 200 | 201 | expect(client).toBeDefined(); 202 | expect(mockLogger.debug).toHaveBeenCalledWith('Creating OCAPI Code Versions Client'); 203 | }); 204 | 205 | it('should return null when credentials are missing', () => { 206 | const context: HandlerContext = { 207 | logger: mockLogger, 208 | config: { 209 | hostname: 'test.com', 210 | } as SFCCConfig, 211 | capabilities: { 212 | canAccessLogs: false, 213 | canAccessOCAPI: true, 214 | }, 215 | }; 216 | factory = new ClientFactory(context, mockLogger); 217 | 218 | const client = factory.createCodeVersionsClient(); 219 | 220 | expect(client).toBeNull(); 221 | expect(mockLogger.debug).toHaveBeenCalledWith('Code versions client not created: missing OCAPI credentials or capability'); 222 | }); 223 | }); 224 | 225 | describe('createCartridgeClient', () => { 226 | it('should create cartridge client with default services', () => { 227 | const context: HandlerContext = { 228 | logger: mockLogger, 229 | config: {} as SFCCConfig, 230 | capabilities: { 231 | canAccessLogs: false, 232 | canAccessOCAPI: false, 233 | }, 234 | }; 235 | factory = new ClientFactory(context, mockLogger); 236 | 237 | const client = factory.createCartridgeClient(); 238 | 239 | expect(client).toBeDefined(); 240 | expect(mockLogger.debug).toHaveBeenCalledWith('Creating Cartridge Generation Client'); 241 | }); 242 | 243 | it('should create cartridge client with custom services', () => { 244 | const context: HandlerContext = { 245 | logger: mockLogger, 246 | config: {} as SFCCConfig, 247 | capabilities: { 248 | canAccessLogs: false, 249 | canAccessOCAPI: false, 250 | }, 251 | }; 252 | factory = new ClientFactory(context, mockLogger); 253 | 254 | const mockFileSystem = { writeFile: jest.fn() } as any; 255 | const mockPath = { join: jest.fn() } as any; 256 | 257 | const client = factory.createCartridgeClient(mockFileSystem, mockPath); 258 | 259 | expect(client).toBeDefined(); 260 | expect(mockLogger.debug).toHaveBeenCalledWith('Creating Cartridge Generation Client'); 261 | }); 262 | }); 263 | 264 | describe('getClientRequiredError', () => { 265 | it('should return OCAPI error message', () => { 266 | const error = ClientFactory.getClientRequiredError('OCAPI'); 267 | expect(error).toBe('OCAPI client not configured - ensure credentials are provided in full mode.'); 268 | }); 269 | 270 | it('should return Log error message', () => { 271 | const error = ClientFactory.getClientRequiredError('Log'); 272 | expect(error).toBe('Log client not configured - ensure log access is enabled.'); 273 | }); 274 | 275 | it('should return default error message for unknown client type', () => { 276 | const error = ClientFactory.getClientRequiredError('Unknown' as any); 277 | expect(error).toBe('Required client not configured.'); 278 | }); 279 | }); 280 | }); 281 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-hook-reference.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Programmatic tests for get_hook_reference tool (docs-only mode) 3 | * 4 | * Response formats discovered via aegis query: 5 | * Success (ocapi_hooks): { content:[{type:'text', text:'[ {"category": ... } ]'}], isError:false } 6 | * Success (scapi_hooks): similar but includes signature fields in JSON text 7 | * Invalid guideName: content text is "[]" (empty JSON array), isError:false 8 | * Validation error (missing/empty guideName): { content:[{text:'Error: guideName must be a non-empty string'}], isError:true } 9 | * 10 | * This suite validates: 11 | * - Tool presence in listTools 12 | * - Successful retrieval for ocapi_hooks & scapi_hooks 13 | * - Structural JSON parsing of categories & hooks 14 | * - Presence of multiple hookPoints for basket endpoints 15 | * - Presence of hook signatures in scapi_hooks 16 | * - Validation errors for missing & empty guideName 17 | * - Empty result handling for invalid guideName 18 | */ 19 | 20 | import { describe, test, before, after, beforeEach } from 'node:test'; 21 | import { strict as assert } from 'node:assert'; 22 | import { connect } from 'mcp-aegis'; 23 | 24 | 25 | function parseHookReference(result) { 26 | assert.equal(result.isError, false, 'Expected non-error result'); 27 | assert.ok(Array.isArray(result.content), 'content must be array'); 28 | assert.ok(result.content.length > 0, 'content should have at least one item'); 29 | const textItem = result.content.find(i => i.type === 'text'); 30 | assert.ok(textItem, 'text content item required'); 31 | const raw = textItem.text.trim(); 32 | let data; 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | try { data = JSON.parse(raw); } catch (e) { 35 | throw new Error('Failed to JSON.parse hook reference payload: ' + raw.slice(0,200)); 36 | } 37 | assert.ok(Array.isArray(data), 'Top-level parsed data must be array of categories'); 38 | return { raw, data }; 39 | } 40 | 41 | function findCategory(data, nameFragment) { 42 | return data.find(c => typeof c.category === 'string' && c.category.toLowerCase().includes(nameFragment.toLowerCase())); 43 | } 44 | 45 | function validateHookSchema(h) { 46 | assert.ok(typeof h.endpoint === 'string' && h.endpoint.length > 0, 'hook.endpoint must be non-empty string'); 47 | assert.ok(Array.isArray(h.hookPoints), 'hook.hookPoints must be array'); 48 | h.hookPoints.forEach(p => assert.ok(typeof p === 'string' && p.length > 0, 'hookPoint must be non-empty string')); 49 | if ('signature' in h) { 50 | assert.ok(typeof h.signature === 'string' && /\w+\(/.test(h.signature), 'signature should look like a function'); 51 | } 52 | } 53 | 54 | describe('get_hook_reference.docs-only (programmatic)', () => { 55 | let client; 56 | 57 | before(async () => { 58 | client = await connect('./aegis.config.docs-only.json'); 59 | }); 60 | 61 | after(async () => { 62 | if (client?.connected) await client.disconnect(); 63 | }); 64 | 65 | beforeEach(() => { 66 | client.clearAllBuffers(); // Recommended - comprehensive protection 67 | }); 68 | 69 | test('tool should be present in listTools', async () => { 70 | const tools = await client.listTools(); 71 | const names = tools.map(t => t.name); 72 | assert.ok(names.includes('get_hook_reference'), 'get_hook_reference should be registered'); 73 | }); 74 | 75 | test('ocapi_hooks reference basic structure', async () => { 76 | const result = await client.callTool('get_hook_reference', { guideName: 'ocapi_hooks' }); 77 | const { data, raw } = parseHookReference(result); 78 | assert.ok(data.length >= 2, 'Expect >=2 categories (Shop API Hooks, Data API Hooks)'); 79 | 80 | const shopCat = findCategory(data, 'shop api'); 81 | const dataCat = findCategory(data, 'data api'); 82 | assert.ok(shopCat, 'Shop API Hooks category missing'); 83 | assert.ok(dataCat, 'Data API Hooks category missing'); 84 | 85 | // Validate hook objects minimally 86 | for (const cat of [shopCat, dataCat]) { 87 | assert.ok(Array.isArray(cat.hooks), 'category.hooks must be array'); 88 | assert.ok(cat.hooks.length > 0, 'category.hooks should not be empty'); 89 | const firstHook = cat.hooks[0]; 90 | assert.ok(firstHook.endpoint, 'hook.endpoint required'); 91 | assert.ok(Array.isArray(firstHook.hookPoints), 'hook.hookPoints must be array'); 92 | } 93 | 94 | // Basket POST should have multiple hookPoints including modifyPOSTResponse or validateBasket 95 | const basketPost = shopCat.hooks.find(h => /POST \/baskets$/.test(h.endpoint)); 96 | if (basketPost) { 97 | const points = basketPost.hookPoints.join(' '); 98 | assert.ok(/modifyPOSTResponse/.test(points) || /validateBasket/.test(points), 'Basket POST should expose modifyPOSTResponse or validateBasket'); 99 | } else { 100 | // Non-fatal, but log for debugging 101 | console.warn('Basket POST endpoint not found in OCAPI shop hooks'); 102 | } 103 | 104 | assert.ok(raw.length > 500, 'Raw OCAPI JSON text should exceed 500 chars for richness'); 105 | }); 106 | 107 | test('scapi_hooks reference includes signatures & structural integrity', async () => { 108 | const result = await client.callTool('get_hook_reference', { guideName: 'scapi_hooks' }); 109 | const { data, raw } = parseHookReference(result); 110 | assert.ok(data.length >= 3, 'Expect >=3 categories for SCAPI'); 111 | 112 | const basketCat = findCategory(data, 'baskets'); 113 | const ordersCat = findCategory(data, 'orders'); 114 | assert.ok(basketCat, 'Basket category missing'); 115 | assert.ok(ordersCat, 'Orders category missing'); 116 | 117 | // Validate full schema for first 3 hooks of basket category (or all if <3) 118 | basketCat.hooks.slice(0,3).forEach(validateHookSchema); 119 | 120 | // SCAPI version adds signature field per hook (at least some hooks) 121 | const signaturePresent = basketCat.hooks.some(h => 'signature' in h && /\w+\(.+\)/.test(h.signature)); 122 | assert.ok(signaturePresent, 'Expected at least one signature field with function pattern in SCAPI hooks'); 123 | 124 | // Validate at least one hook shows beforePOST/afterPOST pair 125 | const anyBefore = basketCat.hooks.some(h => h.hookPoints.some(p => /beforePOST/.test(p))); 126 | const anyAfter = basketCat.hooks.some(h => h.hookPoints.some(p => /afterPOST/.test(p))); 127 | assert.ok(anyBefore && anyAfter, 'Expect beforePOST and afterPOST patterns in SCAPI baskets'); 128 | 129 | // Endpoint uniqueness across all categories 130 | const allEndpoints = data.flatMap(c => c.hooks.map(h => h.endpoint)); 131 | const uniqueCount = new Set(allEndpoints).size; 132 | // Relaxed: just ensure we have a reasonable number of endpoints and log uniqueness ratio for diagnostic purposes 133 | assert.ok(allEndpoints.length > 10, 'Should expose more than 10 endpoints total'); 134 | const uniquenessRatio = uniqueCount / allEndpoints.length; 135 | assert.ok(uniquenessRatio > 0.2, 'Uniqueness ratio should be >20% (diagnostic sanity check)'); 136 | // console.debug(`Endpoint uniqueness ratio: ${(uniquenessRatio*100).toFixed(1)}%`); 137 | 138 | assert.ok(raw.length > 800, 'Raw SCAPI JSON text should exceed 800 chars for richness'); 139 | }); 140 | 141 | // Additional targeted signature regex test 142 | test('SCAPI signatures follow expected function pattern', async () => { 143 | const result = await client.callTool('get_hook_reference', { guideName: 'scapi_hooks' }); 144 | const { data } = parseHookReference(result); 145 | const signatures = data.flatMap(c => c.hooks.filter(h => h.signature).map(h => h.signature)); 146 | assert.ok(signatures.length > 5, 'Expect multiple signatures'); 147 | // All signatures should have pattern name(args) : returnType 148 | const invalid = signatures.filter(sig => !/^\w+\([^)]*\)\s*:\s*\w+\.?\w*/.test(sig)); 149 | assert.ok(invalid.length === 0, 'All signatures should match basic function signature pattern'); 150 | }); 151 | 152 | test('OCAPI hook objects basic schema validation', async () => { 153 | const result = await client.callTool('get_hook_reference', { guideName: 'ocapi_hooks' }); 154 | const { data } = parseHookReference(result); 155 | data.forEach(cat => { 156 | cat.hooks.slice(0,5).forEach(h => { 157 | validateHookSchema(h); 158 | assert.ok(!('signature' in h) || typeof h.signature === 'string', 'signature optional but must be string if present'); 159 | }); 160 | }); 161 | }); 162 | 163 | test('invalid guideName returns empty array string (non-error)', async () => { 164 | const result = await client.callTool('get_hook_reference', { guideName: 'invalid_hooks' }); 165 | assert.equal(result.isError, false, 'invalid guide should not set isError'); 166 | const textItem = result.content.find(i => i.type === 'text'); 167 | assert.ok(textItem, 'text item required'); 168 | assert.equal(textItem.text.trim(), '[]', 'Expected empty JSON array payload'); 169 | }); 170 | 171 | test('missing guideName validation error', async () => { 172 | const result = await client.callTool('get_hook_reference', {}); // missing param 173 | assert.equal(result.isError, true, 'Should be error'); 174 | const msg = result.content[0].text; 175 | assert.ok(/guideName must be a non-empty string/.test(msg), 'Validation message missing'); 176 | }); 177 | 178 | test('empty guideName validation error', async () => { 179 | const result = await client.callTool('get_hook_reference', { guideName: '' }); 180 | assert.equal(result.isError, true, 'Should be error'); 181 | const msg = result.content[0].text; 182 | assert.ok(/guideName must be a non-empty string/.test(msg), 'Validation message missing'); 183 | }); 184 | 185 | }); 186 | ``` -------------------------------------------------------------------------------- /docs/dw_extensions.paymentrequest/PaymentRequestHooks.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.extensions.paymentrequest 2 | 3 | # Class PaymentRequestHooks 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - dw.extensions.paymentrequest.PaymentRequestHooks 8 | 9 | ## Description 10 | 11 | PaymentRequestHooks interface containing extension points for customizing Payment Requests. These hooks are executed in a transaction. The extension points (hook names), and the functions that are called by each extension point. A function must be defined inside a JavaScript source and must be exported. The script with the exported hook function must be located inside a site cartridge. Inside the site cartridge a 'package.json' file with a 'hooks' entry must exist. "hooks": "./hooks.json" The hooks entry links to a json file, relative to the 'package.json' file. This file lists all registered hooks inside the hooks property: "hooks": [ {"name": "dw.extensions.paymentrequest.getPaymentRequest", "script": "./paymentrequest.ds"} {"name": "dw.extensions.paymentrequest.shippingAddressChange", "script": "./paymentrequest.ds"} ] A hook entry has a 'name' and a 'script' property. The 'name' contains the extension point, the hook name. The 'script' contains the script relative to the hooks file, with the exported hook function. 12 | 13 | ## Constants 14 | 15 | ## Properties 16 | 17 | ## Constructor Summary 18 | 19 | ## Method Summary 20 | 21 | ### abort 22 | 23 | **Signature:** `abort(basket : Basket) : PaymentRequestHookResult` 24 | 25 | Called after the Payment Request user interface was canceled. 26 | 27 | ### authorizeOrderPayment 28 | 29 | **Signature:** `authorizeOrderPayment(order : Order, response : Object) : Status` 30 | 31 | Called after the shopper accepts the Payment Request payment for the given order. 32 | 33 | ### getPaymentRequest 34 | 35 | **Signature:** `getPaymentRequest(basket : Basket, parameters : Object) : PaymentRequestHookResult` 36 | 37 | Called to get the PaymentRequest constructor parameters for the given basket. 38 | 39 | ### placeOrder 40 | 41 | **Signature:** `placeOrder(order : Order) : PaymentRequestHookResult` 42 | 43 | Called after payment has been authorized and the given Payment Request order is ready to be placed. 44 | 45 | ### shippingAddressChange 46 | 47 | **Signature:** `shippingAddressChange(basket : Basket, details : Object) : PaymentRequestHookResult` 48 | 49 | Called after handling the Payment Request shippingaddresschange event for the given basket. 50 | 51 | ### shippingOptionChange 52 | 53 | **Signature:** `shippingOptionChange(basket : Basket, shippingMethod : ShippingMethod, details : Object) : PaymentRequestHookResult` 54 | 55 | Called after handling the Payment Request shippingoptionchange event for the given basket. 56 | 57 | ## Method Detail 58 | 59 | ## Method Details 60 | 61 | ### abort 62 | 63 | **Signature:** `abort(basket : Basket) : PaymentRequestHookResult` 64 | 65 | **Description:** Called after the Payment Request user interface was canceled. The given basket is the one that was passed to other hooks earlier in the Payment Request checkout process. It is not guaranteed that this hook will be executed for all Payment Request user interfaces canceled by shoppers or otherwise ended without a successful order. Calls to this hook are provided on a best-effort basis. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if possible. It is not guaranteed that the response with the hook result will be handled in the shopper browser in all cases. 66 | 67 | **Parameters:** 68 | 69 | - `basket`: the basket that was being checked out using Payment Request 70 | 71 | **Returns:** 72 | 73 | a non-null result ends the hook execution 74 | 75 | --- 76 | 77 | ### authorizeOrderPayment 78 | 79 | **Signature:** `authorizeOrderPayment(order : Order, response : Object) : Status` 80 | 81 | **Description:** Called after the shopper accepts the Payment Request payment for the given order. Basket customer information, billing address, and/or shipping address for the default shipment will have already been updated to reflect the available contact information provided by Payment Request. Any preexisting payment instruments on the basket will have been removed, and a single DW_ANDROID_PAY payment instrument added for the total amount. The given order will have been created from this updated basket. The purpose of this hook is to authorize the Payment Request payment for the order. If a non-error status is returned that means that you have successfully authorized the payment with your payment service provider. Your hook implementation must set the necessary payment status and transaction identifier data on the order as returned by the provider. Return an error status to indicate a problem, including unsuccessful authorization. See the Payment Request API for more information. 82 | 83 | **Parameters:** 84 | 85 | - `order`: the order paid using Payment Request 86 | - `response`: response to the accepted PaymentRequest 87 | 88 | **Returns:** 89 | 90 | a non-null status ends the hook execution 91 | 92 | --- 93 | 94 | ### getPaymentRequest 95 | 96 | **Signature:** `getPaymentRequest(basket : Basket, parameters : Object) : PaymentRequestHookResult` 97 | 98 | **Description:** Called to get the PaymentRequest constructor parameters for the given basket. You can set properties in the given parameters object to extend or override default properties set automatically based on the Google Pay configuration for your site. The parameters object will contain the following properties by default: methodData - array containing payment methods the web site accepts details - information about the transaction that the user is being asked to complete options - information about what options the web page wishes to use from the payment request system Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interaction is canceled. See the Payment Request API for more information. 99 | 100 | **Parameters:** 101 | 102 | - `basket`: the basket for the Payment Request request 103 | - `parameters`: object containing PaymentRequest constructor parameters 104 | 105 | **Returns:** 106 | 107 | a non-null result ends the hook execution 108 | 109 | --- 110 | 111 | ### placeOrder 112 | 113 | **Signature:** `placeOrder(order : Order) : PaymentRequestHookResult` 114 | 115 | **Description:** Called after payment has been authorized and the given Payment Request order is ready to be placed. The purpose of this hook is to place the order, or return a redirect URL that results in the order being placed when the shopper browser is navigated to it. The default implementation of this hook returns a redirect to COPlaceOrder-Submit with URL parameters order_id set to Order.getOrderNo() and order_token set to Order.getOrderToken() which corresponds to SiteGenesis-based implementations. Your hook implementation should return a result with a different redirect URL as necessary to place the order and show an order confirmation. Alternatively, your hook implementation itself can place the order and return a result with a redirect URL to an order confirmation page that does not place the order. This is inconsistent with SiteGenesis-based implementations so is not the default. Return an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled. 116 | 117 | **Parameters:** 118 | 119 | - `order`: the order paid using PaymentRequest 120 | 121 | **Returns:** 122 | 123 | a non-null result ends the hook execution 124 | 125 | --- 126 | 127 | ### shippingAddressChange 128 | 129 | **Signature:** `shippingAddressChange(basket : Basket, details : Object) : PaymentRequestHookResult` 130 | 131 | **Description:** Called after handling the Payment Request shippingaddresschange event for the given basket. Basket customer information and/or shipping address for the default shipment will have already been updated to reflect the available shipping address information provided by Payment Request. The basket will have already been calculated before this hook is called. Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled. See the Payment Request API for more information. 132 | 133 | **Parameters:** 134 | 135 | - `basket`: the basket being checked out using Payment Request 136 | - `details`: updated PaymentRequest object details 137 | 138 | **Returns:** 139 | 140 | a non-null result ends the hook execution 141 | 142 | --- 143 | 144 | ### shippingOptionChange 145 | 146 | **Signature:** `shippingOptionChange(basket : Basket, shippingMethod : ShippingMethod, details : Object) : PaymentRequestHookResult` 147 | 148 | **Description:** Called after handling the Payment Request shippingoptionchange event for the given basket. The given shipping method will have already been set on the basket. The basket will have already been calculated before this hook is called. Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled. See the Payment Request API for more information. 149 | 150 | **Parameters:** 151 | 152 | - `basket`: the basket being checked out using Payment Request 153 | - `shippingMethod`: the shipping method that was selected 154 | - `details`: updated PaymentRequest object details 155 | 156 | **Returns:** 157 | 158 | a non-null result ends the hook execution 159 | 160 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - search_system_object_attribute_groups Tool YAML Tests (Docs-Only Mode) 3 | # Tests that system object attribute group tools are NOT available in docs-only mode 4 | # This tool requires SFCC credentials and should not be available without them 5 | # However, the tool can still be called and should return authentication error 6 | # 7 | # Quick Test Commands: 8 | # aegis "tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --verbose 9 | # aegis "tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --debug --timing 10 | # aegis query --config "aegis.config.docs-only.json" 11 | # aegis query search_system_object_attribute_groups '{"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.docs-only.json" 12 | # ================================================================================== 13 | 14 | description: "search_system_object_attribute_groups tool tests - Docs-only mode tool availability and authentication errors" 15 | 16 | # ================================================================================== 17 | # TOOL UNAVAILABILITY IN DOCS-ONLY MODE 18 | # ================================================================================== 19 | tests: 20 | - it: "should NOT list search_system_object_attribute_groups tool in docs-only mode" 21 | request: 22 | jsonrpc: "2.0" 23 | id: "tool-not-available-docs" 24 | method: "tools/list" 25 | params: {} 26 | expect: 27 | response: 28 | jsonrpc: "2.0" 29 | id: "tool-not-available-docs" 30 | result: 31 | match:extractField: "tools.*.name" 32 | value: "match:not:arrayContains:search_system_object_attribute_groups" 33 | stderr: "toBeEmpty" 34 | 35 | # ================================================================================== 36 | # AUTHENTICATION ERROR TESTS (Tool Can Be Called But Returns Error) 37 | # ================================================================================== 38 | 39 | - it: "should return authentication error when calling search_system_object_attribute_groups in docs-only mode" 40 | request: 41 | jsonrpc: "2.0" 42 | id: "auth-error-product" 43 | method: "tools/call" 44 | params: 45 | name: "search_system_object_attribute_groups" 46 | arguments: 47 | objectType: "Product" 48 | searchRequest: 49 | query: 50 | match_all_query: {} 51 | count: 3 52 | expect: 53 | response: 54 | jsonrpc: "2.0" 55 | id: "auth-error-product" 56 | result: 57 | content: 58 | - type: "text" 59 | text: "match:contains:OCAPI client not configured" 60 | isError: true 61 | performance: 62 | maxResponseTime: "500ms" 63 | stderr: "toBeEmpty" 64 | 65 | - it: "should return authentication error for SitePreferences object type" 66 | request: 67 | jsonrpc: "2.0" 68 | id: "auth-error-siteprefs" 69 | method: "tools/call" 70 | params: 71 | name: "search_system_object_attribute_groups" 72 | arguments: 73 | objectType: "SitePreferences" 74 | searchRequest: 75 | query: 76 | match_all_query: {} 77 | count: 2 78 | expect: 79 | response: 80 | jsonrpc: "2.0" 81 | id: "auth-error-siteprefs" 82 | result: 83 | content: 84 | - type: "text" 85 | text: "match:contains:credentials are provided" 86 | isError: true 87 | stderr: "toBeEmpty" 88 | 89 | - it: "should return authentication error for Customer object type" 90 | request: 91 | jsonrpc: "2.0" 92 | id: "auth-error-customer" 93 | method: "tools/call" 94 | params: 95 | name: "search_system_object_attribute_groups" 96 | arguments: 97 | objectType: "Customer" 98 | searchRequest: 99 | query: 100 | text_query: 101 | fields: ["id"] 102 | search_phrase: "PersonalInfo" 103 | count: 5 104 | expect: 105 | response: 106 | jsonrpc: "2.0" 107 | id: "auth-error-customer" 108 | result: 109 | content: 110 | - type: "text" 111 | text: "match:contains:full mode" 112 | isError: true 113 | stderr: "toBeEmpty" 114 | 115 | - it: "should return authentication error for any object type with complex query" 116 | request: 117 | jsonrpc: "2.0" 118 | id: "auth-error-complex" 119 | method: "tools/call" 120 | params: 121 | name: "search_system_object_attribute_groups" 122 | arguments: 123 | objectType: "Order" 124 | searchRequest: 125 | query: 126 | bool_query: 127 | must: 128 | - text_query: 129 | fields: ["id", "display_name"] 130 | search_phrase: "Billing" 131 | sorts: 132 | - field: "id" 133 | sort_order: "asc" 134 | count: 3 135 | expect: 136 | response: 137 | jsonrpc: "2.0" 138 | id: "auth-error-complex" 139 | result: 140 | content: 141 | - type: "text" 142 | text: "match:contains:OCAPI client not configured" 143 | isError: true 144 | stderr: "toBeEmpty" 145 | 146 | # ================================================================================== 147 | # BEHAVIOR VALIDATION IN DOCS-ONLY MODE 148 | # ================================================================================== 149 | 150 | - it: "should return authentication error even for missing objectType parameter in docs-only mode" 151 | request: 152 | jsonrpc: "2.0" 153 | id: "validation-missing-object-type" 154 | method: "tools/call" 155 | params: 156 | name: "search_system_object_attribute_groups" 157 | arguments: 158 | searchRequest: 159 | query: 160 | match_all_query: {} 161 | count: 3 162 | expect: 163 | response: 164 | jsonrpc: "2.0" 165 | id: "validation-missing-object-type" 166 | result: 167 | content: 168 | - type: "text" 169 | text: "match:contains:OCAPI client not configured" 170 | isError: true 171 | performance: 172 | maxResponseTime: "500ms" 173 | stderr: "toBeEmpty" 174 | 175 | - it: "should return authentication error even for empty objectType parameter in docs-only mode" 176 | request: 177 | jsonrpc: "2.0" 178 | id: "validation-empty-object-type" 179 | method: "tools/call" 180 | params: 181 | name: "search_system_object_attribute_groups" 182 | arguments: 183 | objectType: "" 184 | searchRequest: 185 | query: 186 | match_all_query: {} 187 | count: 2 188 | expect: 189 | response: 190 | jsonrpc: "2.0" 191 | id: "validation-empty-object-type" 192 | result: 193 | content: 194 | - type: "text" 195 | text: "match:contains:credentials are provided" 196 | isError: true 197 | performance: 198 | maxResponseTime: "500ms" 199 | stderr: "toBeEmpty" 200 | 201 | - it: "should return authentication error even for missing searchRequest parameter in docs-only mode" 202 | request: 203 | jsonrpc: "2.0" 204 | id: "validation-missing-search-request" 205 | method: "tools/call" 206 | params: 207 | name: "search_system_object_attribute_groups" 208 | arguments: 209 | objectType: "Product" 210 | expect: 211 | response: 212 | jsonrpc: "2.0" 213 | id: "validation-missing-search-request" 214 | result: 215 | content: 216 | - type: "text" 217 | text: "match:contains:full mode" 218 | isError: true 219 | performance: 220 | maxResponseTime: "500ms" 221 | stderr: "toBeEmpty" 222 | 223 | # ================================================================================== 224 | # PERFORMANCE VALIDATION IN DOCS-ONLY MODE 225 | # ================================================================================== 226 | 227 | - it: "should return authentication errors quickly in docs-only mode" 228 | request: 229 | jsonrpc: "2.0" 230 | id: "performance-auth-error" 231 | method: "tools/call" 232 | params: 233 | name: "search_system_object_attribute_groups" 234 | arguments: 235 | objectType: "Product" 236 | searchRequest: 237 | query: 238 | match_all_query: {} 239 | count: 10 240 | expect: 241 | response: 242 | jsonrpc: "2.0" 243 | id: "performance-auth-error" 244 | result: 245 | content: 246 | - type: "text" 247 | text: "match:contains:OCAPI client not configured" 248 | isError: true 249 | performance: 250 | maxResponseTime: "400ms" 251 | stderr: "toBeEmpty" 252 | 253 | - it: "should handle authentication errors quickly even with missing parameters in docs-only mode" 254 | request: 255 | jsonrpc: "2.0" 256 | id: "performance-validation" 257 | method: "tools/call" 258 | params: 259 | name: "search_system_object_attribute_groups" 260 | arguments: 261 | searchRequest: 262 | query: 263 | text_query: 264 | fields: ["id"] 265 | search_phrase: "test" 266 | count: 5 267 | expect: 268 | response: 269 | jsonrpc: "2.0" 270 | id: "performance-validation" 271 | result: 272 | content: 273 | - type: "text" 274 | text: "match:contains:OCAPI client not configured" 275 | isError: true 276 | performance: 277 | maxResponseTime: "300ms" 278 | stderr: "toBeEmpty" ``` -------------------------------------------------------------------------------- /docs-site/components/Search.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect, useCallback, useRef } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | import { searchDocs, SearchResult } from '../utils/search'; 4 | import { SearchIcon } from './icons'; 5 | 6 | const Highlight: React.FC<{ text: string; query: string }> = ({ text, query }) => { 7 | if (!query) return <>{text}</>; 8 | const parts = text.split(new RegExp(`(${query})`, 'gi')); 9 | return ( 10 | <> 11 | {parts.map((part, i) => 12 | part.toLowerCase() === query.toLowerCase() ? ( 13 | <mark key={i} className="bg-orange-200 text-orange-800 font-semibold rounded px-0.5"> 14 | {part} 15 | </mark> 16 | ) : ( 17 | part 18 | ) 19 | )} 20 | </> 21 | ); 22 | }; 23 | 24 | 25 | const Search: React.FC = () => { 26 | const [query, setQuery] = useState(''); 27 | const [results, setResults] = useState<SearchResult[]>([]); 28 | const [isOpen, setIsOpen] = useState(false); 29 | const [activeIndex, setActiveIndex] = useState(-1); 30 | const inputRef = useRef<HTMLInputElement>(null); 31 | const resultsRef = useRef<HTMLUListElement>(null); 32 | const navigate = useNavigate(); 33 | const location = useLocation(); 34 | 35 | const openSearch = useCallback(() => { 36 | setIsOpen(true); 37 | }, []); 38 | 39 | const closeSearch = useCallback(() => { 40 | setIsOpen(false); 41 | setQuery(''); 42 | setResults([]); 43 | setActiveIndex(-1); 44 | }, []); 45 | 46 | useEffect(() => { 47 | // Only run on client side 48 | if (typeof window === 'undefined') return; 49 | 50 | const handleKeyDown = (e: KeyboardEvent) => { 51 | if (e.metaKey && e.key === 'k') { 52 | e.preventDefault(); 53 | if (isOpen) { 54 | closeSearch(); 55 | } else { 56 | openSearch(); 57 | } 58 | } 59 | 60 | if (isOpen) { 61 | if (e.key === 'Escape') { 62 | closeSearch(); 63 | } else if (e.key === 'ArrowDown') { 64 | e.preventDefault(); 65 | setActiveIndex(prev => prev < results.length - 1 ? prev + 1 : prev); 66 | } else if (e.key === 'ArrowUp') { 67 | e.preventDefault(); 68 | setActiveIndex(prev => prev > 0 ? prev - 1 : prev); 69 | } else if (e.key === 'Enter' && activeIndex >= 0) { 70 | const result = results[activeIndex]; 71 | handleNavigation(result.path, result.heading, result.headingId); 72 | } 73 | } 74 | }; 75 | 76 | window.addEventListener('keydown', handleKeyDown); 77 | return () => window.removeEventListener('keydown', handleKeyDown); 78 | }, [isOpen, results.length, activeIndex, openSearch, closeSearch]); 79 | 80 | useEffect(() => { 81 | if (isOpen && inputRef.current) { 82 | inputRef.current.focus(); 83 | } 84 | }, [isOpen]); 85 | 86 | useEffect(() => { 87 | closeSearch(); 88 | }, [location.pathname, closeSearch]); 89 | 90 | useEffect(() => { 91 | if (activeIndex >= 0 && resultsRef.current) { 92 | const activeElement = resultsRef.current.children[activeIndex] as HTMLLIElement; 93 | if (activeElement) { 94 | activeElement.scrollIntoView({ block: 'nearest' }); 95 | } 96 | } 97 | }, [activeIndex]); 98 | 99 | const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { 100 | const newQuery = e.target.value; 101 | setQuery(newQuery); 102 | if (newQuery.length > 1) { 103 | setResults(searchDocs(newQuery)); 104 | } else { 105 | setResults([]); 106 | } 107 | setActiveIndex(0); 108 | }; 109 | 110 | const handleNavigation = (path: string, heading?: string, headingId?: string) => { 111 | // Use the actual headingId if available, otherwise generate one from the heading 112 | let targetPath = path; 113 | let hashFragment = ''; 114 | 115 | if (headingId) { 116 | hashFragment = headingId; 117 | } else if (heading && heading !== 'Introduction' && heading !== path.split('/').pop()) { 118 | // Convert heading to a URL-safe ID as fallback 119 | const generatedId = heading 120 | .toLowerCase() 121 | .replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens 122 | .replace(/\s+/g, '-') // Replace spaces with hyphens 123 | .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen 124 | .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens 125 | 126 | if (generatedId) { 127 | hashFragment = generatedId; 128 | } 129 | } 130 | 131 | // Navigate to the path first, then handle hash navigation 132 | if (hashFragment) { 133 | navigate(targetPath + '#' + hashFragment); 134 | } else { 135 | navigate(targetPath); 136 | } 137 | 138 | closeSearch(); 139 | }; 140 | 141 | return ( 142 | <> 143 | <div className="relative"> 144 | <SearchIcon className="absolute top-1/2 left-3 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> 145 | <button 146 | type="button" 147 | onClick={openSearch} 148 | className="w-full bg-slate-100 border border-slate-200 rounded-lg py-2 pl-9 pr-12 sm:pr-3 text-sm text-left text-slate-500 hover:border-slate-300 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-400" 149 | > 150 | Search... 151 | </button> 152 | <div className="absolute top-1/2 right-3 -translate-y-1/2 text-xs text-slate-400 border border-slate-300 rounded-md px-1.5 py-0.5 pointer-events-none hidden sm:block"> 153 | ⌘K 154 | </div> 155 | </div> 156 | 157 | {isOpen && ( 158 | <div className="fixed inset-0 z-50 flex justify-center items-start pt-4 sm:pt-20 p-4" aria-modal="true"> 159 | <div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={closeSearch}></div> 160 | <div className="relative bg-white w-full max-w-2xl rounded-lg shadow-lg max-h-[90vh] flex flex-col"> 161 | <div className="relative flex-shrink-0"> 162 | <SearchIcon className="absolute top-1/2 left-4 -translate-y-1/2 w-5 h-5 text-slate-400" /> 163 | <input 164 | ref={inputRef} 165 | type="text" 166 | value={query} 167 | onChange={handleSearch} 168 | placeholder="Search documentation..." 169 | className="w-full text-base sm:text-lg py-3 sm:py-4 pl-12 pr-4 border-b border-slate-200 focus:outline-none" 170 | /> 171 | </div> 172 | {query.length > 1 && ( 173 | <div className="flex-1 overflow-y-auto min-h-0"> 174 | {results.length > 0 ? ( 175 | <ul ref={resultsRef} className="p-3 sm:p-4 space-y-2"> 176 | {results.map((result, index) => ( 177 | <li key={`${result.path}-${result.heading}`}> 178 | <button 179 | onClick={() => handleNavigation(result.path, result.heading, result.headingId)} 180 | className={`w-full text-left p-3 rounded-md transition-colors ${activeIndex === index ? 'bg-orange-100' : 'hover:bg-slate-100'}`} 181 | > 182 | <div className="font-semibold text-slate-800 text-sm sm:text-base"> 183 | <Highlight text={result.pageTitle} query={query} /> 184 | </div> 185 | <div className="text-xs sm:text-sm text-slate-600 mb-1"> 186 | <Highlight text={result.heading} query={query} /> 187 | </div> 188 | <p className="text-xs sm:text-sm text-slate-500 line-clamp-2"> 189 | <Highlight text={result.snippet} query={query} /> 190 | </p> 191 | </button> 192 | </li> 193 | ))} 194 | </ul> 195 | ) : ( 196 | <p className="p-6 sm:p-8 text-center text-slate-500 text-sm sm:text-base">No results found for "{query}"</p> 197 | )} 198 | </div> 199 | )} 200 | </div> 201 | </div> 202 | )} 203 | </> 204 | ); 205 | }; 206 | 207 | export default Search; ``` -------------------------------------------------------------------------------- /docs/sfra/response.md: -------------------------------------------------------------------------------- ```markdown 1 | # Class Response 2 | 3 | ## Inheritance Hierarchy 4 | 5 | - Object 6 | - sfra.models.Response 7 | 8 | ## Description 9 | 10 | The SFRA Response object is a local wrapper around the global response object that provides enhanced functionality for SFRA (Storefront Reference Architecture) applications. This class serves as a centralized interface for managing response data, rendering templates, handling redirects, and controlling HTTP response behavior. The Response object maintains state for template rendering, view data, redirect URLs, logging messages, and HTTP headers while providing a consistent API for different types of responses (ISML templates, JSON, XML, Page Designer pages). It includes built-in support for caching, content type management, and response status codes, making it the primary interface for controller response handling in SFRA applications. 11 | 12 | ## Properties 13 | 14 | ### view 15 | 16 | **Type:** String 17 | 18 | The template name/path to be rendered. 19 | 20 | ### viewData 21 | 22 | **Type:** Object 23 | 24 | Data object containing all variables to be passed to the template during rendering. 25 | 26 | ### redirectUrl 27 | 28 | **Type:** String 29 | 30 | URL to redirect to when a redirect response is triggered. 31 | 32 | ### redirectStatus 33 | 34 | **Type:** String 35 | 36 | HTTP status code for redirect responses (e.g., "301", "302"). 37 | 38 | ### messageLog 39 | 40 | **Type:** Array 41 | 42 | Collection of log messages for debugging and error output. 43 | 44 | ### base 45 | 46 | **Type:** dw.system.Response 47 | 48 | Reference to the original global response object. 49 | 50 | ### cachePeriod 51 | 52 | **Type:** Number 53 | 54 | Cache expiration period value. 55 | 56 | ### cachePeriodUnit 57 | 58 | **Type:** String 59 | 60 | Unit for cache period (typically hours). 61 | 62 | ### personalized 63 | 64 | **Type:** Boolean 65 | 66 | Indicates whether the response contains personalized content. 67 | 68 | ### renderings 69 | 70 | **Type:** Array 71 | 72 | Collection of rendering steps to be executed in order. 73 | 74 | ### isJson 75 | 76 | **Type:** Boolean 77 | 78 | Flag indicating if the response should be rendered as JSON. 79 | 80 | ### isXml 81 | 82 | **Type:** Boolean 83 | 84 | Flag indicating if the response should be rendered as XML. 85 | 86 | ## Constructor Summary 87 | 88 | ### Response 89 | 90 | **Signature:** `Response(response)` 91 | 92 | Creates a new SFRA Response object from the global response object. 93 | 94 | **Parameters:** 95 | - `response` (Object) - Global response object 96 | 97 | ## Method Summary 98 | 99 | ### render 100 | 101 | **Signature:** `render(name, data) : void` 102 | 103 | Stores template name and data for ISML template rendering. 104 | 105 | ### json 106 | 107 | **Signature:** `json(data) : void` 108 | 109 | Configures response to render data as JSON. 110 | 111 | ### xml 112 | 113 | **Signature:** `xml(xmlString) : void` 114 | 115 | Configures response to render data as XML. 116 | 117 | ### page 118 | 119 | **Signature:** `page(page, data, aspectAttributes) : void` 120 | 121 | Configures response to render a Page Designer page. 122 | 123 | ### redirect 124 | 125 | **Signature:** `redirect(url) : void` 126 | 127 | Sets up URL redirection for the response. 128 | 129 | ### setRedirectStatus 130 | 131 | **Signature:** `setRedirectStatus(redirectStatus) : void` 132 | 133 | Sets the HTTP status code for redirects. 134 | 135 | ### getViewData 136 | 137 | **Signature:** `getViewData() : Object` 138 | 139 | Retrieves the current view data object. 140 | 141 | ### setViewData 142 | 143 | **Signature:** `setViewData(data) : void` 144 | 145 | Updates the view data with new data. 146 | 147 | ### log 148 | 149 | **Signature:** `log(...arguments) : void` 150 | 151 | Logs messages for debugging and error output. 152 | 153 | ### setContentType 154 | 155 | **Signature:** `setContentType(type) : void` 156 | 157 | Sets the HTTP content type for the response. 158 | 159 | ### setStatusCode 160 | 161 | **Signature:** `setStatusCode(code) : void` 162 | 163 | Sets the HTTP status code for the response. 164 | 165 | ### print 166 | 167 | **Signature:** `print(message) : void` 168 | 169 | Adds a print step to the rendering pipeline. 170 | 171 | ### cacheExpiration 172 | 173 | **Signature:** `cacheExpiration(period) : void` 174 | 175 | Sets cache expiration period in hours. 176 | 177 | ### setHttpHeader 178 | 179 | **Signature:** `setHttpHeader(name, value) : void` 180 | 181 | Adds a custom HTTP header to the response. 182 | 183 | ## Method Detail 184 | 185 | ### render 186 | 187 | **Signature:** `render(name, data) : void` 188 | 189 | **Description:** Stores template name and data for rendering an ISML template at execution time. The data is merged with existing view data, and a render step is added to the renderings pipeline. 190 | 191 | **Parameters:** 192 | - `name` (String) - Path to the ISML template file 193 | - `data` (Object) - Data object to be passed to the template 194 | 195 | **Returns:** 196 | void 197 | 198 | ### json 199 | 200 | **Signature:** `json(data) : void` 201 | 202 | **Description:** Configures the response to render the provided data as JSON. Sets the isJson flag and merges data with existing view data. 203 | 204 | **Parameters:** 205 | - `data` (Object) - Data object to be serialized as JSON 206 | 207 | **Returns:** 208 | void 209 | 210 | ### xml 211 | 212 | **Signature:** `xml(xmlString) : void` 213 | 214 | **Description:** Configures the response to render the provided XML string. Sets the isXml flag and stores the XML content in view data. 215 | 216 | **Parameters:** 217 | - `xmlString` (String) - Valid XML string to be rendered 218 | 219 | **Returns:** 220 | void 221 | 222 | ### page 223 | 224 | **Signature:** `page(page, data, aspectAttributes) : void` 225 | 226 | **Description:** Configures the response to render a Page Designer page with optional aspect attributes for advanced page management. 227 | 228 | **Parameters:** 229 | - `page` (String) - ID of the Page Designer page to render 230 | - `data` (Object) - Data object to be passed to the page 231 | - `aspectAttributes` (dw.util.HashMap) - Optional aspect attributes for PageMgr 232 | 233 | **Returns:** 234 | void 235 | 236 | ### redirect 237 | 238 | **Signature:** `redirect(url) : void` 239 | 240 | **Description:** Sets up URL redirection for the response. The redirect will be executed during response processing. 241 | 242 | **Parameters:** 243 | - `url` (String) - Target URL for redirection 244 | 245 | **Returns:** 246 | void 247 | 248 | ### setRedirectStatus 249 | 250 | **Signature:** `setRedirectStatus(redirectStatus) : void` 251 | 252 | **Description:** Sets the HTTP status code for redirect responses. Common values are "301" for permanent redirects and "302" for temporary redirects. 253 | 254 | **Parameters:** 255 | - `redirectStatus` (String) - HTTP status code for the redirect 256 | 257 | **Returns:** 258 | void 259 | 260 | ### getViewData 261 | 262 | **Signature:** `getViewData() : Object` 263 | 264 | **Description:** Retrieves the current view data object containing all variables that will be passed to the template during rendering. 265 | 266 | **Returns:** 267 | Object containing all view data variables. 268 | 269 | ### setViewData 270 | 271 | **Signature:** `setViewData(data) : void` 272 | 273 | **Description:** Updates the view data by merging the provided data object with existing view data. Existing properties with the same keys will be overwritten. 274 | 275 | **Parameters:** 276 | - `data` (Object) - Data object to merge with existing view data 277 | 278 | **Returns:** 279 | void 280 | 281 | ### log 282 | 283 | **Signature:** `log(...arguments) : void` 284 | 285 | **Description:** Logs multiple arguments for debugging and error output. Objects and arrays are automatically JSON.stringified, while other types are converted to strings. 286 | 287 | **Parameters:** 288 | - `...arguments` - Variable number of arguments to log 289 | 290 | **Returns:** 291 | void 292 | 293 | ### setContentType 294 | 295 | **Signature:** `setContentType(type) : void` 296 | 297 | **Description:** Sets the HTTP content type header for the response (e.g., "application/json", "text/xml", "text/html"). 298 | 299 | **Parameters:** 300 | - `type` (String) - MIME type for the response content 301 | 302 | **Returns:** 303 | void 304 | 305 | ### setStatusCode 306 | 307 | **Signature:** `setStatusCode(code) : void` 308 | 309 | **Description:** Sets the HTTP status code for the response (e.g., 200, 404, 500). 310 | 311 | **Parameters:** 312 | - `code` (Number) - Valid HTTP status code 313 | 314 | **Returns:** 315 | void 316 | 317 | ### print 318 | 319 | **Signature:** `print(message) : void` 320 | 321 | **Description:** Adds a print step to the rendering pipeline that will output the message directly to the response stream. 322 | 323 | **Parameters:** 324 | - `message` (String) - Message to be printed to the response 325 | 326 | **Returns:** 327 | void 328 | 329 | ### cacheExpiration 330 | 331 | **Signature:** `cacheExpiration(period) : void` 332 | 333 | **Description:** Sets the cache expiration period for the current page response in hours from the current time. 334 | 335 | **Parameters:** 336 | - `period` (Number) - Number of hours from current time for cache expiration 337 | 338 | **Returns:** 339 | void 340 | 341 | ### setHttpHeader 342 | 343 | **Signature:** `setHttpHeader(name, value) : void` 344 | 345 | **Description:** Adds a custom HTTP header to the response with the specified name and value. 346 | 347 | **Parameters:** 348 | - `name` (String) - Header name 349 | - `value` (String) - Header value 350 | 351 | **Returns:** 352 | void 353 | 354 | ## Property Details 355 | 356 | ### viewData 357 | 358 | **Type:** Object 359 | 360 | **Description:** Central data object that accumulates all variables to be passed to templates during rendering. Data is merged using the assign utility, allowing for incremental data building throughout controller execution. 361 | 362 | ### renderings 363 | 364 | **Type:** Array 365 | 366 | **Description:** Ordered collection of rendering steps that define how the response should be processed. Each step contains: 367 | 368 | **For Render Steps:** 369 | - `type` - Always "render" 370 | - `subType` - Type of rendering ("isml", "json", "xml", "page") 371 | - `view` - Template name (for ISML) 372 | - `page` - Page ID (for Page Designer) 373 | - `aspectAttributes` - Page attributes (for Page Designer) 374 | 375 | **For Print Steps:** 376 | - `type` - Always "print" 377 | - `message` - Message to output 378 | 379 | ### messageLog 380 | 381 | **Type:** Array 382 | 383 | **Description:** Collection of debug and error messages logged during controller execution. Messages are automatically formatted, with objects and arrays converted to JSON strings. 384 | 385 | ### base 386 | 387 | **Type:** dw.system.Response 388 | 389 | **Description:** Reference to the original global response object, providing access to core response functionality like setting HTTP headers, content types, and status codes. 390 | 391 | --- 392 | ```