This is page 26 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/log-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { LogToolHandler } from '../src/core/handlers/log-handler.js'; 2 | import { HandlerContext } from '../src/core/handlers/base-handler.js'; 3 | import { SFCCLogClient } from '../src/clients/log-client.js'; 4 | import { Logger } from '../src/utils/logger.js'; 5 | 6 | // Mock the SFCCLogClient 7 | jest.mock('../src/clients/log-client.js'); 8 | 9 | describe('LogToolHandler', () => { 10 | let mockLogger: jest.Mocked<Logger>; 11 | let mockLogClient: jest.Mocked<SFCCLogClient>; 12 | let context: HandlerContext; 13 | let handler: LogToolHandler; 14 | 15 | beforeEach(() => { 16 | mockLogger = { 17 | debug: jest.fn(), 18 | log: jest.fn(), 19 | error: jest.fn(), 20 | timing: jest.fn(), 21 | methodEntry: jest.fn(), 22 | methodExit: jest.fn(), 23 | } as any; 24 | 25 | mockLogClient = { 26 | getLatestLogs: jest.fn(), 27 | summarizeLogs: jest.fn(), 28 | searchLogs: jest.fn(), 29 | listLogFiles: jest.fn(), 30 | getLogFileContents: jest.fn(), 31 | } as any; 32 | 33 | (SFCCLogClient as jest.MockedClass<typeof SFCCLogClient>).mockImplementation(() => mockLogClient); 34 | 35 | jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); 36 | 37 | context = { 38 | logger: mockLogger, 39 | config: { 40 | hostname: 'test.demandware.net', 41 | username: 'test', 42 | password: 'test', 43 | clientId: 'test', 44 | clientSecret: 'test', 45 | }, 46 | capabilities: { canAccessLogs: true, canAccessOCAPI: true }, 47 | }; 48 | 49 | handler = new LogToolHandler(context, 'Log'); 50 | }); 51 | 52 | afterEach(() => { 53 | jest.restoreAllMocks(); 54 | }); 55 | 56 | describe('canHandle', () => { 57 | it('should handle log-related tools', () => { 58 | expect(handler.canHandle('get_latest_error')).toBe(true); 59 | expect(handler.canHandle('get_latest_warn')).toBe(true); 60 | expect(handler.canHandle('get_latest_info')).toBe(true); 61 | expect(handler.canHandle('get_latest_debug')).toBe(true); 62 | expect(handler.canHandle('summarize_logs')).toBe(true); 63 | expect(handler.canHandle('search_logs')).toBe(true); 64 | expect(handler.canHandle('list_log_files')).toBe(true); 65 | expect(handler.canHandle('get_log_file_contents')).toBe(true); 66 | }); 67 | 68 | it('should not handle non-log tools', () => { 69 | expect(handler.canHandle('get_sfcc_class_info')).toBe(false); 70 | expect(handler.canHandle('unknown_tool')).toBe(false); 71 | }); 72 | }); 73 | 74 | // Helper function to initialize handler for tests that need it 75 | const initializeHandler = async () => { 76 | await (handler as any).initialize(); 77 | }; 78 | 79 | describe('initialization', () => { 80 | it('should initialize log client when capabilities allow', async () => { 81 | // Manually trigger initialization 82 | await (handler as any).initialize(); 83 | 84 | expect(SFCCLogClient).toHaveBeenCalledWith(context.config); 85 | expect(mockLogger.debug).toHaveBeenCalledWith('Log client initialized'); 86 | }); 87 | 88 | it('should not initialize log client when capabilities do not allow', async () => { 89 | const contextWithoutLogs = { 90 | ...context, 91 | capabilities: { canAccessLogs: false, canAccessOCAPI: false }, 92 | }; 93 | const handlerWithoutLogs = new LogToolHandler(contextWithoutLogs, 'Log'); 94 | 95 | // Manually trigger initialization to test the capabilities check 96 | await (handlerWithoutLogs as any).initialize(); 97 | 98 | // Reset the mock counter for this test 99 | (SFCCLogClient as jest.MockedClass<typeof SFCCLogClient>).mockClear(); 100 | 101 | const result = await handlerWithoutLogs.handle('get_latest_error', {}, Date.now()); 102 | expect(result.isError).toBe(true); 103 | expect(result.content[0].text).toContain('Log client not configured - ensure log access is enabled.'); 104 | 105 | expect(SFCCLogClient).not.toHaveBeenCalled(); 106 | }); 107 | 108 | it('should not initialize without config', async () => { 109 | const contextWithoutConfig = { 110 | ...context, 111 | config: null as any, 112 | }; 113 | const handlerWithoutConfig = new LogToolHandler(contextWithoutConfig, 'Log'); 114 | 115 | const result = await handlerWithoutConfig.handle('get_latest_error', {}, Date.now()); 116 | expect(result.isError).toBe(true); 117 | expect(result.content[0].text).toContain('Log client not configured - ensure log access is enabled.'); 118 | }); 119 | }); 120 | 121 | describe('disposal', () => { 122 | it('should dispose log client properly', async () => { 123 | // Initialize first 124 | await initializeHandler(); 125 | 126 | // Dispose 127 | await (handler as any).dispose(); 128 | 129 | expect(mockLogger.debug).toHaveBeenCalledWith('Log client disposed'); 130 | }); 131 | }); 132 | 133 | describe('get_latest_* tools', () => { 134 | beforeEach(async () => { 135 | await initializeHandler(); 136 | mockLogClient.getLatestLogs.mockResolvedValue('Test log entry\n2023-01-01T00:00:00Z'); 137 | }); 138 | 139 | it('should handle get_latest_error', async () => { 140 | const result = await handler.handle('get_latest_error', { limit: 5, date: '20230101' }, Date.now()); 141 | 142 | expect(mockLogClient.getLatestLogs).toHaveBeenCalledWith('error', 5, '20230101'); 143 | expect(result.content[0].text).toContain('Test log entry'); 144 | expect(mockLogger.debug).toHaveBeenCalledWith('Fetching latest error logs limit=5 date=20230101'); 145 | }); 146 | 147 | it('should handle get_latest_warn with default parameters', async () => { 148 | await handler.handle('get_latest_warn', {}, Date.now()); 149 | 150 | expect(mockLogClient.getLatestLogs).toHaveBeenCalledWith('warn', 10, undefined); 151 | expect(mockLogger.debug).toHaveBeenCalledWith('Fetching latest warn logs limit=10 date=today'); 152 | }); 153 | 154 | it('should handle get_latest_info', async () => { 155 | await handler.handle('get_latest_info', { limit: 15 }, Date.now()); 156 | 157 | expect(mockLogClient.getLatestLogs).toHaveBeenCalledWith('info', 15, undefined); 158 | }); 159 | 160 | it('should handle get_latest_debug', async () => { 161 | await handler.handle('get_latest_debug', { date: '20230101' }, Date.now()); 162 | 163 | expect(mockLogClient.getLatestLogs).toHaveBeenCalledWith('debug', 10, '20230101'); 164 | }); 165 | }); 166 | 167 | describe('summarize_logs tool', () => { 168 | beforeEach(async () => { 169 | await initializeHandler(); 170 | }); 171 | it('should handle summarize_logs', async () => { 172 | const mockSummary = JSON.stringify({ 173 | date: '20230101', 174 | totalLogs: 100, 175 | errorCount: 5, 176 | warnCount: 10, 177 | infoCount: 85, 178 | }); 179 | mockLogClient.summarizeLogs.mockResolvedValue(mockSummary); 180 | 181 | await handler.handle('summarize_logs', { date: '20230101' }, Date.now()); 182 | 183 | expect(mockLogClient.summarizeLogs).toHaveBeenCalledWith('20230101'); 184 | expect(mockLogger.debug).toHaveBeenCalledWith('Summarizing logs for date 20230101'); 185 | }); 186 | 187 | it('should handle summarize_logs with default date', async () => { 188 | const mockSummary = JSON.stringify({ date: 'today', totalLogs: 50 }); 189 | mockLogClient.summarizeLogs.mockResolvedValue(mockSummary); 190 | 191 | await handler.handle('summarize_logs', {}, Date.now()); 192 | 193 | expect(mockLogClient.summarizeLogs).toHaveBeenCalledWith(undefined); 194 | expect(mockLogger.debug).toHaveBeenCalledWith('Summarizing logs for date today'); 195 | }); 196 | }); 197 | 198 | describe('search_logs tool', () => { 199 | beforeEach(async () => { 200 | await initializeHandler(); 201 | }); 202 | it('should handle search_logs with required pattern', async () => { 203 | const mockResults = JSON.stringify({ 204 | results: [{ message: 'Error occurred', timestamp: '2023-01-01T00:00:00Z' }], 205 | total: 1, 206 | }); 207 | mockLogClient.searchLogs.mockResolvedValue(mockResults); 208 | 209 | const args = { pattern: 'error', logLevel: 'error', limit: 25, date: '20230101' }; 210 | const result = await handler.handle('search_logs', args, Date.now()); 211 | 212 | expect(mockLogClient.searchLogs).toHaveBeenCalledWith('error', 'error', 25, '20230101'); 213 | expect(result.content[0].text).toContain('Error occurred'); 214 | expect(mockLogger.debug).toHaveBeenCalledWith('Searching logs pattern="error" level=error limit=25'); 215 | }); 216 | 217 | it('should handle search_logs with default parameters', async () => { 218 | const mockResults = JSON.stringify({ results: [], total: 0 }); 219 | mockLogClient.searchLogs.mockResolvedValue(mockResults); 220 | 221 | await handler.handle('search_logs', { pattern: 'test' }, Date.now()); 222 | 223 | expect(mockLogClient.searchLogs).toHaveBeenCalledWith('test', undefined, 20, undefined); 224 | expect(mockLogger.debug).toHaveBeenCalledWith('Searching logs pattern="test" level=all limit=20'); 225 | }); 226 | 227 | it('should throw error when pattern is missing', async () => { 228 | const result = await handler.handle('search_logs', {}, Date.now()); 229 | expect(result.isError).toBe(true); 230 | expect(result.content[0].text).toContain('pattern must be a non-empty string'); 231 | }); 232 | 233 | it('should throw error when pattern is empty', async () => { 234 | const result = await handler.handle('search_logs', { pattern: '' }, Date.now()); 235 | expect(result.isError).toBe(true); 236 | expect(result.content[0].text).toContain('pattern must be a non-empty string'); 237 | }); 238 | }); 239 | 240 | describe('get_log_file_contents tool', () => { 241 | beforeEach(async () => { 242 | await initializeHandler(); 243 | }); 244 | 245 | it('should handle get_log_file_contents with filename', async () => { 246 | const mockFileContents = 'Log file contents with some test data\nMore log entries...'; 247 | mockLogClient.getLogFileContents.mockResolvedValue(mockFileContents); 248 | 249 | const result = await handler.handle('get_log_file_contents', { filename: 'error-2023-01-01.log' }, Date.now()); 250 | 251 | expect(mockLogClient.getLogFileContents).toHaveBeenCalledWith('error-2023-01-01.log', undefined, undefined); 252 | expect(result.content[0].text).toContain('Log file contents with some test data'); 253 | expect(mockLogger.debug).toHaveBeenCalledWith('Reading log file contents: error-2023-01-01.log (maxBytes=default, tailOnly=false)'); 254 | }); 255 | 256 | it('should handle get_log_file_contents with maxBytes and tailOnly options', async () => { 257 | const mockFileContents = 'Tail content of log file...'; 258 | mockLogClient.getLogFileContents.mockResolvedValue(mockFileContents); 259 | 260 | const result = await handler.handle('get_log_file_contents', { 261 | filename: 'large-log.log', 262 | maxBytes: 1024, 263 | tailOnly: true, 264 | }, Date.now()); 265 | 266 | expect(mockLogClient.getLogFileContents).toHaveBeenCalledWith('large-log.log', 1024, true); 267 | expect(result.content[0].text).toContain('Tail content of log file'); 268 | expect(mockLogger.debug).toHaveBeenCalledWith('Reading log file contents: large-log.log (maxBytes=1024, tailOnly=true)'); 269 | }); 270 | 271 | it('should handle get_log_file_contents with maxBytes and tailOnly=false (full file with size limit)', async () => { 272 | const mockFileContents = 'Full file content with size limit applied...'; 273 | mockLogClient.getLogFileContents.mockResolvedValue(mockFileContents); 274 | 275 | const result = await handler.handle('get_log_file_contents', { 276 | filename: 'large-log.log', 277 | maxBytes: 512, 278 | tailOnly: false, 279 | }, Date.now()); 280 | 281 | expect(mockLogClient.getLogFileContents).toHaveBeenCalledWith('large-log.log', 512, false); 282 | expect(result.content[0].text).toContain('Full file content with size limit'); 283 | expect(mockLogger.debug).toHaveBeenCalledWith('Reading log file contents: large-log.log (maxBytes=512, tailOnly=false)'); 284 | }); 285 | 286 | it('should require filename parameter', async () => { 287 | const result = await handler.handle('get_log_file_contents', {}, Date.now()); 288 | 289 | expect(result.isError).toBe(true); 290 | expect(result.content[0].text).toContain('filename must be a non-empty string'); 291 | }); 292 | 293 | it('should handle empty filename', async () => { 294 | const result = await handler.handle('get_log_file_contents', { filename: '' }, Date.now()); 295 | 296 | expect(result.isError).toBe(true); 297 | expect(result.content[0].text).toContain('filename must be a non-empty string'); 298 | }); 299 | }); 300 | 301 | describe('list_log_files tool', () => { 302 | beforeEach(async () => { 303 | await initializeHandler(); 304 | }); 305 | it('should handle list_log_files', async () => { 306 | const mockFiles = JSON.stringify([ 307 | { name: 'error-2023-01-01.log', size: 1024, modified: '2023-01-01T00:00:00Z' }, 308 | { name: 'info-2023-01-01.log', size: 2048, modified: '2023-01-01T00:00:00Z' }, 309 | ]); 310 | mockLogClient.listLogFiles.mockResolvedValue(mockFiles); 311 | 312 | const result = await handler.handle('list_log_files', {}, Date.now()); 313 | 314 | expect(mockLogClient.listLogFiles).toHaveBeenCalled(); 315 | expect(result.content[0].text).toContain('error-2023-01-01.log'); 316 | expect(result.content[0].text).toContain('info-2023-01-01.log'); 317 | expect(mockLogger.debug).toHaveBeenCalledWith('Listing log files'); 318 | }); 319 | }); 320 | 321 | describe('error handling', () => { 322 | beforeEach(async () => { 323 | await initializeHandler(); 324 | }); 325 | 326 | it('should handle client errors gracefully', async () => { 327 | mockLogClient.getLatestLogs.mockRejectedValue(new Error('Client connection failed')); 328 | 329 | const result = await handler.handle('get_latest_error', {}, Date.now()); 330 | expect(result.isError).toBe(true); 331 | expect(result.content[0].text).toContain('Client connection failed'); 332 | }); 333 | 334 | it('should throw error for unsupported tools', async () => { 335 | await expect(handler.handle('unsupported_tool', {}, Date.now())) 336 | .rejects.toThrow('Unsupported tool'); 337 | }); 338 | }); 339 | 340 | describe('timing and logging', () => { 341 | beforeEach(async () => { 342 | await initializeHandler(); 343 | }); 344 | it('should log timing information', async () => { 345 | mockLogClient.getLatestLogs.mockResolvedValue('empty logs'); 346 | 347 | const startTime = Date.now(); 348 | await handler.handle('get_latest_error', {}, startTime); 349 | 350 | expect(mockLogger.timing).toHaveBeenCalledWith('get_latest_error', startTime); 351 | }); 352 | 353 | it('should log execution details', async () => { 354 | mockLogClient.getLatestLogs.mockResolvedValue('test logs'); 355 | 356 | await handler.handle('get_latest_error', { limit: 5 }, Date.now()); 357 | 358 | expect(mockLogger.debug).toHaveBeenCalledWith('Fetching latest error logs limit=5 date=today'); 359 | expect(mockLogger.debug).toHaveBeenCalledWith( 360 | 'get_latest_error completed successfully', 361 | expect.any(Object), 362 | ); 363 | }); 364 | }); 365 | }); 366 | ``` -------------------------------------------------------------------------------- /docs/TopLevel/Object.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: TopLevel 2 | 3 | # Class Object 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | 9 | ## Description 10 | 11 | The Object object is the foundation of all native JavaScript objects. Also, the Object object can be used to generate items in your scripts with behaviors that are defined by custom properties and/or methods. You generally start by creating a blank object with the constructor function and then assign values to new properties of that object. 12 | 13 | ## Constructor Summary 14 | 15 | Object() Object constructor. 16 | 17 | ## Method Summary 18 | 19 | ### assign 20 | 21 | **Signature:** `static assign(target : Object, sources : Object...) : Object` 22 | 23 | Copies the values of all of the enumerable own properties from one or more source objects to a target object. 24 | 25 | ### create 26 | 27 | **Signature:** `static create(prototype : Object) : Object` 28 | 29 | Creates a new object based on a prototype object. 30 | 31 | ### create 32 | 33 | **Signature:** `static create(prototype : Object, properties : Object) : Object` 34 | 35 | Creates a new object based on a prototype object and additional property definitions. 36 | 37 | ### defineProperties 38 | 39 | **Signature:** `static defineProperties(object : Object, properties : Object) : Object` 40 | 41 | Defines or modifies properties of the passed object. 42 | 43 | ### defineProperty 44 | 45 | **Signature:** `static defineProperty(object : Object, propertyKey : Object, descriptor : Object) : Object` 46 | 47 | Defines or modifies a single property of the passed object. 48 | 49 | ### entries 50 | 51 | **Signature:** `static entries(object : Object) : Array` 52 | 53 | Returns the enumerable property names and their values of the passed object. 54 | 55 | ### freeze 56 | 57 | **Signature:** `static freeze(object : Object) : Object` 58 | 59 | Freezes the passed object. 60 | 61 | ### fromEntries 62 | 63 | **Signature:** `static fromEntries(properties : Iterable) : Object` 64 | 65 | Creates a new object with defined properties. 66 | 67 | ### getOwnPropertyDescriptor 68 | 69 | **Signature:** `static getOwnPropertyDescriptor(object : Object, propertyKey : Object) : Object` 70 | 71 | Returns the descriptor for a single property of the passed object. 72 | 73 | ### getOwnPropertyNames 74 | 75 | **Signature:** `static getOwnPropertyNames(object : Object) : Array` 76 | 77 | Returns an arrays containing the names of all enumerable and non-enumerable properties owned by the passed object. 78 | 79 | ### getOwnPropertySymbols 80 | 81 | **Signature:** `static getOwnPropertySymbols(object : Object) : Array` 82 | 83 | Returns an array containing the symbol of all symbol properties owned by the passed object. 84 | 85 | ### getPrototypeOf 86 | 87 | **Signature:** `static getPrototypeOf(object : Object) : Object` 88 | 89 | Returns the prototype of the passed object. 90 | 91 | ### hasOwnProperty 92 | 93 | **Signature:** `hasOwnProperty(propName : String) : boolean` 94 | 95 | Returns Boolean true if at the time the current object's instance was created its constructor (or literal assignment) contained a property with a name that matches the parameter value. 96 | 97 | ### is 98 | 99 | **Signature:** `static is(value1 : Object, value2 : Object) : boolean` 100 | 101 | Checks if the two values are equal in terms of being the same value. 102 | 103 | ### isExtensible 104 | 105 | **Signature:** `static isExtensible(object : Object) : boolean` 106 | 107 | Returns if new properties can be added to an object. 108 | 109 | ### isFrozen 110 | 111 | **Signature:** `static isFrozen(object : Object) : boolean` 112 | 113 | Returns if the object is frozen. 114 | 115 | ### isPrototypeOf 116 | 117 | **Signature:** `isPrototypeOf(prototype : Object) : boolean` 118 | 119 | Returns true if the current object and the object passed as a prameter conincide at some point along each object's prototype inheritance chain. 120 | 121 | ### isSealed 122 | 123 | **Signature:** `static isSealed(object : Object) : boolean` 124 | 125 | Returns if the object is sealed. 126 | 127 | ### keys 128 | 129 | **Signature:** `static keys(object : Object) : Array` 130 | 131 | Returns the enumerable property names of the passed object. 132 | 133 | ### preventExtensions 134 | 135 | **Signature:** `static preventExtensions(object : Object) : Object` 136 | 137 | Makes the passed object non-extensible. 138 | 139 | ### propertyIsEnumerable 140 | 141 | **Signature:** `propertyIsEnumerable(propName : String) : boolean` 142 | 143 | Return true if the specified property exposes itself to for/in property inspection through the object. 144 | 145 | ### seal 146 | 147 | **Signature:** `static seal(object : Object) : Object` 148 | 149 | Seals the passed object. 150 | 151 | ### setPrototypeOf 152 | 153 | **Signature:** `static setPrototypeOf(object : Object, prototype : Object) : Object` 154 | 155 | Changes the prototype of the passed object. 156 | 157 | ### toLocaleString 158 | 159 | **Signature:** `toLocaleString() : String` 160 | 161 | Converts the object to a localized String. 162 | 163 | ### toString 164 | 165 | **Signature:** `toString() : String` 166 | 167 | Converts the object to a String. 168 | 169 | ### valueOf 170 | 171 | **Signature:** `valueOf() : Object` 172 | 173 | Returns the object's value. 174 | 175 | ### values 176 | 177 | **Signature:** `static values(object : Object) : Array` 178 | 179 | Returns the enumerable property values of the passed object. 180 | 181 | ## Constructor Detail 182 | 183 | ## Method Detail 184 | 185 | ## Method Details 186 | 187 | ### assign 188 | 189 | **Signature:** `static assign(target : Object, sources : Object...) : Object` 190 | 191 | **Description:** Copies the values of all of the enumerable own properties from one or more source objects to a target object. 192 | 193 | **API Versioned:** 194 | 195 | From version 21.2. 196 | 197 | **Parameters:** 198 | 199 | - `target`: The target object. 200 | - `sources`: The source objects. 201 | 202 | **Returns:** 203 | 204 | The target object. 205 | 206 | --- 207 | 208 | ### create 209 | 210 | **Signature:** `static create(prototype : Object) : Object` 211 | 212 | **Description:** Creates a new object based on a prototype object. 213 | 214 | **Parameters:** 215 | 216 | - `prototype`: The prototype for the new object. 217 | 218 | **Returns:** 219 | 220 | The newly created object. 221 | 222 | --- 223 | 224 | ### create 225 | 226 | **Signature:** `static create(prototype : Object, properties : Object) : Object` 227 | 228 | **Description:** Creates a new object based on a prototype object and additional property definitions. The properties are given in the same format as described for defineProperties(Object, Object). 229 | 230 | **Parameters:** 231 | 232 | - `prototype`: The prototype for the new object. 233 | - `properties`: The property definitions. 234 | 235 | **Returns:** 236 | 237 | The newly created object. 238 | 239 | --- 240 | 241 | ### defineProperties 242 | 243 | **Signature:** `static defineProperties(object : Object, properties : Object) : Object` 244 | 245 | **Description:** Defines or modifies properties of the passed object. A descriptor for a property supports these properties: configurable, enumerable, value, writable, set and get. 246 | 247 | **Parameters:** 248 | 249 | - `object`: The object to change. 250 | - `properties`: The new property definitions. 251 | 252 | **Returns:** 253 | 254 | The modified object. 255 | 256 | --- 257 | 258 | ### defineProperty 259 | 260 | **Signature:** `static defineProperty(object : Object, propertyKey : Object, descriptor : Object) : Object` 261 | 262 | **Description:** Defines or modifies a single property of the passed object. A descriptor for a property supports these properties: configurable, enumerable, value, writable, set and get. 263 | 264 | **Parameters:** 265 | 266 | - `object`: The object to change. 267 | - `propertyKey`: The property name. 268 | - `descriptor`: The property descriptor object. 269 | 270 | **Returns:** 271 | 272 | The modified object. 273 | 274 | --- 275 | 276 | ### entries 277 | 278 | **Signature:** `static entries(object : Object) : Array` 279 | 280 | **Description:** Returns the enumerable property names and their values of the passed object. 281 | 282 | **API Versioned:** 283 | 284 | From version 22.7. 285 | 286 | **Parameters:** 287 | 288 | - `object`: The object to get the enumerable property names from. 289 | 290 | **Returns:** 291 | 292 | An array of key/value pairs ( as two element arrays ) that holds all the enumerable properties of the given object. 293 | 294 | --- 295 | 296 | ### freeze 297 | 298 | **Signature:** `static freeze(object : Object) : Object` 299 | 300 | **Description:** Freezes the passed object. Properties can't be added or removed from the frozen object. Also, definitions of existing object properties can't be changed. Although property values are immutable, setters and getters can be called. 301 | 302 | **Parameters:** 303 | 304 | - `object`: The object to be frozen. 305 | 306 | **Returns:** 307 | 308 | The frozen object. 309 | 310 | --- 311 | 312 | ### fromEntries 313 | 314 | **Signature:** `static fromEntries(properties : Iterable) : Object` 315 | 316 | **Description:** Creates a new object with defined properties. The properties are defined by an iterable that produces two element array like objects, which are the key-value pairs. Iterables are e.g. Array, Map or any other Iterable. 317 | 318 | **API Versioned:** 319 | 320 | From version 22.7. 321 | 322 | **Parameters:** 323 | 324 | - `properties`: The properties. 325 | 326 | **Returns:** 327 | 328 | The newly created object. 329 | 330 | --- 331 | 332 | ### getOwnPropertyDescriptor 333 | 334 | **Signature:** `static getOwnPropertyDescriptor(object : Object, propertyKey : Object) : Object` 335 | 336 | **Description:** Returns the descriptor for a single property of the passed object. 337 | 338 | **Parameters:** 339 | 340 | - `object`: The property owning object. 341 | - `propertyKey`: The property to look for. 342 | 343 | **Returns:** 344 | 345 | The descriptor object for the property or undefined if the property does not exist. 346 | 347 | --- 348 | 349 | ### getOwnPropertyNames 350 | 351 | **Signature:** `static getOwnPropertyNames(object : Object) : Array` 352 | 353 | **Description:** Returns an arrays containing the names of all enumerable and non-enumerable properties owned by the passed object. 354 | 355 | **Parameters:** 356 | 357 | - `object`: The object owning properties. 358 | 359 | **Returns:** 360 | 361 | An array of strings that are the properties found directly in the passed object. 362 | 363 | --- 364 | 365 | ### getOwnPropertySymbols 366 | 367 | **Signature:** `static getOwnPropertySymbols(object : Object) : Array` 368 | 369 | **Description:** Returns an array containing the symbol of all symbol properties owned by the passed object. 370 | 371 | **API Versioned:** 372 | 373 | From version 21.2. 374 | 375 | **Parameters:** 376 | 377 | - `object`: The object owning properties. 378 | 379 | **Returns:** 380 | 381 | An array of symbol properties found directly in the passed object. 382 | 383 | --- 384 | 385 | ### getPrototypeOf 386 | 387 | **Signature:** `static getPrototypeOf(object : Object) : Object` 388 | 389 | **Description:** Returns the prototype of the passed object. 390 | 391 | **Parameters:** 392 | 393 | - `object`: The object to get the prototype from. 394 | 395 | **Returns:** 396 | 397 | The prototype object or null if there is none. 398 | 399 | --- 400 | 401 | ### hasOwnProperty 402 | 403 | **Signature:** `hasOwnProperty(propName : String) : boolean` 404 | 405 | **Description:** Returns Boolean true if at the time the current object's instance was created its constructor (or literal assignment) contained a property with a name that matches the parameter value. 406 | 407 | **Parameters:** 408 | 409 | - `propName`: the property name of the object's property. 410 | 411 | **Returns:** 412 | 413 | true if at the object contains a property that matches the parameter, false otherwise. 414 | 415 | --- 416 | 417 | ### is 418 | 419 | **Signature:** `static is(value1 : Object, value2 : Object) : boolean` 420 | 421 | **Description:** Checks if the two values are equal in terms of being the same value. No coercion is performed, thus -0 and +0 is not equal and NaN is equal to NaN. 422 | 423 | **API Versioned:** 424 | 425 | From version 21.2. 426 | 427 | **Parameters:** 428 | 429 | - `value1`: The first value. 430 | - `value2`: The second value. 431 | 432 | **Returns:** 433 | 434 | true if both values are the same value else false. 435 | 436 | --- 437 | 438 | ### isExtensible 439 | 440 | **Signature:** `static isExtensible(object : Object) : boolean` 441 | 442 | **Description:** Returns if new properties can be added to an object. By default new objects are extensible. The methods freeze(Object), seal(Object) and preventExtensions(Object) make objects non-extensible. 443 | 444 | **Parameters:** 445 | 446 | - `object`: The object to check. 447 | 448 | **Returns:** 449 | 450 | true if new properties can be added else false. 451 | 452 | --- 453 | 454 | ### isFrozen 455 | 456 | **Signature:** `static isFrozen(object : Object) : boolean` 457 | 458 | **Description:** Returns if the object is frozen. 459 | 460 | **Parameters:** 461 | 462 | - `object`: The object to check. 463 | 464 | **Returns:** 465 | 466 | true if the object is frozen else false. 467 | 468 | --- 469 | 470 | ### isPrototypeOf 471 | 472 | **Signature:** `isPrototypeOf(prototype : Object) : boolean` 473 | 474 | **Description:** Returns true if the current object and the object passed as a prameter conincide at some point along each object's prototype inheritance chain. 475 | 476 | **Parameters:** 477 | 478 | - `prototype`: the object to test. 479 | 480 | **Returns:** 481 | 482 | true if the current object and the object passed as a prameter conincide at some point, false otherwise. 483 | 484 | --- 485 | 486 | ### isSealed 487 | 488 | **Signature:** `static isSealed(object : Object) : boolean` 489 | 490 | **Description:** Returns if the object is sealed. 491 | 492 | **Parameters:** 493 | 494 | - `object`: The object to check. 495 | 496 | **Returns:** 497 | 498 | true if the object is sealed else false. 499 | 500 | --- 501 | 502 | ### keys 503 | 504 | **Signature:** `static keys(object : Object) : Array` 505 | 506 | **Description:** Returns the enumerable property names of the passed object. 507 | 508 | **Parameters:** 509 | 510 | - `object`: The object to get the enumerable property names from. 511 | 512 | **Returns:** 513 | 514 | An array of strings that holds all the enumerable properties of the given object. 515 | 516 | --- 517 | 518 | ### preventExtensions 519 | 520 | **Signature:** `static preventExtensions(object : Object) : Object` 521 | 522 | **Description:** Makes the passed object non-extensible. This means that no new properties can be added to this object. 523 | 524 | **Parameters:** 525 | 526 | - `object`: The object to make non-extensible. 527 | 528 | **Returns:** 529 | 530 | The passed object. 531 | 532 | --- 533 | 534 | ### propertyIsEnumerable 535 | 536 | **Signature:** `propertyIsEnumerable(propName : String) : boolean` 537 | 538 | **Description:** Return true if the specified property exposes itself to for/in property inspection through the object. 539 | 540 | **Parameters:** 541 | 542 | - `propName`: the property to test. 543 | 544 | **Returns:** 545 | 546 | true if the specified property exposes itself to for/in property inspection through the object, false otherwise. 547 | 548 | --- 549 | 550 | ### seal 551 | 552 | **Signature:** `static seal(object : Object) : Object` 553 | 554 | **Description:** Seals the passed object. This means properties can't be added or removed. Also, property definitions of existing properties can't be changed. 555 | 556 | **Parameters:** 557 | 558 | - `object`: The object to be frozen. 559 | 560 | **Returns:** 561 | 562 | The sealed object. 563 | 564 | --- 565 | 566 | ### setPrototypeOf 567 | 568 | **Signature:** `static setPrototypeOf(object : Object, prototype : Object) : Object` 569 | 570 | **Description:** Changes the prototype of the passed object. 571 | 572 | **API Versioned:** 573 | 574 | From version 21.2. 575 | 576 | **Parameters:** 577 | 578 | - `object`: The object whose prototype should change. 579 | - `prototype`: The object to set as the new prototype. 580 | 581 | **Returns:** 582 | 583 | The object with the changed prototype. 584 | 585 | --- 586 | 587 | ### toLocaleString 588 | 589 | **Signature:** `toLocaleString() : String` 590 | 591 | **Description:** Converts the object to a localized String. 592 | 593 | **Returns:** 594 | 595 | a localized version of the object. 596 | 597 | --- 598 | 599 | ### toString 600 | 601 | **Signature:** `toString() : String` 602 | 603 | **Description:** Converts the object to a String. 604 | 605 | **Returns:** 606 | 607 | the String representation of the object. 608 | 609 | --- 610 | 611 | ### valueOf 612 | 613 | **Signature:** `valueOf() : Object` 614 | 615 | **Description:** Returns the object's value. 616 | 617 | **Returns:** 618 | 619 | the object's value. 620 | 621 | --- 622 | 623 | ### values 624 | 625 | **Signature:** `static values(object : Object) : Array` 626 | 627 | **Description:** Returns the enumerable property values of the passed object. 628 | 629 | **API Versioned:** 630 | 631 | From version 22.7. 632 | 633 | **Parameters:** 634 | 635 | - `object`: The object to get the enumerable property values from. 636 | 637 | **Returns:** 638 | 639 | An array of values that holds all the enumerable properties of the given object. 640 | 641 | --- ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/mock-data/ocapi/site-preferences-storefront.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_v": "23.2", 3 | "_type": "preference_value_search_result", 4 | "count": 8, 5 | "hits": [ 6 | { 7 | "_type": "preference_value", 8 | "attribute_definition": { 9 | "_type": "object_attribute_definition", 10 | "_resource_state": "storefront1-resource-state", 11 | "creation_date": "2024-01-01T00:00:00.000Z", 12 | "description": { 13 | "default": "Enable or disable the shopping cart functionality" 14 | }, 15 | "display_name": { 16 | "default": "Shopping Cart Enabled" 17 | }, 18 | "effective_id": "c_shoppingCartEnabled", 19 | "externally_defined": false, 20 | "externally_managed": false, 21 | "id": "shoppingCartEnabled", 22 | "key": false, 23 | "last_modified": "2024-01-01T00:00:00.000Z", 24 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/shoppingCartEnabled", 25 | "localizable": false, 26 | "mandatory": false, 27 | "multi_value_type": false, 28 | "order_required": false, 29 | "queryable": false, 30 | "read_only": false, 31 | "requires_encoding": false, 32 | "searchable": false, 33 | "set_value_type": false, 34 | "site_specific": true, 35 | "system": false, 36 | "value_type": "boolean", 37 | "visible": true 38 | }, 39 | "description": { 40 | "default": "Enable or disable the shopping cart functionality" 41 | }, 42 | "display_name": { 43 | "default": "Shopping Cart Enabled" 44 | }, 45 | "id": "shoppingCartEnabled", 46 | "site_values": { 47 | "RefArch": true, 48 | "RefArchGlobal": true, 49 | "pxl_1": false, 50 | "pxl_2": true, 51 | "pxl_3": null, 52 | "pxl_4": null, 53 | "pxl_5": null, 54 | "pxl_6": null 55 | }, 56 | "value_type": "boolean" 57 | }, 58 | { 59 | "_type": "preference_value", 60 | "attribute_definition": { 61 | "_type": "object_attribute_definition", 62 | "_resource_state": "storefront2-resource-state", 63 | "creation_date": "2024-01-01T00:00:00.000Z", 64 | "description": { 65 | "default": "Maximum number of items allowed in shopping cart" 66 | }, 67 | "display_name": { 68 | "default": "Max Cart Items" 69 | }, 70 | "effective_id": "c_maxCartItems", 71 | "externally_defined": false, 72 | "externally_managed": false, 73 | "id": "maxCartItems", 74 | "key": false, 75 | "last_modified": "2024-01-01T00:00:00.000Z", 76 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/maxCartItems", 77 | "localizable": false, 78 | "mandatory": true, 79 | "multi_value_type": false, 80 | "order_required": false, 81 | "queryable": false, 82 | "read_only": false, 83 | "requires_encoding": false, 84 | "searchable": false, 85 | "set_value_type": false, 86 | "site_specific": true, 87 | "system": false, 88 | "value_type": "int", 89 | "visible": true 90 | }, 91 | "description": { 92 | "default": "Maximum number of items allowed in shopping cart" 93 | }, 94 | "display_name": { 95 | "default": "Max Cart Items" 96 | }, 97 | "id": "maxCartItems", 98 | "site_values": { 99 | "RefArch": 50, 100 | "RefArchGlobal": 100, 101 | "pxl_1": 25, 102 | "pxl_2": 75, 103 | "pxl_3": null, 104 | "pxl_4": null, 105 | "pxl_5": null, 106 | "pxl_6": null 107 | }, 108 | "value_type": "int" 109 | }, 110 | { 111 | "_type": "preference_value", 112 | "attribute_definition": { 113 | "_type": "object_attribute_definition", 114 | "_resource_state": "storefront3-resource-state", 115 | "creation_date": "2024-01-01T00:00:00.000Z", 116 | "description": { 117 | "default": "Welcome message displayed on homepage" 118 | }, 119 | "display_name": { 120 | "default": "Homepage Welcome Message" 121 | }, 122 | "effective_id": "c_homepageWelcomeMessage", 123 | "externally_defined": false, 124 | "externally_managed": false, 125 | "field_height": 5, 126 | "id": "homepageWelcomeMessage", 127 | "key": false, 128 | "last_modified": "2024-01-01T00:00:00.000Z", 129 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/homepageWelcomeMessage", 130 | "localizable": true, 131 | "mandatory": false, 132 | "multi_value_type": false, 133 | "order_required": false, 134 | "queryable": false, 135 | "read_only": false, 136 | "requires_encoding": true, 137 | "searchable": true, 138 | "set_value_type": false, 139 | "site_specific": true, 140 | "system": false, 141 | "value_type": "text", 142 | "visible": true 143 | }, 144 | "description": { 145 | "default": "Welcome message displayed on homepage" 146 | }, 147 | "display_name": { 148 | "default": "Homepage Welcome Message" 149 | }, 150 | "id": "homepageWelcomeMessage", 151 | "site_values": { 152 | "RefArch": "Welcome to our amazing store!", 153 | "RefArchGlobal": "Discover the best products worldwide!", 154 | "pxl_1": "Pixels Store - Your tech destination", 155 | "pxl_2": "Premium electronics await you", 156 | "pxl_3": null, 157 | "pxl_4": null, 158 | "pxl_5": null, 159 | "pxl_6": null 160 | }, 161 | "value_type": "text" 162 | }, 163 | { 164 | "_type": "preference_value", 165 | "attribute_definition": { 166 | "_type": "object_attribute_definition", 167 | "_resource_state": "storefront4-resource-state", 168 | "creation_date": "2024-01-01T00:00:00.000Z", 169 | "description": { 170 | "default": "Discount percentage for new customers" 171 | }, 172 | "display_name": { 173 | "default": "New Customer Discount" 174 | }, 175 | "effective_id": "c_newCustomerDiscount", 176 | "externally_defined": false, 177 | "externally_managed": false, 178 | "id": "newCustomerDiscount", 179 | "key": false, 180 | "last_modified": "2024-01-01T00:00:00.000Z", 181 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/newCustomerDiscount", 182 | "localizable": false, 183 | "mandatory": false, 184 | "multi_value_type": false, 185 | "order_required": false, 186 | "queryable": false, 187 | "read_only": false, 188 | "requires_encoding": false, 189 | "searchable": false, 190 | "set_value_type": false, 191 | "site_specific": true, 192 | "system": false, 193 | "value_type": "double", 194 | "visible": true 195 | }, 196 | "description": { 197 | "default": "Discount percentage for new customers" 198 | }, 199 | "display_name": { 200 | "default": "New Customer Discount" 201 | }, 202 | "id": "newCustomerDiscount", 203 | "site_values": { 204 | "RefArch": 10.5, 205 | "RefArchGlobal": 15.0, 206 | "pxl_1": 5.0, 207 | "pxl_2": 12.5, 208 | "pxl_3": null, 209 | "pxl_4": null, 210 | "pxl_5": null, 211 | "pxl_6": null 212 | }, 213 | "value_type": "double" 214 | }, 215 | { 216 | "_type": "preference_value", 217 | "attribute_definition": { 218 | "_type": "object_attribute_definition", 219 | "_resource_state": "storefront5-resource-state", 220 | "creation_date": "2024-01-01T00:00:00.000Z", 221 | "description": { 222 | "default": "Available payment methods for checkout" 223 | }, 224 | "display_name": { 225 | "default": "Payment Methods" 226 | }, 227 | "effective_id": "c_paymentMethods", 228 | "externally_defined": false, 229 | "externally_managed": false, 230 | "id": "paymentMethods", 231 | "key": false, 232 | "last_modified": "2024-01-01T00:00:00.000Z", 233 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/paymentMethods", 234 | "localizable": false, 235 | "mandatory": false, 236 | "multi_value_type": true, 237 | "order_required": false, 238 | "queryable": false, 239 | "read_only": false, 240 | "requires_encoding": false, 241 | "searchable": false, 242 | "set_value_type": true, 243 | "site_specific": true, 244 | "system": false, 245 | "value_type": "set_of_string", 246 | "visible": true 247 | }, 248 | "description": { 249 | "default": "Available payment methods for checkout" 250 | }, 251 | "display_name": { 252 | "default": "Payment Methods" 253 | }, 254 | "id": "paymentMethods", 255 | "site_values": { 256 | "RefArch": "[\"CREDIT_CARD\", \"PAYPAL\", \"APPLE_PAY\"]", 257 | "RefArchGlobal": "[\"CREDIT_CARD\", \"PAYPAL\", \"GOOGLE_PAY\", \"BANK_TRANSFER\"]", 258 | "pxl_1": "[\"CREDIT_CARD\", \"PAYPAL\"]", 259 | "pxl_2": "[\"CREDIT_CARD\", \"APPLE_PAY\", \"GOOGLE_PAY\"]", 260 | "pxl_3": null, 261 | "pxl_4": null, 262 | "pxl_5": null, 263 | "pxl_6": null 264 | }, 265 | "value_type": "set_of_string" 266 | }, 267 | { 268 | "_type": "preference_value", 269 | "attribute_definition": { 270 | "_type": "object_attribute_definition", 271 | "_resource_state": "storefront6-resource-state", 272 | "creation_date": "2024-01-01T00:00:00.000Z", 273 | "description": { 274 | "default": "Date when the current promotion ends" 275 | }, 276 | "display_name": { 277 | "default": "Promotion End Date" 278 | }, 279 | "effective_id": "c_promotionEndDate", 280 | "externally_defined": false, 281 | "externally_managed": false, 282 | "id": "promotionEndDate", 283 | "key": false, 284 | "last_modified": "2024-01-01T00:00:00.000Z", 285 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/promotionEndDate", 286 | "localizable": false, 287 | "mandatory": false, 288 | "multi_value_type": false, 289 | "order_required": false, 290 | "queryable": false, 291 | "read_only": false, 292 | "requires_encoding": false, 293 | "searchable": false, 294 | "set_value_type": false, 295 | "site_specific": true, 296 | "system": false, 297 | "value_type": "date", 298 | "visible": true 299 | }, 300 | "description": { 301 | "default": "Date when the current promotion ends" 302 | }, 303 | "display_name": { 304 | "default": "Promotion End Date" 305 | }, 306 | "id": "promotionEndDate", 307 | "site_values": { 308 | "RefArch": "2025-12-31", 309 | "RefArchGlobal": "2025-11-30", 310 | "pxl_1": "2025-10-31", 311 | "pxl_2": "2025-12-25", 312 | "pxl_3": null, 313 | "pxl_4": null, 314 | "pxl_5": null, 315 | "pxl_6": null 316 | }, 317 | "value_type": "date" 318 | }, 319 | { 320 | "_type": "preference_value", 321 | "attribute_definition": { 322 | "_type": "object_attribute_definition", 323 | "_resource_state": "storefront7-resource-state", 324 | "creation_date": "2024-01-01T00:00:00.000Z", 325 | "description": { 326 | "default": "Custom CSS for site styling" 327 | }, 328 | "display_name": { 329 | "default": "Custom Site CSS" 330 | }, 331 | "effective_id": "c_customSiteCSS", 332 | "externally_defined": false, 333 | "externally_managed": false, 334 | "field_height": 15, 335 | "id": "customSiteCSS", 336 | "key": false, 337 | "last_modified": "2024-01-01T00:00:00.000Z", 338 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/customSiteCSS", 339 | "localizable": false, 340 | "mandatory": false, 341 | "multi_value_type": false, 342 | "order_required": false, 343 | "queryable": false, 344 | "read_only": false, 345 | "requires_encoding": true, 346 | "searchable": false, 347 | "set_value_type": false, 348 | "site_specific": true, 349 | "system": false, 350 | "value_type": "html", 351 | "visible": true 352 | }, 353 | "description": { 354 | "default": "Custom CSS for site styling" 355 | }, 356 | "display_name": { 357 | "default": "Custom Site CSS" 358 | }, 359 | "id": "customSiteCSS", 360 | "site_values": { 361 | "RefArch": ".header { background-color: #ff6b35; }", 362 | "RefArchGlobal": ".global-header { background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); }", 363 | "pxl_1": ".tech-theme { color: #00ff00; background: #000; }", 364 | "pxl_2": ".premium-theme { border: 2px solid gold; }", 365 | "pxl_3": null, 366 | "pxl_4": null, 367 | "pxl_5": null, 368 | "pxl_6": null 369 | }, 370 | "value_type": "html" 371 | }, 372 | { 373 | "_type": "preference_value", 374 | "attribute_definition": { 375 | "_type": "object_attribute_definition", 376 | "_resource_state": "storefront8-resource-state", 377 | "creation_date": "2024-01-01T00:00:00.000Z", 378 | "description": { 379 | "default": "Password strength requirement level" 380 | }, 381 | "display_name": { 382 | "default": "Password Strength Level" 383 | }, 384 | "effective_id": "c_passwordStrengthLevel", 385 | "externally_defined": false, 386 | "externally_managed": false, 387 | "id": "passwordStrengthLevel", 388 | "key": false, 389 | "last_modified": "2024-01-01T00:00:00.000Z", 390 | "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/passwordStrengthLevel", 391 | "localizable": false, 392 | "mandatory": true, 393 | "multi_value_type": false, 394 | "order_required": false, 395 | "queryable": false, 396 | "read_only": false, 397 | "requires_encoding": false, 398 | "searchable": false, 399 | "set_value_type": false, 400 | "site_specific": false, 401 | "system": false, 402 | "value_type": "enum_of_string", 403 | "value_definitions": { 404 | "WEAK": { 405 | "display_value": {"default": "Weak"}, 406 | "value": "WEAK" 407 | }, 408 | "MEDIUM": { 409 | "display_value": {"default": "Medium"}, 410 | "value": "MEDIUM" 411 | }, 412 | "STRONG": { 413 | "display_value": {"default": "Strong"}, 414 | "value": "STRONG" 415 | } 416 | }, 417 | "visible": true 418 | }, 419 | "description": { 420 | "default": "Password strength requirement level" 421 | }, 422 | "display_name": { 423 | "default": "Password Strength Level" 424 | }, 425 | "id": "passwordStrengthLevel", 426 | "site_values": { 427 | "RefArch": "MEDIUM", 428 | "RefArchGlobal": "STRONG", 429 | "pxl_1": "WEAK", 430 | "pxl_2": "STRONG", 431 | "pxl_3": null, 432 | "pxl_4": null, 433 | "pxl_5": null, 434 | "pxl_6": null 435 | }, 436 | "value_type": "enum_of_string" 437 | } 438 | ], 439 | "query": { 440 | "match_all_query": { 441 | "_type": "match_all_query" 442 | } 443 | }, 444 | "select": "(**)", 445 | "start": 0, 446 | "total": 8 447 | } ``` -------------------------------------------------------------------------------- /docs/dw_customer/Customer.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.customer 2 | 3 | # Class Customer 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.customer.Customer 9 | 10 | ## Description 11 | 12 | Represents a customer. 13 | 14 | ## Properties 15 | 16 | ### activeData 17 | 18 | **Type:** CustomerActiveData (Read Only) 19 | 20 | The active data for this customer. 21 | 22 | ### addressBook 23 | 24 | **Type:** AddressBook (Read Only) 25 | 26 | The address book for the profile of this customer, 27 | or null if this customer has no profile, such as for an 28 | anonymous customer. 29 | 30 | ### anonymous 31 | 32 | **Type:** boolean (Read Only) 33 | 34 | Identifies if the customer is anonymous. An anonymous 35 | customer is the opposite of a registered customer. 36 | 37 | ### authenticated 38 | 39 | **Type:** boolean (Read Only) 40 | 41 | Identifies if the customer is authenticated. This method checks whether 42 | this customer is the customer associated with the session and than checks 43 | whether the session in an authenticated state. 44 | 45 | Note: The pipeline debugger will always show 'false' for this value 46 | regardless of whether the customer is authenticated or not. 47 | 48 | ### CDPData 49 | 50 | **Type:** CustomerCDPData (Read Only) 51 | 52 | The Salesforce CDP (Customer Data Platform) data for this customer. 53 | 54 | ### customerGroups 55 | 56 | **Type:** Collection (Static) (Read Only) 57 | 58 | The customer groups this customer is member of. 59 | 60 | Result contains static customer groups in storefront and job session 61 | Result contains dynamic customer groups in storefront and job session. 62 | Dynamic customer groups referring session or request data are not available 63 | when processing the customer in a job session, or when this customer is not the customer assigned to the current session. 64 | 65 | Result contains system groups 'Everyone', 'Unregistered', 'Registered' for all customers in storefront and job sessions 66 | 67 | ### externallyAuthenticated 68 | 69 | **Type:** boolean (Read Only) 70 | 71 | Identifies if the customer is externally authenticated. An externally 72 | authenticated customer does not have the password stored in our system 73 | but logs in through an external OAuth provider (Google, Facebook, LinkedIn, etc.) 74 | 75 | ### externalProfiles 76 | 77 | **Type:** Collection (Read Only) 78 | 79 | A collection of any external profiles the customer may have 80 | 81 | ### globalPartyID 82 | 83 | **Type:** String (Read Only) 84 | 85 | The Global Party ID for the customer, if there is one. 86 | Global Party ID is created by Customer 360 and identifies a person across multiple systems. 87 | 88 | ### ID 89 | 90 | **Type:** String (Read Only) 91 | 92 | The unique, system generated ID of the customer. 93 | 94 | ### note 95 | 96 | **Type:** String 97 | 98 | The note for this customer, or null if this customer has no note, such as for an anonymous 99 | customer or when note has 0 length. 100 | 101 | ### orderHistory 102 | 103 | **Type:** OrderHistory (Read Only) 104 | 105 | The customer order history. 106 | 107 | ### profile 108 | 109 | **Type:** Profile (Read Only) 110 | 111 | The customer profile. 112 | 113 | ### registered 114 | 115 | **Type:** boolean (Read Only) 116 | 117 | Identifies if the customer is registered. A registered customer 118 | may or may not be authenticated. This method checks whether 119 | the user has a profile. 120 | 121 | ## Constructor Summary 122 | 123 | ## Method Summary 124 | 125 | ### createExternalProfile 126 | 127 | **Signature:** `createExternalProfile(authenticationProviderId : String, externalId : String) : ExternalProfile` 128 | 129 | Creates an externalProfile and attaches it to the list of external profiles for the customer 130 | 131 | ### getActiveData 132 | 133 | **Signature:** `getActiveData() : CustomerActiveData` 134 | 135 | Returns the active data for this customer. 136 | 137 | ### getAddressBook 138 | 139 | **Signature:** `getAddressBook() : AddressBook` 140 | 141 | Returns the address book for the profile of this customer, or null if this customer has no profile, such as for an anonymous customer. 142 | 143 | ### getCDPData 144 | 145 | **Signature:** `getCDPData() : CustomerCDPData` 146 | 147 | Returns the Salesforce CDP (Customer Data Platform) data for this customer. 148 | 149 | ### getCustomerGroups 150 | 151 | **Signature:** `getCustomerGroups() : Collection` 152 | 153 | Returns the customer groups this customer is member of. 154 | 155 | ### getExternalProfile 156 | 157 | **Signature:** `getExternalProfile(authenticationProviderId : String, externalId : String) : ExternalProfile` 158 | 159 | A convenience method for finding an external profile among the customer's external profiles collection 160 | 161 | ### getExternalProfiles 162 | 163 | **Signature:** `getExternalProfiles() : Collection` 164 | 165 | Returns a collection of any external profiles the customer may have 166 | 167 | ### getGlobalPartyID 168 | 169 | **Signature:** `getGlobalPartyID() : String` 170 | 171 | Returns the Global Party ID for the customer, if there is one. 172 | 173 | ### getID 174 | 175 | **Signature:** `getID() : String` 176 | 177 | Returns the unique, system generated ID of the customer. 178 | 179 | ### getNote 180 | 181 | **Signature:** `getNote() : String` 182 | 183 | Returns the note for this customer, or null if this customer has no note, such as for an anonymous customer or when note has 0 length. 184 | 185 | ### getOrderHistory 186 | 187 | **Signature:** `getOrderHistory() : OrderHistory` 188 | 189 | Returns the customer order history. 190 | 191 | ### getProductLists 192 | 193 | **Signature:** `getProductLists(type : Number) : Collection` 194 | 195 | Returns the product lists of the specified type. 196 | 197 | ### getProfile 198 | 199 | **Signature:** `getProfile() : Profile` 200 | 201 | Returns the customer profile. 202 | 203 | ### isAnonymous 204 | 205 | **Signature:** `isAnonymous() : boolean` 206 | 207 | Identifies if the customer is anonymous. 208 | 209 | ### isAuthenticated 210 | 211 | **Signature:** `isAuthenticated() : boolean` 212 | 213 | Identifies if the customer is authenticated. 214 | 215 | ### isExternallyAuthenticated 216 | 217 | **Signature:** `isExternallyAuthenticated() : boolean` 218 | 219 | Identifies if the customer is externally authenticated. 220 | 221 | ### isMemberOfAnyCustomerGroup 222 | 223 | **Signature:** `isMemberOfAnyCustomerGroup(groupIDs : String...) : boolean` 224 | 225 | Returns true if there exist CustomerGroup for all of the given IDs and the customer is member of at least one of that groups. 226 | 227 | ### isMemberOfCustomerGroup 228 | 229 | **Signature:** `isMemberOfCustomerGroup(group : CustomerGroup) : boolean` 230 | 231 | Returns true if the customer is member of the specified CustomerGroup. 232 | 233 | ### isMemberOfCustomerGroup 234 | 235 | **Signature:** `isMemberOfCustomerGroup(groupID : String) : boolean` 236 | 237 | Returns true if there is a CustomerGroup with such an ID and the customer is member of that group. 238 | 239 | ### isMemberOfCustomerGroups 240 | 241 | **Signature:** `isMemberOfCustomerGroups(groupIDs : String...) : boolean` 242 | 243 | Returns true if there exist CustomerGroup for all of the given IDs and the customer is member of all that groups. 244 | 245 | ### isRegistered 246 | 247 | **Signature:** `isRegistered() : boolean` 248 | 249 | Identifies if the customer is registered. 250 | 251 | ### removeExternalProfile 252 | 253 | **Signature:** `removeExternalProfile(externalProfile : ExternalProfile) : void` 254 | 255 | Removes an external profile from the customer 256 | 257 | ### setNote 258 | 259 | **Signature:** `setNote(aValue : String) : void` 260 | 261 | Sets the note for this customer. 262 | 263 | ## Method Detail 264 | 265 | ## Method Details 266 | 267 | ### createExternalProfile 268 | 269 | **Signature:** `createExternalProfile(authenticationProviderId : String, externalId : String) : ExternalProfile` 270 | 271 | **Description:** Creates an externalProfile and attaches it to the list of external profiles for the customer 272 | 273 | **Parameters:** 274 | 275 | - `authenticationProviderId`: the authenticationProviderId for the externalProfile 276 | - `externalId`: the externalId for the external Profile 277 | 278 | **Returns:** 279 | 280 | the new externalProfile 281 | 282 | --- 283 | 284 | ### getActiveData 285 | 286 | **Signature:** `getActiveData() : CustomerActiveData` 287 | 288 | **Description:** Returns the active data for this customer. 289 | 290 | **Returns:** 291 | 292 | the active data for this customer. 293 | 294 | --- 295 | 296 | ### getAddressBook 297 | 298 | **Signature:** `getAddressBook() : AddressBook` 299 | 300 | **Description:** Returns the address book for the profile of this customer, or null if this customer has no profile, such as for an anonymous customer. 301 | 302 | --- 303 | 304 | ### getCDPData 305 | 306 | **Signature:** `getCDPData() : CustomerCDPData` 307 | 308 | **Description:** Returns the Salesforce CDP (Customer Data Platform) data for this customer. 309 | 310 | **Returns:** 311 | 312 | the Salesforce CDP data for this customer. 313 | 314 | --- 315 | 316 | ### getCustomerGroups 317 | 318 | **Signature:** `getCustomerGroups() : Collection` 319 | 320 | **Description:** Returns the customer groups this customer is member of. Result contains static customer groups in storefront and job session Result contains dynamic customer groups in storefront and job session. Dynamic customer groups referring session or request data are not available when processing the customer in a job session, or when this customer is not the customer assigned to the current session. Result contains system groups 'Everyone', 'Unregistered', 'Registered' for all customers in storefront and job sessions 321 | 322 | **Returns:** 323 | 324 | Collection of customer groups of this customer 325 | 326 | --- 327 | 328 | ### getExternalProfile 329 | 330 | **Signature:** `getExternalProfile(authenticationProviderId : String, externalId : String) : ExternalProfile` 331 | 332 | **Description:** A convenience method for finding an external profile among the customer's external profiles collection 333 | 334 | **Parameters:** 335 | 336 | - `authenticationProviderId`: the authenticationProviderId to look for 337 | - `externalId`: the externalId to look for 338 | 339 | **Returns:** 340 | 341 | the externalProfile found among the customer's external profile or null if not found 342 | 343 | --- 344 | 345 | ### getExternalProfiles 346 | 347 | **Signature:** `getExternalProfiles() : Collection` 348 | 349 | **Description:** Returns a collection of any external profiles the customer may have 350 | 351 | **Returns:** 352 | 353 | a collection of any external profiles the customer may have 354 | 355 | --- 356 | 357 | ### getGlobalPartyID 358 | 359 | **Signature:** `getGlobalPartyID() : String` 360 | 361 | **Description:** Returns the Global Party ID for the customer, if there is one. Global Party ID is created by Customer 360 and identifies a person across multiple systems. 362 | 363 | **Returns:** 364 | 365 | The global party ID 366 | 367 | --- 368 | 369 | ### getID 370 | 371 | **Signature:** `getID() : String` 372 | 373 | **Description:** Returns the unique, system generated ID of the customer. 374 | 375 | **Returns:** 376 | 377 | the ID of the customer. 378 | 379 | --- 380 | 381 | ### getNote 382 | 383 | **Signature:** `getNote() : String` 384 | 385 | **Description:** Returns the note for this customer, or null if this customer has no note, such as for an anonymous customer or when note has 0 length. 386 | 387 | **Returns:** 388 | 389 | the note for this customer. 390 | 391 | --- 392 | 393 | ### getOrderHistory 394 | 395 | **Signature:** `getOrderHistory() : OrderHistory` 396 | 397 | **Description:** Returns the customer order history. 398 | 399 | **Returns:** 400 | 401 | the customer order history. 402 | 403 | --- 404 | 405 | ### getProductLists 406 | 407 | **Signature:** `getProductLists(type : Number) : Collection` 408 | 409 | **Description:** Returns the product lists of the specified type. 410 | 411 | **Parameters:** 412 | 413 | - `type`: the type of product lists to return. 414 | 415 | **Returns:** 416 | 417 | the product lists of the specified type. 418 | 419 | **See Also:** 420 | 421 | ProductList 422 | 423 | --- 424 | 425 | ### getProfile 426 | 427 | **Signature:** `getProfile() : Profile` 428 | 429 | **Description:** Returns the customer profile. 430 | 431 | **Returns:** 432 | 433 | the customer profile. 434 | 435 | --- 436 | 437 | ### isAnonymous 438 | 439 | **Signature:** `isAnonymous() : boolean` 440 | 441 | **Description:** Identifies if the customer is anonymous. An anonymous customer is the opposite of a registered customer. 442 | 443 | **Returns:** 444 | 445 | true if the customer is anonymous, false otherwise. Note: this method handles sensitive security-related data. Pay special attention to PCI DSS v3. requirements 2, 4, and 12. 446 | 447 | --- 448 | 449 | ### isAuthenticated 450 | 451 | **Signature:** `isAuthenticated() : boolean` 452 | 453 | **Description:** Identifies if the customer is authenticated. This method checks whether this customer is the customer associated with the session and than checks whether the session in an authenticated state. Note: The pipeline debugger will always show 'false' for this value regardless of whether the customer is authenticated or not. 454 | 455 | **Returns:** 456 | 457 | true if the customer is authenticated, false otherwise. 458 | 459 | --- 460 | 461 | ### isExternallyAuthenticated 462 | 463 | **Signature:** `isExternallyAuthenticated() : boolean` 464 | 465 | **Description:** Identifies if the customer is externally authenticated. An externally authenticated customer does not have the password stored in our system but logs in through an external OAuth provider (Google, Facebook, LinkedIn, etc.) 466 | 467 | **Returns:** 468 | 469 | true if the customer is externally authenticated, false otherwise. Note: this method handles sensitive security-related data. Pay special attention to PCI DSS v3. requirements 2, 4, and 12. 470 | 471 | --- 472 | 473 | ### isMemberOfAnyCustomerGroup 474 | 475 | **Signature:** `isMemberOfAnyCustomerGroup(groupIDs : String...) : boolean` 476 | 477 | **Description:** Returns true if there exist CustomerGroup for all of the given IDs and the customer is member of at least one of that groups. 478 | 479 | **Parameters:** 480 | 481 | - `groupIDs`: A list of unique semantic customer group IDs. 482 | 483 | **Returns:** 484 | 485 | True if customer groups exist for the given IDs and the customer is member of at least one of that existing groups. False if none of customer groups exist or if the customer is not a member of any of that existing groups. 486 | 487 | --- 488 | 489 | ### isMemberOfCustomerGroup 490 | 491 | **Signature:** `isMemberOfCustomerGroup(group : CustomerGroup) : boolean` 492 | 493 | **Description:** Returns true if the customer is member of the specified CustomerGroup. 494 | 495 | **Parameters:** 496 | 497 | - `group`: Customer group 498 | 499 | **Returns:** 500 | 501 | True if customer is member of the group, otherwise false. 502 | 503 | --- 504 | 505 | ### isMemberOfCustomerGroup 506 | 507 | **Signature:** `isMemberOfCustomerGroup(groupID : String) : boolean` 508 | 509 | **Description:** Returns true if there is a CustomerGroup with such an ID and the customer is member of that group. 510 | 511 | **Parameters:** 512 | 513 | - `groupID`: The unique semantic customer group ID. 514 | 515 | **Returns:** 516 | 517 | True if a customer group with such an ID exist and the customer is member of that group. False if no such customer group exist or, if the group exist, the customer is not member of that group. 518 | 519 | --- 520 | 521 | ### isMemberOfCustomerGroups 522 | 523 | **Signature:** `isMemberOfCustomerGroups(groupIDs : String...) : boolean` 524 | 525 | **Description:** Returns true if there exist CustomerGroup for all of the given IDs and the customer is member of all that groups. 526 | 527 | **Parameters:** 528 | 529 | - `groupIDs`: A list of unique semantic customer group IDs. 530 | 531 | **Returns:** 532 | 533 | True if customer groups exist for all of the given IDs and the customer is member of all that groups. False if there is at least one ID for which no customer group exist or, if all groups exist, the customer is not member of all that groups. 534 | 535 | --- 536 | 537 | ### isRegistered 538 | 539 | **Signature:** `isRegistered() : boolean` 540 | 541 | **Description:** Identifies if the customer is registered. A registered customer may or may not be authenticated. This method checks whether the user has a profile. 542 | 543 | **Returns:** 544 | 545 | true if the customer is registered, false otherwise. 546 | 547 | --- 548 | 549 | ### removeExternalProfile 550 | 551 | **Signature:** `removeExternalProfile(externalProfile : ExternalProfile) : void` 552 | 553 | **Description:** Removes an external profile from the customer 554 | 555 | **Parameters:** 556 | 557 | - `externalProfile`: the externalProfile to be removed 558 | 559 | --- 560 | 561 | ### setNote 562 | 563 | **Signature:** `setNote(aValue : String) : void` 564 | 565 | **Description:** Sets the note for this customer. This is a no-op for an anonymous customer. 566 | 567 | **Parameters:** 568 | 569 | - `aValue`: the value of the note 570 | 571 | --- ``` -------------------------------------------------------------------------------- /tests/cache.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { InMemoryCache, CacheManager } from '../src/utils/cache'; 2 | 3 | describe('InMemoryCache', () => { 4 | let cache: InMemoryCache<string>; 5 | 6 | beforeEach(() => { 7 | cache = new InMemoryCache<string>({ 8 | maxSize: 3, 9 | ttlMs: 1000, // 1 second for testing 10 | cleanupIntervalMs: 100, // 100ms for faster testing 11 | }); 12 | }); 13 | 14 | afterEach(() => { 15 | cache.destroy(); 16 | }); 17 | 18 | describe('constructor', () => { 19 | it('should use default options when none provided', () => { 20 | const defaultCache = new InMemoryCache(); 21 | const stats = defaultCache.getStats(); 22 | 23 | expect(stats.maxSize).toBe(1000); 24 | defaultCache.destroy(); 25 | }); 26 | 27 | it('should accept custom options', () => { 28 | const customCache = new InMemoryCache({ 29 | maxSize: 50, 30 | ttlMs: 5000, 31 | cleanupIntervalMs: 1000, 32 | }); 33 | const stats = customCache.getStats(); 34 | 35 | expect(stats.maxSize).toBe(50); 36 | customCache.destroy(); 37 | }); 38 | }); 39 | 40 | describe('set and get', () => { 41 | it('should store and retrieve values', () => { 42 | cache.set('key1', 'value1'); 43 | expect(cache.get('key1')).toBe('value1'); 44 | }); 45 | 46 | it('should return undefined for non-existent keys', () => { 47 | expect(cache.get('nonexistent')).toBeUndefined(); 48 | }); 49 | 50 | it('should update access statistics on get', () => { 51 | cache.set('key1', 'value1'); 52 | cache.get('key1'); // First access 53 | cache.get('key1'); // Second access 54 | 55 | const stats = cache.getStats(); 56 | const entry = stats.entries.find(e => e.key === 'key1'); 57 | expect(entry?.accessCount).toBe(2); 58 | }); 59 | 60 | it('should update lastAccessed timestamp on get', async () => { 61 | cache.set('key1', 'value1'); 62 | const initialGet = cache.get('key1'); 63 | 64 | // Wait a bit and access again 65 | await new Promise(resolve => setTimeout(resolve, 10)); 66 | const secondGet = cache.get('key1'); 67 | 68 | expect(initialGet).toBe('value1'); 69 | expect(secondGet).toBe('value1'); 70 | 71 | const stats = cache.getStats(); 72 | const entry = stats.entries.find(e => e.key === 'key1'); 73 | expect(entry?.accessCount).toBe(2); 74 | }); 75 | }); 76 | 77 | describe('has', () => { 78 | it('should return true for existing keys', () => { 79 | cache.set('key1', 'value1'); 80 | expect(cache.has('key1')).toBe(true); 81 | }); 82 | 83 | it('should return false for non-existent keys', () => { 84 | expect(cache.has('nonexistent')).toBe(false); 85 | }); 86 | 87 | it('should not update access statistics', () => { 88 | cache.set('key1', 'value1'); 89 | cache.has('key1'); 90 | 91 | const stats = cache.getStats(); 92 | const entry = stats.entries.find(e => e.key === 'key1'); 93 | expect(entry?.accessCount).toBe(0); 94 | }); 95 | }); 96 | 97 | describe('delete', () => { 98 | it('should remove existing keys', () => { 99 | cache.set('key1', 'value1'); 100 | expect(cache.has('key1')).toBe(true); 101 | 102 | const deleted = cache.delete('key1'); 103 | expect(deleted).toBe(true); 104 | expect(cache.has('key1')).toBe(false); 105 | }); 106 | 107 | it('should return false for non-existent keys', () => { 108 | const deleted = cache.delete('nonexistent'); 109 | expect(deleted).toBe(false); 110 | }); 111 | }); 112 | 113 | describe('clear', () => { 114 | it('should remove all entries', () => { 115 | cache.set('key1', 'value1'); 116 | cache.set('key2', 'value2'); 117 | expect(cache.getStats().size).toBe(2); 118 | 119 | cache.clear(); 120 | expect(cache.getStats().size).toBe(0); 121 | }); 122 | }); 123 | 124 | describe('TTL (Time To Live)', () => { 125 | it('should expire entries after TTL', async () => { 126 | const shortTtlCache = new InMemoryCache<string>({ 127 | ttlMs: 50, // 50ms 128 | cleanupIntervalMs: 1000, // Don't auto-cleanup for this test 129 | }); 130 | 131 | shortTtlCache.set('key1', 'value1'); 132 | expect(shortTtlCache.get('key1')).toBe('value1'); 133 | 134 | // Wait for expiration 135 | await new Promise(resolve => setTimeout(resolve, 60)); 136 | expect(shortTtlCache.get('key1')).toBeUndefined(); 137 | 138 | shortTtlCache.destroy(); 139 | }); 140 | 141 | it('should remove expired entries when checking has()', async () => { 142 | const shortTtlCache = new InMemoryCache<string>({ 143 | ttlMs: 50, 144 | cleanupIntervalMs: 1000, 145 | }); 146 | 147 | shortTtlCache.set('key1', 'value1'); 148 | expect(shortTtlCache.has('key1')).toBe(true); 149 | 150 | await new Promise(resolve => setTimeout(resolve, 60)); 151 | expect(shortTtlCache.has('key1')).toBe(false); 152 | 153 | shortTtlCache.destroy(); 154 | }); 155 | }); 156 | 157 | describe('LRU (Least Recently Used) eviction', () => { 158 | it('should evict LRU item when max size is reached', async () => { 159 | // Fill cache to max capacity 160 | cache.set('key1', 'value1'); 161 | await new Promise(resolve => setTimeout(resolve, 1)); // Small delay 162 | cache.set('key2', 'value2'); 163 | await new Promise(resolve => setTimeout(resolve, 1)); // Small delay 164 | cache.set('key3', 'value3'); 165 | expect(cache.getStats().size).toBe(3); 166 | 167 | // Wait a bit then access key1 and key2 to make key3 the least recently used 168 | await new Promise(resolve => setTimeout(resolve, 5)); 169 | cache.get('key1'); 170 | cache.get('key2'); 171 | 172 | // Add new item, should evict key3 173 | cache.set('key4', 'value4'); 174 | expect(cache.getStats().size).toBe(3); 175 | expect(cache.has('key3')).toBe(false); 176 | expect(cache.has('key1')).toBe(true); 177 | expect(cache.has('key2')).toBe(true); 178 | expect(cache.has('key4')).toBe(true); 179 | }); 180 | 181 | it('should not evict when updating existing key', () => { 182 | cache.set('key1', 'value1'); 183 | cache.set('key2', 'value2'); 184 | cache.set('key3', 'value3'); 185 | 186 | // Update existing key 187 | cache.set('key1', 'newvalue1'); 188 | expect(cache.getStats().size).toBe(3); 189 | expect(cache.get('key1')).toBe('newvalue1'); 190 | }); 191 | }); 192 | 193 | describe('getStats', () => { 194 | it('should return correct cache statistics', () => { 195 | cache.set('key1', 'value1'); 196 | cache.set('key2', 'value2'); 197 | cache.get('key1'); // Access key1 198 | 199 | const stats = cache.getStats(); 200 | expect(stats.size).toBe(2); 201 | expect(stats.maxSize).toBe(3); 202 | expect(stats.entries).toHaveLength(2); 203 | 204 | const key1Entry = stats.entries.find(e => e.key === 'key1'); 205 | const key2Entry = stats.entries.find(e => e.key === 'key2'); 206 | 207 | expect(key1Entry?.accessCount).toBe(1); 208 | expect(key2Entry?.accessCount).toBe(0); 209 | }); 210 | 211 | it('should calculate hit rate correctly', () => { 212 | cache.set('key1', 'value1'); 213 | cache.set('key2', 'value2'); 214 | 215 | cache.get('key1'); // Hit 216 | cache.get('key1'); // Hit 217 | // key2 never accessed 218 | 219 | const stats = cache.getStats(); 220 | // Total accesses: 2, hits: 1 (key1 was accessed, key2 wasn't) 221 | expect(stats.hitRate).toBe(0.5); 222 | }); 223 | }); 224 | 225 | describe('cleanup', () => { 226 | it('should automatically cleanup expired entries', async () => { 227 | const autoCleanupCache = new InMemoryCache<string>({ 228 | ttlMs: 50, 229 | cleanupIntervalMs: 60, // Very frequent cleanup 230 | }); 231 | 232 | autoCleanupCache.set('key1', 'value1'); 233 | expect(autoCleanupCache.getStats().size).toBe(1); 234 | 235 | // Wait for TTL + cleanup interval 236 | await new Promise(resolve => setTimeout(resolve, 120)); 237 | 238 | const stats = autoCleanupCache.getStats(); 239 | expect(stats.size).toBe(0); 240 | 241 | autoCleanupCache.destroy(); 242 | }); 243 | }); 244 | 245 | describe('destroy', () => { 246 | it('should clear cache and stop cleanup timer', () => { 247 | cache.set('key1', 'value1'); 248 | expect(cache.getStats().size).toBe(1); 249 | 250 | cache.destroy(); 251 | expect(cache.getStats().size).toBe(0); 252 | 253 | // Cache should still be usable but without automatic cleanup 254 | cache.set('key2', 'value2'); 255 | expect(cache.get('key2')).toBe('value2'); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('CacheManager', () => { 261 | let cacheManager: CacheManager; 262 | 263 | beforeEach(() => { 264 | cacheManager = new CacheManager(); 265 | }); 266 | 267 | afterEach(() => { 268 | cacheManager.destroy(); 269 | }); 270 | 271 | describe('file content cache', () => { 272 | it('should store and retrieve file content', () => { 273 | const content = 'file content'; 274 | cacheManager.setFileContent('file1.ts', content); 275 | 276 | expect(cacheManager.getFileContent('file1.ts')).toBe(content); 277 | expect(cacheManager.getFileContent('nonexistent.ts')).toBeUndefined(); 278 | }); 279 | }); 280 | 281 | describe('class details cache', () => { 282 | it('should store and retrieve class details', () => { 283 | const details = { name: 'TestClass', methods: ['method1', 'method2'] }; 284 | cacheManager.setClassDetails('TestClass', details); 285 | 286 | expect(cacheManager.getClassDetails('TestClass')).toEqual(details); 287 | expect(cacheManager.getClassDetails('NonExistentClass')).toBeUndefined(); 288 | }); 289 | }); 290 | 291 | describe('search results cache', () => { 292 | it('should store and retrieve search results', () => { 293 | const results = [{ name: 'result1' }, { name: 'result2' }]; 294 | cacheManager.setSearchResults('query1', results); 295 | 296 | expect(cacheManager.getSearchResults('query1')).toEqual(results); 297 | expect(cacheManager.getSearchResults('query2')).toBeUndefined(); 298 | }); 299 | }); 300 | 301 | describe('method search cache', () => { 302 | it('should store and retrieve method search results', () => { 303 | const results = [{ method: 'getValue', class: 'TestClass' }]; 304 | cacheManager.setMethodSearch('getValue', results); 305 | 306 | expect(cacheManager.getMethodSearch('getValue')).toEqual(results); 307 | expect(cacheManager.getMethodSearch('nonexistent')).toBeUndefined(); 308 | }); 309 | }); 310 | 311 | describe('getAllStats', () => { 312 | it('should return statistics for all caches', () => { 313 | cacheManager.setFileContent('file1.ts', 'content'); 314 | cacheManager.setClassDetails('Class1', { name: 'Class1' }); 315 | cacheManager.setSearchResults('query1', []); 316 | cacheManager.setMethodSearch('method1', []); 317 | 318 | const allStats = cacheManager.getAllStats(); 319 | 320 | expect(allStats.fileContent.size).toBe(1); 321 | expect(allStats.classDetails.size).toBe(1); 322 | expect(allStats.searchResults.size).toBe(1); 323 | expect(allStats.methodSearch.size).toBe(1); 324 | 325 | expect(allStats.fileContent.maxSize).toBe(500); 326 | expect(allStats.classDetails.maxSize).toBe(300); 327 | expect(allStats.searchResults.maxSize).toBe(200); 328 | expect(allStats.methodSearch.maxSize).toBe(100); 329 | }); 330 | }); 331 | 332 | describe('clearAll', () => { 333 | it('should clear all caches', () => { 334 | cacheManager.setFileContent('file1.ts', 'content'); 335 | cacheManager.setClassDetails('Class1', { name: 'Class1' }); 336 | cacheManager.setSearchResults('query1', []); 337 | cacheManager.setMethodSearch('method1', []); 338 | 339 | let allStats = cacheManager.getAllStats(); 340 | expect(allStats.fileContent.size).toBe(1); 341 | expect(allStats.classDetails.size).toBe(1); 342 | expect(allStats.searchResults.size).toBe(1); 343 | expect(allStats.methodSearch.size).toBe(1); 344 | 345 | cacheManager.clearAll(); 346 | 347 | allStats = cacheManager.getAllStats(); 348 | expect(allStats.fileContent.size).toBe(0); 349 | expect(allStats.classDetails.size).toBe(0); 350 | expect(allStats.searchResults.size).toBe(0); 351 | expect(allStats.methodSearch.size).toBe(0); 352 | }); 353 | }); 354 | 355 | describe('destroy', () => { 356 | it('should destroy all underlying caches', () => { 357 | cacheManager.setFileContent('file1.ts', 'content'); 358 | 359 | let allStats = cacheManager.getAllStats(); 360 | expect(allStats.fileContent.size).toBe(1); 361 | 362 | cacheManager.destroy(); 363 | 364 | // After destroy, the caches should be cleared 365 | allStats = cacheManager.getAllStats(); 366 | expect(allStats.fileContent.size).toBe(0); 367 | }); 368 | }); 369 | 370 | describe('different TTL configurations', () => { 371 | it('should have different TTL settings for different cache types', () => { 372 | // We can't directly test TTL values, but we can verify the caches work independently 373 | cacheManager.setFileContent('file1.ts', 'content'); 374 | cacheManager.setClassDetails('Class1', { name: 'Class1' }); 375 | cacheManager.setSearchResults('query1', ['result1']); 376 | cacheManager.setMethodSearch('method1', ['method1']); 377 | 378 | // All should be accessible 379 | expect(cacheManager.getFileContent('file1.ts')).toBe('content'); 380 | expect(cacheManager.getClassDetails('Class1')).toEqual({ name: 'Class1' }); 381 | expect(cacheManager.getSearchResults('query1')).toEqual(['result1']); 382 | expect(cacheManager.getMethodSearch('method1')).toEqual(['method1']); 383 | }); 384 | }); 385 | }); 386 | 387 | describe('Edge cases and error handling', () => { 388 | describe('InMemoryCache edge cases', () => { 389 | it('should handle zero max size gracefully', () => { 390 | const zeroSizeCache = new InMemoryCache<string>({ 391 | maxSize: 0, 392 | ttlMs: 1000, 393 | }); 394 | 395 | zeroSizeCache.set('key1', 'value1'); 396 | // With maxSize 0, nothing should be stored 397 | expect(zeroSizeCache.get('key1')).toBeUndefined(); 398 | expect(zeroSizeCache.getStats().size).toBe(0); 399 | 400 | zeroSizeCache.destroy(); 401 | }); 402 | 403 | it('should handle very short TTL', async () => { 404 | const shortTtlCache = new InMemoryCache<string>({ 405 | ttlMs: 1, // 1ms 406 | cleanupIntervalMs: 1000, 407 | }); 408 | 409 | shortTtlCache.set('key1', 'value1'); 410 | 411 | // Wait longer than TTL 412 | await new Promise(resolve => setTimeout(resolve, 5)); 413 | 414 | expect(shortTtlCache.get('key1')).toBeUndefined(); 415 | 416 | shortTtlCache.destroy(); 417 | }); 418 | 419 | it('should handle multiple destroy calls', () => { 420 | const cache = new InMemoryCache<string>(); 421 | cache.set('key1', 'value1'); 422 | 423 | cache.destroy(); 424 | cache.destroy(); // Second destroy should not throw 425 | 426 | expect(cache.getStats().size).toBe(0); 427 | }); 428 | }); 429 | 430 | describe('Type safety', () => { 431 | it('should maintain type safety for different value types', () => { 432 | const stringCache = new InMemoryCache<string>(); 433 | const numberCache = new InMemoryCache<number>(); 434 | const objectCache = new InMemoryCache<{ id: number; name: string }>(); 435 | 436 | stringCache.set('key1', 'string value'); 437 | numberCache.set('key1', 42); 438 | objectCache.set('key1', { id: 1, name: 'test' }); 439 | 440 | const stringValue: string | undefined = stringCache.get('key1'); 441 | const numberValue: number | undefined = numberCache.get('key1'); 442 | const objectValue: { id: number; name: string } | undefined = objectCache.get('key1'); 443 | 444 | expect(typeof stringValue).toBe('string'); 445 | expect(typeof numberValue).toBe('number'); 446 | expect(typeof objectValue).toBe('object'); 447 | 448 | stringCache.destroy(); 449 | numberCache.destroy(); 450 | objectCache.destroy(); 451 | }); 452 | }); 453 | }); 454 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-sfcc-classes.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Programmatic tests for search_sfcc_classes tool 3 | * 4 | * These tests provide advanced verification capabilities beyond YAML pattern matching, 5 | * including dynamic validation, error categorization and 6 | * comprehensive response structure analysis. 7 | * 8 | * Response format discovered via aegis query: 9 | * - Success: { content: [{ type: "text", text: "["class1", "class2", ...]" }] } 10 | * - Empty: { content: [{ type: "text", text: "[]" }] } 11 | * - Error: { content: [{ type: "text", text: "Error: ..." }], isError: true } 12 | */ 13 | 14 | import { test, describe, before, after, beforeEach } from 'node:test'; 15 | import { strict as assert } from 'node:assert'; 16 | import { connect } from 'mcp-aegis'; 17 | 18 | describe('search_sfcc_classes Programmatic Tests', () => { 19 | let client; 20 | 21 | before(async () => { 22 | client = await connect('./aegis.config.docs-only.json'); 23 | }); 24 | 25 | after(async () => { 26 | if (client?.connected) { 27 | await client.disconnect(); 28 | } 29 | }); 30 | 31 | beforeEach(() => { 32 | // CRITICAL: Clear all buffers to prevent test interference 33 | client.clearAllBuffers(); // Recommended - comprehensive protection 34 | }); 35 | 36 | describe('Protocol Compliance', () => { 37 | test('should be properly connected to MCP server', async () => { 38 | assert.ok(client.connected, 'Client should be connected'); 39 | }); 40 | 41 | test('should have search_sfcc_classes tool available', async () => { 42 | const tools = await client.listTools(); 43 | const searchTool = tools.find(tool => tool.name === 'search_sfcc_classes'); 44 | 45 | assert.ok(searchTool, 'search_sfcc_classes tool should be available'); 46 | assert.equal(searchTool.name, 'search_sfcc_classes'); 47 | assert.ok(searchTool.description, 'Tool should have description'); 48 | assert.ok(searchTool.inputSchema, 'Tool should have input schema'); 49 | assert.equal(searchTool.inputSchema.type, 'object'); 50 | assert.ok(searchTool.inputSchema.properties.query, 'Tool should require query parameter'); 51 | }); 52 | }); 53 | 54 | describe('Response Structure Validation', () => { 55 | test('should return properly structured MCP response for valid query', async () => { 56 | const result = await client.callTool('search_sfcc_classes', { query: 'catalog' }); 57 | 58 | // Validate MCP response structure 59 | assertValidMCPResponse(result); 60 | assert.equal(result.isError, false, 'Should not be an error response'); 61 | assert.equal(result.content.length, 1, 'Should have exactly one content item'); 62 | assert.equal(result.content[0].type, 'text', 'Content should be text type'); 63 | 64 | // Validate JSON array structure 65 | const classArray = parseClassArray(result.content[0].text); 66 | assert.ok(Array.isArray(classArray), 'Response should contain valid JSON array'); 67 | assert.ok(classArray.length > 0, 'Should return at least one class for catalog query'); 68 | 69 | // Validate class name format 70 | classArray.forEach(className => { 71 | assert.equal(typeof className, 'string', 'Each class name should be a string'); 72 | // SFCC includes multiple namespaces 73 | assert.ok( 74 | className.startsWith('dw.') || 75 | className.startsWith('TopLevel.') || 76 | className.startsWith('best-practices.') || 77 | className.startsWith('sfra.'), 78 | `Class name "${className}" should start with recognized namespace` 79 | ); 80 | assert.ok(className.includes('catalog'), 'Results should be relevant to query'); 81 | }); 82 | }); 83 | 84 | test('should return empty array for no matches', async () => { 85 | const result = await client.callTool('search_sfcc_classes', { query: 'zzznothingfound' }); 86 | 87 | assertValidMCPResponse(result); 88 | assert.equal(result.isError, false, 'Should not be an error response'); 89 | 90 | const classArray = parseClassArray(result.content[0].text); 91 | assert.ok(Array.isArray(classArray), 'Response should be valid JSON array'); 92 | assert.equal(classArray.length, 0, 'Should return empty array for no matches'); 93 | }); 94 | 95 | test('should return error response for invalid parameters', async () => { 96 | const result = await client.callTool('search_sfcc_classes', { query: '' }); 97 | 98 | assertValidMCPResponse(result); 99 | assert.equal(result.isError, true, 'Should be an error response'); 100 | assert.ok(result.content[0].text.includes('Error:'), 'Should contain error message'); 101 | assert.ok(result.content[0].text.includes('non-empty string'), 'Should specify validation requirement'); 102 | }); 103 | }); 104 | 105 | describe('Dynamic Search Testing', () => { 106 | const testQueries = [ 107 | { query: 'catalog', expectedMin: 10, category: 'namespace' }, 108 | { query: 'product', expectedMin: 5, category: 'common' }, 109 | { query: 'customer', expectedMin: 3, category: 'namespace' }, 110 | { query: 'order', expectedMin: 5, category: 'namespace' }, 111 | { query: 'system', expectedMin: 3, category: 'namespace' }, 112 | { query: 'Campaign', expectedMin: 1, category: 'specific' }, 113 | { query: 'Manager', expectedMin: 0, category: 'pattern' } // "Manager" returns no results - SFCC uses "Mgr" 114 | ]; 115 | 116 | testQueries.forEach(({ query, expectedMin, category }) => { 117 | test(`should find relevant classes for ${category} query: "${query}"`, async () => { 118 | const result = await client.callTool('search_sfcc_classes', { query }); 119 | 120 | assertValidMCPResponse(result); 121 | assert.equal(result.isError, false, 'Should not be an error'); 122 | 123 | const classArray = parseClassArray(result.content[0].text); 124 | assert.ok(classArray.length >= expectedMin, 125 | `Should find at least ${expectedMin} classes for "${query}", found ${classArray.length}`); 126 | 127 | // Validate relevance - all results should contain the query term (case insensitive) 128 | classArray.forEach(className => { 129 | const lowerClassName = className.toLowerCase(); 130 | const lowerQuery = query.toLowerCase(); 131 | assert.ok(lowerClassName.includes(lowerQuery), 132 | `Class "${className}" should contain query term "${query}"`); 133 | }); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('Edge Case Validation', () => { 139 | const edgeCases = [ 140 | { query: 'A', description: 'single character' }, 141 | { query: 'dw', description: 'namespace prefix' }, 142 | { query: 'CATALOG', description: 'uppercase query' }, 143 | { query: 'catalog.Product', description: 'full class path segment' }, 144 | { query: '123', description: 'numeric query' }, 145 | { query: 'xyz_nonexistent_abc', description: 'clearly non-existent term' } 146 | ]; 147 | 148 | edgeCases.forEach(({ query, description }) => { 149 | test(`should handle ${description} query: "${query}"`, async () => { 150 | const result = await client.callTool('search_sfcc_classes', { query }); 151 | 152 | assertValidMCPResponse(result); 153 | assert.equal(result.isError, false, 'Should not be an error for valid string'); 154 | 155 | const classArray = parseClassArray(result.content[0].text); 156 | assert.ok(Array.isArray(classArray), 'Should return valid array'); 157 | 158 | // All results should be valid class names 159 | classArray.forEach(className => { 160 | assert.equal(typeof className, 'string', 'Should be string'); 161 | // SFCC includes dw.*, TopLevel.*, best-practices.*, and sfra.* classes 162 | assert.ok( 163 | className.startsWith('dw.') || 164 | className.startsWith('TopLevel.') || 165 | className.startsWith('best-practices.') || 166 | className.startsWith('sfra.'), 167 | `Class name "${className}" should start with dw., TopLevel., best-practices., or sfra.` 168 | ); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('Error Handling and Validation', () => { 175 | const errorCases = [ 176 | { params: {}, expectedError: 'non-empty string', description: 'missing query parameter' }, 177 | { params: { query: '' }, expectedError: 'non-empty string', description: 'empty string query' }, 178 | { params: { query: null }, expectedError: 'string', description: 'null query' }, 179 | { params: { query: 123 }, expectedError: 'string', description: 'numeric query parameter' }, 180 | { params: { query: [] }, expectedError: 'string', description: 'array query parameter' }, 181 | { params: { query: {} }, expectedError: 'string', description: 'object query parameter' } 182 | ]; 183 | 184 | errorCases.forEach(({ params, expectedError, description }) => { 185 | test(`should validate ${description}`, async () => { 186 | const result = await client.callTool('search_sfcc_classes', params); 187 | 188 | assertValidMCPResponse(result); 189 | assert.equal(result.isError, true, 'Should be an error response'); 190 | 191 | const errorMessage = result.content[0].text.toLowerCase(); 192 | assert.ok(errorMessage.includes('error'), 'Should contain error indicator'); 193 | assert.ok(errorMessage.includes(expectedError.toLowerCase()), 194 | `Error message should mention "${expectedError}"`); 195 | 196 | // Error categorization 197 | const errorType = categorizeError(result.content[0].text); 198 | assert.equal(errorType, 'validation', 'Should be categorized as validation error'); 199 | }); 200 | }); 201 | }); 202 | 203 | describe('Cross-Validation with YAML Tests', () => { 204 | test('should match YAML test expectations for common queries', async () => { 205 | // Test cases that mirror the YAML test file 206 | const yamlTestCases = [ 207 | { query: 'catalog', shouldHave: ['dw.catalog.Catalog', 'dw.catalog.Product'] }, 208 | { query: 'customer', shouldHave: ['dw.customer.Customer'] }, 209 | { query: 'order', shouldHave: ['dw.order.Order'] }, 210 | { query: 'system', shouldHave: ['dw.system.System'] } 211 | ]; 212 | 213 | for (const testCase of yamlTestCases) { 214 | const result = await client.callTool('search_sfcc_classes', { query: testCase.query }); 215 | 216 | assertValidMCPResponse(result); 217 | assert.equal(result.isError, false); 218 | 219 | const classArray = parseClassArray(result.content[0].text); 220 | 221 | // Verify expected classes are present 222 | testCase.shouldHave.forEach(expectedClass => { 223 | assert.ok(classArray.includes(expectedClass), 224 | `Results for "${testCase.query}" should include "${expectedClass}"`); 225 | }); 226 | } 227 | }); 228 | 229 | test('should handle edge cases consistently with YAML tests', async () => { 230 | // Mirror YAML edge case tests 231 | const result = await client.callTool('search_sfcc_classes', { query: 'zzznothingfound' }); 232 | 233 | assertValidMCPResponse(result); 234 | assert.equal(result.isError, false, 'Should not error for non-matching query'); 235 | 236 | const classArray = parseClassArray(result.content[0].text); 237 | assert.equal(classArray.length, 0, 'Should return empty array for no matches'); 238 | }); 239 | }); 240 | 241 | describe('Response Data Quality', () => { 242 | test('should return unique class names without duplicates', async () => { 243 | const result = await client.callTool('search_sfcc_classes', { query: 'catalog' }); 244 | 245 | assertValidMCPResponse(result); 246 | const classArray = parseClassArray(result.content[0].text); 247 | 248 | const uniqueClasses = new Set(classArray); 249 | assert.equal(classArray.length, uniqueClasses.size, 250 | 'Should not contain duplicate class names'); 251 | }); 252 | 253 | test('should return results in consistent order', async () => { 254 | // Call the same query multiple times sequentially to avoid message interference 255 | const results = []; 256 | 257 | for (let i = 0; i < 3; i++) { 258 | const result = await client.callTool('search_sfcc_classes', { query: 'catalog' }); 259 | results.push(result); 260 | 261 | } 262 | 263 | const arrays = results.map(result => parseClassArray(result.content[0].text)); 264 | 265 | // All arrays should be identical 266 | assert.deepEqual(arrays[0], arrays[1], 'Results should be consistent across calls'); 267 | assert.deepEqual(arrays[1], arrays[2], 'Results should be consistent across calls'); 268 | }); 269 | 270 | test('should validate class name patterns and format', async () => { 271 | const result = await client.callTool('search_sfcc_classes', { query: 'catalog' }); 272 | 273 | assertValidMCPResponse(result); 274 | const classArray = parseClassArray(result.content[0].text); 275 | 276 | classArray.forEach(className => { 277 | // Validate class name format - updated for all SFCC namespaces 278 | assert.match(className, /^(dw\.|TopLevel\.|best-practices\.|sfra\.)[a-zA-Z0-9_./-]+$/, 279 | `Class name "${className}" should follow valid pattern`); 280 | 281 | // Should not contain spaces or invalid characters 282 | assert.ok(!className.includes(' '), `Class name "${className}" should not contain spaces`); 283 | 284 | // Should have reasonable length 285 | assert.ok(className.length > 3, `Class name "${className}" should be reasonable length`); 286 | assert.ok(className.length < 100, `Class name "${className}" should not be excessively long`); 287 | }); 288 | }); 289 | }); 290 | }); 291 | 292 | // Helper functions 293 | 294 | /** 295 | * Validates that a response follows proper MCP structure 296 | */ 297 | function assertValidMCPResponse(result) { 298 | assert.ok(result.content, 'Response should have content property'); 299 | assert.ok(Array.isArray(result.content), 'Content should be an array'); 300 | assert.ok(result.content.length > 0, 'Content array should not be empty'); 301 | assert.equal(result.content[0].type, 'text', 'First content item should be text type'); 302 | assert.equal(typeof result.content[0].text, 'string', 'Text content should be a string'); 303 | 304 | // isError property should always be present and boolean 305 | assert.ok(Object.prototype.hasOwnProperty.call(result, 'isError'), 'isError property should always be present'); 306 | assert.equal(typeof result.isError, 'boolean', 'isError should be a boolean'); 307 | } 308 | 309 | /** 310 | * Parses the class array from the response text 311 | */ 312 | function parseClassArray(text) { 313 | try { 314 | return JSON.parse(text); 315 | } catch { 316 | throw new Error(`Failed to parse class array from response: ${text}`); 317 | } 318 | } 319 | 320 | /** 321 | * Categorizes error messages by type 322 | */ 323 | function categorizeError(errorText) { 324 | const errorPatterns = [ 325 | { type: 'validation', keywords: ['required', 'invalid', 'missing', 'non-empty', 'string'] }, 326 | { type: 'not_found', keywords: ['not found', 'does not exist'] }, 327 | { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] }, 328 | { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] } 329 | ]; 330 | 331 | const lowerText = errorText.toLowerCase(); 332 | for (const pattern of errorPatterns) { 333 | if (pattern.keywords.some(keyword => lowerText.includes(keyword))) { 334 | return pattern.type; 335 | } 336 | } 337 | return 'unknown'; 338 | } 339 | ``` -------------------------------------------------------------------------------- /tests/sfcc-mock-server.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; 2 | import { SFCCMockServerManager, withSFCCMockServer } from './servers/sfcc-mock-server-manager'; 3 | 4 | /** 5 | * Integration tests for the Unified SFCC Mock Server 6 | * 7 | * These tests demonstrate how to use the unified SFCC mock server 8 | * for testing both WebDAV log functionality and OCAPI simulation. 9 | * The server combines both protocols into a single endpoint. 10 | */ 11 | 12 | describe('Unified SFCC Mock Server Integration', () => { 13 | let serverManager: SFCCMockServerManager; 14 | 15 | beforeAll(async () => { 16 | serverManager = new SFCCMockServerManager({ 17 | port: 3002, // Use different port for tests 18 | dev: false, 19 | autoSetup: true, 20 | }); 21 | 22 | // Check if server is available before running tests 23 | const isAvailable = await serverManager.isServerAvailable(); 24 | if (!isAvailable) { 25 | console.warn('⚠️ SFCC mock server not available, skipping tests'); 26 | return; 27 | } 28 | 29 | await serverManager.start(); 30 | }, 20000); // 20 second timeout for server startup (includes log setup) 31 | 32 | afterAll(async () => { 33 | if (serverManager?.isRunning()) { 34 | await serverManager.stop(); 35 | } 36 | }, 10000); // 10 second timeout for server shutdown 37 | 38 | describe('Server Startup and Basic Functionality', () => { 39 | test('should start server and be accessible', async () => { 40 | if (!await serverManager.isServerAvailable()) { 41 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 42 | return; 43 | } 44 | 45 | expect(serverManager.isRunning()).toBe(true); 46 | expect(serverManager.getServerUrl()).toBe('http://localhost:3002'); 47 | expect(serverManager.getWebDAVLogsUrl()).toBe('http://localhost:3002/on/demandware.servlet/webdav/Sites/Logs/'); 48 | expect(serverManager.getDirectLogsUrl()).toBe('http://localhost:3002/Logs/'); 49 | expect(serverManager.getOCAPIUrl()).toBe('http://localhost:3002/s/-/dw/data/v23_2'); 50 | expect(serverManager.getOAuthUrl()).toBe('http://localhost:3002/dw/oauth2/access_token'); 51 | }); 52 | 53 | test('should respond to health check', async () => { 54 | if (!await serverManager.isServerAvailable()) { 55 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 56 | return; 57 | } 58 | 59 | const response = await fetch(`${serverManager.getServerUrl()}/health`); 60 | expect(response.status).toBe(200); 61 | 62 | const healthData = await response.json(); 63 | expect(healthData).toHaveProperty('status', 'ok'); 64 | expect(healthData).toHaveProperty('message'); 65 | }); 66 | }); 67 | 68 | describe('WebDAV Functionality', () => { 69 | test('should serve WebDAV directory listing', async () => { 70 | if (!await serverManager.isServerAvailable()) { 71 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 72 | return; 73 | } 74 | 75 | // Test SFCC WebDAV path 76 | const response = await fetch(serverManager.getWebDAVLogsUrl(), { 77 | method: 'PROPFIND', 78 | headers: { 79 | 'Depth': '1', 80 | 'Content-Type': 'application/xml', 81 | }, 82 | }); 83 | 84 | expect(response.status).toBe(207); // Multi-Status (WebDAV response) 85 | 86 | const responseText = await response.text(); 87 | expect(responseText).toContain('error-blade-'); 88 | expect(responseText).toContain('warn-blade-'); 89 | expect(responseText).toContain('info-blade-'); 90 | expect(responseText).toContain('debug-blade-'); 91 | expect(responseText).toContain('jobs'); 92 | 93 | // Also test direct path for backward compatibility 94 | const directResponse = await fetch(serverManager.getDirectLogsUrl(), { 95 | method: 'PROPFIND', 96 | headers: { 97 | 'Depth': '1', 98 | 'Content-Type': 'application/xml', 99 | }, 100 | }); 101 | 102 | expect(directResponse.status).toBe(207); 103 | }); 104 | 105 | test('should serve log file content', async () => { 106 | if (!await serverManager.isServerAvailable()) { 107 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 108 | return; 109 | } 110 | 111 | // First get the directory listing to find an actual log file 112 | const listResponse = await fetch(serverManager.getWebDAVLogsUrl(), { 113 | method: 'PROPFIND', 114 | headers: { 115 | 'Depth': '1', 116 | 'Content-Type': 'application/xml', 117 | }, 118 | }); 119 | 120 | const listingXml = await listResponse.text(); 121 | 122 | // Extract error log filename from the XML response 123 | const errorLogMatch = listingXml.match(/error-blade-[^<]+\.log/); 124 | if (!errorLogMatch) { 125 | throw new Error('No error log file found in directory listing'); 126 | } 127 | 128 | const errorLogFile = errorLogMatch[0]; 129 | const logFileUrl = `${serverManager.getWebDAVLogsUrl()}${errorLogFile}`; 130 | 131 | // Get the log file content 132 | const logResponse = await fetch(logFileUrl); 133 | expect(logResponse.status).toBe(200); 134 | 135 | const logContent = await logResponse.text(); 136 | expect(logContent).toContain('ERROR'); 137 | expect(logContent).toContain('SystemJobThread'); 138 | expect(logContent).toContain('PipelineCallServlet'); 139 | }); 140 | 141 | test('should serve job logs directory', async () => { 142 | if (!await serverManager.isServerAvailable()) { 143 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 144 | return; 145 | } 146 | 147 | const jobsUrl = `${serverManager.getWebDAVLogsUrl()}jobs/`; 148 | const response = await fetch(jobsUrl, { 149 | method: 'PROPFIND', 150 | headers: { 151 | 'Depth': '1', 152 | 'Content-Type': 'application/xml', 153 | }, 154 | }); 155 | 156 | expect(response.status).toBe(207); // Multi-Status (WebDAV response) 157 | 158 | const responseText = await response.text(); 159 | expect(responseText).toContain('ProcessOrders'); 160 | expect(responseText).toContain('ImportCatalog'); 161 | }); 162 | 163 | test('should serve job log file content', async () => { 164 | if (!await serverManager.isServerAvailable()) { 165 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 166 | return; 167 | } 168 | 169 | const jobLogUrl = `${serverManager.getWebDAVLogsUrl()}jobs/ProcessOrders/Job-ProcessOrders-1234567890.log`; 170 | const response = await fetch(jobLogUrl); 171 | 172 | expect(response.status).toBe(200); 173 | 174 | const logContent = await response.text(); 175 | expect(logContent).toContain('Executing job [ProcessOrders][1234567890]...'); 176 | expect(logContent).toContain('INFO'); 177 | expect(logContent).toContain('SystemJobThread'); 178 | expect(logContent).toContain('ValidateOrdersStep'); 179 | }); 180 | 181 | test('should support range requests for log files', async () => { 182 | if (!await serverManager.isServerAvailable()) { 183 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 184 | return; 185 | } 186 | 187 | // Get a log file with range request (last 100 bytes) 188 | const listResponse = await fetch(serverManager.getWebDAVLogsUrl(), { 189 | method: 'PROPFIND', 190 | headers: { 'Depth': '1' }, 191 | }); 192 | 193 | const listingXml = await listResponse.text(); 194 | const errorLogMatch = listingXml.match(/error-blade-[^<]+\.log/); 195 | 196 | if (!errorLogMatch) { 197 | console.warn('⚠️ No error log file found, skipping range request test'); 198 | return; 199 | } 200 | 201 | const errorLogFile = errorLogMatch[0]; 202 | const logFileUrl = `${serverManager.getWebDAVLogsUrl()}${errorLogFile}`; 203 | 204 | const rangeResponse = await fetch(logFileUrl, { 205 | headers: { 206 | 'Range': 'bytes=-100', // Last 100 bytes 207 | }, 208 | }); 209 | 210 | expect([200, 206, 416]) 211 | .toContain(rangeResponse.status); // 200 OK, 206 Partial Content, or 416 Range Not Satisfiable 212 | }); 213 | }); 214 | 215 | describe('OCAPI Functionality', () => { 216 | test('should handle OAuth token request', async () => { 217 | if (!await serverManager.isServerAvailable()) { 218 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 219 | return; 220 | } 221 | 222 | const response = await fetch(serverManager.getOAuthUrl(), { 223 | method: 'POST', 224 | headers: { 225 | 'Content-Type': 'application/x-www-form-urlencoded', 226 | 'Authorization': 'Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0', // test-client-id:test-client-secret 227 | }, 228 | body: 'grant_type=client_credentials', 229 | }); 230 | 231 | expect(response.status).toBe(200); 232 | 233 | const tokenData = await response.json(); 234 | expect(tokenData).toHaveProperty('access_token'); 235 | expect(tokenData).toHaveProperty('token_type', 'Bearer'); 236 | expect(tokenData).toHaveProperty('expires_in'); 237 | }); 238 | 239 | test('should serve system object types', async () => { 240 | if (!await serverManager.isServerAvailable()) { 241 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 242 | return; 243 | } 244 | 245 | // First get OAuth token 246 | const tokenResponse = await fetch(serverManager.getOAuthUrl(), { 247 | method: 'POST', 248 | headers: { 249 | 'Content-Type': 'application/x-www-form-urlencoded', 250 | 'Authorization': 'Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0', 251 | }, 252 | body: 'grant_type=client_credentials', 253 | }); 254 | 255 | const tokenData = await tokenResponse.json(); 256 | const accessToken = tokenData.access_token; 257 | 258 | // Test system object types endpoint 259 | const response = await fetch(`${serverManager.getOCAPIUrl()}/system_object_definitions`, { 260 | headers: { 261 | 'Authorization': `Bearer ${accessToken}`, 262 | 'Content-Type': 'application/json', 263 | }, 264 | }); 265 | 266 | expect(response.status).toBe(200); 267 | 268 | const data = await response.json(); 269 | expect(data).toHaveProperty('count'); 270 | expect(data).toHaveProperty('data'); 271 | expect(Array.isArray(data.data)).toBe(true); 272 | 273 | // Check for some common system objects 274 | const objectIds = data.data.map((obj: any) => obj.object_type); 275 | expect(objectIds).toContain('Basket'); 276 | expect(objectIds).toContain('CustomObject'); 277 | }); 278 | 279 | test('should serve site preferences', async () => { 280 | if (!await serverManager.isServerAvailable()) { 281 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 282 | return; 283 | } 284 | 285 | // First get OAuth token 286 | const tokenResponse = await fetch(serverManager.getOAuthUrl(), { 287 | method: 'POST', 288 | headers: { 289 | 'Content-Type': 'application/x-www-form-urlencoded', 290 | 'Authorization': 'Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0', 291 | }, 292 | body: 'grant_type=client_credentials', 293 | }); 294 | 295 | const tokenData = await tokenResponse.json(); 296 | const accessToken = tokenData.access_token; 297 | 298 | // Test site preferences search endpoint 299 | const searchBody = { 300 | query: { 301 | match_all_query: {}, 302 | }, 303 | select: '(**)', 304 | count: 200, 305 | }; 306 | 307 | const response = await fetch(`${serverManager.getOCAPIUrl()}/site_preferences/preference_groups/CCV/sandbox/preference_search`, { 308 | method: 'POST', 309 | headers: { 310 | 'Authorization': `Bearer ${accessToken}`, 311 | 'Content-Type': 'application/json', 312 | }, 313 | body: JSON.stringify(searchBody), 314 | }); 315 | 316 | expect(response.status).toBe(200); 317 | 318 | const data = await response.json(); 319 | expect(data).toHaveProperty('count'); 320 | expect(data).toHaveProperty('hits'); 321 | expect(Array.isArray(data.hits)).toBe(true); 322 | }); 323 | 324 | test('should handle invalid OAuth requests', async () => { 325 | if (!await serverManager.isServerAvailable()) { 326 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 327 | return; 328 | } 329 | 330 | const response = await fetch(serverManager.getOAuthUrl(), { 331 | method: 'POST', 332 | headers: { 333 | 'Content-Type': 'application/x-www-form-urlencoded', 334 | 'Authorization': 'Basic invalid_credentials', 335 | }, 336 | body: 'grant_type=client_credentials', 337 | }); 338 | 339 | expect(response.status).toBe(401); 340 | }); 341 | 342 | test('should handle unauthorized OCAPI requests', async () => { 343 | if (!await serverManager.isServerAvailable()) { 344 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 345 | return; 346 | } 347 | 348 | const response = await fetch(`${serverManager.getOCAPIUrl()}/system_object_definitions`, { 349 | headers: { 350 | 'Content-Type': 'application/json', 351 | }, 352 | }); 353 | 354 | expect(response.status).toBe(401); 355 | }); 356 | }); 357 | 358 | describe('CORS and Cross-Origin Support', () => { 359 | test('should include CORS headers', async () => { 360 | if (!await serverManager.isServerAvailable()) { 361 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 362 | return; 363 | } 364 | 365 | const response = await fetch(serverManager.getServerUrl(), { 366 | method: 'OPTIONS', 367 | }); 368 | 369 | expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); 370 | expect(response.headers.get('Access-Control-Allow-Methods')).toContain('GET'); 371 | expect(response.headers.get('Access-Control-Allow-Methods')).toContain('POST'); 372 | expect(response.headers.get('Access-Control-Allow-Methods')).toContain('PROPFIND'); 373 | }); 374 | }); 375 | }); 376 | 377 | /** 378 | * Example of how to use withSFCCMockServer utility 379 | */ 380 | describe('SFCC Mock Server Utility Function', () => { 381 | test('should work with utility function', async () => { 382 | const manager = new SFCCMockServerManager(); 383 | const isAvailable = await manager.isServerAvailable(); 384 | if (!isAvailable) { 385 | console.warn('⚠️ Skipping test - SFCC mock server not available'); 386 | return; 387 | } 388 | 389 | const result = await withSFCCMockServer( 390 | async (serverUrl, webdavLogsUrl, directLogsUrl, ocapiUrl, oauthUrl) => { 391 | expect(serverUrl).toContain('http://localhost:'); 392 | expect(webdavLogsUrl).toContain('/on/demandware.servlet/webdav/Sites/Logs/'); 393 | expect(directLogsUrl).toContain('/Logs/'); 394 | expect(ocapiUrl).toContain('/s/-/dw/data'); 395 | expect(oauthUrl).toContain('/dw/oauth2/access_token'); 396 | 397 | // Test both WebDAV and OCAPI functionality 398 | const webdavResponse = await fetch(webdavLogsUrl, { 399 | method: 'PROPFIND', 400 | headers: { 'Depth': '1' }, 401 | }); 402 | 403 | const oauthResponse = await fetch(oauthUrl, { 404 | method: 'POST', 405 | headers: { 406 | 'Content-Type': 'application/x-www-form-urlencoded', 407 | 'Authorization': 'Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0', 408 | }, 409 | body: 'grant_type=client_credentials', 410 | }); 411 | 412 | return { 413 | webdavStatus: webdavResponse.status, 414 | oauthStatus: oauthResponse.status, 415 | }; 416 | }, 417 | { port: 3005 }, 418 | ); 419 | 420 | expect(result.webdavStatus).toBe(207); // WebDAV Multi-Status 421 | expect(result.oauthStatus).toBe(200); // OAuth success 422 | }, 25000); // 25 second timeout for utility test 423 | }); 424 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/get-sfra-documents-by-category.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - get_sfra_documents_by_category Tool YAML Tests (full-mode mode) 3 | # Tests SFRA document category filtering functionality with comprehensive validation 4 | # 5 | # Tool: get_sfra_documents_by_category 6 | # Purpose: Get SFRA documents filtered by category (core, product, order, customer, pricing, store, other) 7 | # Parameters: category (required) - Category to filter by 8 | # 9 | # Quick Test Commands: 10 | # aegis "tests/mcp/yaml/get-sfra-documents-by-category.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose 11 | # aegis "tests/mcp/yaml/get-sfra-documents-by-category.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --debug --timing 12 | # aegis query get_sfra_documents_by_category '{"category": "core"}' --config "aegis.config.with-dw.json" 13 | # ================================================================================== 14 | description: "SFCC MCP Server - get_sfra_documents_by_category tool comprehensive tests" 15 | 16 | # ================================================================================== 17 | # SUCCESSFUL OPERATIONS - VALID CATEGORIES 18 | # ================================================================================== 19 | tests: 20 | - it: "should retrieve core SFRA documents with proper structure" 21 | request: 22 | jsonrpc: "2.0" 23 | id: "core-docs-1" 24 | method: "tools/call" 25 | params: 26 | name: "get_sfra_documents_by_category" 27 | arguments: 28 | category: "core" 29 | expect: 30 | response: 31 | jsonrpc: "2.0" 32 | id: "core-docs-1" 33 | result: 34 | content: 35 | - type: "text" 36 | text: "match:regex:\\[\\s*\\{[\\s\\S]*\\}\\s*\\]" # Valid JSON array structure 37 | isError: false 38 | performance: 39 | maxResponseTime: "500ms" 40 | stderr: "toBeEmpty" 41 | 42 | - it: "should return valid JSON array for core category" 43 | request: 44 | jsonrpc: "2.0" 45 | id: "core-json-1" 46 | method: "tools/call" 47 | params: 48 | name: "get_sfra_documents_by_category" 49 | arguments: 50 | category: "core" 51 | expect: 52 | response: 53 | jsonrpc: "2.0" 54 | id: "core-json-1" 55 | result: 56 | content: 57 | - type: "text" 58 | text: "match:contains:server" # Core category should contain server document 59 | isError: false 60 | stderr: "toBeEmpty" 61 | 62 | - it: "should include required document fields for core category" 63 | request: 64 | jsonrpc: "2.0" 65 | id: "core-fields-1" 66 | method: "tools/call" 67 | params: 68 | name: "get_sfra_documents_by_category" 69 | arguments: 70 | category: "core" 71 | expect: 72 | response: 73 | jsonrpc: "2.0" 74 | id: "core-fields-1" 75 | result: 76 | content: 77 | - type: "text" 78 | text: "match:regex:[\\s\\S]*name[\\s\\S]*title[\\s\\S]*description[\\s\\S]*type[\\s\\S]*category[\\s\\S]*filename" 79 | isError: false 80 | stderr: "toBeEmpty" 81 | 82 | - it: "should contain expected core documents" 83 | request: 84 | jsonrpc: "2.0" 85 | id: "core-content-1" 86 | method: "tools/call" 87 | params: 88 | name: "get_sfra_documents_by_category" 89 | arguments: 90 | category: "core" 91 | expect: 92 | response: 93 | jsonrpc: "2.0" 94 | id: "core-content-1" 95 | result: 96 | content: 97 | - type: "text" 98 | text: "match:regex:[\\s\\S]*querystring[\\s\\S]*render[\\s\\S]*request[\\s\\S]*response[\\s\\S]*server" # Core SFRA classes in alphabetical order 99 | isError: false 100 | stderr: "toBeEmpty" 101 | 102 | - it: "should retrieve product SFRA documents with proper structure" 103 | request: 104 | jsonrpc: "2.0" 105 | id: "product-docs-1" 106 | method: "tools/call" 107 | params: 108 | name: "get_sfra_documents_by_category" 109 | arguments: 110 | category: "product" 111 | expect: 112 | response: 113 | jsonrpc: "2.0" 114 | id: "product-docs-1" 115 | result: 116 | content: 117 | - type: "text" 118 | text: "match:regex:\\[\\s*\\{[\\s\\S]*\\}\\s*\\]" # Valid JSON array structure 119 | isError: false 120 | performance: 121 | maxResponseTime: "500ms" 122 | stderr: "toBeEmpty" 123 | 124 | - it: "should contain product model documents" 125 | request: 126 | jsonrpc: "2.0" 127 | id: "product-content-1" 128 | method: "tools/call" 129 | params: 130 | name: "get_sfra_documents_by_category" 131 | arguments: 132 | category: "product" 133 | expect: 134 | response: 135 | jsonrpc: "2.0" 136 | id: "product-content-1" 137 | result: 138 | content: 139 | - type: "text" 140 | text: "match:regex:[\\s\\S]*product-full[\\s\\S]*product-tile" # Product models 141 | isError: false 142 | stderr: "toBeEmpty" 143 | 144 | - it: "should retrieve order category documents" 145 | request: 146 | jsonrpc: "2.0" 147 | id: "order-docs-1" 148 | method: "tools/call" 149 | params: 150 | name: "get_sfra_documents_by_category" 151 | arguments: 152 | category: "order" 153 | expect: 154 | response: 155 | jsonrpc: "2.0" 156 | id: "order-docs-1" 157 | result: 158 | content: 159 | - type: "text" 160 | text: "match:regex:\\[[\\s\\S]*\\]" # Valid JSON array (may be empty) 161 | isError: false 162 | stderr: "toBeEmpty" 163 | 164 | - it: "should retrieve customer category documents" 165 | request: 166 | jsonrpc: "2.0" 167 | id: "customer-docs-1" 168 | method: "tools/call" 169 | params: 170 | name: "get_sfra_documents_by_category" 171 | arguments: 172 | category: "customer" 173 | expect: 174 | response: 175 | jsonrpc: "2.0" 176 | id: "customer-docs-1" 177 | result: 178 | content: 179 | - type: "text" 180 | text: "match:regex:\\[[\\s\\S]*\\]" # Valid JSON array 181 | isError: false 182 | stderr: "toBeEmpty" 183 | 184 | - it: "should retrieve pricing category documents" 185 | request: 186 | jsonrpc: "2.0" 187 | id: "pricing-docs-1" 188 | method: "tools/call" 189 | params: 190 | name: "get_sfra_documents_by_category" 191 | arguments: 192 | category: "pricing" 193 | expect: 194 | response: 195 | jsonrpc: "2.0" 196 | id: "pricing-docs-1" 197 | result: 198 | content: 199 | - type: "text" 200 | text: "match:regex:\\[[\\s\\S]*\\]" # Valid JSON array 201 | isError: false 202 | stderr: "toBeEmpty" 203 | 204 | - it: "should retrieve store category documents" 205 | request: 206 | jsonrpc: "2.0" 207 | id: "store-docs-1" 208 | method: "tools/call" 209 | params: 210 | name: "get_sfra_documents_by_category" 211 | arguments: 212 | category: "store" 213 | expect: 214 | response: 215 | jsonrpc: "2.0" 216 | id: "store-docs-1" 217 | result: 218 | content: 219 | - type: "text" 220 | text: "match:regex:\\[[\\s\\S]*\\]" # Valid JSON array 221 | isError: false 222 | stderr: "toBeEmpty" 223 | 224 | - it: "should retrieve other category documents" 225 | request: 226 | jsonrpc: "2.0" 227 | id: "other-docs-1" 228 | method: "tools/call" 229 | params: 230 | name: "get_sfra_documents_by_category" 231 | arguments: 232 | category: "other" 233 | expect: 234 | response: 235 | jsonrpc: "2.0" 236 | id: "other-docs-1" 237 | result: 238 | content: 239 | - type: "text" 240 | text: "match:regex:\\[[\\s\\S]*\\]" # Valid JSON array 241 | isError: false 242 | stderr: "toBeEmpty" 243 | 244 | # ================================================================================== 245 | # EDGE CASES - INVALID/EMPTY CATEGORIES 246 | # ================================================================================== 247 | 248 | - it: "should handle invalid category gracefully" 249 | request: 250 | jsonrpc: "2.0" 251 | id: "invalid-category-1" 252 | method: "tools/call" 253 | params: 254 | name: "get_sfra_documents_by_category" 255 | arguments: 256 | category: "invalid_category_xyz" 257 | expect: 258 | response: 259 | jsonrpc: "2.0" 260 | id: "invalid-category-1" 261 | result: 262 | content: 263 | - type: "text" 264 | text: "match:regex:^\\[\\s*\\]$" # Empty array for invalid category 265 | isError: false 266 | stderr: "toBeEmpty" 267 | 268 | - it: "should handle empty category gracefully" 269 | request: 270 | jsonrpc: "2.0" 271 | id: "empty-category-1" 272 | method: "tools/call" 273 | params: 274 | name: "get_sfra_documents_by_category" 275 | arguments: 276 | category: "" 277 | expect: 278 | response: 279 | jsonrpc: "2.0" 280 | id: "empty-category-1" 281 | result: 282 | content: 283 | - type: "text" 284 | text: "match:contains:Error" 285 | isError: true 286 | stderr: "toBeEmpty" 287 | 288 | # ================================================================================== 289 | # ERROR HANDLING - MISSING PARAMETERS 290 | # ================================================================================== 291 | 292 | - it: "should require category parameter" 293 | request: 294 | jsonrpc: "2.0" 295 | id: "missing-category-1" 296 | method: "tools/call" 297 | params: 298 | name: "get_sfra_documents_by_category" 299 | arguments: {} 300 | expect: 301 | response: 302 | jsonrpc: "2.0" 303 | id: "missing-category-1" 304 | result: 305 | content: 306 | - type: "text" 307 | text: "match:contains:category must be a non-empty string" 308 | isError: true 309 | stderr: "toBeEmpty" 310 | 311 | - it: "should handle null category parameter" 312 | request: 313 | jsonrpc: "2.0" 314 | id: "null-category-1" 315 | method: "tools/call" 316 | params: 317 | name: "get_sfra_documents_by_category" 318 | arguments: 319 | category: null 320 | expect: 321 | response: 322 | jsonrpc: "2.0" 323 | id: "null-category-1" 324 | result: 325 | content: 326 | - type: "text" 327 | text: "match:contains:Error" 328 | isError: true 329 | stderr: "toBeEmpty" 330 | 331 | # ================================================================================== 332 | # DATA VALIDATION - CONTENT STRUCTURE 333 | # ================================================================================== 334 | 335 | - it: "should return documents with valid category field in core" 336 | request: 337 | jsonrpc: "2.0" 338 | id: "category-field-1" 339 | method: "tools/call" 340 | params: 341 | name: "get_sfra_documents_by_category" 342 | arguments: 343 | category: "core" 344 | expect: 345 | response: 346 | jsonrpc: "2.0" 347 | id: "category-field-1" 348 | result: 349 | content: 350 | - type: "text" 351 | text: "match:regex:[\\s\\S]*category[\\s\\S]*:[\\s\\S]*core" # Category field should match request 352 | isError: false 353 | stderr: "toBeEmpty" 354 | 355 | - it: "should return documents with type field in product category" 356 | request: 357 | jsonrpc: "2.0" 358 | id: "type-field-1" 359 | method: "tools/call" 360 | params: 361 | name: "get_sfra_documents_by_category" 362 | arguments: 363 | category: "product" 364 | expect: 365 | response: 366 | jsonrpc: "2.0" 367 | id: "type-field-1" 368 | result: 369 | content: 370 | - type: "text" 371 | text: "match:regex:[\\s\\S]*type[\\s\\S]*:[\\s\\S]*(model|class|module)" # Valid type values 372 | isError: false 373 | stderr: "toBeEmpty" 374 | 375 | - it: "should return documents with filename field" 376 | request: 377 | jsonrpc: "2.0" 378 | id: "filename-field-1" 379 | method: "tools/call" 380 | params: 381 | name: "get_sfra_documents_by_category" 382 | arguments: 383 | category: "core" 384 | expect: 385 | response: 386 | jsonrpc: "2.0" 387 | id: "filename-field-1" 388 | result: 389 | content: 390 | - type: "text" 391 | text: "match:regex:[\\s\\S]*filename[\\s\\S]*:[\\s\\S]*\\.md" # Markdown filename 392 | isError: false 393 | stderr: "toBeEmpty" 394 | 395 | # ================================================================================== 396 | # PERFORMANCE VALIDATION 397 | # ================================================================================== 398 | 399 | - it: "should respond quickly for core category lookup" 400 | request: 401 | jsonrpc: "2.0" 402 | id: "perf-core-1" 403 | method: "tools/call" 404 | params: 405 | name: "get_sfra_documents_by_category" 406 | arguments: 407 | category: "core" 408 | expect: 409 | response: 410 | jsonrpc: "2.0" 411 | id: "perf-core-1" 412 | result: 413 | content: 414 | - type: "text" 415 | text: "match:type:string" 416 | isError: false 417 | performance: 418 | maxResponseTime: "300ms" # Fast metadata operation 419 | stderr: "toBeEmpty" 420 | 421 | - it: "should respond quickly for product category lookup" 422 | request: 423 | jsonrpc: "2.0" 424 | id: "perf-product-1" 425 | method: "tools/call" 426 | params: 427 | name: "get_sfra_documents_by_category" 428 | arguments: 429 | category: "product" 430 | expect: 431 | response: 432 | jsonrpc: "2.0" 433 | id: "perf-product-1" 434 | result: 435 | content: 436 | - type: "text" 437 | text: "match:type:string" 438 | isError: false 439 | performance: 440 | maxResponseTime: "300ms" # Fast metadata operation 441 | stderr: "toBeEmpty" 442 | 443 | - it: "should handle error cases quickly" 444 | request: 445 | jsonrpc: "2.0" 446 | id: "perf-error-1" 447 | method: "tools/call" 448 | params: 449 | name: "get_sfra_documents_by_category" 450 | arguments: 451 | category: "" 452 | expect: 453 | response: 454 | jsonrpc: "2.0" 455 | id: "perf-error-1" 456 | result: 457 | content: 458 | - type: "text" 459 | text: "match:contains:Error" 460 | isError: true 461 | performance: 462 | maxResponseTime: "200ms" # Error handling should be very fast 463 | stderr: "toBeEmpty" 464 | 465 | # ================================================================================== 466 | # CASE SENSITIVITY TESTING 467 | # ================================================================================== 468 | 469 | - it: "should handle uppercase category names" 470 | request: 471 | jsonrpc: "2.0" 472 | id: "case-upper-1" 473 | method: "tools/call" 474 | params: 475 | name: "get_sfra_documents_by_category" 476 | arguments: 477 | category: "CORE" 478 | expect: 479 | response: 480 | jsonrpc: "2.0" 481 | id: "case-upper-1" 482 | result: 483 | content: 484 | - type: "text" 485 | text: "match:regex:^\\[\\s*\\]$" # Expect empty array for case mismatch 486 | isError: false 487 | stderr: "toBeEmpty" 488 | 489 | - it: "should handle mixed case category names" 490 | request: 491 | jsonrpc: "2.0" 492 | id: "case-mixed-1" 493 | method: "tools/call" 494 | params: 495 | name: "get_sfra_documents_by_category" 496 | arguments: 497 | category: "Core" 498 | expect: 499 | response: 500 | jsonrpc: "2.0" 501 | id: "case-mixed-1" 502 | result: 503 | content: 504 | - type: "text" 505 | text: "match:regex:^\\[\\s*\\]$" # Expect empty array for case mismatch 506 | isError: false 507 | stderr: "toBeEmpty" 508 | ```