This is page 20 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 -------------------------------------------------------------------------------- /tests/mcp/yaml/search-custom-object-attribute-definitions.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "Tests for search_custom_object_attribute_definitions tool" 2 | # Run with: npx aegis "tests/mcp/yaml/search-custom-object-attribute-definitions.test.mcp.yml" --config ./aegis.config.with-dw.json 3 | 4 | tests: 5 | # Full Mode Tests (primary tests with mock server) 6 | - it: "should successfully search custom object attribute definitions with all query parameters" 7 | request: 8 | jsonrpc: "2.0" 9 | method: "tools/call" 10 | id: 1 11 | params: 12 | name: "search_custom_object_attribute_definitions" 13 | arguments: 14 | objectType: "VersionHistory" 15 | searchRequest: 16 | query: 17 | match_all_query: {} 18 | start: 0 19 | count: 10 20 | select: "(**)" 21 | expect: 22 | response: 23 | jsonrpc: "2.0" 24 | id: 1 25 | result: 26 | content: 27 | match:arrayElements: 28 | type: "text" 29 | text: "match:contains:object_attribute_definition_search_result" 30 | isError: false 31 | stderr: "toBeEmpty" 32 | performance: 33 | maxResponseTime: "2000ms" 34 | 35 | - it: "should return valid JSON structure for custom object attributes" 36 | request: 37 | jsonrpc: "2.0" 38 | method: "tools/call" 39 | id: 2 40 | params: 41 | name: "search_custom_object_attribute_definitions" 42 | arguments: 43 | objectType: "VersionHistory" 44 | searchRequest: 45 | query: 46 | match_all_query: {} 47 | expect: 48 | response: 49 | jsonrpc: "2.0" 50 | id: 2 51 | result: 52 | content: 53 | match:arrayElements: 54 | match:partial: 55 | type: "text" 56 | text: "match:regex:[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_definition_search_result\"[\\s\\S]*" 57 | isError: false 58 | stderr: "toBeEmpty" 59 | 60 | - it: "should validate attribute definition structure in response" 61 | request: 62 | jsonrpc: "2.0" 63 | method: "tools/call" 64 | id: 3 65 | params: 66 | name: "search_custom_object_attribute_definitions" 67 | arguments: 68 | objectType: "VersionHistory" 69 | searchRequest: 70 | query: 71 | match_all_query: {} 72 | expect: 73 | response: 74 | jsonrpc: "2.0" 75 | id: 3 76 | result: 77 | content: 78 | match:arrayElements: 79 | match:partial: 80 | type: "text" 81 | text: "match:regex:[\\s\\S]*\"count\"[\\s\\S]*\"hits\"[\\s\\S]*\"total\"[\\s\\S]*" 82 | isError: false 83 | stderr: "toBeEmpty" 84 | 85 | - it: "should include required attribute definition fields" 86 | request: 87 | jsonrpc: "2.0" 88 | method: "tools/call" 89 | id: 4 90 | params: 91 | name: "search_custom_object_attribute_definitions" 92 | arguments: 93 | objectType: "VersionHistory" 94 | searchRequest: 95 | query: 96 | match_all_query: {} 97 | expect: 98 | response: 99 | jsonrpc: "2.0" 100 | id: 4 101 | result: 102 | content: 103 | match:arrayElements: 104 | match:partial: 105 | type: "text" 106 | text: "match:regex:[\\s\\S]*\"id\"[\\s\\S]*\"value_type\"[\\s\\S]*\"mandatory\"[\\s\\S]*" 107 | isError: false 108 | stderr: "toBeEmpty" 109 | 110 | - it: "should work with minimal search request (missing searchRequest parameter)" 111 | request: 112 | jsonrpc: "2.0" 113 | method: "tools/call" 114 | id: 5 115 | params: 116 | name: "search_custom_object_attribute_definitions" 117 | arguments: 118 | objectType: "VersionHistory" 119 | expect: 120 | response: 121 | jsonrpc: "2.0" 122 | id: 5 123 | result: 124 | content: 125 | match:arrayElements: 126 | match:partial: 127 | type: "text" 128 | text: "match:contains:object_attribute_definition_search_result" 129 | isError: false 130 | stderr: "toBeEmpty" 131 | 132 | - it: "should handle text search query" 133 | request: 134 | jsonrpc: "2.0" 135 | method: "tools/call" 136 | id: 6 137 | params: 138 | name: "search_custom_object_attribute_definitions" 139 | arguments: 140 | objectType: "VersionHistory" 141 | searchRequest: 142 | query: 143 | text_query: 144 | fields: ["id", "display_name"] 145 | search_phrase: "UUID" 146 | start: 0 147 | count: 5 148 | expect: 149 | response: 150 | jsonrpc: "2.0" 151 | id: 6 152 | result: 153 | content: 154 | match:arrayElements: 155 | match:partial: 156 | type: "text" 157 | text: "match:contains:object_attribute_definition_search_result" 158 | isError: false 159 | stderr: "toBeEmpty" 160 | 161 | - it: "should handle pagination parameters" 162 | request: 163 | jsonrpc: "2.0" 164 | method: "tools/call" 165 | id: 7 166 | params: 167 | name: "search_custom_object_attribute_definitions" 168 | arguments: 169 | objectType: "VersionHistory" 170 | searchRequest: 171 | query: 172 | match_all_query: {} 173 | start: 2 174 | count: 3 175 | expect: 176 | response: 177 | jsonrpc: "2.0" 178 | id: 7 179 | result: 180 | content: 181 | match:arrayElements: 182 | match:partial: 183 | type: "text" 184 | text: "match:regex:[\\s\\S]*\"start\"\\s*:\\s*2[\\s\\S]*" 185 | isError: false 186 | stderr: "toBeEmpty" 187 | 188 | - it: "should handle sorting parameters" 189 | request: 190 | jsonrpc: "2.0" 191 | method: "tools/call" 192 | id: 8 193 | params: 194 | name: "search_custom_object_attribute_definitions" 195 | arguments: 196 | objectType: "VersionHistory" 197 | searchRequest: 198 | query: 199 | match_all_query: {} 200 | sorts: 201 | - field: "id" 202 | sort_order: "asc" 203 | expect: 204 | response: 205 | jsonrpc: "2.0" 206 | id: 8 207 | result: 208 | content: 209 | match:arrayElements: 210 | match:partial: 211 | type: "text" 212 | text: "match:contains:object_attribute_definition_search_result" 213 | isError: false 214 | stderr: "toBeEmpty" 215 | 216 | # Error Handling Tests (Full Mode) 217 | - it: "should reject empty objectType parameter" 218 | request: 219 | jsonrpc: "2.0" 220 | method: "tools/call" 221 | id: 9 222 | params: 223 | name: "search_custom_object_attribute_definitions" 224 | arguments: 225 | objectType: "" 226 | searchRequest: 227 | query: 228 | match_all_query: {} 229 | expect: 230 | response: 231 | jsonrpc: "2.0" 232 | id: 9 233 | result: 234 | content: 235 | match:arrayElements: 236 | match:partial: 237 | type: "text" 238 | text: "match:contains:objectType must be a non-empty string" 239 | isError: true 240 | stderr: "toBeEmpty" 241 | performance: 242 | maxResponseTime: "800ms" 243 | 244 | - it: "should handle unknown custom object type" 245 | request: 246 | jsonrpc: "2.0" 247 | method: "tools/call" 248 | id: 10 249 | params: 250 | name: "search_custom_object_attribute_definitions" 251 | arguments: 252 | objectType: "UnknownCustomObject" 253 | searchRequest: 254 | query: 255 | match_all_query: {} 256 | expect: 257 | response: 258 | jsonrpc: "2.0" 259 | id: 10 260 | result: 261 | content: 262 | match:arrayElements: 263 | match:partial: 264 | type: "text" 265 | text: "match:regex:[\\s\\S]*ObjectTypeNotFoundException[\\s\\S]*" 266 | isError: true 267 | stderr: "toBeEmpty" 268 | performance: 269 | maxResponseTime: "800ms" 270 | 271 | - it: "should handle invalid search request structure" 272 | request: 273 | jsonrpc: "2.0" 274 | method: "tools/call" 275 | id: 11 276 | params: 277 | name: "search_custom_object_attribute_definitions" 278 | arguments: 279 | objectType: "VersionHistory" 280 | searchRequest: 281 | invalid: "structure" 282 | expect: 283 | response: 284 | jsonrpc: "2.0" 285 | id: 11 286 | result: 287 | content: 288 | match:arrayElements: 289 | match:partial: 290 | type: "text" 291 | text: "match:regex:[\\s\\S]*PropertyConstraintViolationException[\\s\\S]*" 292 | isError: true 293 | stderr: "toBeEmpty" 294 | performance: 295 | maxResponseTime: "800ms" 296 | 297 | - it: "should reject missing objectType parameter" 298 | request: 299 | jsonrpc: "2.0" 300 | method: "tools/call" 301 | id: 12 302 | params: 303 | name: "search_custom_object_attribute_definitions" 304 | arguments: 305 | searchRequest: 306 | query: 307 | match_all_query: {} 308 | expect: 309 | response: 310 | jsonrpc: "2.0" 311 | id: 12 312 | result: 313 | content: 314 | match:arrayElements: 315 | match:partial: 316 | type: "text" 317 | text: "match:contains:objectType" 318 | isError: true 319 | stderr: "toBeEmpty" 320 | 321 | # Performance Tests 322 | - it: "should respond quickly for basic search operations" 323 | request: 324 | jsonrpc: "2.0" 325 | method: "tools/call" 326 | id: 13 327 | params: 328 | name: "search_custom_object_attribute_definitions" 329 | arguments: 330 | objectType: "VersionHistory" 331 | searchRequest: 332 | query: 333 | match_all_query: {} 334 | expect: 335 | response: 336 | jsonrpc: "2.0" 337 | id: 13 338 | result: 339 | content: 340 | match:arrayElements: 341 | match:partial: 342 | type: "text" 343 | isError: false 344 | stderr: "toBeEmpty" 345 | performance: 346 | maxResponseTime: "1500ms" 347 | 348 | - it: "should handle complex queries within reasonable time" 349 | request: 350 | jsonrpc: "2.0" 351 | method: "tools/call" 352 | id: 14 353 | params: 354 | name: "search_custom_object_attribute_definitions" 355 | arguments: 356 | objectType: "VersionHistory" 357 | searchRequest: 358 | query: 359 | bool_query: 360 | must: 361 | - text_query: 362 | fields: ["id"] 363 | search_phrase: "component" 364 | should: 365 | - term_query: 366 | fields: ["value_type"] 367 | operator: "is" 368 | values: ["string"] 369 | sorts: 370 | - field: "id" 371 | sort_order: "desc" 372 | start: 0 373 | count: 20 374 | expect: 375 | response: 376 | jsonrpc: "2.0" 377 | id: 14 378 | result: 379 | content: 380 | match:arrayElements: 381 | match:partial: 382 | type: "text" 383 | text: "match:contains:object_attribute_definition_search_result" 384 | isError: false 385 | stderr: "toBeEmpty" 386 | performance: 387 | maxResponseTime: "2000ms" ``` -------------------------------------------------------------------------------- /docs/dw_content/Content.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.content 2 | 3 | # Class Content 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.PersistentObject 9 | - dw.object.ExtensibleObject 10 | - dw.content.Content 11 | 12 | ## Description 13 | 14 | Class representing a Content asset in Commerce Cloud Digital. 15 | 16 | ## Properties 17 | 18 | ### classificationFolder 19 | 20 | **Type:** Folder (Read Only) 21 | 22 | The Folder associated with this Content. The folder is 23 | used to determine the classification of the content. 24 | 25 | ### description 26 | 27 | **Type:** String (Read Only) 28 | 29 | The description in the current locale or null. 30 | 31 | ### folders 32 | 33 | **Type:** Collection (Read Only) 34 | 35 | All folders to which this content is assigned. 36 | 37 | ### ID 38 | 39 | **Type:** String (Read Only) 40 | 41 | The ID of the content asset. 42 | 43 | ### name 44 | 45 | **Type:** String (Read Only) 46 | 47 | The name of the content asset. 48 | 49 | ### online 50 | 51 | **Type:** boolean (Read Only) 52 | 53 | The online status of the content. 54 | 55 | ### onlineFlag 56 | 57 | **Type:** boolean (Read Only) 58 | 59 | The online status flag of the content. 60 | 61 | ### page 62 | 63 | **Type:** Page (Read Only) 64 | 65 | Returns if the content is a Page or not. 66 | 67 | ### pageDescription 68 | 69 | **Type:** String (Read Only) 70 | 71 | The page description for the content in the current locale 72 | or null if there is no page description. 73 | 74 | ### pageKeywords 75 | 76 | **Type:** String (Read Only) 77 | 78 | The page keywords for the content in the current locale 79 | or null if there is no page title. 80 | 81 | ### pageMetaTags 82 | 83 | **Type:** Array (Read Only) 84 | 85 | All page meta tags, defined for this instance for which content can be generated. 86 | 87 | The meta tag content is generated based on the content detail page meta tag context and rules. 88 | The rules are obtained from the current content or inherited from the default folder, 89 | up to the root folder. 90 | 91 | ### pageTitle 92 | 93 | **Type:** String (Read Only) 94 | 95 | The page title for the content in the current locale 96 | or null if there is no page title. 97 | 98 | ### pageURL 99 | 100 | **Type:** String (Read Only) 101 | 102 | The page URL for the content in the current locale 103 | or null if there is no page URL. 104 | 105 | ### searchable 106 | 107 | **Type:** boolean (Read Only) 108 | 109 | The search status of the content. 110 | 111 | ### searchableFlag 112 | 113 | **Type:** boolean (Read Only) 114 | 115 | The online status flag of the content. 116 | 117 | ### siteMapChangeFrequency 118 | 119 | **Type:** String (Read Only) 120 | 121 | The contents change frequency needed for the sitemap creation. 122 | 123 | ### siteMapIncluded 124 | 125 | **Type:** Number (Read Only) 126 | 127 | The status if the content is included into the sitemap. 128 | 129 | ### siteMapPriority 130 | 131 | **Type:** Number (Read Only) 132 | 133 | The contents priority needed for the sitemap creation. 134 | If no priority is defined, the method returns 0.0. 135 | 136 | ### template 137 | 138 | **Type:** String (Read Only) 139 | 140 | The value of attribute 'template'. 141 | 142 | ## Constructor Summary 143 | 144 | ## Method Summary 145 | 146 | ### getClassificationFolder 147 | 148 | **Signature:** `getClassificationFolder() : Folder` 149 | 150 | Returns the Folder associated with this Content. 151 | 152 | ### getDescription 153 | 154 | **Signature:** `getDescription() : String` 155 | 156 | Returns the description in the current locale or null. 157 | 158 | ### getFolders 159 | 160 | **Signature:** `getFolders() : Collection` 161 | 162 | Returns all folders to which this content is assigned. 163 | 164 | ### getID 165 | 166 | **Signature:** `getID() : String` 167 | 168 | Returns the ID of the content asset. 169 | 170 | ### getName 171 | 172 | **Signature:** `getName() : String` 173 | 174 | Returns the name of the content asset. 175 | 176 | ### getOnlineFlag 177 | 178 | **Signature:** `getOnlineFlag() : boolean` 179 | 180 | Returns the online status flag of the content. 181 | 182 | ### getPageDescription 183 | 184 | **Signature:** `getPageDescription() : String` 185 | 186 | Returns the page description for the content in the current locale or null if there is no page description. 187 | 188 | ### getPageKeywords 189 | 190 | **Signature:** `getPageKeywords() : String` 191 | 192 | Returns the page keywords for the content in the current locale or null if there is no page title. 193 | 194 | ### getPageMetaTag 195 | 196 | **Signature:** `getPageMetaTag(id : String) : PageMetaTag` 197 | 198 | Returns the page meta tag for the specified id. 199 | 200 | ### getPageMetaTags 201 | 202 | **Signature:** `getPageMetaTags() : Array` 203 | 204 | Returns all page meta tags, defined for this instance for which content can be generated. 205 | 206 | ### getPageTitle 207 | 208 | **Signature:** `getPageTitle() : String` 209 | 210 | Returns the page title for the content in the current locale or null if there is no page title. 211 | 212 | ### getPageURL 213 | 214 | **Signature:** `getPageURL() : String` 215 | 216 | Returns the page URL for the content in the current locale or null if there is no page URL. 217 | 218 | ### getSearchableFlag 219 | 220 | **Signature:** `getSearchableFlag() : boolean` 221 | 222 | Returns the online status flag of the content. 223 | 224 | ### getSiteMapChangeFrequency 225 | 226 | **Signature:** `getSiteMapChangeFrequency() : String` 227 | 228 | Returns the contents change frequency needed for the sitemap creation. 229 | 230 | ### getSiteMapIncluded 231 | 232 | **Signature:** `getSiteMapIncluded() : Number` 233 | 234 | Returns the status if the content is included into the sitemap. 235 | 236 | ### getSiteMapPriority 237 | 238 | **Signature:** `getSiteMapPriority() : Number` 239 | 240 | Returns the contents priority needed for the sitemap creation. 241 | 242 | ### getTemplate 243 | 244 | **Signature:** `getTemplate() : String` 245 | 246 | Returns the value of attribute 'template'. 247 | 248 | ### isOnline 249 | 250 | **Signature:** `isOnline() : boolean` 251 | 252 | Returns the online status of the content. 253 | 254 | ### isPage 255 | 256 | **Signature:** `isPage() : boolean` 257 | 258 | Returns if the content is a Page or not. 259 | 260 | ### isSearchable 261 | 262 | **Signature:** `isSearchable() : boolean` 263 | 264 | Returns the search status of the content. 265 | 266 | ### toPage 267 | 268 | **Signature:** `toPage() : Page` 269 | 270 | Converts the content into the Page representation if isPage() yields true. 271 | 272 | ## Method Detail 273 | 274 | ## Method Details 275 | 276 | ### getClassificationFolder 277 | 278 | **Signature:** `getClassificationFolder() : Folder` 279 | 280 | **Description:** Returns the Folder associated with this Content. The folder is used to determine the classification of the content. 281 | 282 | **Returns:** 283 | 284 | the classification Folder. 285 | 286 | --- 287 | 288 | ### getDescription 289 | 290 | **Signature:** `getDescription() : String` 291 | 292 | **Description:** Returns the description in the current locale or null. 293 | 294 | **Returns:** 295 | 296 | the description in the current locale or null. 297 | 298 | --- 299 | 300 | ### getFolders 301 | 302 | **Signature:** `getFolders() : Collection` 303 | 304 | **Description:** Returns all folders to which this content is assigned. 305 | 306 | **Returns:** 307 | 308 | Collection of Folder objects. 309 | 310 | --- 311 | 312 | ### getID 313 | 314 | **Signature:** `getID() : String` 315 | 316 | **Description:** Returns the ID of the content asset. 317 | 318 | **Returns:** 319 | 320 | the ID of the content asset. 321 | 322 | --- 323 | 324 | ### getName 325 | 326 | **Signature:** `getName() : String` 327 | 328 | **Description:** Returns the name of the content asset. 329 | 330 | **Returns:** 331 | 332 | the name of the content asset. 333 | 334 | --- 335 | 336 | ### getOnlineFlag 337 | 338 | **Signature:** `getOnlineFlag() : boolean` 339 | 340 | **Description:** Returns the online status flag of the content. 341 | 342 | **Returns:** 343 | 344 | true if the content is online, false otherwise. 345 | 346 | --- 347 | 348 | ### getPageDescription 349 | 350 | **Signature:** `getPageDescription() : String` 351 | 352 | **Description:** Returns the page description for the content in the current locale or null if there is no page description. 353 | 354 | **Returns:** 355 | 356 | the page description for the content in the current locale or null if there is no page description. 357 | 358 | --- 359 | 360 | ### getPageKeywords 361 | 362 | **Signature:** `getPageKeywords() : String` 363 | 364 | **Description:** Returns the page keywords for the content in the current locale or null if there is no page title. 365 | 366 | **Returns:** 367 | 368 | the page keywords for the content in the current locale or null if there is no page title. 369 | 370 | --- 371 | 372 | ### getPageMetaTag 373 | 374 | **Signature:** `getPageMetaTag(id : String) : PageMetaTag` 375 | 376 | **Description:** Returns the page meta tag for the specified id. The meta tag content is generated based on the content detail page meta tag context and rule. The rule is obtained from the current content or inherited from the default folder, up to the root folder. Null will be returned if the meta tag is undefined on the current instance, or if no rule can be found for the current context, or if the rule resolves to an empty string. 377 | 378 | **Parameters:** 379 | 380 | - `id`: the ID to get the page meta tag for 381 | 382 | **Returns:** 383 | 384 | page meta tag containing content generated based on rules 385 | 386 | --- 387 | 388 | ### getPageMetaTags 389 | 390 | **Signature:** `getPageMetaTags() : Array` 391 | 392 | **Description:** Returns all page meta tags, defined for this instance for which content can be generated. The meta tag content is generated based on the content detail page meta tag context and rules. The rules are obtained from the current content or inherited from the default folder, up to the root folder. 393 | 394 | **Returns:** 395 | 396 | page meta tags defined for this instance, containing content generated based on rules 397 | 398 | --- 399 | 400 | ### getPageTitle 401 | 402 | **Signature:** `getPageTitle() : String` 403 | 404 | **Description:** Returns the page title for the content in the current locale or null if there is no page title. 405 | 406 | **Returns:** 407 | 408 | the page title for the content in the current locale or null if there is no page title. 409 | 410 | --- 411 | 412 | ### getPageURL 413 | 414 | **Signature:** `getPageURL() : String` 415 | 416 | **Description:** Returns the page URL for the content in the current locale or null if there is no page URL. 417 | 418 | **Returns:** 419 | 420 | the page URL for the content in the current locale or null if there is no page URL. 421 | 422 | --- 423 | 424 | ### getSearchableFlag 425 | 426 | **Signature:** `getSearchableFlag() : boolean` 427 | 428 | **Description:** Returns the online status flag of the content. 429 | 430 | **Returns:** 431 | 432 | true if the content is searchable, false otherwise. 433 | 434 | --- 435 | 436 | ### getSiteMapChangeFrequency 437 | 438 | **Signature:** `getSiteMapChangeFrequency() : String` 439 | 440 | **Description:** Returns the contents change frequency needed for the sitemap creation. 441 | 442 | **Returns:** 443 | 444 | The contents sitemap change frequency. 445 | 446 | --- 447 | 448 | ### getSiteMapIncluded 449 | 450 | **Signature:** `getSiteMapIncluded() : Number` 451 | 452 | **Description:** Returns the status if the content is included into the sitemap. 453 | 454 | **Returns:** 455 | 456 | the value of the attribute 'siteMapIncluded' 457 | 458 | --- 459 | 460 | ### getSiteMapPriority 461 | 462 | **Signature:** `getSiteMapPriority() : Number` 463 | 464 | **Description:** Returns the contents priority needed for the sitemap creation. If no priority is defined, the method returns 0.0. 465 | 466 | **Returns:** 467 | 468 | The contents sitemap priority. 469 | 470 | --- 471 | 472 | ### getTemplate 473 | 474 | **Signature:** `getTemplate() : String` 475 | 476 | **Description:** Returns the value of attribute 'template'. 477 | 478 | **Returns:** 479 | 480 | the value of the attribute 'template' 481 | 482 | --- 483 | 484 | ### isOnline 485 | 486 | **Signature:** `isOnline() : boolean` 487 | 488 | **Description:** Returns the online status of the content. 489 | 490 | **Returns:** 491 | 492 | true if the content is online, false otherwise. 493 | 494 | --- 495 | 496 | ### isPage 497 | 498 | **Signature:** `isPage() : boolean` 499 | 500 | **Description:** Returns if the content is a Page or not. 501 | 502 | **Returns:** 503 | 504 | true if the content is a Page, false otherwise. 505 | 506 | --- 507 | 508 | ### isSearchable 509 | 510 | **Signature:** `isSearchable() : boolean` 511 | 512 | **Description:** Returns the search status of the content. 513 | 514 | **Returns:** 515 | 516 | true if the content is searchable, false otherwise. 517 | 518 | --- 519 | 520 | ### toPage 521 | 522 | **Signature:** `toPage() : Page` 523 | 524 | **Description:** Converts the content into the Page representation if isPage() yields true. 525 | 526 | **Returns:** 527 | 528 | the Page representation of the content if it is a page, null otherwise. 529 | 530 | **See Also:** 531 | 532 | PageMgr.getPage(String) 533 | 534 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-sfra-documentation.docs-only.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "Tests for search_sfra_documentation tool in docs-only mode" 2 | 3 | tests: 4 | - it: "should return structured search results for valid query 'render'" 5 | request: 6 | jsonrpc: "2.0" 7 | method: "tools/call" 8 | id: "test-1" 9 | params: 10 | name: "search_sfra_documentation" 11 | arguments: 12 | query: "render" 13 | expect: 14 | response: 15 | jsonrpc: "2.0" 16 | id: "test-1" 17 | result: 18 | isError: false 19 | content: 20 | match:arrayElements: 21 | type: "text" 22 | text: "match:regex:\\[[\\s\\S]*\\]" 23 | stderr: "toBeEmpty" 24 | 25 | - it: "should validate search result structure contains JSON array with documents" 26 | request: 27 | jsonrpc: "2.0" 28 | method: "tools/call" 29 | id: "test-2" 30 | params: 31 | name: "search_sfra_documentation" 32 | arguments: 33 | query: "server" 34 | expect: 35 | response: 36 | jsonrpc: "2.0" 37 | id: "test-2" 38 | result: 39 | isError: false 40 | content: 41 | match:arrayElements: 42 | type: "text" 43 | text: "match:contains:document" 44 | stderr: "toBeEmpty" 45 | 46 | - it: "should include relevance scores and matches in search results" 47 | request: 48 | jsonrpc: "2.0" 49 | method: "tools/call" 50 | id: "test-3" 51 | params: 52 | name: "search_sfra_documentation" 53 | arguments: 54 | query: "response" 55 | expect: 56 | response: 57 | jsonrpc: "2.0" 58 | id: "test-3" 59 | result: 60 | isError: false 61 | content: 62 | match:arrayElements: 63 | type: "text" 64 | text: "match:regex:(?:relevanceScore)[\\s\\S]*(?:matches)" 65 | stderr: "toBeEmpty" 66 | 67 | - it: "should return empty array for query with no matches" 68 | request: 69 | jsonrpc: "2.0" 70 | method: "tools/call" 71 | id: "test-4" 72 | params: 73 | name: "search_sfra_documentation" 74 | arguments: 75 | query: "zzznothingfound" 76 | expect: 77 | response: 78 | jsonrpc: "2.0" 79 | id: "test-4" 80 | result: 81 | isError: false 82 | content: 83 | match:arrayElements: 84 | type: "text" 85 | text: "match:regex:^\\[\\s*\\]$" 86 | stderr: "toBeEmpty" 87 | 88 | - it: "should handle empty query with validation error" 89 | request: 90 | jsonrpc: "2.0" 91 | method: "tools/call" 92 | id: "test-5" 93 | params: 94 | name: "search_sfra_documentation" 95 | arguments: 96 | query: "" 97 | expect: 98 | response: 99 | jsonrpc: "2.0" 100 | id: "test-5" 101 | result: 102 | isError: true 103 | content: 104 | match:arrayElements: 105 | type: "text" 106 | text: "match:contains:query must be a non-empty string" 107 | stderr: "toBeEmpty" 108 | 109 | - it: "should reject missing query parameter" 110 | request: 111 | jsonrpc: "2.0" 112 | method: "tools/call" 113 | id: "test-6" 114 | params: 115 | name: "search_sfra_documentation" 116 | arguments: {} 117 | expect: 118 | response: 119 | jsonrpc: "2.0" 120 | id: "test-6" 121 | result: 122 | isError: true 123 | content: 124 | match:arrayElements: 125 | type: "text" 126 | text: "match:contains:query must be a non-empty string" 127 | stderr: "toBeEmpty" 128 | 129 | - it: "should return results with document categories for complex queries" 130 | request: 131 | jsonrpc: "2.0" 132 | method: "tools/call" 133 | id: "test-7" 134 | params: 135 | name: "search_sfra_documentation" 136 | arguments: 137 | query: "product" 138 | expect: 139 | response: 140 | jsonrpc: "2.0" 141 | id: "test-7" 142 | result: 143 | isError: false 144 | content: 145 | match:arrayElements: 146 | type: "text" 147 | text: "match:regex:(?:category)[\\s\\S]*(?:core|product|order|customer|pricing|store|other)" 148 | stderr: "toBeEmpty" 149 | 150 | - it: "should include document types in search results" 151 | request: 152 | jsonrpc: "2.0" 153 | method: "tools/call" 154 | id: "test-8" 155 | params: 156 | name: "search_sfra_documentation" 157 | arguments: 158 | query: "model" 159 | expect: 160 | response: 161 | jsonrpc: "2.0" 162 | id: "test-8" 163 | result: 164 | isError: false 165 | content: 166 | match:arrayElements: 167 | type: "text" 168 | text: "match:regex:(?:type)[\\s\\S]*(?:class|module|model)" 169 | stderr: "toBeEmpty" 170 | 171 | - it: "should respond within reasonable time for complex search" 172 | request: 173 | jsonrpc: "2.0" 174 | method: "tools/call" 175 | id: "test-9" 176 | params: 177 | name: "search_sfra_documentation" 178 | arguments: 179 | query: "cart billing shipping" 180 | expect: 181 | response: 182 | jsonrpc: "2.0" 183 | id: "test-9" 184 | result: 185 | isError: false 186 | content: 187 | match:arrayElements: 188 | type: "text" 189 | text: "match:type:string" 190 | performance: 191 | maxResponseTime: "2000ms" 192 | stderr: "toBeEmpty" 193 | 194 | - it: "should handle special characters in search query" 195 | request: 196 | jsonrpc: "2.0" 197 | method: "tools/call" 198 | id: "test-10" 199 | params: 200 | name: "search_sfra_documentation" 201 | arguments: 202 | query: "dw.util" 203 | expect: 204 | response: 205 | jsonrpc: "2.0" 206 | id: "test-10" 207 | result: 208 | isError: false 209 | content: 210 | match:arrayElements: 211 | type: "text" 212 | text: "match:type:string" 213 | stderr: "toBeEmpty" 214 | 215 | - it: "should handle very long search queries efficiently" 216 | request: 217 | jsonrpc: "2.0" 218 | method: "tools/call" 219 | id: "test-11" 220 | params: 221 | name: "search_sfra_documentation" 222 | arguments: 223 | query: "template rendering isml view data processing controller middleware response request server router" 224 | expect: 225 | response: 226 | jsonrpc: "2.0" 227 | id: "test-11" 228 | result: 229 | isError: false 230 | content: 231 | match:arrayElements: 232 | type: "text" 233 | text: "match:regex:\\[[\\s\\S]*\\]" 234 | performance: 235 | maxResponseTime: "3000ms" 236 | stderr: "toBeEmpty" 237 | 238 | - it: "should return results for single character queries" 239 | request: 240 | jsonrpc: "2.0" 241 | method: "tools/call" 242 | id: "test-12" 243 | params: 244 | name: "search_sfra_documentation" 245 | arguments: 246 | query: "a" 247 | expect: 248 | response: 249 | jsonrpc: "2.0" 250 | id: "test-12" 251 | result: 252 | isError: false 253 | content: 254 | match:arrayElements: 255 | type: "text" 256 | text: "match:regex:\\[[\\s\\S]*\\]" 257 | stderr: "toBeEmpty" 258 | 259 | - it: "should handle numeric search terms" 260 | request: 261 | jsonrpc: "2.0" 262 | method: "tools/call" 263 | id: "test-13" 264 | params: 265 | name: "search_sfra_documentation" 266 | arguments: 267 | query: "200" 268 | expect: 269 | response: 270 | jsonrpc: "2.0" 271 | id: "test-13" 272 | result: 273 | isError: false 274 | content: 275 | match:arrayElements: 276 | type: "text" 277 | text: "match:type:string" 278 | stderr: "toBeEmpty" 279 | 280 | - it: "should return consistent results for same query" 281 | request: 282 | jsonrpc: "2.0" 283 | method: "tools/call" 284 | id: "test-14" 285 | params: 286 | name: "search_sfra_documentation" 287 | arguments: 288 | query: "cart" 289 | expect: 290 | response: 291 | jsonrpc: "2.0" 292 | id: "test-14" 293 | result: 294 | isError: false 295 | content: 296 | match:arrayElements: 297 | type: "text" 298 | text: "match:contains:relevanceScore" 299 | stderr: "toBeEmpty" 300 | 301 | - it: "should handle case-insensitive searches" 302 | request: 303 | jsonrpc: "2.0" 304 | method: "tools/call" 305 | id: "test-15" 306 | params: 307 | name: "search_sfra_documentation" 308 | arguments: 309 | query: "PRODUCT" 310 | expect: 311 | response: 312 | jsonrpc: "2.0" 313 | id: "test-15" 314 | result: 315 | isError: false 316 | content: 317 | match:arrayElements: 318 | type: "text" 319 | text: "match:regex:\\[[\\s\\S]*\\]" 320 | stderr: "toBeEmpty" 321 | 322 | - it: "should find results for core SFRA concepts" 323 | request: 324 | jsonrpc: "2.0" 325 | method: "tools/call" 326 | id: "test-16" 327 | params: 328 | name: "search_sfra_documentation" 329 | arguments: 330 | query: "middleware" 331 | expect: 332 | response: 333 | jsonrpc: "2.0" 334 | id: "test-16" 335 | result: 336 | isError: false 337 | content: 338 | match:arrayElements: 339 | type: "text" 340 | text: "match:regex:(?:matches)[\\s\\S]*(?:section)[\\s\\S]*(?:content)" 341 | stderr: "toBeEmpty" 342 | 343 | - it: "should handle hyphenated search terms" 344 | request: 345 | jsonrpc: "2.0" 346 | method: "tools/call" 347 | id: "test-17" 348 | params: 349 | name: "search_sfra_documentation" 350 | arguments: 351 | query: "product-full" 352 | expect: 353 | response: 354 | jsonrpc: "2.0" 355 | id: "test-17" 356 | result: 357 | isError: false 358 | content: 359 | match:arrayElements: 360 | type: "text" 361 | text: "match:type:string" 362 | stderr: "toBeEmpty" 363 | 364 | - it: "should return structured results for pricing queries" 365 | request: 366 | jsonrpc: "2.0" 367 | method: "tools/call" 368 | id: "test-18" 369 | params: 370 | name: "search_sfra_documentation" 371 | arguments: 372 | query: "price" 373 | expect: 374 | response: 375 | jsonrpc: "2.0" 376 | id: "test-18" 377 | result: 378 | isError: false 379 | content: 380 | match:arrayElements: 381 | type: "text" 382 | text: "match:regex:(?:category)[\\s\\S]*(?:pricing|product|core)" 383 | stderr: "toBeEmpty" 384 | 385 | - it: "should handle underscore in search terms" 386 | request: 387 | jsonrpc: "2.0" 388 | method: "tools/call" 389 | id: "test-19" 390 | params: 391 | name: "search_sfra_documentation" 392 | arguments: 393 | query: "view_data" 394 | expect: 395 | response: 396 | jsonrpc: "2.0" 397 | id: "test-19" 398 | result: 399 | isError: false 400 | content: 401 | match:arrayElements: 402 | type: "text" 403 | text: "match:type:string" 404 | stderr: "toBeEmpty" 405 | 406 | - it: "should respond quickly for common search terms" 407 | request: 408 | jsonrpc: "2.0" 409 | method: "tools/call" 410 | id: "test-20" 411 | params: 412 | name: "search_sfra_documentation" 413 | arguments: 414 | query: "model" 415 | expect: 416 | response: 417 | jsonrpc: "2.0" 418 | id: "test-20" 419 | result: 420 | isError: false 421 | content: 422 | match:arrayElements: 423 | type: "text" 424 | text: "match:contains:document" 425 | performance: 426 | maxResponseTime: "800ms" 427 | stderr: "toBeEmpty" 428 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-sfra-documentation.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "Tests for search_sfra_documentation tool in full-mode mode" 2 | 3 | tests: 4 | - it: "should return structured search results for valid query 'render'" 5 | request: 6 | jsonrpc: "2.0" 7 | method: "tools/call" 8 | id: "test-1" 9 | params: 10 | name: "search_sfra_documentation" 11 | arguments: 12 | query: "render" 13 | expect: 14 | response: 15 | jsonrpc: "2.0" 16 | id: "test-1" 17 | result: 18 | isError: false 19 | content: 20 | match:arrayElements: 21 | type: "text" 22 | text: "match:regex:\\[[\\s\\S]*\\]" 23 | stderr: "toBeEmpty" 24 | 25 | - it: "should validate search result structure contains JSON array with documents" 26 | request: 27 | jsonrpc: "2.0" 28 | method: "tools/call" 29 | id: "test-2" 30 | params: 31 | name: "search_sfra_documentation" 32 | arguments: 33 | query: "server" 34 | expect: 35 | response: 36 | jsonrpc: "2.0" 37 | id: "test-2" 38 | result: 39 | isError: false 40 | content: 41 | match:arrayElements: 42 | type: "text" 43 | text: "match:contains:document" 44 | stderr: "toBeEmpty" 45 | 46 | - it: "should include relevance scores and matches in search results" 47 | request: 48 | jsonrpc: "2.0" 49 | method: "tools/call" 50 | id: "test-3" 51 | params: 52 | name: "search_sfra_documentation" 53 | arguments: 54 | query: "response" 55 | expect: 56 | response: 57 | jsonrpc: "2.0" 58 | id: "test-3" 59 | result: 60 | isError: false 61 | content: 62 | match:arrayElements: 63 | type: "text" 64 | text: "match:regex:(?:relevanceScore)[\\s\\S]*(?:matches)" 65 | stderr: "toBeEmpty" 66 | 67 | - it: "should return empty array for query with no matches" 68 | request: 69 | jsonrpc: "2.0" 70 | method: "tools/call" 71 | id: "test-4" 72 | params: 73 | name: "search_sfra_documentation" 74 | arguments: 75 | query: "zzznothingfound" 76 | expect: 77 | response: 78 | jsonrpc: "2.0" 79 | id: "test-4" 80 | result: 81 | isError: false 82 | content: 83 | match:arrayElements: 84 | type: "text" 85 | text: "match:regex:^\\[\\s*\\]$" 86 | stderr: "toBeEmpty" 87 | 88 | - it: "should handle empty query with validation error" 89 | request: 90 | jsonrpc: "2.0" 91 | method: "tools/call" 92 | id: "test-5" 93 | params: 94 | name: "search_sfra_documentation" 95 | arguments: 96 | query: "" 97 | expect: 98 | response: 99 | jsonrpc: "2.0" 100 | id: "test-5" 101 | result: 102 | isError: true 103 | content: 104 | match:arrayElements: 105 | type: "text" 106 | text: "match:contains:query must be a non-empty string" 107 | stderr: "toBeEmpty" 108 | 109 | - it: "should reject missing query parameter" 110 | request: 111 | jsonrpc: "2.0" 112 | method: "tools/call" 113 | id: "test-6" 114 | params: 115 | name: "search_sfra_documentation" 116 | arguments: {} 117 | expect: 118 | response: 119 | jsonrpc: "2.0" 120 | id: "test-6" 121 | result: 122 | isError: true 123 | content: 124 | match:arrayElements: 125 | type: "text" 126 | text: "match:contains:query must be a non-empty string" 127 | stderr: "toBeEmpty" 128 | 129 | - it: "should return results with document categories for complex queries" 130 | request: 131 | jsonrpc: "2.0" 132 | method: "tools/call" 133 | id: "test-7" 134 | params: 135 | name: "search_sfra_documentation" 136 | arguments: 137 | query: "product" 138 | expect: 139 | response: 140 | jsonrpc: "2.0" 141 | id: "test-7" 142 | result: 143 | isError: false 144 | content: 145 | match:arrayElements: 146 | type: "text" 147 | text: "match:regex:(?:category)[\\s\\S]*(?:core|product|order|customer|pricing|store|other)" 148 | stderr: "toBeEmpty" 149 | 150 | - it: "should include document types in search results" 151 | request: 152 | jsonrpc: "2.0" 153 | method: "tools/call" 154 | id: "test-8" 155 | params: 156 | name: "search_sfra_documentation" 157 | arguments: 158 | query: "model" 159 | expect: 160 | response: 161 | jsonrpc: "2.0" 162 | id: "test-8" 163 | result: 164 | isError: false 165 | content: 166 | match:arrayElements: 167 | type: "text" 168 | text: "match:regex:(?:type)[\\s\\S]*(?:class|module|model)" 169 | stderr: "toBeEmpty" 170 | 171 | - it: "should respond within reasonable time for complex search" 172 | request: 173 | jsonrpc: "2.0" 174 | method: "tools/call" 175 | id: "test-9" 176 | params: 177 | name: "search_sfra_documentation" 178 | arguments: 179 | query: "cart billing shipping" 180 | expect: 181 | response: 182 | jsonrpc: "2.0" 183 | id: "test-9" 184 | result: 185 | isError: false 186 | content: 187 | match:arrayElements: 188 | type: "text" 189 | text: "match:type:string" 190 | performance: 191 | maxResponseTime: "2000ms" 192 | stderr: "toBeEmpty" 193 | 194 | - it: "should handle special characters in search query" 195 | request: 196 | jsonrpc: "2.0" 197 | method: "tools/call" 198 | id: "test-10" 199 | params: 200 | name: "search_sfra_documentation" 201 | arguments: 202 | query: "dw.util" 203 | expect: 204 | response: 205 | jsonrpc: "2.0" 206 | id: "test-10" 207 | result: 208 | isError: false 209 | content: 210 | match:arrayElements: 211 | type: "text" 212 | text: "match:type:string" 213 | stderr: "toBeEmpty" 214 | 215 | - it: "should handle very long search queries efficiently" 216 | request: 217 | jsonrpc: "2.0" 218 | method: "tools/call" 219 | id: "test-11" 220 | params: 221 | name: "search_sfra_documentation" 222 | arguments: 223 | query: "template rendering isml view data processing controller middleware response request server router" 224 | expect: 225 | response: 226 | jsonrpc: "2.0" 227 | id: "test-11" 228 | result: 229 | isError: false 230 | content: 231 | match:arrayElements: 232 | type: "text" 233 | text: "match:regex:\\[[\\s\\S]*\\]" 234 | performance: 235 | maxResponseTime: "3000ms" 236 | stderr: "toBeEmpty" 237 | 238 | - it: "should return results for single character queries" 239 | request: 240 | jsonrpc: "2.0" 241 | method: "tools/call" 242 | id: "test-12" 243 | params: 244 | name: "search_sfra_documentation" 245 | arguments: 246 | query: "a" 247 | expect: 248 | response: 249 | jsonrpc: "2.0" 250 | id: "test-12" 251 | result: 252 | isError: false 253 | content: 254 | match:arrayElements: 255 | type: "text" 256 | text: "match:regex:\\[[\\s\\S]*\\]" 257 | stderr: "toBeEmpty" 258 | 259 | - it: "should handle numeric search terms" 260 | request: 261 | jsonrpc: "2.0" 262 | method: "tools/call" 263 | id: "test-13" 264 | params: 265 | name: "search_sfra_documentation" 266 | arguments: 267 | query: "200" 268 | expect: 269 | response: 270 | jsonrpc: "2.0" 271 | id: "test-13" 272 | result: 273 | isError: false 274 | content: 275 | match:arrayElements: 276 | type: "text" 277 | text: "match:type:string" 278 | stderr: "toBeEmpty" 279 | 280 | - it: "should return consistent results for same query" 281 | request: 282 | jsonrpc: "2.0" 283 | method: "tools/call" 284 | id: "test-14" 285 | params: 286 | name: "search_sfra_documentation" 287 | arguments: 288 | query: "cart" 289 | expect: 290 | response: 291 | jsonrpc: "2.0" 292 | id: "test-14" 293 | result: 294 | isError: false 295 | content: 296 | match:arrayElements: 297 | type: "text" 298 | text: "match:contains:relevanceScore" 299 | stderr: "toBeEmpty" 300 | 301 | - it: "should handle case-insensitive searches" 302 | request: 303 | jsonrpc: "2.0" 304 | method: "tools/call" 305 | id: "test-15" 306 | params: 307 | name: "search_sfra_documentation" 308 | arguments: 309 | query: "PRODUCT" 310 | expect: 311 | response: 312 | jsonrpc: "2.0" 313 | id: "test-15" 314 | result: 315 | isError: false 316 | content: 317 | match:arrayElements: 318 | type: "text" 319 | text: "match:regex:\\[[\\s\\S]*\\]" 320 | stderr: "toBeEmpty" 321 | 322 | - it: "should find results for core SFRA concepts" 323 | request: 324 | jsonrpc: "2.0" 325 | method: "tools/call" 326 | id: "test-16" 327 | params: 328 | name: "search_sfra_documentation" 329 | arguments: 330 | query: "middleware" 331 | expect: 332 | response: 333 | jsonrpc: "2.0" 334 | id: "test-16" 335 | result: 336 | isError: false 337 | content: 338 | match:arrayElements: 339 | type: "text" 340 | text: "match:regex:(?:matches)[\\s\\S]*(?:section)[\\s\\S]*(?:content)" 341 | stderr: "toBeEmpty" 342 | 343 | - it: "should handle hyphenated search terms" 344 | request: 345 | jsonrpc: "2.0" 346 | method: "tools/call" 347 | id: "test-17" 348 | params: 349 | name: "search_sfra_documentation" 350 | arguments: 351 | query: "product-full" 352 | expect: 353 | response: 354 | jsonrpc: "2.0" 355 | id: "test-17" 356 | result: 357 | isError: false 358 | content: 359 | match:arrayElements: 360 | type: "text" 361 | text: "match:type:string" 362 | stderr: "toBeEmpty" 363 | 364 | - it: "should return structured results for pricing queries" 365 | request: 366 | jsonrpc: "2.0" 367 | method: "tools/call" 368 | id: "test-18" 369 | params: 370 | name: "search_sfra_documentation" 371 | arguments: 372 | query: "price" 373 | expect: 374 | response: 375 | jsonrpc: "2.0" 376 | id: "test-18" 377 | result: 378 | isError: false 379 | content: 380 | match:arrayElements: 381 | type: "text" 382 | text: "match:regex:(?:category)[\\s\\S]*(?:pricing|product|core)" 383 | stderr: "toBeEmpty" 384 | 385 | - it: "should handle underscore in search terms" 386 | request: 387 | jsonrpc: "2.0" 388 | method: "tools/call" 389 | id: "test-19" 390 | params: 391 | name: "search_sfra_documentation" 392 | arguments: 393 | query: "view_data" 394 | expect: 395 | response: 396 | jsonrpc: "2.0" 397 | id: "test-19" 398 | result: 399 | isError: false 400 | content: 401 | match:arrayElements: 402 | type: "text" 403 | text: "match:type:string" 404 | stderr: "toBeEmpty" 405 | 406 | - it: "should respond quickly for common search terms" 407 | request: 408 | jsonrpc: "2.0" 409 | method: "tools/call" 410 | id: "test-20" 411 | params: 412 | name: "search_sfra_documentation" 413 | arguments: 414 | query: "model" 415 | expect: 416 | response: 417 | jsonrpc: "2.0" 418 | id: "test-20" 419 | result: 420 | isError: false 421 | content: 422 | match:arrayElements: 423 | type: "text" 424 | text: "match:contains:document" 425 | performance: 426 | maxResponseTime: "800ms" 427 | stderr: "toBeEmpty" 428 | ``` -------------------------------------------------------------------------------- /tests/logger.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Logger } from '../src/utils/logger'; 2 | import { existsSync, readFileSync, rmSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { tmpdir } from 'os'; 5 | 6 | describe('Logger', () => { 7 | let logger: Logger; 8 | let testLogDir: string; 9 | 10 | beforeEach(() => { 11 | // Create a unique test log directory for each test 12 | testLogDir = join(tmpdir(), `sfcc-mcp-logs-test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); 13 | 14 | // Clean up if directory somehow exists 15 | if (existsSync(testLogDir)) { 16 | rmSync(testLogDir, { recursive: true, force: true }); 17 | } 18 | }); 19 | 20 | afterEach(() => { 21 | // Clean up test log directory 22 | if (existsSync(testLogDir)) { 23 | rmSync(testLogDir, { recursive: true, force: true }); 24 | } 25 | }); 26 | 27 | describe('constructor', () => { 28 | it('should create logger and create log directory', () => { 29 | logger = new Logger('TEST', true, false, testLogDir); 30 | 31 | expect(existsSync(testLogDir)).toBe(true); 32 | expect(logger.getLogDirectory()).toBe(testLogDir); 33 | }); 34 | 35 | it('should create logger with custom context and write logs correctly', () => { 36 | logger = new Logger('CUSTOM-CONTEXT', true, false, testLogDir); 37 | 38 | logger.log('test message'); 39 | 40 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 41 | expect(existsSync(logFile)).toBe(true); 42 | 43 | const logContent = readFileSync(logFile, 'utf8'); 44 | expect(logContent).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[CUSTOM-CONTEXT\] test message\n$/); 45 | }); 46 | 47 | it('should create logger with timestamp disabled', () => { 48 | logger = new Logger('TEST', false, false, testLogDir); 49 | 50 | logger.log('test message'); 51 | 52 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 53 | expect(existsSync(logFile)).toBe(true); 54 | 55 | const logContent = readFileSync(logFile, 'utf8'); 56 | expect(logContent).toBe('[TEST] test message\n'); 57 | }); 58 | 59 | it('should create logger with debug enabled', () => { 60 | logger = new Logger('TEST', true, true, testLogDir); 61 | 62 | logger.debug('debug message'); 63 | 64 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 65 | expect(existsSync(logFile)).toBe(true); 66 | 67 | const logContent = readFileSync(logFile, 'utf8'); 68 | expect(logContent).toMatch( 69 | /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] \[DEBUG\] debug message\n$/, 70 | ); 71 | }); 72 | }); 73 | 74 | describe('logging methods', () => { 75 | beforeEach(() => { 76 | logger = new Logger('TEST', true, false, testLogDir); 77 | }); 78 | 79 | it('should write info messages to info log file', () => { 80 | logger.info('info message'); 81 | 82 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 83 | expect(existsSync(logFile)).toBe(true); 84 | 85 | const logContent = readFileSync(logFile, 'utf8'); 86 | expect(logContent).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] info message\n$/); 87 | }); 88 | 89 | it('should write log messages to info log file', () => { 90 | logger.log('log message'); 91 | 92 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 93 | expect(existsSync(logFile)).toBe(true); 94 | 95 | const logContent = readFileSync(logFile, 'utf8'); 96 | expect(logContent).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] log message\n$/); 97 | }); 98 | 99 | it('should write warning messages to warn log file', () => { 100 | logger.warn('warning message'); 101 | 102 | const logFile = join(testLogDir, 'sfcc-mcp-warn.log'); 103 | expect(existsSync(logFile)).toBe(true); 104 | 105 | const logContent = readFileSync(logFile, 'utf8'); 106 | expect(logContent).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] warning message\n$/); 107 | }); 108 | 109 | it('should write error messages to error log file', () => { 110 | logger.error('error message'); 111 | 112 | const logFile = join(testLogDir, 'sfcc-mcp-error.log'); 113 | expect(existsSync(logFile)).toBe(true); 114 | 115 | const logContent = readFileSync(logFile, 'utf8'); 116 | expect(logContent).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] error message\n$/); 117 | }); 118 | 119 | it('should write debug messages to debug log file when debug is enabled', () => { 120 | logger = new Logger('TEST', true, true, testLogDir); 121 | logger.debug('debug message'); 122 | 123 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 124 | expect(existsSync(logFile)).toBe(true); 125 | 126 | const logContent = readFileSync(logFile, 'utf8'); 127 | expect(logContent).toMatch( 128 | /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[TEST\] \[DEBUG\] debug message\n$/, 129 | ); 130 | }); 131 | 132 | it('should not write debug messages when debug is disabled', () => { 133 | logger = new Logger('TEST', true, false, testLogDir); 134 | logger.debug('debug message'); 135 | 136 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 137 | expect(existsSync(logFile)).toBe(false); 138 | }); 139 | 140 | it('should handle additional arguments', () => { 141 | const testObject = { key: 'value', number: 42 }; 142 | logger.info('message with args', 'string arg', testObject); 143 | 144 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 145 | expect(existsSync(logFile)).toBe(true); 146 | 147 | const logContent = readFileSync(logFile, 'utf8'); 148 | expect(logContent).toContain('message with args'); 149 | expect(logContent).toContain('string arg'); 150 | expect(logContent).toContain('"key": "value"'); 151 | expect(logContent).toContain('"number": 42'); 152 | }); 153 | }); 154 | 155 | describe('debug logging methods', () => { 156 | beforeEach(() => { 157 | logger = new Logger('TEST', true, true, testLogDir); 158 | }); 159 | 160 | it('should log method entry', () => { 161 | logger.methodEntry('testMethod', { param1: 'value1' }); 162 | 163 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 164 | expect(existsSync(logFile)).toBe(true); 165 | 166 | const logContent = readFileSync(logFile, 'utf8'); 167 | expect(logContent).toContain('[DEBUG] Entering method: testMethod with params:'); 168 | expect(logContent).toContain('"param1":"value1"'); 169 | }); 170 | 171 | it('should log method entry without params', () => { 172 | logger.methodEntry('testMethod'); 173 | 174 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 175 | expect(existsSync(logFile)).toBe(true); 176 | 177 | const logContent = readFileSync(logFile, 'utf8'); 178 | expect(logContent).toContain('[DEBUG] Entering method: testMethod'); 179 | expect(logContent).not.toContain('with params:'); 180 | }); 181 | 182 | it('should log method exit', () => { 183 | logger.methodExit('testMethod', { result: 'success' }); 184 | 185 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 186 | expect(existsSync(logFile)).toBe(true); 187 | 188 | const logContent = readFileSync(logFile, 'utf8'); 189 | expect(logContent).toContain('[DEBUG] Exiting method: testMethod with result:'); 190 | expect(logContent).toContain('"result":"success"'); 191 | }); 192 | 193 | it('should log method exit without result', () => { 194 | logger.methodExit('testMethod'); 195 | 196 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 197 | expect(existsSync(logFile)).toBe(true); 198 | 199 | const logContent = readFileSync(logFile, 'utf8'); 200 | expect(logContent).toContain('[DEBUG] Exiting method: testMethod'); 201 | expect(logContent).not.toContain('with result:'); 202 | }); 203 | 204 | it('should log timing information', () => { 205 | const startTime = Date.now() - 100; // Simulate 100ms operation 206 | logger.timing('testOperation', startTime); 207 | 208 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 209 | expect(existsSync(logFile)).toBe(true); 210 | 211 | const logContent = readFileSync(logFile, 'utf8'); 212 | expect(logContent).toMatch(/\[DEBUG\] Performance: testOperation took \d+ms/); 213 | }); 214 | }); 215 | 216 | describe('utility methods', () => { 217 | beforeEach(() => { 218 | logger = new Logger('TEST', true, false, testLogDir); 219 | }); 220 | 221 | it('should create child logger with combined context', () => { 222 | const childLogger = logger.createChildLogger('CHILD'); 223 | childLogger.info('child message'); 224 | 225 | const logFile = join(testLogDir, 'sfcc-mcp-info.log'); 226 | expect(existsSync(logFile)).toBe(true); 227 | 228 | const logContent = readFileSync(logFile, 'utf8'); 229 | expect(logContent).toContain('[TEST:CHILD] child message'); 230 | }); 231 | 232 | it('should enable debug logging dynamically', () => { 233 | logger.setDebugEnabled(true); 234 | logger.debug('now debug is enabled'); 235 | 236 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 237 | expect(existsSync(logFile)).toBe(true); 238 | 239 | const logContent = readFileSync(logFile, 'utf8'); 240 | expect(logContent).toContain('[DEBUG] now debug is enabled'); 241 | }); 242 | 243 | it('should disable debug logging dynamically', () => { 244 | logger = new Logger('TEST', true, true, testLogDir); 245 | logger.setDebugEnabled(false); 246 | logger.debug('this should not appear'); 247 | 248 | const logFile = join(testLogDir, 'sfcc-mcp-debug.log'); 249 | expect(existsSync(logFile)).toBe(false); 250 | }); 251 | 252 | it('should return log directory path', () => { 253 | const logDirectory = logger.getLogDirectory(); 254 | expect(logDirectory).toBe(testLogDir); 255 | expect(existsSync(logDirectory)).toBe(true); 256 | }); 257 | }); 258 | 259 | describe('error handling', () => { 260 | it('should handle file write errors gracefully', () => { 261 | // Mock appendFileSync to throw an error 262 | // eslint-disable-next-line @typescript-eslint/no-require-imports 263 | const fsMock = jest.spyOn(require('fs'), 'appendFileSync'); 264 | fsMock.mockImplementation(() => { 265 | throw new Error('File write error'); 266 | }); 267 | 268 | // Mock stderr.write to capture fallback behavior 269 | const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); 270 | 271 | logger = new Logger('TEST', true, false, testLogDir); 272 | logger.error('test error message'); 273 | 274 | // Should have attempted to write to stderr as fallback 275 | expect(stderrSpy).toHaveBeenCalledWith('[LOGGER ERROR] Could not write to log file: Error: File write error\n'); 276 | expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('test error message')); 277 | 278 | stderrSpy.mockRestore(); 279 | fsMock.mockRestore(); 280 | }); 281 | 282 | it('should not fallback to stderr for non-error log levels', () => { 283 | // Mock appendFileSync to throw an error 284 | // eslint-disable-next-line @typescript-eslint/no-require-imports 285 | const fsMock = jest.spyOn(require('fs'), 'appendFileSync'); 286 | fsMock.mockImplementation(() => { 287 | throw new Error('File write error'); 288 | }); 289 | 290 | // Mock stderr.write to capture fallback behavior 291 | const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); 292 | 293 | logger = new Logger('TEST', true, false, testLogDir); 294 | logger.info('test info message'); 295 | 296 | // Should not have written to stderr for non-error levels 297 | expect(stderrSpy).not.toHaveBeenCalled(); 298 | 299 | stderrSpy.mockRestore(); 300 | fsMock.mockRestore(); 301 | }); 302 | }); 303 | }); 304 | ``` -------------------------------------------------------------------------------- /docs/sfra/server.md: -------------------------------------------------------------------------------- ```markdown 1 | # Class Server 2 | 3 | ## Inheritance Hierarchy 4 | 5 | - Object 6 | - sfra.models.Server 7 | 8 | ## Description 9 | 10 | The SFRA Server class is the core routing solution for Salesforce Commerce Cloud's Storefront Reference Architecture (SFRA). This class provides a comprehensive middleware-based routing system that handles HTTP requests, manages route registration, and coordinates the request-response lifecycle. The Server class supports route definition with middleware chains, HTTP method shortcuts (GET/POST), route modification capabilities (prepend/append/replace), and integration with SFCC's hook system for extensibility. It serves as the foundation for all SFRA controller functionality, providing a structured approach to handling web requests with support for caching, personalization, redirects, and various rendering strategies. 11 | 12 | ## Properties 13 | 14 | ### routes 15 | 16 | **Type:** Object 17 | 18 | Internal registry of all registered routes, indexed by route name. 19 | 20 | ## Constructor Summary 21 | 22 | ### Server 23 | 24 | **Signature:** `Server()` 25 | 26 | Creates a new Server instance with an empty routes registry. 27 | 28 | ## Method Summary 29 | 30 | ### use 31 | 32 | **Signature:** `use(name, ...middleware) : Route` 33 | 34 | Creates a new route with middleware chain. 35 | 36 | ### get 37 | 38 | **Signature:** `get(name, ...middleware) : Route` 39 | 40 | Creates a GET-specific route with automatic HTTP method validation. 41 | 42 | ### post 43 | 44 | **Signature:** `post(name, ...middleware) : Route` 45 | 46 | Creates a POST-specific route with automatic HTTP method validation. 47 | 48 | ### exports 49 | 50 | **Signature:** `exports() : Object` 51 | 52 | Returns an exportable object containing all registered routes. 53 | 54 | ### extend 55 | 56 | **Signature:** `extend(server) : void` 57 | 58 | Extends the current server with routes from another server object. 59 | 60 | ### prepend 61 | 62 | **Signature:** `prepend(name, ...middleware) : void` 63 | 64 | Adds middleware to the beginning of an existing route's chain. 65 | 66 | ### append 67 | 68 | **Signature:** `append(name, ...middleware) : void` 69 | 70 | Adds middleware to the end of an existing route's chain. 71 | 72 | ### replace 73 | 74 | **Signature:** `replace(name, ...middleware) : void` 75 | 76 | Replaces an existing route with a new middleware chain. 77 | 78 | ### getRoute 79 | 80 | **Signature:** `getRoute(name) : Route` 81 | 82 | Retrieves a specific route by name. 83 | 84 | ## Method Detail 85 | 86 | ### use 87 | 88 | **Signature:** `use(name, ...middleware) : Route` 89 | 90 | **Description:** Creates a new route with the specified name and middleware chain. Automatically creates Request and Response objects from global SFCC objects and sets up route completion handling including caching, personalization, and rendering. 91 | 92 | **Parameters:** 93 | - `name` (String) - Unique name for the route 94 | - `...middleware` (Function[]) - Variable number of middleware functions to execute 95 | 96 | **Returns:** 97 | Route object representing the created route. 98 | 99 | **Throws:** 100 | - Error if name is not a string 101 | - Error if middleware contains non-function items 102 | - Error if route name already exists 103 | 104 | **Route Completion Handling:** 105 | - Sets cache expiration based on `res.cachePeriod` and `res.cachePeriodUnit` 106 | - Applies personalization with `price_promotion` vary-by header when `res.personalized` is true 107 | - Handles redirects with optional status codes 108 | - Applies rendering through the render system 109 | 110 | ### get 111 | 112 | **Signature:** `get(name, ...middleware) : Route` 113 | 114 | **Description:** Convenience method for creating GET-only routes. Automatically prepends HTTP GET validation middleware to the chain. 115 | 116 | **Parameters:** 117 | - `name` (String) - Unique name for the route 118 | - `...middleware` (Function[]) - Variable number of middleware functions 119 | 120 | **Returns:** 121 | Route object with GET method validation. 122 | 123 | ### post 124 | 125 | **Signature:** `post(name, ...middleware) : Route` 126 | 127 | **Description:** Convenience method for creating POST-only routes. Automatically prepends HTTP POST validation middleware to the chain. 128 | 129 | **Parameters:** 130 | - `name` (String) - Unique name for the route 131 | - `...middleware` (Function[]) - Variable number of middleware functions 132 | 133 | **Returns:** 134 | Route object with POST method validation. 135 | 136 | ### exports 137 | 138 | **Signature:** `exports() : Object` 139 | 140 | **Description:** Creates an exportable object containing all registered routes with their public interfaces. Includes internal routes registry for extension capabilities. 141 | 142 | **Returns:** 143 | Object with route names as keys and route interfaces as values, plus `__routes` property containing internal routes. 144 | 145 | **Export Format:** 146 | ```javascript 147 | { 148 | "routeName": { /* route interface */ }, 149 | "__routes": { /* internal routes registry */ } 150 | } 151 | ``` 152 | 153 | ### extend 154 | 155 | **Signature:** `extend(server) : void` 156 | 157 | **Description:** Extends the current server with routes from another server object created by the `exports()` method. Validates the server object structure before extension. 158 | 159 | **Parameters:** 160 | - `server` (Object) - Server object created by `exports()` method 161 | 162 | **Throws:** 163 | - Error if server object is invalid (missing `__routes`) 164 | - Error if server has no routes to extend 165 | 166 | ### prepend 167 | 168 | **Signature:** `prepend(name, ...middleware) : void` 169 | 170 | **Description:** Adds middleware functions to the beginning of an existing route's middleware chain, allowing for route enhancement and modification. 171 | 172 | **Parameters:** 173 | - `name` (String) - Name of existing route to modify 174 | - `...middleware` (Function[]) - Middleware functions to prepend 175 | 176 | **Throws:** 177 | - Error if name is not a string 178 | - Error if middleware contains non-function items 179 | - Error if route does not exist 180 | 181 | ### append 182 | 183 | **Signature:** `append(name, ...middleware) : void` 184 | 185 | **Description:** Adds middleware functions to the end of an existing route's middleware chain, enabling route extension and additional processing. 186 | 187 | **Parameters:** 188 | - `name` (String) - Name of existing route to modify 189 | - `...middleware` (Function[]) - Middleware functions to append 190 | 191 | **Throws:** 192 | - Error if name is not a string 193 | - Error if middleware contains non-function items 194 | - Error if route does not exist 195 | 196 | ### replace 197 | 198 | **Signature:** `replace(name, ...middleware) : void` 199 | 200 | **Description:** Completely replaces an existing route with a new middleware chain, maintaining the same route name but changing implementation. 201 | 202 | **Parameters:** 203 | - `name` (String) - Name of existing route to replace 204 | - `...middleware` (Function[]) - New middleware functions for the route 205 | 206 | **Throws:** 207 | - Error if name is not a string 208 | - Error if middleware contains non-function items 209 | - Error if route does not exist 210 | 211 | ### getRoute 212 | 213 | **Signature:** `getRoute(name) : Route` 214 | 215 | **Description:** Retrieves a specific route object by name for inspection or direct manipulation. 216 | 217 | **Parameters:** 218 | - `name` (String) - Name of the route to retrieve 219 | 220 | **Returns:** 221 | Route object if found, undefined otherwise. 222 | 223 | ## Route Lifecycle Management 224 | 225 | ### Route Creation Process 226 | 227 | 1. **Parameter Validation**: Validates route name and middleware chain 228 | 2. **Request/Response Creation**: Creates SFRA Request and Response objects from globals 229 | 3. **Route Object Creation**: Instantiates Route with name, middleware, request, and response 230 | 4. **Event Handler Registration**: Sets up `route:Complete` event handler 231 | 5. **Route Registration**: Stores route in internal registry 232 | 6. **Hook Integration**: Calls `app.server.registerRoute` hook if available 233 | 234 | ### Route Completion Handling 235 | 236 | **Cache Management:** 237 | - Applies cache expiration when `res.cachePeriod` is set 238 | - Supports minutes or hours as cache period units (defaults to hours) 239 | - Sets HTTP expires header on the response 240 | 241 | **Personalization:** 242 | - Sets `price_promotion` vary-by header when `res.personalized` is true 243 | - Enables proper caching behavior for personalized content 244 | 245 | **Redirect Processing:** 246 | - Handles redirect URLs with optional status codes 247 | - Emits `route:Redirect` event before redirecting 248 | - Supports both default and custom redirect status codes 249 | 250 | **Rendering Pipeline:** 251 | - Applies all accumulated renderings through the render system 252 | - Processes ISML templates, JSON responses, XML output, and Page Designer pages 253 | 254 | ## Integration Points 255 | 256 | ### Global Object Integration 257 | 258 | **Request Object Creation:** 259 | ```javascript 260 | var rq = new Request( 261 | typeof request !== 'undefined' ? request : {}, 262 | typeof customer !== 'undefined' ? customer : {}, 263 | typeof session !== 'undefined' ? session : {} 264 | ); 265 | ``` 266 | 267 | **Response Object Creation:** 268 | ```javascript 269 | var rs = new Response(typeof response !== 'undefined' ? response : {}); 270 | ``` 271 | 272 | ### Hook System Integration 273 | 274 | The server integrates with SFCC's hook system through the `app.server.registerRoute` hook, allowing: 275 | - Custom route event handlers 276 | - Route-specific processing extensions 277 | - Integration with custom middleware systems 278 | 279 | ### Middleware Architecture 280 | 281 | **Middleware Function Signature:** 282 | ```javascript 283 | function middleware(req, res, next) { 284 | // Middleware logic 285 | next(); // Continue to next middleware 286 | } 287 | ``` 288 | 289 | **Middleware Chain Execution:** 290 | - Sequential execution of middleware functions 291 | - Support for asynchronous operations through next() callbacks 292 | - Automatic error handling and route completion 293 | 294 | ## Usage Examples 295 | 296 | ### Basic Route Creation 297 | ```javascript 298 | server.use('HomePage', function(req, res, next) { 299 | res.render('home/homepage', { title: 'Welcome' }); 300 | next(); 301 | }); 302 | ``` 303 | 304 | ### HTTP Method-Specific Routes 305 | ```javascript 306 | server.get('ProductShow', middleware.cache, function(req, res, next) { 307 | // GET-only route with caching 308 | next(); 309 | }); 310 | 311 | server.post('CartAdd', middleware.csrf, function(req, res, next) { 312 | // POST-only route with CSRF protection 313 | next(); 314 | }); 315 | ``` 316 | 317 | ### Route Modification 318 | ```javascript 319 | // Add authentication to existing route 320 | server.prepend('Account-Show', middleware.auth); 321 | 322 | // Add logging to existing route 323 | server.append('Checkout-Begin', middleware.logging); 324 | 325 | // Replace route implementation 326 | server.replace('Search-Show', newSearchMiddleware); 327 | ``` 328 | 329 | ### Server Extension 330 | ```javascript 331 | // Export routes from one server 332 | var exportedRoutes = server.exports(); 333 | 334 | // Extend another server 335 | var newServer = new Server(); 336 | newServer.extend(exportedRoutes); 337 | ``` 338 | 339 | ## Error Handling 340 | 341 | ### Parameter Validation Errors 342 | 343 | - **Invalid Route Name**: Throws error if first parameter is not a string 344 | - **Invalid Middleware**: Throws error if middleware chain contains non-functions 345 | - **Duplicate Route**: Throws error if route name already exists 346 | 347 | ### Extension Errors 348 | 349 | - **Invalid Server Object**: Throws error for objects missing `__routes` property 350 | - **Empty Server**: Throws error when extending with server that has no routes 351 | 352 | ### Route Modification Errors 353 | 354 | - **Non-Existent Route**: Throws error when trying to modify routes that don't exist 355 | - **Invalid Parameters**: Validates parameters before attempting modifications 356 | 357 | ## Property Details 358 | 359 | ### routes 360 | 361 | **Type:** Object 362 | 363 | **Description:** Internal registry storing all route definitions indexed by route name. Each entry contains a Route object with the complete middleware chain and configuration. 364 | 365 | **Structure:** 366 | ```javascript 367 | { 368 | "routeName": Route, // Route object instance 369 | "anotherRoute": Route 370 | } 371 | ``` 372 | 373 | This registry enables route lookup, modification, and export functionality while maintaining the complete route configuration and state. 374 | 375 | --- 376 | ``` -------------------------------------------------------------------------------- /tests/system-objects-client.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for OCAPISystemObjectsClient 3 | * Tests system objects operations 4 | */ 5 | 6 | import { OCAPISystemObjectsClient } from '../src/clients/ocapi/system-objects-client.js'; 7 | import { OCAPIConfig } from '../src/types/types.js'; 8 | import { QueryBuilder } from '../src/utils/query-builder.js'; 9 | import { Validator } from '../src/utils/validator.js'; 10 | 11 | // Mock dependencies 12 | jest.mock('../src/clients/base/ocapi-auth-client.js'); 13 | jest.mock('../src/utils/query-builder.js'); 14 | jest.mock('../src/utils/validator.js'); 15 | 16 | describe('OCAPISystemObjectsClient', () => { 17 | let client: OCAPISystemObjectsClient; 18 | let mockValidateRequired: jest.MockedFunction<typeof Validator.validateRequired>; 19 | let mockValidateObjectType: jest.MockedFunction<typeof Validator.validateObjectType>; 20 | let mockValidateSearchRequest: jest.MockedFunction<typeof Validator.validateSearchRequest>; 21 | let mockQueryBuilderFromObject: jest.MockedFunction<typeof QueryBuilder.fromObject>; 22 | 23 | const mockConfig: OCAPIConfig = { 24 | hostname: 'test-instance.demandware.net', 25 | clientId: 'test-client-id', 26 | clientSecret: 'test-client-secret', 27 | version: 'v21_3', 28 | }; 29 | 30 | beforeEach(() => { 31 | jest.clearAllMocks(); 32 | 33 | // Mock Validator methods 34 | mockValidateRequired = Validator.validateRequired as jest.MockedFunction<typeof Validator.validateRequired>; 35 | mockValidateObjectType = Validator.validateObjectType as jest.MockedFunction<typeof Validator.validateObjectType>; 36 | mockValidateSearchRequest = Validator.validateSearchRequest as jest.MockedFunction< 37 | typeof Validator.validateSearchRequest 38 | >; 39 | 40 | // Reset mock implementations to default behavior 41 | mockValidateRequired.mockImplementation(() => {}); 42 | mockValidateObjectType.mockImplementation(() => {}); 43 | mockValidateSearchRequest.mockImplementation(() => {}); 44 | 45 | // Mock QueryBuilder 46 | mockQueryBuilderFromObject = QueryBuilder.fromObject as jest.MockedFunction<typeof QueryBuilder.fromObject>; 47 | 48 | client = new OCAPISystemObjectsClient(mockConfig); 49 | 50 | // Mock the inherited methods by adding them as properties - avoid protected access 51 | (client as any).get = jest.fn().mockResolvedValue({ data: 'mocked' }); 52 | (client as any).post = jest.fn().mockResolvedValue({ data: 'mocked' }); 53 | }); 54 | 55 | describe('constructor', () => { 56 | it('should initialize with correct base URL', () => { 57 | expect(client).toBeInstanceOf(OCAPISystemObjectsClient); 58 | }); 59 | 60 | it('should use default version when not provided', () => { 61 | const configWithoutVersion = { 62 | hostname: 'test.demandware.net', 63 | clientId: 'client-id', 64 | clientSecret: 'client-secret', 65 | }; 66 | 67 | const clientWithDefaults = new OCAPISystemObjectsClient(configWithoutVersion); 68 | expect(clientWithDefaults).toBeInstanceOf(OCAPISystemObjectsClient); 69 | }); 70 | }); 71 | 72 | describe('getSystemObjectDefinitions', () => { 73 | it('should make GET request to system_object_definitions endpoint', async () => { 74 | await client.getSystemObjectDefinitions(); 75 | 76 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions'); 77 | }); 78 | 79 | it('should include query parameters when provided', async () => { 80 | const params = { start: 0, count: 10, select: '(**)' }; 81 | mockQueryBuilderFromObject.mockReturnValue('start=0&count=10&select=%28%2A%2A%29'); 82 | 83 | await client.getSystemObjectDefinitions(params); 84 | 85 | expect(QueryBuilder.fromObject).toHaveBeenCalledWith(params); 86 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions?start=0&count=10&select=%28%2A%2A%29'); 87 | }); 88 | 89 | it('should not include query string when no parameters provided', async () => { 90 | mockQueryBuilderFromObject.mockReturnValue(''); 91 | 92 | await client.getSystemObjectDefinitions({}); 93 | 94 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions'); 95 | }); 96 | }); 97 | 98 | describe('getSystemObjectDefinition', () => { 99 | it('should validate required parameters', async () => { 100 | const objectType = 'Product'; 101 | 102 | await client.getSystemObjectDefinition(objectType); 103 | 104 | expect(Validator.validateRequired).toHaveBeenCalledWith({ objectType }, ['objectType']); 105 | expect(Validator.validateObjectType).toHaveBeenCalledWith(objectType); 106 | }); 107 | 108 | it('should make GET request with encoded object type', async () => { 109 | const objectType = 'Custom Object'; 110 | 111 | await client.getSystemObjectDefinition(objectType); 112 | 113 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions/Custom%20Object'); 114 | }); 115 | 116 | it('should handle special characters in object type', async () => { 117 | const objectType = 'Product/Variant'; 118 | 119 | await client.getSystemObjectDefinition(objectType); 120 | 121 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions/Product%2FVariant'); 122 | }); 123 | }); 124 | 125 | describe('searchSystemObjectDefinitions', () => { 126 | it('should validate search request', async () => { 127 | const searchRequest = { 128 | query: { 129 | text_query: { 130 | fields: ['id', 'display_name'], 131 | search_phrase: 'product', 132 | }, 133 | }, 134 | }; 135 | 136 | await client.searchSystemObjectDefinitions(searchRequest); 137 | 138 | expect(Validator.validateSearchRequest).toHaveBeenCalledWith(searchRequest); 139 | }); 140 | 141 | it('should make POST request to system object definition search endpoint', async () => { 142 | const searchRequest = { 143 | query: { 144 | text_query: { 145 | fields: ['id', 'display_name'], 146 | search_phrase: 'product', 147 | }, 148 | }, 149 | }; 150 | 151 | await client.searchSystemObjectDefinitions(searchRequest); 152 | 153 | expect((client as any).post).toHaveBeenCalledWith('/system_object_definition_search', searchRequest); 154 | }); 155 | }); 156 | 157 | describe('searchSystemObjectAttributeDefinitions', () => { 158 | it('should validate all required parameters', async () => { 159 | const objectType = 'Product'; 160 | const searchRequest = { 161 | query: { 162 | text_query: { 163 | fields: ['id', 'display_name'], 164 | search_phrase: 'custom', 165 | }, 166 | }, 167 | }; 168 | 169 | await client.searchSystemObjectAttributeDefinitions(objectType, searchRequest); 170 | 171 | expect(Validator.validateRequired).toHaveBeenCalledWith({ objectType }, ['objectType']); 172 | expect(Validator.validateObjectType).toHaveBeenCalledWith(objectType); 173 | expect(Validator.validateSearchRequest).toHaveBeenCalledWith(searchRequest); 174 | }); 175 | 176 | it('should make POST request to attribute definition search endpoint', async () => { 177 | const objectType = 'Product'; 178 | const searchRequest = { 179 | query: { 180 | text_query: { 181 | fields: ['id', 'display_name'], 182 | search_phrase: 'custom', 183 | }, 184 | }, 185 | }; 186 | 187 | await client.searchSystemObjectAttributeDefinitions(objectType, searchRequest); 188 | 189 | expect((client as any).post).toHaveBeenCalledWith('/system_object_definitions/Product/attribute_definition_search', searchRequest); 190 | }); 191 | }); 192 | 193 | describe('searchSystemObjectAttributeGroups', () => { 194 | it('should validate all required parameters', async () => { 195 | const objectType = 'SitePreferences'; 196 | const searchRequest = { 197 | query: { match_all_query: {} }, 198 | }; 199 | 200 | await client.searchSystemObjectAttributeGroups(objectType, searchRequest); 201 | 202 | expect(Validator.validateRequired).toHaveBeenCalledWith({ objectType }, ['objectType']); 203 | expect(Validator.validateObjectType).toHaveBeenCalledWith(objectType); 204 | expect(Validator.validateSearchRequest).toHaveBeenCalledWith(searchRequest); 205 | }); 206 | 207 | it('should make POST request to attribute group search endpoint', async () => { 208 | const objectType = 'SitePreferences'; 209 | const searchRequest = { 210 | query: { 211 | text_query: { 212 | fields: ['id', 'display_name'], 213 | search_phrase: 'general', 214 | }, 215 | }, 216 | sorts: [{ field: 'position', sort_order: 'asc' as const }], 217 | }; 218 | 219 | await client.searchSystemObjectAttributeGroups(objectType, searchRequest); 220 | 221 | expect((client as any).post).toHaveBeenCalledWith( 222 | '/system_object_definitions/SitePreferences/attribute_group_search', 223 | searchRequest, 224 | ); 225 | }); 226 | }); 227 | 228 | describe('error handling', () => { 229 | it('should propagate validation errors', async () => { 230 | const validationError = new Error('Validation failed'); 231 | mockValidateRequired.mockImplementation(() => { 232 | throw validationError; 233 | }); 234 | 235 | await expect(client.getSystemObjectDefinition('Product')).rejects.toThrow(validationError); 236 | }); 237 | 238 | it('should propagate HTTP errors from base client', async () => { 239 | const httpError = new Error('HTTP request failed'); 240 | (client as any).get = jest.fn().mockRejectedValue(httpError); 241 | 242 | await expect(client.getSystemObjectDefinitions()).rejects.toThrow(httpError); 243 | }); 244 | 245 | it('should propagate search validation errors', async () => { 246 | const searchValidationError = new Error('Invalid search request'); 247 | mockValidateSearchRequest.mockImplementation(() => { 248 | throw searchValidationError; 249 | }); 250 | 251 | const searchRequest = { query: {} }; 252 | await expect(client.searchSystemObjectDefinitions(searchRequest)).rejects.toThrow(searchValidationError); 253 | }); 254 | }); 255 | 256 | describe('integration scenarios', () => { 257 | it('should handle complex search with all options', async () => { 258 | const objectType = 'Product'; 259 | const searchRequest = { 260 | query: { 261 | bool_query: { 262 | must: [ 263 | { 264 | text_query: { 265 | fields: ['id'], 266 | search_phrase: 'custom', 267 | }, 268 | }, 269 | ], 270 | must_not: [ 271 | { 272 | term_query: { 273 | fields: ['system'], 274 | operator: 'is', 275 | values: [true], 276 | }, 277 | }, 278 | ], 279 | }, 280 | }, 281 | sorts: [ 282 | { field: 'display_name', sort_order: 'asc' as const }, 283 | { field: 'id' }, 284 | ], 285 | start: 10, 286 | count: 25, 287 | select: '(**)', 288 | }; 289 | 290 | await client.searchSystemObjectAttributeDefinitions(objectType, searchRequest); 291 | 292 | expect(Validator.validateRequired).toHaveBeenCalled(); 293 | expect(Validator.validateObjectType).toHaveBeenCalled(); 294 | expect(Validator.validateSearchRequest).toHaveBeenCalledWith(searchRequest); 295 | expect((client as any).post).toHaveBeenCalledWith( 296 | '/system_object_definitions/Product/attribute_definition_search', 297 | searchRequest, 298 | ); 299 | }); 300 | 301 | it('should handle empty query parameters gracefully', async () => { 302 | const params = {}; 303 | mockQueryBuilderFromObject.mockReturnValue(''); 304 | 305 | await client.getSystemObjectDefinitions(params); 306 | 307 | expect((client as any).get).toHaveBeenCalledWith('/system_object_definitions'); 308 | }); 309 | }); 310 | }); 311 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-latest-error.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { test, describe, before, after, beforeEach } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import { connect } from 'mcp-aegis'; 4 | 5 | describe('get_latest_error - Full Mode Programmatic Tests (Optimized)', () => { 6 | let client; 7 | 8 | before(async () => { 9 | client = await connect('./aegis.config.with-dw.json'); 10 | }); 11 | 12 | after(async () => { 13 | if (client?.connected) { 14 | await client.disconnect(); 15 | } 16 | }); 17 | 18 | beforeEach(() => { 19 | // CRITICAL: Clear all buffers to prevent leaking into next tests 20 | client.clearAllBuffers(); // Recommended - comprehensive protection 21 | }); 22 | 23 | // Simplified helper functions for common validations 24 | function assertValidMCPResponse(result) { 25 | assert.ok(result.content, 'Should have content'); 26 | assert.ok(Array.isArray(result.content), 'Content should be array'); 27 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 28 | assert.equal(result.content[0].type, 'text', 'Content should be text type'); 29 | } 30 | 31 | function assertErrorResponse(result, expectedErrorText) { 32 | assertValidMCPResponse(result); 33 | assert.equal(result.isError, true, 'Should be an error response'); 34 | assert.ok(result.content[0].text.includes(expectedErrorText), 35 | `Expected error text "${expectedErrorText}" in "${result.content[0].text}"`); 36 | } 37 | 38 | function assertSuccessWithLimit(result, expectedLimit) { 39 | assertValidMCPResponse(result); 40 | assert.equal(result.isError, false, 'Should not be an error response'); 41 | const text = result.content[0].text; 42 | assert.ok(text.includes(`Latest ${expectedLimit} error messages`), 43 | `Should mention "${expectedLimit}" error messages`); 44 | assert.ok(/error-blade-\d{8}-\d{6}\.log/.test(text), 'Should contain log file pattern'); 45 | assert.ok(text.includes('ERROR'), 'Should contain ERROR level entries'); 46 | } 47 | 48 | // Helper function to get current date in YYYYMMDD format 49 | function getCurrentDateString() { 50 | const now = new Date(); 51 | const year = now.getFullYear(); 52 | const month = String(now.getMonth() + 1).padStart(2, '0'); 53 | const day = String(now.getDate()).padStart(2, '0'); 54 | return `${year}${month}${day}`; 55 | } 56 | 57 | // Core functionality tests (essential scenarios) 58 | describe('Core Functionality', () => { 59 | test('should retrieve error messages with default parameters', async () => { 60 | const result = await client.callTool('get_latest_error', {}); 61 | 62 | assertSuccessWithLimit(result, 10); // Default limit is 10 63 | 64 | // Verify SFCC-specific content is present 65 | const text = result.content[0].text; 66 | assert.ok(/PipelineCallServlet|SystemJobThread/.test(text), 'Should contain SFCC thread patterns'); 67 | assert.ok(text.includes('Sites-'), 'Should contain Sites information'); 68 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(text), 'Should contain GMT timestamps'); 69 | }); 70 | 71 | test('should respect limit parameter and return ordered results', async () => { 72 | const result = await client.callTool('get_latest_error', { limit: 3 }); 73 | 74 | assertSuccessWithLimit(result, 3); 75 | 76 | const text = result.content[0].text; 77 | // Should contain separators for multiple entries 78 | const separatorCount = (text.match(/---/g) || []).length; 79 | assert.ok(separatorCount >= 1, 'Should have separators between entries'); 80 | 81 | // Verify chronological order (newest first) with known mock data 82 | assert.ok(text.includes('CQ - AWS S3 Configuration Issue'), 'Should contain latest error'); 83 | assert.ok(text.includes('Payment authorization failed'), 'Should contain second latest error'); 84 | }); 85 | 86 | test('should handle date parameter correctly', async () => { 87 | const result = await client.callTool('get_latest_error', { 88 | date: getCurrentDateString(), 89 | limit: 2 90 | }); 91 | 92 | assertSuccessWithLimit(result, 2); 93 | }); 94 | }); 95 | 96 | // Parameter validation tests (core error handling) 97 | describe('Parameter Validation', () => { 98 | test('should reject invalid limit types and values', async () => { 99 | // Test string limit 100 | const stringResult = await client.callTool('get_latest_error', { limit: '5' }); 101 | assertErrorResponse(stringResult, 'Invalid limit \'5\' for get_latest_error. Must be a valid number'); 102 | 103 | // Test zero limit 104 | const zeroResult = await client.callTool('get_latest_error', { limit: 0 }); 105 | assertErrorResponse(zeroResult, 'Invalid limit \'0\' for get_latest_error'); 106 | 107 | // Test negative limit 108 | const negativeResult = await client.callTool('get_latest_error', { limit: -5 }); 109 | assertErrorResponse(negativeResult, 'Invalid limit'); 110 | }); 111 | 112 | test('should handle large limits appropriately', async () => { 113 | const largeResult = await client.callTool('get_latest_error', { limit: 50 }); 114 | assertSuccessWithLimit(largeResult, 50); 115 | 116 | // Test extremely large limit (should error) 117 | const hugeResult = await client.callTool('get_latest_error', { limit: 9999 }); 118 | assertErrorResponse(hugeResult, 'Invalid limit'); 119 | }); 120 | 121 | test('should handle date parameters gracefully', async () => { 122 | // Valid YYYYMMDD date 123 | const validResult = await client.callTool('get_latest_error', { 124 | date: '20240101', 125 | limit: 1 126 | }); 127 | assertValidMCPResponse(validResult); 128 | assert.equal(validResult.isError, false, 'Valid date should not error'); 129 | 130 | // Invalid date format (should handle gracefully) 131 | const invalidResult = await client.callTool('get_latest_error', { 132 | date: '2024-01-01', 133 | limit: 1 134 | }); 135 | assertValidMCPResponse(invalidResult); 136 | // Should not crash, may succeed or fail gracefully 137 | }); 138 | 139 | test('should handle missing arguments gracefully', async () => { 140 | const result = await client.callTool('get_latest_error'); 141 | assertSuccessWithLimit(result, 10); // Should use default limit 142 | }); 143 | }); 144 | 145 | // Content validation tests (SFCC-specific patterns) 146 | describe('Content Validation', () => { 147 | test('should include realistic SFCC error scenarios and structure', async () => { 148 | const result = await client.callTool('get_latest_error', { limit: 5 }); 149 | 150 | assertValidMCPResponse(result); 151 | assert.equal(result.isError, false, 'Should not be an error'); 152 | const text = result.content[0].text; 153 | 154 | // Should contain common SFCC error patterns 155 | const errorPatterns = [ 156 | 'Custom cartridge error', 157 | 'Product import failed', 158 | 'Customer profile creation failed', 159 | 'Payment authorization failed', 160 | 'AWS S3 Configuration Issue' 161 | ]; 162 | 163 | const foundPatterns = errorPatterns.filter(pattern => text.includes(pattern)); 164 | assert.ok(foundPatterns.length > 0, 165 | `Should contain at least one error pattern. Found: ${foundPatterns.join(', ')}`); 166 | 167 | // Validate basic log structure elements 168 | assert.ok(text.includes('[') && text.includes(']'), 'Should contain log brackets'); 169 | assert.ok(/\|\d+\|/.test(text), 'Should contain thread IDs with pipes'); 170 | }); 171 | 172 | test('should contain comprehensive SFCC-specific patterns', async () => { 173 | const result = await client.callTool('get_latest_error', { limit: 3 }); 174 | 175 | assertValidMCPResponse(result); 176 | assert.equal(result.isError, false, 'Should not be an error'); 177 | const text = result.content[0].text; 178 | 179 | // SFCC-specific validation patterns 180 | const sfccPatterns = [ 181 | { pattern: /Sites-\w+/, name: 'Sites names' }, 182 | { pattern: /PipelineCallServlet|SystemJobThread/, name: 'Thread types' }, 183 | { pattern: /\|\d+\|/, name: 'Thread IDs' }, 184 | { pattern: /custom \[\]/, name: 'Custom category' } 185 | ]; 186 | 187 | const matchedPatterns = sfccPatterns.filter(({ pattern }) => pattern.test(text)); 188 | assert.ok(matchedPatterns.length >= 2, 189 | `Should match at least 2 SFCC patterns. Matched: ${matchedPatterns.map(p => p.name).join(', ')}`); 190 | }); 191 | }); 192 | 193 | // Error recovery and resilience testing 194 | describe('Error Recovery and Resilience', () => { 195 | test('should handle error scenarios and recover properly', async () => { 196 | // Test invalid parameter 197 | const invalidResult = await client.callTool('get_latest_error', { limit: 0 }); 198 | assertErrorResponse(invalidResult, 'Invalid limit'); 199 | 200 | // Test recovery with valid parameters 201 | const validResult = await client.callTool('get_latest_error', { limit: 1 }); 202 | assertValidMCPResponse(validResult); 203 | assert.equal(validResult.isError, false, 'Should work normally after error'); 204 | assertSuccessWithLimit(validResult, 1); 205 | 206 | // Test edge cases without breaking 207 | const edgeCases = [ 208 | { limit: '1' }, // String limit (should error) 209 | { limit: 1000 }, // Large limit (should error) 210 | { date: '' }, // Empty date (should handle gracefully) 211 | { invalid: 'param' } // Invalid parameter name (should handle gracefully) 212 | ]; 213 | 214 | // Test all edge cases sequentially 215 | for (const testCase of edgeCases) { 216 | const result = await client.callTool('get_latest_error', testCase); 217 | assertValidMCPResponse(result); 218 | // Some may error, some may succeed - but none should crash 219 | } 220 | 221 | // Verify tool still works after edge cases 222 | const finalResult = await client.callTool('get_latest_error', { limit: 1 }); 223 | assertValidMCPResponse(finalResult); 224 | assert.equal(finalResult.isError, false, 'Should work after edge cases'); 225 | }); 226 | }); 227 | 228 | // Advanced scenarios (simplified multi-step workflow) 229 | describe('Advanced Scenarios', () => { 230 | test('should support typical error analysis workflow', async () => { 231 | // Simulate a typical workflow: recent errors -> specific investigation -> recovery validation 232 | 233 | // Step 1: Get recent errors for overview 234 | const recentErrors = await client.callTool('get_latest_error', { limit: 2 }); 235 | assertValidMCPResponse(recentErrors); 236 | assert.equal(recentErrors.isError, false, 'Recent errors should succeed'); 237 | 238 | // Step 2: Get more detailed view for specific analysis 239 | const detailedErrors = await client.callTool('get_latest_error', { 240 | date: getCurrentDateString(), 241 | limit: 5 242 | }); 243 | assertValidMCPResponse(detailedErrors); 244 | assert.equal(detailedErrors.isError, false, 'Detailed errors should succeed'); 245 | 246 | // Step 3: Verify both contain expected error content 247 | [recentErrors, detailedErrors].forEach((result, index) => { 248 | const text = result.content[0].text; 249 | assert.ok(text.includes('ERROR'), `Result ${index} should contain ERROR level`); 250 | assert.ok(text.includes('Latest'), `Result ${index} should contain 'Latest' message`); 251 | assert.ok(/error-blade-.*\.log/.test(text), `Result ${index} should contain log filename`); 252 | }); 253 | }); 254 | }); 255 | }); 256 | ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/mock-data/ocapi/system-object-attribute-groups-product.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_v": "23.2", 3 | "_type": "object_attribute_group_search_result", 4 | "count": 35, 5 | "hits": [ 6 | { 7 | "_type": "object_attribute_group", 8 | "_resource_state": "f5c17fdc7ebeaf9136d6fef12d76d68cf238abb98ccaa282e40a657b1a5fcfb6", 9 | "id": "ChannelIntegration", 10 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/ChannelIntegration" 11 | }, 12 | { 13 | "_type": "object_attribute_group", 14 | "_resource_state": "9787c54106885bf78ff45cd7f3a7f64a073d911485473db6289f4fb0252e345d", 15 | "id": "ExternalSearch", 16 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/ExternalSearch" 17 | }, 18 | { 19 | "_type": "object_attribute_group", 20 | "_resource_state": "01b32a52ad9a64e426f08a21c0d1c3732fa8f573399db8bcf8d68a49dbdea9b6", 21 | "id": "Order", 22 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/Order" 23 | }, 24 | { 25 | "_type": "object_attribute_group", 26 | "_resource_state": "3ad88093fb2451eb026a2c103b26018e9ab265391da20a39fb8bf3a8c5bb477d", 27 | "id": "PXL3_Hoesjes", 28 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/PXL3_Hoesjes" 29 | }, 30 | { 31 | "_type": "object_attribute_group", 32 | "_resource_state": "dc899d1d79f89a7a1845bc0fdea1e7b76b826f155f59807be55c0d8ce4763dc9", 33 | "id": "PXL3_Oordopjes", 34 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/PXL3_Oordopjes" 35 | }, 36 | { 37 | "_type": "object_attribute_group", 38 | "_resource_state": "d01dd2dca011264ffadd379d3c56151a7636dc1d6faf4ada51a081f2c72999eb", 39 | "id": "PXL3_Opladers", 40 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/PXL3_Opladers" 41 | }, 42 | { 43 | "_type": "object_attribute_group", 44 | "_resource_state": "cc17ba20294c83f5322e26ace9f60843de0c8dd55ff1bbfa4ad7b4e414595b47", 45 | "id": "Presentation", 46 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/Presentation" 47 | }, 48 | { 49 | "_type": "object_attribute_group", 50 | "_resource_state": "e0f82a956514fb4f24c601b1197248b2f21987bb339a7bddf14df5b7a309a1f9", 51 | "id": "SearchRanking", 52 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/SearchRanking" 53 | }, 54 | { 55 | "_type": "object_attribute_group", 56 | "_resource_state": "3495916efcfef9381c9630e7ac29f740c4ecd2c2e020692db7a8046838ee5a61", 57 | "id": "SiteMap", 58 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/SiteMap" 59 | }, 60 | { 61 | "_type": "object_attribute_group", 62 | "_resource_state": "e63730013e6fd915009e27898def794a844029e6fc2b96487bf00089b648d03a", 63 | "id": "Store", 64 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/Store" 65 | }, 66 | { 67 | "_type": "object_attribute_group", 68 | "_resource_state": "89c739821f23ba2e2c1e99d6fd1056228bda5046cc530cd045a2c307ce09d9a1", 69 | "id": "WashingInstructions", 70 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/WashingInstructions" 71 | }, 72 | { 73 | "_type": "object_attribute_group", 74 | "_resource_state": "0a8bf616f0b756ab850b50b8e6fff58906a4bbe5086a7361d889e01e9e9595b0", 75 | "id": "backInStock", 76 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/backInStock" 77 | }, 78 | { 79 | "_type": "object_attribute_group", 80 | "_resource_state": "750ad1eb7d39a27b5af165b0d09d37616867c379a5b737b2ff5831581a0c1937", 81 | "id": "clothingAttributes", 82 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/clothingAttributes" 83 | }, 84 | { 85 | "_type": "object_attribute_group", 86 | "_resource_state": "15add012dad5d62be8278d2962b188f59635c55b236538ed5a8f354e71b3ee73", 87 | "id": "custom", 88 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/custom" 89 | }, 90 | { 91 | "_type": "object_attribute_group", 92 | "_resource_state": "65e5cd4c2fd95724f96d8c1f838f9254d0f3a01c072e441dd73e518474458f2b", 93 | "id": "electronicsDigitalCameraAttributes", 94 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/electronicsDigitalCameraAttributes" 95 | }, 96 | { 97 | "_type": "object_attribute_group", 98 | "_resource_state": "37f492a3eb597f59395358cfb40eeb3a6294178dce09f8c377170e69daa30837", 99 | "id": "electronicsDigitalMediaPlayerAttributes", 100 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/electronicsDigitalMediaPlayerAttributes" 101 | }, 102 | { 103 | "_type": "object_attribute_group", 104 | "_resource_state": "fe9a646c9f866142f2d28011c26f65f90372ff7fc48067a77c57d54eb58c96a7", 105 | "id": "electronicsDimensionsAndWeight", 106 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/electronicsDimensionsAndWeight" 107 | }, 108 | { 109 | "_type": "object_attribute_group", 110 | "_resource_state": "dc186ea27a80c3369c603011027f46d2d34f7a43b60d44cf0bf1208346c40151", 111 | "id": "electronicsGameRatings", 112 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/electronicsGameRatings" 113 | }, 114 | { 115 | "_type": "object_attribute_group", 116 | "_resource_state": "8a3b2e949bdc9929ba41876929676f26e9b0527a5f30001273d09e9f2a53f89c", 117 | "id": "electronicsGpsAttributes", 118 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/electronicsGpsAttributes" 119 | }, 120 | { 121 | "_type": "object_attribute_group", 122 | "_resource_state": "0c9a9c20a3d1d3a3c5b6378c14551b2ce0f8352bad88de81ac357de2bb1d9145", 123 | "id": "inStorePickup", 124 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/inStorePickup" 125 | }, 126 | { 127 | "_type": "object_attribute_group", 128 | "_resource_state": "756e58068a70de9dd92c15babbb87e8c7b0856b6701f22a974cbf6f084bfaa3b", 129 | "id": "mainAttributes", 130 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mainAttributes" 131 | }, 132 | { 133 | "_type": "object_attribute_group", 134 | "_resource_state": "87361af2c433c890fe2afcd4bfb24056b763e8ddaa31e6e568a53336a7af78a7", 135 | "id": "mainAttributes", 136 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mainAttributes" 137 | }, 138 | { 139 | "_type": "object_attribute_group", 140 | "_resource_state": "eb2824b3e077e3bbc7c6626ebf4bcba9ca21b96f9804c4128e26120d898a0829", 141 | "id": "mainAttributes", 142 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mainAttributes" 143 | }, 144 | { 145 | "_type": "object_attribute_group", 146 | "_resource_state": "76beb3fafcfbddb4830c18b3382544456122a12c25d2fe5012ebc945a30b1b5d", 147 | "id": "mainAttributes", 148 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mainAttributes" 149 | }, 150 | { 151 | "_type": "object_attribute_group", 152 | "_resource_state": "37e37519eed074a1d4ca5f2be6914b8f32f19bea20475d7b63c6e684f63a2e37", 153 | "id": "mainAttributes", 154 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mainAttributes" 155 | }, 156 | { 157 | "_type": "object_attribute_group", 158 | "_resource_state": "e3f0676a3f0a5a212fbf171dd9f2d57b73c918ed27135f48a5e0a3d0b32dfb8a", 159 | "id": "material", 160 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/material" 161 | }, 162 | { 163 | "_type": "object_attribute_group", 164 | "_resource_state": "1810005767e31409c70ae256c98bd2570bf0a9664c82a464a9c097056251b949", 165 | "id": "materialWashingInstructions", 166 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/materialWashingInstructions" 167 | }, 168 | { 169 | "_type": "object_attribute_group", 170 | "_resource_state": "39d412aa0c6d48528b5efa0a5f639c9b20791744f69c3a18a3932e587071831d", 171 | "id": "mensAccessoriesAttributes", 172 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mensAccessoriesAttributes" 173 | }, 174 | { 175 | "_type": "object_attribute_group", 176 | "_resource_state": "f696e3513a04d3e4ea494165e9e69e6a9f4d3a0d22a0565921d5860c735c73f9", 177 | "id": "mensClothingAttributes", 178 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/mensClothingAttributes" 179 | }, 180 | { 181 | "_type": "object_attribute_group", 182 | "_resource_state": "747af7428d1f04dcfe79c2e22a34fe562277ad0f8b13dc4322708121233da7d8", 183 | "id": "searchRefinements", 184 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/searchRefinements" 185 | }, 186 | { 187 | "_type": "object_attribute_group", 188 | "_resource_state": "59811b6d815972e1403936b0fa5b6de7ca8101ebb27cb1dd83302eb1d9c8b0ff", 189 | "id": "storefrontAttributes", 190 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/storefrontAttributes" 191 | }, 192 | { 193 | "_type": "object_attribute_group", 194 | "_resource_state": "b29d3cb7452c7a6749ca009a024fe1b865d7642ebbf2a7392d5472e7641c5bcf", 195 | "id": "storefrontAttributes", 196 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/storefrontAttributes" 197 | }, 198 | { 199 | "_type": "object_attribute_group", 200 | "_resource_state": "0706cc287e8e9dda03acfc351d70b20e3eb4f9e26ba1f66929f7c68cc4b443d8", 201 | "id": "storefrontAttributes", 202 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/storefrontAttributes" 203 | }, 204 | { 205 | "_type": "object_attribute_group", 206 | "_resource_state": "7c529b5a59acfe0958ff98e98ce9a53c5045b1638edcce0abdf1ad7608a2ae8b", 207 | "id": "womensAccessoriesAttributes", 208 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/womensAccessoriesAttributes" 209 | }, 210 | { 211 | "_type": "object_attribute_group", 212 | "_resource_state": "436a1fc1e34643a96e42672f536b3f7802af3f512ec69ccdda05079af88f0277", 213 | "id": "womensClothingAttributes", 214 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/Product/attribute_groups/womensClothingAttributes" 215 | } 216 | ], 217 | "query": { 218 | "match_all_query": { 219 | "_type": "match_all_query" 220 | } 221 | }, 222 | "start": 0, 223 | "total": 35 224 | } 225 | ``` -------------------------------------------------------------------------------- /docs/dw_order/ReturnCase.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.order 2 | 3 | # Class ReturnCase 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.Extensible 9 | - dw.order.AbstractItemCtnr 10 | - dw.order.ReturnCase 11 | 12 | ## Description 13 | 14 | All returns exist in the context of a ReturnCase, each Order can have any number of ReturnCases. The ReturnCase has ReturnCaseItems, each of which is associated with an OrderItem (an extension to either a ProductLineItem or a ShippingLineItem). Each ReturnCaseItem defines ReturnCaseItem.getAuthorizedQuantity() representing the maximum quantity expected to be returned. The ReturnCaseItem may be associated with 0..n ReturnItems - ReturnItems are added to the ReturnCaseItem when Returns are created. Either - a ReturnCase may be used as an RMA, in which case they are created when a customer first shows a wish to return item(s). The customer then includes the RMA number with the returned item(s). The Return created as a result is then associated with the existing ReturnCase. Or - a ReturnCase is automatically created as part of the return creation, i.e. the customer returns some item(s) leading to a creation of both a Return and an associated ReturnCase. The scripting api allows access to the ReturnCases, whether the ReturnCase is an RMA or not, and the ReturnCase status. Both the ReturnCaseItems and any Returns associated with the ReturnCase can be accessed. A ReturnCase has one of these status values: NEW - the ReturnCase has been created and can be edited previous to its authorization CONFIRMED - the ReturnCase is CONFIRMED, can no longer be edited, no Returns have been associated with it. Only a NEW- ReturnCase can be CONFIRMED PARTIAL_RETURNED - the ReturnCase has been associated with at least one Return, but is not yet complete. Only a CONFIRMED- ReturnCase can be set to PARTIAL_RETURNED RETURNED - the ReturnCase has been associated with Returns which match the expected authorized quantity. Only an CONFIRMED- or PARTIAL_RETURNED- return-case can be set to RETURNED CANCELLED - the ReturnCase has been cancelled (only a NEW- or CONFIRMED- ReturnCase can be cancelled) Order post-processing APIs (gillian) are now inactive by default and will throw an exception if accessed. Activation needs preliminary approval by Product Management. Please contact support in this case. Existing customers using these APIs are not affected by this change and can use the APIs until further notice. 15 | 16 | ## Constants 17 | 18 | ### ORDERBY_ITEMID 19 | 20 | **Type:** Object 21 | 22 | Sorting by item id. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 23 | 24 | ### ORDERBY_ITEMPOSITION 25 | 26 | **Type:** Object 27 | 28 | Sorting by the position of the related oder item. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 29 | 30 | ### ORDERBY_UNSORTED 31 | 32 | **Type:** Object 33 | 34 | Unsorted , as it is. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 35 | 36 | ### QUALIFIER_PRODUCTITEMS 37 | 38 | **Type:** Object 39 | 40 | Selects the product items. Use with method getItems() as an argument to method FilteringCollection.select(Object). 41 | 42 | ### QUALIFIER_SERVICEITEMS 43 | 44 | **Type:** Object 45 | 46 | Selects for the service items. Use with method getItems() as an argument to method FilteringCollection.select(Object). 47 | 48 | ### STATUS_CANCELLED 49 | 50 | **Type:** String = "CANCELLED" 51 | 52 | constant for ReturnCase Status CANCELLED 53 | 54 | ### STATUS_CONFIRMED 55 | 56 | **Type:** String = "CONFIRMED" 57 | 58 | constant for ReturnCase Status CONFIRMED 59 | 60 | ### STATUS_NEW 61 | 62 | **Type:** String = "NEW" 63 | 64 | constant for ReturnCase Status NEW 65 | 66 | ### STATUS_PARTIAL_RETURNED 67 | 68 | **Type:** String = "PARTIAL_RETURNED" 69 | 70 | constant for ReturnCase Status PARTIAL RETURNED 71 | 72 | ### STATUS_RETURNED 73 | 74 | **Type:** String = "RETURNED" 75 | 76 | constant for ReturnCase Status RETURNED 77 | 78 | ## Properties 79 | 80 | ### invoice 81 | 82 | **Type:** Invoice (Read Only) 83 | 84 | Returns null or the previously created Invoice. 85 | 86 | ### invoiceNumber 87 | 88 | **Type:** String (Read Only) 89 | 90 | Returns null or the invoice-number. 91 | 92 | ### items 93 | 94 | **Type:** FilteringCollection (Read Only) 95 | 96 | Access the collection of ReturnCaseItems. 97 | 98 | This FilteringCollection can be sorted / filtered using: 99 | 100 | FilteringCollection.sort(Object) with ORDERBY_ITEMID 101 | FilteringCollection.sort(Object) with 102 | ORDERBY_ITEMPOSITION 103 | FilteringCollection.sort(Object) with ORDERBY_UNSORTED 104 | FilteringCollection.select(Object) with QUALIFIER_PRODUCTITEMS 105 | FilteringCollection.select(Object) with QUALIFIER_SERVICEITEMS 106 | 107 | ### returnCaseNumber 108 | 109 | **Type:** String (Read Only) 110 | 111 | The mandatory return case number identifying this document. 112 | 113 | ### returns 114 | 115 | **Type:** Collection (Read Only) 116 | 117 | Return the collection of Returns associated with this ReturnCase. 118 | 119 | ### RMA 120 | 121 | **Type:** Order.createReturnCase(String, Boolean) (Read Only) 122 | 123 | Return whether this is an RMA. This is specified when calling Order.createReturnCase(String, Boolean). 124 | 125 | ### status 126 | 127 | **Type:** EnumValue (Read Only) 128 | 129 | Gets the return case item status. The status of a ReturnCase is read-only and calculated from the status of 130 | the associated ReturnCaseItems. 131 | 132 | The possible values are STATUS_NEW,STATUS_CONFIRMED, 133 | STATUS_PARTIAL_RETURNED, STATUS_RETURNED, 134 | STATUS_CANCELLED. 135 | 136 | ## Constructor Summary 137 | 138 | ## Method Summary 139 | 140 | ### confirm 141 | 142 | **Signature:** `confirm() : void` 143 | 144 | Attempt to confirm the ReturnCase. 145 | 146 | ### createInvoice 147 | 148 | **Signature:** `createInvoice() : Invoice` 149 | 150 | Creates a new Invoice based on this ReturnCase. 151 | 152 | ### createInvoice 153 | 154 | **Signature:** `createInvoice(invoiceNumber : String) : Invoice` 155 | 156 | Creates a new Invoice based on this ReturnCase. 157 | 158 | ### createItem 159 | 160 | **Signature:** `createItem(orderItemID : String) : ReturnCaseItem` 161 | 162 | Creates a new item for a given order item. 163 | 164 | ### createReturn 165 | 166 | **Signature:** `createReturn(returnNumber : String) : Return` 167 | 168 | Creates a new Return with the given number and associates it with this ReturnCase. 169 | 170 | ### createReturn 171 | 172 | **Signature:** `createReturn() : Return` 173 | 174 | Creates a new Return with a generated number and associates it with this ReturnCase. 175 | 176 | ### getInvoice 177 | 178 | **Signature:** `getInvoice() : Invoice` 179 | 180 | Returns null or the previously created Invoice. 181 | 182 | ### getInvoiceNumber 183 | 184 | **Signature:** `getInvoiceNumber() : String` 185 | 186 | Returns null or the invoice-number. 187 | 188 | ### getItems 189 | 190 | **Signature:** `getItems() : FilteringCollection` 191 | 192 | Access the collection of ReturnCaseItems. 193 | 194 | ### getReturnCaseNumber 195 | 196 | **Signature:** `getReturnCaseNumber() : String` 197 | 198 | Returns the mandatory return case number identifying this document. 199 | 200 | ### getReturns 201 | 202 | **Signature:** `getReturns() : Collection` 203 | 204 | Return the collection of Returns associated with this ReturnCase. 205 | 206 | ### getStatus 207 | 208 | **Signature:** `getStatus() : EnumValue` 209 | 210 | Gets the return case item status. 211 | 212 | ### isRMA 213 | 214 | **Signature:** `isRMA() : boolean` 215 | 216 | Return whether this is an RMA. 217 | 218 | ## Method Detail 219 | 220 | ## Method Details 221 | 222 | ### confirm 223 | 224 | **Signature:** `confirm() : void` 225 | 226 | **Description:** Attempt to confirm the ReturnCase. Without items the return case will be canceled When confirmed, only the the custom attributes of its return case items can be changed. 227 | 228 | **Throws:** 229 | 230 | IllegalStateException - thrown if Status is not STATUS_NEW 231 | 232 | --- 233 | 234 | ### createInvoice 235 | 236 | **Signature:** `createInvoice() : Invoice` 237 | 238 | **Description:** Creates a new Invoice based on this ReturnCase. The return-case-number will be used as the invoice-number. The Invoice can then be accessed using getInvoice() or its number using getInvoiceNumber(). The method must not be called more than once for a ReturnCase, nor may 2 Invoices exist with the same invoice-number. The new Invoice is a credit-invoice with a Invoice.STATUS_NOT_PAID status, and will be passed to the refund payment-hook in a separate database transaction for processing. 239 | 240 | **Returns:** 241 | 242 | new invoice 243 | 244 | --- 245 | 246 | ### createInvoice 247 | 248 | **Signature:** `createInvoice(invoiceNumber : String) : Invoice` 249 | 250 | **Description:** Creates a new Invoice based on this ReturnCase. The invoice-number must be specified as an argument. The Invoice can then be accessed using getInvoice() or its number using getInvoiceNumber(). The method must not be called more than once for a ReturnCase, nor may 2 Invoices exist with the same invoice-number. The new Invoice is a credit-invoice with a Invoice.STATUS_NOT_PAID status, and will be passed to the refund payment-hook in a separate database transaction for processing. 251 | 252 | **Parameters:** 253 | 254 | - `invoiceNumber`: the invoice-number to be used for the invoice creation 255 | 256 | **Returns:** 257 | 258 | new invoice 259 | 260 | --- 261 | 262 | ### createItem 263 | 264 | **Signature:** `createItem(orderItemID : String) : ReturnCaseItem` 265 | 266 | **Description:** Creates a new item for a given order item. Note: a ReturnCase may have only one item per order item. 267 | 268 | **Parameters:** 269 | 270 | - `orderItemID`: order item id 271 | 272 | **Returns:** 273 | 274 | null or item for given order item 275 | 276 | **Throws:** 277 | 278 | IllegalArgumentException - thrown if getItem(orderItem) returns non null 279 | 280 | --- 281 | 282 | ### createReturn 283 | 284 | **Signature:** `createReturn(returnNumber : String) : Return` 285 | 286 | **Description:** Creates a new Return with the given number and associates it with this ReturnCase. 287 | 288 | **Parameters:** 289 | 290 | - `returnNumber`: return number to assign 291 | 292 | **Returns:** 293 | 294 | new Return instance 295 | 296 | --- 297 | 298 | ### createReturn 299 | 300 | **Signature:** `createReturn() : Return` 301 | 302 | **Description:** Creates a new Return with a generated number and associates it with this ReturnCase. 303 | 304 | **Returns:** 305 | 306 | new Return instance 307 | 308 | --- 309 | 310 | ### getInvoice 311 | 312 | **Signature:** `getInvoice() : Invoice` 313 | 314 | **Description:** Returns null or the previously created Invoice. 315 | 316 | **Returns:** 317 | 318 | null or the previously created invoice. 319 | 320 | **See Also:** 321 | 322 | createInvoice(String) 323 | 324 | --- 325 | 326 | ### getInvoiceNumber 327 | 328 | **Signature:** `getInvoiceNumber() : String` 329 | 330 | **Description:** Returns null or the invoice-number. 331 | 332 | **Returns:** 333 | 334 | null or the previously created invoice. 335 | 336 | **See Also:** 337 | 338 | createInvoice(String) 339 | 340 | --- 341 | 342 | ### getItems 343 | 344 | **Signature:** `getItems() : FilteringCollection` 345 | 346 | **Description:** Access the collection of ReturnCaseItems. This FilteringCollection can be sorted / filtered using: FilteringCollection.sort(Object) with ORDERBY_ITEMID FilteringCollection.sort(Object) with ORDERBY_ITEMPOSITION FilteringCollection.sort(Object) with ORDERBY_UNSORTED FilteringCollection.select(Object) with QUALIFIER_PRODUCTITEMS FilteringCollection.select(Object) with QUALIFIER_SERVICEITEMS 347 | 348 | **Returns:** 349 | 350 | the items 351 | 352 | --- 353 | 354 | ### getReturnCaseNumber 355 | 356 | **Signature:** `getReturnCaseNumber() : String` 357 | 358 | **Description:** Returns the mandatory return case number identifying this document. 359 | 360 | **Returns:** 361 | 362 | the return case number 363 | 364 | --- 365 | 366 | ### getReturns 367 | 368 | **Signature:** `getReturns() : Collection` 369 | 370 | **Description:** Return the collection of Returns associated with this ReturnCase. 371 | 372 | **Returns:** 373 | 374 | the collection of Returns. 375 | 376 | --- 377 | 378 | ### getStatus 379 | 380 | **Signature:** `getStatus() : EnumValue` 381 | 382 | **Description:** Gets the return case item status. The status of a ReturnCase is read-only and calculated from the status of the associated ReturnCaseItems. The possible values are STATUS_NEW,STATUS_CONFIRMED, STATUS_PARTIAL_RETURNED, STATUS_RETURNED, STATUS_CANCELLED. 383 | 384 | **Returns:** 385 | 386 | the status 387 | 388 | --- 389 | 390 | ### isRMA 391 | 392 | **Signature:** `isRMA() : boolean` 393 | 394 | **Description:** Return whether this is an RMA. This is specified when calling Order.createReturnCase(String, Boolean). 395 | 396 | **Returns:** 397 | 398 | whether this is an RMA. 399 | 400 | --- ``` -------------------------------------------------------------------------------- /tests/ocapi-client.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Tests for the refactored OCAPIClient 3 | * Tests the facade pattern that orchestrates specialized client modules 4 | */ 5 | 6 | import { OCAPIClient } from '../src/clients/ocapi-client.js'; 7 | import { TokenManager } from '../src/clients/base/oauth-token.js'; 8 | import { OCAPIConfig } from '../src/types/types.js'; 9 | 10 | // Mock fetch globally 11 | global.fetch = jest.fn(); 12 | 13 | // Mock TokenManager 14 | jest.mock('../src/clients/base/oauth-token.js'); 15 | 16 | // Mock the specialized clients 17 | jest.mock('../src/clients/ocapi/system-objects-client.js'); 18 | jest.mock('../src/clients/ocapi/site-preferences-client.js'); 19 | jest.mock('../src/clients/ocapi/code-versions-client.js'); 20 | jest.mock('../src/clients/base/ocapi-auth-client.js'); 21 | 22 | describe('OCAPIClient', () => { 23 | let client: OCAPIClient; 24 | let mockTokenManager: jest.Mocked<TokenManager>; 25 | const mockConfig: OCAPIConfig = { 26 | hostname: 'test-instance.demandware.net', 27 | clientId: 'test-client-id', 28 | clientSecret: 'test-client-secret', 29 | version: 'v21_3', 30 | }; 31 | 32 | beforeEach(() => { 33 | jest.clearAllMocks(); 34 | 35 | // Setup TokenManager mock 36 | mockTokenManager = { 37 | getValidToken: jest.fn(), 38 | storeToken: jest.fn(), 39 | clearToken: jest.fn(), 40 | getTokenExpiration: jest.fn(), 41 | isTokenValid: jest.fn(), 42 | clearAllTokens: jest.fn(), 43 | } as any; 44 | 45 | (TokenManager.getInstance as jest.Mock).mockReturnValue(mockTokenManager); 46 | (fetch as jest.Mock).mockClear(); 47 | 48 | client = new OCAPIClient(mockConfig); 49 | }); 50 | 51 | describe('constructor', () => { 52 | it('should initialize with provided config', () => { 53 | expect(client).toBeInstanceOf(OCAPIClient); 54 | // Note: TokenManager.getInstance is called by the auth client, not directly by OCAPIClient 55 | expect(client.systemObjects).toBeDefined(); 56 | expect(client.sitePreferences).toBeDefined(); 57 | }); 58 | 59 | it('should use default version when not provided', () => { 60 | const configWithoutVersion = { 61 | hostname: 'test.demandware.net', 62 | clientId: 'client-id', 63 | clientSecret: 'client-secret', 64 | }; 65 | 66 | const clientWithDefaults = new OCAPIClient(configWithoutVersion); 67 | expect(clientWithDefaults).toBeInstanceOf(OCAPIClient); 68 | }); 69 | 70 | it('should initialize all specialized client modules', () => { 71 | expect(client.systemObjects).toBeDefined(); 72 | expect(client.sitePreferences).toBeDefined(); 73 | }); 74 | }); 75 | 76 | describe('System Objects API delegation', () => { 77 | it('should delegate getSystemObjectDefinitions to SystemObjectsClient', async () => { 78 | const mockResponse = { data: 'system-objects' }; 79 | jest.spyOn(client.systemObjects, 'getSystemObjectDefinitions').mockResolvedValue(mockResponse); 80 | 81 | const result = await client.getSystemObjectDefinitions(); 82 | 83 | expect(client.systemObjects.getSystemObjectDefinitions).toHaveBeenCalledWith(undefined); 84 | expect(result).toBe(mockResponse); 85 | }); 86 | 87 | it('should delegate getSystemObjectDefinition with objectType to SystemObjectsClient', async () => { 88 | const mockResponse = { data: 'product-definition' }; 89 | const objectType = 'Product'; 90 | jest.spyOn(client.systemObjects, 'getSystemObjectDefinition').mockResolvedValue(mockResponse); 91 | 92 | const result = await client.getSystemObjectDefinition(objectType); 93 | 94 | expect(client.systemObjects.getSystemObjectDefinition).toHaveBeenCalledWith(objectType); 95 | expect(result).toBe(mockResponse); 96 | }); 97 | 98 | it('should delegate searchSystemObjectDefinitions to SystemObjectsClient', async () => { 99 | const mockResponse = { data: 'search-results' }; 100 | const searchRequest = { query: { match_all_query: {} } }; 101 | jest.spyOn(client.systemObjects, 'searchSystemObjectDefinitions').mockResolvedValue(mockResponse); 102 | 103 | const result = await client.searchSystemObjectDefinitions(searchRequest); 104 | 105 | expect(client.systemObjects.searchSystemObjectDefinitions).toHaveBeenCalledWith(searchRequest); 106 | expect(result).toBe(mockResponse); 107 | }); 108 | 109 | it('should delegate searchSystemObjectAttributeDefinitions to SystemObjectsClient', async () => { 110 | const mockResponse = { data: 'attribute-search-results' }; 111 | const objectType = 'Product'; 112 | const searchRequest = { query: { text_query: { fields: ['id'], search_phrase: 'custom' } } }; 113 | jest.spyOn(client.systemObjects, 'searchSystemObjectAttributeDefinitions').mockResolvedValue(mockResponse); 114 | 115 | const result = await client.searchSystemObjectAttributeDefinitions(objectType, searchRequest); 116 | 117 | expect(client.systemObjects.searchSystemObjectAttributeDefinitions) 118 | .toHaveBeenCalledWith(objectType, searchRequest); 119 | expect(result).toBe(mockResponse); 120 | }); 121 | 122 | it('should delegate searchSystemObjectAttributeGroups to SystemObjectsClient', async () => { 123 | const mockResponse = { data: 'attribute-groups' }; 124 | const objectType = 'SitePreferences'; 125 | const searchRequest = { query: { match_all_query: {} } }; 126 | jest.spyOn(client.systemObjects, 'searchSystemObjectAttributeGroups').mockResolvedValue(mockResponse); 127 | 128 | const result = await client.searchSystemObjectAttributeGroups(objectType, searchRequest); 129 | 130 | expect(client.systemObjects.searchSystemObjectAttributeGroups).toHaveBeenCalledWith(objectType, searchRequest); 131 | expect(result).toBe(mockResponse); 132 | }); 133 | }); 134 | 135 | describe('Site Preferences API delegation', () => { 136 | it('should delegate searchSitePreferences to SitePreferencesClient', async () => { 137 | const mockResponse = { data: 'site-preferences' }; 138 | const groupId = 'SiteGeneral'; 139 | const instanceType = 'sandbox'; 140 | const searchRequest = { query: { match_all_query: {} } }; 141 | const options = { maskPasswords: true }; 142 | jest.spyOn(client.sitePreferences, 'searchSitePreferences').mockResolvedValue(mockResponse); 143 | 144 | const result = await client.searchSitePreferences(groupId, instanceType, searchRequest, options); 145 | 146 | expect(client.sitePreferences.searchSitePreferences) 147 | .toHaveBeenCalledWith(groupId, instanceType, searchRequest, options); 148 | expect(result).toBe(mockResponse); 149 | }); 150 | }); 151 | 152 | describe('Authentication & Token Management delegation', () => { 153 | it('should delegate getTokenExpiration to AuthClient', () => { 154 | const mockExpiration = new Date(); 155 | 156 | // Mock the authClient's getTokenExpiration method 157 | const mockAuthClient = { 158 | getTokenExpiration: jest.fn().mockReturnValue(mockExpiration), 159 | }; 160 | 161 | // Access the private authClient property and mock it 162 | (client as any).authClient = mockAuthClient; 163 | 164 | const result = client.getTokenExpiration(); 165 | 166 | expect(mockAuthClient.getTokenExpiration).toHaveBeenCalled(); 167 | expect(result).toBe(mockExpiration); 168 | }); 169 | 170 | it('should delegate refreshToken to AuthClient', async () => { 171 | // Mock the authClient's refreshToken method 172 | const mockAuthClient = { 173 | refreshToken: jest.fn().mockResolvedValue(undefined), 174 | }; 175 | 176 | // Access the private authClient property and mock it 177 | (client as any).authClient = mockAuthClient; 178 | 179 | await client.refreshToken(); 180 | 181 | expect(mockAuthClient.refreshToken).toHaveBeenCalled(); 182 | }); 183 | 184 | it('should delegate getCodeVersions to CodeVersionsClient', async () => { 185 | // Mock the codeVersions client's getCodeVersions method 186 | const mockCodeVersions = { 187 | _v: '23.2', 188 | _type: 'code_version_result', 189 | count: 1, 190 | data: [ 191 | { 192 | _type: 'code_version', 193 | id: 'version1', 194 | active: true, 195 | cartridges: 'app_storefront_base', 196 | compatibility_mode: '23.2', 197 | activation_time: '2024-01-01T00:00:00Z', 198 | total_size: '1024 KB', 199 | }, 200 | ], 201 | total: 1, 202 | }; 203 | 204 | const mockCodeVersionsClient = { 205 | getCodeVersions: jest.fn().mockResolvedValue(mockCodeVersions), 206 | }; 207 | 208 | // Access the private codeVersions property and mock it 209 | (client as any).codeVersions = mockCodeVersionsClient; 210 | 211 | const result = await client.getCodeVersions(); 212 | 213 | expect(mockCodeVersionsClient.getCodeVersions).toHaveBeenCalled(); 214 | expect(result).toBe(mockCodeVersions); 215 | }); 216 | 217 | it('should delegate activateCodeVersion to CodeVersionsClient', async () => { 218 | // Mock the codeVersions client's activateCodeVersion method 219 | const mockActivatedVersion = { 220 | _v: '23.2', 221 | _type: 'code_version', 222 | _resource_state: 'new-resource-state-12345', 223 | id: 'version2', 224 | active: true, 225 | cartridges: 'app_storefront_base', 226 | compatibility_mode: '23.2', 227 | activation_time: '2024-01-15T10:30:00Z', 228 | total_size: '1024 KB', 229 | }; 230 | 231 | const mockCodeVersionsClient = { 232 | activateCodeVersion: jest.fn().mockResolvedValue(mockActivatedVersion), 233 | }; 234 | 235 | // Access the private codeVersions property and mock it 236 | (client as any).codeVersions = mockCodeVersionsClient; 237 | 238 | const codeVersionId = 'version2'; 239 | const result = await client.activateCodeVersion(codeVersionId); 240 | 241 | expect(mockCodeVersionsClient.activateCodeVersion).toHaveBeenCalledWith(codeVersionId); 242 | expect(result).toBe(mockActivatedVersion); 243 | }); 244 | }); 245 | 246 | describe('Configuration handling', () => { 247 | it('should merge config with defaults', () => { 248 | const configWithoutVersion = { 249 | hostname: 'test.demandware.net', 250 | clientId: 'client-id', 251 | clientSecret: 'client-secret', 252 | }; 253 | 254 | const clientWithDefaults = new OCAPIClient(configWithoutVersion); 255 | 256 | // Verify that the client was created successfully (which means defaults were applied) 257 | expect(clientWithDefaults).toBeInstanceOf(OCAPIClient); 258 | expect(clientWithDefaults.systemObjects).toBeDefined(); 259 | expect(clientWithDefaults.sitePreferences).toBeDefined(); 260 | }); 261 | 262 | it('should preserve provided config values', () => { 263 | const customConfig = { 264 | hostname: 'custom.demandware.net', 265 | clientId: 'custom-client-id', 266 | clientSecret: 'custom-client-secret', 267 | version: 'v22_1', 268 | }; 269 | 270 | const customClient = new OCAPIClient(customConfig); 271 | 272 | // Verify that the client was created successfully with custom config 273 | expect(customClient).toBeInstanceOf(OCAPIClient); 274 | expect(customClient.systemObjects).toBeDefined(); 275 | expect(customClient.sitePreferences).toBeDefined(); 276 | }); 277 | }); 278 | 279 | describe('Error handling', () => { 280 | it('should propagate errors from specialized clients', async () => { 281 | const error = new Error('System objects error'); 282 | jest.spyOn(client.systemObjects, 'getSystemObjectDefinitions').mockRejectedValue(error); 283 | 284 | await expect(client.getSystemObjectDefinitions()).rejects.toThrow('System objects error'); 285 | }); 286 | 287 | it('should propagate errors from site preferences client', async () => { 288 | const error = new Error('Site preferences error'); 289 | jest.spyOn(client.sitePreferences, 'searchSitePreferences').mockRejectedValue(error); 290 | 291 | const searchRequest = { query: { match_all_query: {} } }; 292 | await expect(client.searchSitePreferences('groupId', 'sandbox', searchRequest)).rejects.toThrow('Site preferences error'); 293 | }); 294 | }); 295 | }); 296 | ```