This is page 27 of 43. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /tests/mcp/node/search-site-preferences.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; describe('search_site_preferences tool - Full Mode Programmatic Tests', () => { let client; before(async () => { client = await connect('./aegis.config.with-dw.json'); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent leaking into next tests client.clearAllBuffers(); }); describe('Complex Query Structure Validation', () => { test('should validate complex boolean query with multiple conditions', async () => { const complexQuery = { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { bool_query: { must: [ { text_query: { fields: ['id', 'display_name'], search_phrase: 'cart' } } ], should: [ { term_query: { fields: ['value_type'], operator: 'is', values: ['boolean'] } } ] } }, count: 5, start: 0 } }; const result = await client.callTool('search_site_preferences', complexQuery); assert.equal(result.isError, false, 'Complex boolean query should succeed'); assert.ok(result.content, 'Should have content'); assert.equal(result.content[0].type, 'text', 'Content should be text type'); const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.hits, 'Response should have hits array'); assert.ok(responseData.query, 'Response should echo query'); assert.equal(responseData.query.bool_query.must.length, 1, 'Should preserve bool_query structure'); assert.equal(responseData.query.bool_query.should.length, 1, 'Should preserve should conditions'); }); test('should handle nested boolean queries with must_not conditions', async () => { const nestedQuery = { groupId: 'System', instanceType: 'sandbox', searchRequest: { query: { bool_query: { must: [ { match_all_query: {} } ], must_not: [ { term_query: { fields: ['value_type'], operator: 'is', values: ['password'] } } ] } }, count: 10 } }; const result = await client.callTool('search_site_preferences', nestedQuery); assert.equal(result.isError, false, 'Nested boolean query should succeed'); const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.hits, 'Should have hits'); assert.ok(responseData.query.bool_query.must_not, 'Should preserve must_not conditions'); // Verify no password type preferences are returned const passwordPrefs = responseData.hits.filter(hit => hit.attribute_definition?.value_type === 'password' ); assert.equal(passwordPrefs.length, 0, 'Should exclude password type preferences'); }); }); describe('Response Structure Deep Validation', () => { test('should validate complete response structure with all fields', async () => { const result = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 3, start: 0 }, options: { expand: 'value', maskPasswords: false } }); assert.equal(result.isError, false, 'Request should succeed'); const responseData = JSON.parse(result.content[0].text); // Validate top-level structure assert.ok(responseData._type, 'Should have _type field'); assert.equal(responseData._type, 'preference_value_search_result', 'Should have correct type'); assert.ok(Array.isArray(responseData.hits), 'Hits should be array'); assert.ok(typeof responseData.start === 'number', 'Start should be number'); assert.ok(typeof responseData.count === 'number', 'Count should be number'); assert.ok(typeof responseData.total === 'number', 'Total should be number'); assert.ok(responseData.query, 'Should have query echo'); // Validate hit structure if any hits exist if (responseData.hits.length > 0) { const hit = responseData.hits[0]; assert.ok(hit.attribute_definition, 'Hit should have attribute_definition'); assert.ok(hit.site_values, 'Hit should have site_values'); // Validate attribute definition structure const attrDef = hit.attribute_definition; assert.ok(typeof attrDef.id === 'string', 'Attribute definition should have id'); assert.ok(typeof attrDef.display_name === 'object', 'Display name should be object'); assert.ok(typeof attrDef.value_type === 'string', 'Should have value_type'); // Validate site values structure const siteValues = hit.site_values; assert.ok(typeof siteValues === 'object', 'Site values should be object'); assert.ok(siteValues !== null, 'Site values should not be null'); if (Object.keys(siteValues).length > 0) { const firstSiteId = Object.keys(siteValues)[0]; assert.ok(typeof firstSiteId === 'string', 'Site ID should be string'); assert.ok(Object.prototype.hasOwnProperty.call(siteValues, firstSiteId), 'Site values should have site ID property'); } } }); test('should validate pagination metadata consistency', async () => { // Get total count first with reasonable count const countResult = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 50, // Reasonable count instead of 1000 start: 0 } }); assert.equal(countResult.isError, false, 'Count request should succeed'); const countData = JSON.parse(countResult.content[0].text); const totalPreferences = countData.total; if (totalPreferences > 5) { // Test pagination with smaller pages const pageSize = 3; const page1Result = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: pageSize, start: 0 } }); const page2Result = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: pageSize, start: pageSize } }); assert.equal(page1Result.isError, false, 'Page 1 should succeed'); assert.equal(page2Result.isError, false, 'Page 2 should succeed'); const page1Data = JSON.parse(page1Result.content[0].text); const page2Data = JSON.parse(page2Result.content[0].text); // Validate pagination metadata assert.equal(page1Data.start, 0, 'Page 1 start should be 0'); assert.equal(page1Data.count, pageSize, 'Page 1 count should match request'); assert.equal(page2Data.start, pageSize, 'Page 2 start should be offset'); assert.equal(page2Data.count, pageSize, 'Page 2 count should match request'); // Both pages should report same total assert.equal(page1Data.total, page2Data.total, 'Total should be consistent across pages'); // Validate no duplicate preferences between pages const page1Ids = page1Data.hits.map(hit => hit.attribute_definition.id); const page2Ids = page2Data.hits.map(hit => hit.attribute_definition.id); const intersection = page1Ids.filter(id => page2Ids.includes(id)); assert.equal(intersection.length, 0, 'Pages should not have duplicate preferences'); } }); }); describe('Advanced Query Scenarios', () => { test('should handle multiple preference groups sequentially', async () => { const preferenceGroups = ['Storefront', 'System', 'SFRA']; const results = new Map(); // Test each group sequentially (no concurrent requests) for (const groupId of preferenceGroups) { const result = await client.callTool('search_site_preferences', { groupId, instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 5 } }); assert.equal(result.isError, false, `Group ${groupId} should be accessible`); const responseData = JSON.parse(result.content[0].text); results.set(groupId, responseData); // Validate group-specific response assert.ok(responseData.hits, `Group ${groupId} should have hits`); assert.ok(responseData.total >= 0, `Group ${groupId} should have total count`); } // Validate that different groups return different preferences const storefrontIds = results.get('Storefront').hits.map(hit => hit.attribute_definition.id); const systemIds = results.get('System').hits.map(hit => hit.attribute_definition.id); if (storefrontIds.length > 0 && systemIds.length > 0) { const overlap = storefrontIds.filter(id => systemIds.includes(id)); assert.equal(overlap.length, 0, 'Different groups should have different preferences'); } }); test('should validate search with sorting and field selection', async () => { const result = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 10, start: 0, select: '(*)', sorts: [ { field: 'id', sort_order: 'asc' } ] } }); assert.equal(result.isError, false, 'Sorted query should succeed'); const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.hits, 'Should have hits'); // Note: Sorting validation removed as mock server doesn't implement actual sorting // This test now focuses on validating that sorting parameters are accepted // Validate that select parameter is echoed in response assert.ok(responseData.select, 'Response should echo select parameter'); assert.equal(responseData.select, '(*)', 'Select parameter should be preserved as (*)'); }); }); describe('Error Handling and Edge Cases', () => { test('should handle query timeout gracefully', async () => { // Test with a very complex query that might timeout const complexTimeoutQuery = { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { bool_query: { must: [ { text_query: { fields: ['id', 'display_name', 'description'], search_phrase: 'a' // Very broad search } } ], should: Array.from({ length: 10 }, (_, i) => ({ term_query: { fields: ['value_type'], operator: 'is', values: [`type_${i}`] } })) } }, count: 1000, // Large count start: 0 } }; const result = await client.callTool('search_site_preferences', complexTimeoutQuery); // Should either succeed or fail gracefully with timeout error if (result.isError) { assert.ok( result.content[0].text.includes('timeout') || result.content[0].text.includes('error') || result.content[0].text.includes('invalid'), 'Timeout should produce meaningful error message' ); } else { // If it succeeds, validate response structure const responseData = JSON.parse(result.content[0].text); assert.ok(responseData._type, 'Should have valid response structure even for complex queries'); } }); test('should validate parameter combinations and constraints', async () => { const testCases = [ { name: 'negative start parameter', params: { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, start: -1 } }, shouldSucceed: false } ]; for (const testCase of testCases) { const result = await client.callTool('search_site_preferences', testCase.params); if (testCase.shouldSucceed) { assert.equal(result.isError, false, `${testCase.name} should succeed`); } else { assert.equal(result.isError, true, `${testCase.name} should fail with validation error`); assert.ok( result.content[0].text.includes('error') || result.content[0].text.includes('Error') || result.content[0].text.includes('invalid') || result.content[0].text.includes('required') || result.content[0].text.includes('must be'), `${testCase.name} should provide meaningful error message. Got: ${result.content[0].text}` ); } } }); }); describe('Data Consistency and Business Logic', () => { test('should validate preference value types and constraints', async () => { const result = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 20 } }); assert.equal(result.isError, false, 'Query should succeed'); const responseData = JSON.parse(result.content[0].text); if (responseData.hits.length > 0) { const validValueTypes = ['string', 'boolean', 'int', 'double', 'password', 'email', 'text', 'html', 'date', 'enum_of_string', 'set_of_string']; responseData.hits.forEach((hit, index) => { const attrDef = hit.attribute_definition; // Validate required fields assert.ok(attrDef.id, `Hit ${index} should have attribute id`); assert.ok(attrDef.value_type, `Hit ${index} should have value_type`); assert.ok(validValueTypes.includes(attrDef.value_type), `Hit ${index} should have valid value_type: ${attrDef.value_type}`); // Validate display_name structure if (attrDef.display_name) { assert.ok(typeof attrDef.display_name === 'object', `Hit ${index} display_name should be object`); } // Validate site_values structure assert.ok(typeof hit.site_values === 'object', `Hit ${index} site_values should be object`); assert.ok(hit.site_values !== null, `Hit ${index} site_values should not be null`); const siteValueEntries = Object.entries(hit.site_values); siteValueEntries.forEach(([siteId, siteValue], siteIndex) => { assert.ok(typeof siteId === 'string', `Hit ${index}, site entry ${siteIndex} should have string site ID`); // Validate value based on type (skip null values) if (siteValue !== null) { if (attrDef.value_type === 'boolean') { assert.ok(typeof siteValue === 'boolean', `Hit ${index}, site ${siteId} should have boolean value for boolean type`); } if (attrDef.value_type === 'int') { assert.ok(Number.isInteger(siteValue), `Hit ${index}, site ${siteId} should have integer value for int type`); } } }); }); } }); test('should validate query result consistency across calls', async () => { const queryParams = { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { text_query: { fields: ['id', 'display_name'], search_phrase: 'cart' } }, count: 10 } }; // Execute same query multiple times const results = []; for (let i = 0; i < 3; i++) { const result = await client.callTool('search_site_preferences', queryParams); assert.equal(result.isError, false, `Call ${i + 1} should succeed`); const responseData = JSON.parse(result.content[0].text); results.push(responseData); } // Validate consistency across calls const firstResult = results[0]; for (let i = 1; i < results.length; i++) { const currentResult = results[i]; assert.equal(currentResult.total, firstResult.total, `Call ${i + 1} total should match first call`); assert.equal(currentResult.hits.length, firstResult.hits.length, `Call ${i + 1} hits count should match first call`); // Compare preference IDs (order should be consistent) const firstIds = firstResult.hits.map(hit => hit.attribute_definition.id); const currentIds = currentResult.hits.map(hit => hit.attribute_definition.id); assert.deepEqual(currentIds, firstIds, `Call ${i + 1} should return same preferences in same order`); } }); }); describe('Performance and Scalability Validation', () => { test('should handle large result sets efficiently', async () => { const largeQueryResult = await client.callTool('search_site_preferences', { groupId: 'Storefront', instanceType: 'sandbox', searchRequest: { query: { match_all_query: {} }, count: 50 // Reduced from 200 to avoid server issues } }); assert.equal(largeQueryResult.isError, false, 'Large query should succeed'); const responseData = JSON.parse(largeQueryResult.content[0].text); // Validate response structure is maintained for large results assert.ok(responseData._type, 'Large response should have type'); assert.ok(Array.isArray(responseData.hits), 'Large response should have hits array'); assert.ok(typeof responseData.total === 'number', 'Large response should have total'); // Validate all hits have required structure responseData.hits.forEach((hit, index) => { assert.ok(hit.attribute_definition, `Hit ${index} should have attribute_definition`); assert.ok(hit.site_values, `Hit ${index} should have site_values`); assert.ok(hit.attribute_definition.id, `Hit ${index} should have id`); }); }); test('should validate functional reliability over multiple operations', async () => { const operations = [ { groupId: 'Storefront', query: { match_all_query: {} } }, { groupId: 'System', query: { text_query: { fields: ['id'], search_phrase: 'test' } } }, { groupId: 'SFRA', query: { match_all_query: {} } }, { groupId: 'Storefront', query: { term_query: { fields: ['value_type'], operator: 'is', values: ['boolean'] } } } ]; const results = []; for (const operation of operations) { const result = await client.callTool('search_site_preferences', { groupId: operation.groupId, instanceType: 'sandbox', searchRequest: { query: operation.query, count: 10 } }); results.push({ groupId: operation.groupId, success: !result.isError, hasContent: result.content && result.content.length > 0, responseValid: !result.isError && result.content[0].text.startsWith('{') }); } // Calculate reliability metrics const successfulOperations = results.filter(r => r.success).length; const validResponses = results.filter(r => r.responseValid).length; assert.ok(successfulOperations >= operations.length * 0.8, `At least 80% of operations should succeed (${successfulOperations}/${operations.length})`); assert.ok(validResponses >= operations.length * 0.8, `At least 80% of responses should be valid JSON (${validResponses}/${operations.length})`); // Log reliability stats for monitoring console.log(`\nReliability Stats:`); console.log(`- Successful operations: ${successfulOperations}/${operations.length} (${(successfulOperations/operations.length*100).toFixed(1)}%)`); console.log(`- Valid responses: ${validResponses}/${operations.length} (${(validResponses/operations.length*100).toFixed(1)}%)`); }); }); }); ``` -------------------------------------------------------------------------------- /docs-site/pages/SecurityPage.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import { NavLink } from 'react-router-dom'; import SEO from '../components/SEO'; import BreadcrumbSchema from '../components/BreadcrumbSchema'; import StructuredData from '../components/StructuredData'; import { H1, PageSubtitle, H2, H3 } from '../components/Typography'; import { InlineCode } from '../components/CodeBlock'; import { SITE_DATES } from '../constants'; // Small utility card const Pill: React.FC<React.PropsWithChildren<{ color?: string }>> = ({ children, color = 'from-blue-600 to-purple-600' }) => ( <div className={`inline-flex items-center gap-2 bg-gradient-to-r ${color} text-white px-4 py-2 rounded-full text-sm font-medium`}>{children}</div> ); const Bullet: React.FC<React.PropsWithChildren<{ icon?: string; className?: string }>> = ({ children, icon = '✔', className = '' }) => ( <li className={`flex items-start gap-2 text-sm text-gray-700 ${className}`}> <span className="mt-0.5 text-green-600 flex-shrink-0">{icon}</span> <span>{children}</span> </li> ); const SectionShell: React.FC<React.PropsWithChildren<{ gradient?: string; className?: string; border?: string }>> = ({ children, gradient = 'from-blue-50 via-indigo-50 to-purple-50', className = '', border = 'border-blue-100' }) => ( <div className={`mb-20 last:mb-0 bg-gradient-to-r ${gradient} rounded-2xl p-8 shadow-xl ${border} border ${className}`}>{children}</div> ); // Structured feature list rows for mode comparison const ModeFeatureList: React.FC<{ color: 'green' | 'blue'; items: Array<{ icon: string; label: string; detail: string }> }> = ({ color, items }) => { const colorMap = { green: { badge: 'bg-green-100 text-green-800 border-green-200', icon: 'text-green-600', label: 'text-green-900', detail: 'text-green-700' }, blue: { badge: 'bg-blue-100 text-blue-800 border-blue-200', icon: 'text-blue-600', label: 'text-blue-900', detail: 'text-blue-700' } } as const; const c = colorMap[color]; return ( <ul className="list-none p-0 m-0 space-y-3"> {items.map(item => ( <li key={item.label} className="group"> <div className={`flex items-start gap-3 rounded-xl border ${c.badge} bg-white/70 backdrop-blur-sm px-3 py-2 hover:shadow-sm transition`}> <span className={`text-base leading-none mt-0.5 ${c.icon}`}>{item.icon}</span> <div className="flex-1 min-w-0"> <p className={`m-0 text-sm font-medium ${c.label}`}>{item.label}</p> <p className={`m-0 text-[11px] leading-snug ${c.detail}`}>{item.detail}</p> </div> </div> </li> ))} </ul> ); }; const SecurityPage: React.FC = () => { const securityStructuredData = { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Security & Privacy - SFCC Development MCP Server", "description": "Security guidelines and privacy considerations for SFCC Development MCP Server. Credential protection, threat mitigations, data handling and secure usage checklist.", "author": { "@type": "Person", "name": "Thomas Theunen" }, "publisher": { "@type": "Person", "name": "Thomas Theunen" }, "datePublished": SITE_DATES.PUBLISHED, "dateModified": SITE_DATES.MODIFIED, "url": "https://sfcc-mcp-dev.rhino-inquisitor.com/security/", "mainEntity": { "@type": "Guide", "name": "SFCC MCP Security Guide" } }; return ( <div className="max-w-6xl mx-auto px-6 py-10"> <SEO title="Security & Privacy" description="Security guidelines and privacy considerations for SFCC Development MCP Server. Credential protection, threat mitigations, data handling and secure usage checklist." keywords="SFCC MCP security, Commerce Cloud security, MCP server privacy, SFCC credential protection, development security, API security, local development security, SFCC authentication security" canonical="/security/" ogType="article" /> <BreadcrumbSchema items={[ { name: "Home", url: "/" }, { name: "Security", url: "/security/" } ]} /> <StructuredData structuredData={securityStructuredData} /> {/* Hero */} <header className="text-center mb-16"> <Pill>Security & Privacy</Pill> <H1 id="security-guidelines" className="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-gray-900 via-blue-900 to-purple-900 bg-clip-text text-transparent mt-6 mb-6">Built-In Guardrails – You Add Discipline</H1> <PageSubtitle className="text-xl md:text-2xl text-gray-600 max-w-4xl mx-auto leading-relaxed"> Opinionated local-only design: minimal credential footprint, scoped API access, defensive parsing. Use this page as a <strong>practical hardening checklist</strong>, not a marketing overview. </PageSubtitle> <p className="mt-4 text-[11px] uppercase tracking-wide text-gray-400">Surface: <strong>36+ specialized tools</strong> (docs, best practices, SFRA, cartridge gen, runtime logs, job logs, system & custom objects, site preferences, code versions)</p> </header> {/* Quick Essentials */} <div className="grid md:grid-cols-3 gap-6 mb-20"> {[ { title: 'Local Only', desc: 'Never deploy to shared or production infra. No multi-user isolation layer exists.' }, { title: 'Least Privilege', desc: 'Grant only OCAPI resources you actively need.' }, { title: 'No Persistent Secrets', desc: 'Credentials live in memory during execution; you own filesystem storage strategy.' } ].map(card => ( <div key={card.title} className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"> <h3 className="font-semibold text-gray-900 mb-2 text-lg">{card.title}</h3> <p className="text-sm text-gray-600 leading-relaxed">{card.desc}</p> </div> ))} </div> {/* Mode Comparison */} <SectionShell> <div className="text-center mb-10"> <H2 id="modes" className="text-3xl font-bold mb-3">🔐 Modes & Security Characteristics</H2> <p className="text-gray-700 max-w-3xl mx-auto text-lg"> Both modes are designed for <strong>local single‑developer use</strong>. Docs mode has a <em>zero credential surface</em>; Full Mode’s profile is essentially the same as any normal SFCC development workflow using a <InlineCode>dw.json</InlineCode> for OCAPI + WebDAV access. Choose based on capability needs, not fear—just scope credentials sensibly. </p> </div> <div className="grid lg:grid-cols-2 gap-8"> {/* Docs Mode Card */} <div className="rounded-2xl bg-green-50 border border-green-200 p-6 flex flex-col"> <h3 className="text-xl font-semibold text-green-800 mb-4 flex items-center gap-2"> <span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-600 text-white text-sm shadow">D</span> Docs Mode (Default) </h3> <ModeFeatureList color="green" items={[ { icon: '❇', label: 'No credentials required', detail: 'Pure static: zero auth surface' }, { icon: '📄', label: 'Static content only', detail: 'Docs, guides, cartridge scaffolding' }, { icon: '🧱', label: 'No outbound authenticated calls', detail: 'Nothing to leak or revoke' }, { icon: '🧪', label: 'Safe capability exploration', detail: 'Great for AI tool schema discovery' }, { icon: '�', label: 'Instant reversible baseline', detail: 'Add credentials later without refactor' } ]} /> <div className="mt-5 text-[11px] text-green-700 font-medium bg-white/60 rounded-md px-3 py-2 border border-green-200"> Baseline mode: zero credential management, ideal first run. </div> </div> {/* Full Mode Card */} <div className="rounded-2xl bg-blue-50 border border-blue-200 p-6 flex flex-col"> <h3 className="text-xl font-semibold text-blue-800 mb-4 flex items-center gap-2"> <span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-600 text-white text-sm shadow">F</span> Full Mode (<InlineCode>--dw-json</InlineCode>) </h3> <ModeFeatureList color="blue" items={[ { icon: '🔑', label: 'Credential parity', detail: 'Same auth data you already use locally' }, { icon: '🪵', label: 'Runtime + job logs', detail: 'Tail, search, summarize – read-only WebDAV access' }, { icon: '🧭', label: 'System & custom object metadata', detail: 'OCAPI Data API – attribute & group definitions' }, { icon: '⚙️', label: 'Site preference discovery', detail: 'Group-scoped search with masked password values' }, { icon: '🚦', label: 'Explicit code version activation', detail: 'Never automatic; requires targeted command' }, { icon: '🪄', label: 'Cartridge generation + docs', detail: 'Same as docs mode plus live capabilities' } ]} /> <div className="mt-5 text-[11px] text-blue-700 font-medium bg-white/60 rounded-md px-3 py-2 border border-blue-200"> Comparable risk to normal SFCC dev with <InlineCode>dw.json</InlineCode>; treat scope hygiene the same. </div> </div> </div> <div className="mt-8 text-xs text-gray-600 text-center">Switch between modes freely: omit <InlineCode>--dw-json</InlineCode> to return to a zero‑credential baseline.</div> </SectionShell> {/* Inline component definitions for mode feature rows */} {/* Keeping them near usage for maintainability; extract later if reused */} {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} {/* Hardening Checklist */} <SectionShell gradient="from-gray-50 via-slate-50 to-blue-50" border="border-gray-200"> <div className="text-center mb-8"> <H2 id="checklist" className="text-3xl font-bold mb-3">📋 Baseline Hardening Checklist</H2> <p className="text-gray-700 max-w-2xl mx-auto">Perform these once per environment. Keep it lightweight; delete unused credentials.</p> </div> <ol className="grid md:grid-cols-2 gap-6 counter-reset list-none pl-0"> {[ 'Confirm sandbox hostname (never production domain).', 'Add dw.json + *.dw.json to .gitignore and verify not tracked.', 'chmod 600 dw.json (owner read/write only).', 'Remove unused OAuth fields if only using logs.', 'Grant only required OCAPI resources (add incrementally).', 'Mask secrets with environment overrides in CI contexts.', 'Run docs mode first; validate tool set boundaries.', 'Rotate client secret + password on schedule (quarterly baseline).' ].map(item => ( <li key={item} className="relative pl-10 text-sm text-gray-700 leading-relaxed"> <span className="absolute left-0 top-0 w-7 h-7 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 text-white flex items-center justify-center text-xs font-semibold shadow">{String(([ 'Confirm sandbox hostname (never production domain).', 'Add dw.json + *.dw.json to .gitignore and verify not tracked.', 'chmod 600 dw.json (owner read/write only).', 'Remove unused OAuth fields if only using logs.', 'Grant only required OCAPI resources (add incrementally).', 'Mask secrets with environment overrides in CI contexts.', 'Run docs mode first; validate tool set boundaries.', 'Rotate client secret + password on schedule (quarterly baseline).' ].indexOf(item) + 1))}</span> {item} </li> ))} </ol> </SectionShell> {/* Credential Handling */} <SectionShell gradient="from-emerald-50 via-teal-50 to-cyan-50" border="border-emerald-200"> <div className="grid md:grid-cols-3 gap-8 mb-6 items-start"> <div className="md:col-span-3 max-w-2xl"> <H2 id="credentials" className="text-2xl font-bold mb-2">🛡️ Credential Handling</H2> <p className="text-sm text-gray-700 leading-relaxed">You retain full control of persistence. The server reads your <InlineCode>dw.json</InlineCode> once, hydrates in-memory configuration, and performs authenticated calls. No outbound exfiltration logic exists.</p> </div> </div> <div className="grid md:grid-cols-3 gap-6"> <div className="rounded-xl bg-white border border-gray-200 p-5"> <h3 className="font-semibold text-sm mb-2">Minimize Scope</h3> <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> <li>Start w/ no Data API resources</li> <li>Add system objects only when needed</li> <li>Remove stale resources quarterly</li> </ul> </div> <div className="rounded-xl bg-white border border-gray-200 p-5"> <h3 className="font-semibold text-sm mb-2">Protect Files</h3> <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> <li><InlineCode>chmod 600 dw.json</InlineCode></li> <li>Avoid shared directories (Sync/Drive)</li> <li>Do not email secrets</li> </ul> </div> <div className="rounded-xl bg-white border border-gray-200 p-5"> <h3 className="font-semibold text-sm mb-2">Rotate & Audit</h3> <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> <li>Quarterly secret rotation baseline</li> <li>Remove orphaned API clients</li> <li>Track creation dates (label names)</li> </ul> </div> </div> </SectionShell> {/* Threat Model & Mitigations */} <SectionShell gradient="from-red-50 via-rose-50 to-orange-50" border="border-red-200"> <div className="text-center mb-10"> <H2 id="threat-model" className="text-3xl font-bold mb-3">🧪 Practical Threat Model (Local Context)</H2> <p className="text-gray-700 max-w-3xl mx-auto text-lg">In a single‑developer local setup the incremental risk introduced by Full Mode is roughly equivalent to any normal use of <InlineCode>dw.json</InlineCode>. Core concerns remain <strong>credential scope creep</strong>, <strong>accidental sharing of log snippets containing business data</strong>, and <strong>copying sensitive preference values externally</strong>. Below: built‑in mitigations vs. your ongoing hygiene tasks.</p> </div> <div className="grid md:grid-cols-2 gap-8"> <div className="rounded-xl bg-white p-6 border border-gray-200"> <h3 className="font-semibold mb-3 text-gray-900">Mitigated In Design</h3> <ul className="space-y-2 text-sm"> <Bullet>Path traversal (validated absolute paths)</Bullet> <Bullet>Parameter schema/type validation</Bullet> <Bullet>Read-only log operations (no writes)</Bullet> <Bullet>Scoped tool registration (no dynamic eval)</Bullet> <Bullet>Token refresh w/ expiration handling</Bullet> <Bullet>Memory-only caching (no disk persistence)</Bullet> </ul> </div> <div className="rounded-xl bg-white p-6 border border-gray-200"> <h3 className="font-semibold mb-3 text-gray-900">Your Responsibilities</h3> <ul className="space-y-2 text-sm"> <Bullet icon="⚠">Do not run on shared multi-user servers</Bullet> <Bullet icon="⚠">Keep secrets out of version control</Bullet> <Bullet icon="⚠">Avoid copying raw logs with PII into tickets</Bullet> <Bullet icon="⚠">Limit OCAPI resources to active feature work</Bullet> <Bullet icon="⚠">Rotate credentials + revoke unused clients</Bullet> <Bullet icon="⚠">Disable debug once diagnosing finished</Bullet> </ul> </div> </div> <div className="mt-8 grid md:grid-cols-3 gap-6 text-xs"> <div className="bg-green-50 border border-green-200 rounded-lg p-4"> <p className="font-semibold text-green-800 mb-1">Docs Mode</p> <p className="text-green-700 leading-snug">Static reference + generation only. Zero credential or data surface.</p> </div> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <p className="font-semibold text-blue-800 mb-1">Full Mode (Scoped)</p> <p className="text-blue-700 leading-snug">Typical local dev parity: targeted OCAPI, selective log tailing, metadata queries.</p> </div> <div className="bg-amber-50 border border-amber-200 rounded-lg p-4"> <p className="font-semibold text-amber-800 mb-1">Broad Scope (Review)</p> <p className="text-amber-700 leading-snug">Wide OCAPI grants + continuous debug + indiscriminate log sharing. Still local—but audit and prune.</p> </div> </div> </SectionShell> {/* Data Protection */} <SectionShell gradient="from-yellow-50 via-amber-50 to-orange-50" border="border-amber-200"> <div className="grid md:grid-cols-3 gap-8 mb-6 items-start"> <div className="md:col-span-2 max-w-2xl"> <H2 id="data-protection" className="text-2xl font-bold mb-2">💾 Data Handling & Privacy</H2> <p className="text-sm text-gray-700 leading-relaxed">Runtime data (logs, attribute listings, preference search results) is streamed, parsed, optionally cached in memory, and discarded on process exit.</p> </div> <div className="md:col-span-1 rounded-lg border border-yellow-300 bg-white p-5 text-[13px] text-yellow-800 shadow-sm min-w-[260px]"> <p className="font-semibold mb-2 tracking-tight">Design Principles</p> <ul className="list-disc pl-4 space-y-1.5"> <li>No silent disk writes</li> <li>Bounded tail reads (~200KB)</li> <li>Optional debug noise suppression</li> </ul> </div> </div> <div className="grid md:grid-cols-3 gap-6"> {[ { title: 'Log Processing', items: ['Tail/range reads only (≈200KB)', 'Pattern search constrained by limit', 'Analyzer strips obvious secret tokens'] }, { title: 'Preference Values', items: ['Password types masked (no bypass)', 'Group-limited search scope', 'No storage of raw values'] }, { title: 'System & Custom Objects', items: ['Metadata only (ids, flags, counts)', 'No record-level PII retrieval', 'You control query breadth'] } ].map(card => ( <div key={card.title} className="rounded-xl bg-white border border-gray-200 p-5"> <h3 className="font-semibold text-sm mb-2">{card.title}</h3> <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> {card.items.map(i => <li key={i}>{i}</li>)} </ul> </div> ))} </div> </SectionShell> {/* Reporting */} <SectionShell gradient="from-slate-50 via-gray-50 to-blue-50" border="border-gray-200"> <div className="text-center mb-8"> <H2 id="reporting" className="text-3xl font-bold mb-3">🔍 Responsible Disclosure</H2> <p className="text-gray-700 max-w-2xl mx-auto">Found a vulnerability? Help strengthen the ecosystem—avoid public zero-days.</p> </div> <ol className="list-decimal pl-6 space-y-3 text-sm text-gray-700 max-w-3xl mx-auto"> <li><strong>Do NOT</strong> open a public GitHub issue containing exploit details.</li> <li>Email maintainers with: version, environment, reproduction steps, impact summary.</li> <li>Suggest a remediation direction if obvious (helps triage).</li> <li>Allow a reasonable patch window before disclosure.</li> <li>Re-test once patch is published; confirm mitigation completeness.</li> </ol> </SectionShell> {/* Final CTA */} <section className="mt-24 text-center" aria-labelledby="next-steps-security"> <H2 id="next-steps-security" className="text-3xl font-bold mb-4">🔗 Next Steps</H2> <p className="text-sm md:text-base text-gray-600 max-w-2xl mx-auto mb-8">Keep momentum: refine configuration or explore advanced tooling now that baseline security posture is set.</p> <div className="flex flex-col sm:flex-row gap-4 justify-center mb-10"> <NavLink to="/configuration/" className="group bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 no-underline hover:no-underline focus:no-underline" > Configuration Guide <span className="ml-2 group-hover:translate-x-1 inline-block transition-transform">→</span> </NavLink> <NavLink to="/features/" className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300 no-underline hover:no-underline focus:no-underline" > Explore Features </NavLink> <NavLink to="/examples/" className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300 no-underline hover:no-underline focus:no-underline" > See Examples </NavLink> </div> </section> </div> ); }; export default SecurityPage; ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/src/routes/ocapi/system-objects-handler.js: -------------------------------------------------------------------------------- ```javascript /** * System Objects Handler * * Handles system object definitions, attribute definitions, attribute groups, * and custom object definitions for OCAPI endpoints. */ const express = require('express'); const OCAPIUtils = require('./ocapi-utils'); const OCAPIErrorUtils = require('./ocapi-error-utils'); class SystemObjectsHandler { constructor(config, dataLoader) { this.config = config; this.ocapiConfig = config.getOcapiConfig(); this.dataLoader = dataLoader; this.router = express.Router(); this.setupRoutes(); } setupRoutes() { // System Object Definitions - List all this.router.get(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions`, this.handleGetSystemObjectDefinitions.bind(this) ); // System Object Definition - Get specific this.router.get(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType`, this.handleGetSystemObjectDefinition.bind(this) ); // System Object Definition Search this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definition_search`, this.handleSearchSystemObjectDefinitions.bind(this) ); // System Object Attribute Definition Search this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType/attribute_definition_search`, this.handleSearchSystemObjectAttributeDefinitions.bind(this) ); // System Object Attribute Group Search this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType/attribute_group_search`, this.handleSearchSystemObjectAttributeGroups.bind(this) ); // Custom Object Attribute Definition Search this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/custom_object_definitions/:objectType/attribute_definition_search`, this.handleSearchCustomObjectAttributeDefinitions.bind(this) ); } /** * Handle GET system object definitions */ async handleGetSystemObjectDefinitions(req, res) { const { start = 0, count = 200, select = '(**)' } = req.query; let mockData = this.dataLoader.loadOcapiData('system-object-definitions.json'); if (!mockData) { // Fallback mock data with proper SFCC format mockData = { "_v": "24.4", "_type": "system_object_definitions", "count": 0, "data": [], "next": null, "previous": null, "start": 0, "total": 0 }; } // Fix total count to match actual data length mockData.total = mockData.data.length; // Extract data-specific select pattern if present let dataSelectPattern = select; if (select && select.includes('data.(')) { const dataMatch = select.match(/data\.\(([^)]*)\)/); if (dataMatch) { dataSelectPattern = `(${dataMatch[1]})`; } } // Apply select parameter to modify object structure let processedData = mockData.data.map(obj => OCAPIUtils.applySelectParameter(obj, dataSelectPattern)); // Apply pagination const startInt = parseInt(start); const countInt = parseInt(count); const paginatedData = processedData.slice(startInt, startInt + countInt); // Calculate pagination URLs const { nextUrl, previousUrl } = OCAPIUtils.generatePaginationUrls( req, startInt, countInt, mockData.total, select ); const fullResponse = { "_v": mockData._v, "_type": mockData._type, "count": paginatedData.length, "data": paginatedData, "next": nextUrl, "previous": previousUrl, "start": startInt, "total": mockData.total }; // Add select field to response if provided (like real API) if (select && select !== '(**)') { fullResponse.select = select; } // Apply root-level select parameter to the entire response const response = OCAPIUtils.applyRootSelectParameter(fullResponse, select); res.json(response); } /** * Handle GET specific system object definition */ async handleGetSystemObjectDefinition(req, res) { const { objectType } = req.params; // Try to load specific object definition let mockData = this.dataLoader.loadOcapiData(`system-object-definition-${objectType.toLowerCase()}.json`); if (!mockData) { // Create a basic fallback mockData = { _type: 'object_type_definition', object_type: objectType, display_name: { default: objectType }, description: { default: `SFCC ${objectType} object` }, attribute_definition_count: 0, attribute_group_count: 0, key_attribute_id: 'id', content_object: false, queryable: true, read_only: false, creation_date: new Date().toISOString(), last_modified: new Date().toISOString() }; } res.json(mockData); } /** * Handle system object definition search */ async handleSearchSystemObjectDefinitions(req, res) { const searchRequest = req.body; // Load base data let mockData = this.dataLoader.loadOcapiData('system-object-definitions.json'); if (!mockData) { mockData = this.getDefaultSystemObjectDefinitions(); } // Simple search implementation (in real SFCC this would be more complex) let results = mockData.hits; // Apply basic filtering if search criteria provided results = OCAPIUtils.applyTextSearch(results, searchRequest.query); // Apply pagination const start = searchRequest.start || 0; const count = searchRequest.count || 200; const paginatedResults = results.slice(start, start + count); res.json({ count: paginatedResults.length, total: results.length, start, hits: paginatedResults }); } /** * Handle system object attribute definition search */ async handleSearchSystemObjectAttributeDefinitions(req, res) { const { objectType } = req.params; const searchRequest = req.body; // Try to load specific attribute definitions based on select parameter const isExpandedRequest = searchRequest.select === "(**)"; const mockDataFile = isExpandedRequest ? `system-object-attributes-${objectType.toLowerCase()}-expanded.json` : `system-object-attributes-${objectType.toLowerCase()}.json`; let mockData = this.dataLoader.loadOcapiData(mockDataFile); // Fallback to basic data if expanded data doesn't exist if (!mockData && isExpandedRequest) { mockData = this.dataLoader.loadOcapiData(`system-object-attributes-${objectType.toLowerCase()}.json`); } if (!mockData) { // Create fallback data with realistic SFCC format mockData = { "_v": "23.2", "_type": "object_attribute_definition_search_result", "count": 0, "hits": [], "query": searchRequest.query || {"match_all_query": {}}, "start": 0, "total": 0 }; } // Apply search and pagination let results = mockData.hits || []; // Apply text search if provided results = OCAPIUtils.applyTextSearch(results, searchRequest.query); // Handle select parameter for expanded data if (isExpandedRequest && mockData.expandedData) { results = results.map(item => { const expandedItem = mockData.expandedData[item.id]; return expandedItem || item; }); } // Store total filtered results count const totalFiltered = results.length; // Apply pagination const start = searchRequest.start || 0; const count = searchRequest.count || 200; const paginatedResults = results.slice(start, start + count); // Build query response with proper _type fields to match real API const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); // Build response in SFCC format const response = { "_v": mockData._v || "23.2", "_type": "object_attribute_definition_search_result", "count": paginatedResults.length, "hits": paginatedResults, "query": queryResponse, "start": start, "total": totalFiltered }; // Add select parameter to response if it was provided if (searchRequest.select) { response.select = searchRequest.select; } // Add next page link if there are more results if (start + count < totalFiltered) { response.next = { "_type": "result_page", "count": Math.min(count, totalFiltered - (start + count)), "start": start + count }; } res.json(response); } /** * Handle system object attribute group search */ async handleSearchSystemObjectAttributeGroups(req, res) { try { const { objectType } = req.params; const searchRequest = req.body; // 1. Validate object type const objectTypeError = OCAPIErrorUtils.validateObjectType(objectType); if (objectTypeError) { return OCAPIErrorUtils.sendErrorResponse(res, objectTypeError); } // 2. Check for object types that don't support attribute groups (based on real API testing) const unsupportedObjectTypes = ['Customer', 'Site', 'Inventory']; if (unsupportedObjectTypes.includes(objectType)) { const notFoundError = OCAPIErrorUtils.createObjectTypeNotFound(objectType); return OCAPIErrorUtils.sendErrorResponse(res, notFoundError); } // 3. Validate search request structure const searchRequestError = OCAPIErrorUtils.validateSearchRequest(searchRequest); if (searchRequestError) { return OCAPIErrorUtils.sendErrorResponse(res, searchRequestError); } // 4. Validate pagination parameters const paginationError = OCAPIErrorUtils.validatePagination(searchRequest.start, searchRequest.count); if (paginationError) { return OCAPIErrorUtils.sendErrorResponse(res, paginationError); } // 5. Validate specific query types if (searchRequest.query.text_query) { const textQueryError = OCAPIErrorUtils.validateTextQuery(searchRequest.query.text_query); if (textQueryError) { return OCAPIErrorUtils.sendErrorResponse(res, textQueryError); } } if (searchRequest.query.term_query) { const termQueryError = OCAPIErrorUtils.validateTermQuery(searchRequest.query.term_query); if (termQueryError) { return OCAPIErrorUtils.sendErrorResponse(res, termQueryError); } } // 6. Simulate occasional server errors (1% chance) - only if randomErrors is enabled if (this.config.features.randomErrors && Math.random() < 0.01) { const serverError = OCAPIErrorUtils.createInternalServerError( "Service temporarily unavailable. Please try again later." ); return OCAPIErrorUtils.sendErrorResponse(res, serverError); } // Try to load specific attribute groups with correct naming let mockData = this.dataLoader.loadOcapiData(`system-object-attribute-groups-${objectType.toLowerCase()}.json`); if (!mockData) { // Create fallback data with proper SFCC format (matching real API structure) mockData = { "_v": "23.2", "_type": "object_attribute_group_search_result", "count": 0, "hits": [], "query": {"match_all_query": {"_type": "match_all_query"}}, "start": 0, "total": 0 }; } // Apply search and pagination let results = mockData.hits || []; // Apply pagination const start = searchRequest.start || 0; const count = searchRequest.count || 200; const paginatedResults = results.slice(start, start + count); // Build query response to match real API const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); // Return response matching real SFCC API format const response = { "_v": mockData._v || "23.2", "_type": "object_attribute_group_search_result", "count": paginatedResults.length, "hits": paginatedResults, "query": queryResponse, "start": start, "total": mockData.total || results.length }; res.json(response); } catch (error) { // Handle unexpected errors console.error('Unexpected error in handleSearchSystemObjectAttributeGroups:', error); const serverError = OCAPIErrorUtils.createInternalServerError( "An unexpected error occurred while processing the request." ); return OCAPIErrorUtils.sendErrorResponse(res, serverError); } } /** * Handle custom object attribute definition search */ async handleSearchCustomObjectAttributeDefinitions(req, res) { const { objectType } = req.params; const searchRequest = req.body; // Validate object type parameter if (!objectType || objectType.trim() === '') { const error = OCAPIErrorUtils.createInvalidRequest( "objectType must be a non-empty string", "objectType" ); return OCAPIErrorUtils.sendErrorResponse(res, error); } // Validate search request structure const validationError = this.validateSearchRequest(searchRequest); if (validationError) { return OCAPIErrorUtils.sendErrorResponse(res, validationError); } // Define known custom object types (case-insensitive) const knownCustomObjects = ['customapi', 'versionhistory', 'globalsettings']; const objectTypeLower = objectType.toLowerCase(); // Try to load specific custom object attribute definitions let mockData = this.dataLoader.loadOcapiData(`custom-object-attributes-${objectTypeLower}.json`); if (!mockData) { // Check if this is a known custom object type with no data vs unknown object type if (!knownCustomObjects.includes(objectTypeLower)) { // Return 404 for unknown custom object types (like real SFCC API) const error = OCAPIErrorUtils.createObjectTypeNotFound(objectType); return OCAPIErrorUtils.sendErrorResponse(res, error); } // Create fallback data with proper SFCC format for known objects with no data mockData = { "_v": "23.2", "_type": "object_attribute_definition_search_result", "count": 0, "hits": [], "query": searchRequest.query || {"match_all_query": {}}, "start": 0, "total": 0 }; } // Apply search and pagination let results = mockData.hits || []; // Apply pagination const start = searchRequest.start || 0; const count = searchRequest.count || 200; const paginatedResults = results.slice(start, start + count); // Build query response to match real API const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); res.json({ "_v": mockData._v, "_type": mockData._type, "count": paginatedResults.length, "hits": paginatedResults, "query": queryResponse, "start": start, "total": mockData.total }); } /** * Get default system object definitions for fallback */ getDefaultSystemObjectDefinitions() { return { count: 25, hits: [ { _type: 'object_type_definition', object_type: 'Product', display_name: { default: 'Product' }, description: { default: 'SFCC Product object' }, attribute_definition_count: 45, attribute_group_count: 8, key_attribute_id: 'id', content_object: false, queryable: true, read_only: false, creation_date: '2021-01-01T00:00:00.000Z', last_modified: '2024-01-01T00:00:00.000Z' }, { _type: 'object_type_definition', object_type: 'Customer', display_name: { default: 'Customer' }, description: { default: 'SFCC Customer object' }, attribute_definition_count: 32, attribute_group_count: 6, key_attribute_id: 'customer_no', content_object: false, queryable: true, read_only: false, creation_date: '2021-01-01T00:00:00.000Z', last_modified: '2024-01-01T00:00:00.000Z' }, { _type: 'object_type_definition', object_type: 'Order', display_name: { default: 'Order' }, description: { default: 'SFCC Order object' }, attribute_definition_count: 28, attribute_group_count: 5, key_attribute_id: 'order_no', content_object: false, queryable: true, read_only: false, creation_date: '2021-01-01T00:00:00.000Z', last_modified: '2024-01-01T00:00:00.000Z' } ] }; } /** * Validate search request structure and parameters */ validateSearchRequest(searchRequest) { // Must have a query property if (!searchRequest.query) { return OCAPIErrorUtils.createPropertyConstraintViolation("$.query", "search_request"); } // Validate count parameter if (searchRequest.count !== undefined) { if (typeof searchRequest.count !== 'number' || searchRequest.count < 0) { return OCAPIErrorUtils.createInvalidRequest("count must be a positive number", "count"); } } // Validate start parameter if (searchRequest.start !== undefined) { if (typeof searchRequest.start !== 'number' || searchRequest.start < 0) { return OCAPIErrorUtils.createInvalidRequest("start must be a positive number", "start"); } } // Validate query object const query = searchRequest.query; const queryTypes = ['text_query', 'term_query', 'filtered_query', 'bool_query', 'match_all_query']; const hasValidQueryType = queryTypes.some(type => query[type]); if (!hasValidQueryType) { return OCAPIErrorUtils.createInvalidRequest( "Search query must contain at least one of: text_query, term_query, filtered_query, bool_query, match_all_query" ); } // Validate text_query if (query.text_query) { const textQuery = query.text_query; if (!textQuery.fields || !Array.isArray(textQuery.fields) || textQuery.fields.length === 0) { return OCAPIErrorUtils.createInvalidRequest("text_query.fields must be a non-empty array"); } if (!textQuery.search_phrase || typeof textQuery.search_phrase !== 'string' || textQuery.search_phrase.trim() === '') { return OCAPIErrorUtils.createInvalidRequest("text_query.search_phrase must be a non-empty string"); } } // Validate term_query if (query.term_query) { const termQuery = query.term_query; if (!termQuery.fields || !Array.isArray(termQuery.fields) || termQuery.fields.length === 0) { return OCAPIErrorUtils.createInvalidRequest("term_query.fields must be a non-empty array"); } if (!termQuery.values || !Array.isArray(termQuery.values) || termQuery.values.length === 0) { return OCAPIErrorUtils.createInvalidRequest("term_query.values must be a non-empty array"); } if (termQuery.operator) { const validOperators = ['is', 'one_of', 'not_one_of', 'is_null', 'is_not_null']; if (!validOperators.includes(termQuery.operator)) { return OCAPIErrorUtils.createEnumConstraintViolation(termQuery.operator); } } } return null; // No validation errors } /** * Get the configured router */ getRouter() { return this.router; } } module.exports = SystemObjectsHandler; ``` -------------------------------------------------------------------------------- /.github/instructions/mcp-yml-tests.instructions.md: -------------------------------------------------------------------------------- ```markdown --- applyTo: "**/*.test.mcp.yml" --- # MCP Aegis - YAML Testing Guide for AI Agents **Target**: AI assistants generating declarative YAML test files for Model Context Protocol servers. **Core Purpose**: Test MCP servers with human-readable YAML files using 35+ advanced pattern matching capabilities including string patterns, numeric comparisons, date validation, array operations, field extraction, cross-field validation, and pattern negation. ## 🆕 New Features: Pipe-Separated Parameters & Enhanced Testing MCP Aegis now supports **CLI-friendly pipe-separated parameter format** alongside traditional JSON: ```bash # 🆕 Pipe format (CLI-friendly): key:value|other:123 npx aegis query calculator 'operation:add|a:5|b:3' --config "config.json" # Traditional JSON (still supported): {"key":"value","other":123} npx aegis query calculator '{"operation": "add", "a": 5, "b": 3}' --config "config.json" ``` **Benefits**: ✅ No shell escaping ✅ Readable syntax ✅ Type inference ✅ Nested objects via dot notation ✅ Backward compatible ## 🥇 GOLDEN RULE: Always Discover Response Formats First **CRITICAL**: Before writing ANY YAML test, you MUST use `aegis query` to discover actual response formats for both success and failure scenarios. Never assume response structure. ### Test Development Workflow (Discovery-First) **Step 1: Discovery Commands (Mandatory)** ```bash # Test successful execution npx aegis query [tool_name] '[valid_params]' --config "config.json" # Test failure scenarios npx aegis query [tool_name] '[invalid_params]' --config "config.json" npx aegis query [tool_name] '' --config "config.json" # Empty/missing params ``` **Step 2: Document Findings & Write Tests** ```yaml # Document discovery results as comments # Discovery: npx aegis query search_sfcc_classes 'query:catalog' # Success: ["dw.catalog.Product", "dw.catalog.Catalog"] (simple array) # Empty: [] (empty array) # Error: {"content": [{"type": "text", "text": "Error: ..."}], "isError": true} - it: "should return class array" expect: response: result: text: "match:regex:\\[[\\s\\S]*\\]" # Based on actual format text: "match:contains:dw.catalog" # Contains expected content ``` **Step 3: Common Discovery Patterns** ```bash # Examples with expected patterns: npx aegis query list_tools --config "config.json" # → ["tool1", "tool2"] → YAML: text: "match:regex:\\[[\\s\\S]*\\]" npx aegis query search_nothing 'query:zzznothingfound' --config "config.json" # → [] → YAML: text: "match:regex:^\\[\\s*\\]$" npx aegis query invalid_tool 'bad:params' --config "config.json" # → {"content": [...], "isError": true} → YAML: text: "match:contains:Error" ``` ## Configuration & Basic Usage ### Required Configuration (`*.config.json`) ```json { "name": "My MCP Server", // Human-readable name for reports "command": "node", // Executable (node, python, ./binary) "args": ["./server.js"], // Arguments array "cwd": "./optional/directory", // Working directory (optional) "env": {"CUSTOM_VAR": "value"}, // Environment variables (optional) "startupTimeout": 5000, // Max startup wait (ms, default: 10000) "readyPattern": "Server ready" // Stderr regex for ready state (optional) } ``` ### Common Configurations ```json // Node.js Server {"name": "Node.js MCP", "command": "node", "args": ["./dist/index.js"], "startupTimeout": 5000} // Python Server {"name": "Python MCP", "command": "python", "args": ["-m", "my_mcp_server"], "env": {"PYTHONPATH": "./src"}} // Development Server {"name": "Dev Server", "command": "npm", "args": ["run", "dev"], "startupTimeout": 15000} ``` ### Basic Test Structure (`*.test.mcp.yml`) ```yaml description: "Test suite description" tests: - it: "Test description" request: jsonrpc: "2.0" id: "unique-id" method: "tools/list|tools/call" params: {} # or tool call params expect: response: jsonrpc: "2.0" id: "unique-id" result: {} # expected response stderr: "toBeEmpty" # optional ``` ### Execute Tests ```bash # 🥇 GOLDEN RULE: ALWAYS discover response formats first! # Before writing any test, run these discovery commands: npx aegis query [tool_name] '[success_params]' --config "config.json" npx aegis query [tool_name] '[failure_params]' --config "config.json" # Then run your tests based on discovered formats npx aegis "tests/**/*.test.mcp.yml" --config "config.json" npx aegis "tests/*.yml" --config "config.json" --verbose --filter "tools" ``` ## Parameter Formats: Pipe vs JSON ### Pipe Format (Recommended for CLI) ```bash # Simple: No parameters npx aegis query get_code_versions --config "config.json" # Simple: key:value|other:123 npx aegis query read_file 'path:test.txt' --config "config.json" # Nested via dot notation: config.host:localhost|config.port:8080 npx aegis query api_client 'config.host:localhost|config.port:8080|timeout:30' --config "config.json" # Method syntax: name:tool|arguments.key:value npx aegis query --method tools/call --params 'name:read_file|arguments.path:test.txt' --config "config.json" ``` ### JSON Format (Complex Structures) ```bash npx aegis query complex_tool '{"config": {"host": "localhost"}, "data": [1,2,3]}' --config "config.json" ``` **Note**: For tools with no parameters, omit arguments entirely rather than using `'{}'` - use `npx aegis query tool_name --config "config.json"` instead of `npx aegis query tool_name '{}' --config "config.json"`. ## Complete Pattern Matching Reference (35+) ### Core Patterns ```yaml # 1. DEEP EQUALITY (default) - Exact match result: {tools: [{name: "read_file", description: "Reads a file"}]} # 2. TYPE VALIDATION result: tools: "match:type:array" count: "match:type:number" name: "match:type:string" # 3. STRING PATTERNS result: text: "match:contains:substring" name: "match:startsWith:prefix" file: "match:endsWith:.txt" pattern: "match:regex:\\d{4}-\\d{2}-\\d{2}" # YAML: escape backslashes # String length validation title: "match:stringLength:10" # Exactly 10 characters description: "match:stringLengthGreaterThan:5" # More than 5 chars summary: "match:stringLengthLessThan:100" # Less than 100 chars content: "match:stringLengthBetween:10:200" # Between 10-200 chars error: "match:stringEmpty" # Must be empty text: "match:stringNotEmpty" # Must not be empty # 4. ARRAY PATTERNS result: tools: "match:arrayLength:3" data: "match:arrayContains:value" tools: "match:arrayContains:name:read_file" # Object field matching tools: match:arrayElements: # All elements must match name: "match:type:string" description: "match:contains:tool" # 5. FIELD EXTRACTION (dot notation) result: match:extractField: "tools.*.name" # Extract all tool names value: ["read_file", "write_file"] # 6. NUMERIC COMPARISONS result: count: "match:greaterThan:5" price: "match:lessThanOrEqual:100.50" amount: "match:greaterThanOrEqual:0" score: "match:between:0:100" exact: "match:equals:42" not_equal: "match:notEquals:0" approximate: "match:approximately:3.14159:0.001" # tolerance decimal: "match:decimalPlaces:2" # exactly 2 decimal places multiple: "match:multipleOf:5" # divisible by 5 # 7. DATE/TIMESTAMP PATTERNS result: createdAt: "match:dateValid" publishDate: "match:dateAfter:2023-01-01" expireDate: "match:dateBefore:2025-01-01" eventDate: "match:dateBetween:2023-01-01:2024-12-31" lastUpdate: "match:dateAge:1d" # within last day # 8. CROSS-FIELD VALIDATION result: "match:crossField": "price > minPrice" # Field comparison "match:crossField": "endDate >= startDate" # 9. PATTERN NEGATION (prefix with "not:") result: tools: "match:not:arrayLength:0" # NOT empty text: "match:not:contains:error" # NOT containing error # 10. PARTIAL MATCHING result: match:partial: # Only check specified fields tools: - name: "read_file" description: "match:contains:Reads" # 11. COMBINED PATTERNS - arrayElements + partial (POWERFUL!) result: tools: match:arrayElements: # Apply to ALL array elements match:partial: # But only validate specified fields name: "match:regex:^[a-z_]+$" description: "match:contains:tool" # Ignores any other fields like inputSchema, etc. # 12. ADVANCED PATTERNS result: email: "match:regex:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" name: "match:equalsIgnoreCase:Hello World" text: "match:containsIgnoreCase:ERROR" value: "match:range:0:100" # Between 0-100 inclusive score: "match:between:60:90" # Between 60-90 exclusive match:extractField: "tools.*.inputSchema.properties.*.type" value: ["string", "number", "object"] ``` ## YAML Syntax Rules & Real-World Examples ### ❌ Common Errors to Avoid ```yaml # WRONG - Duplicate YAML keys (overwrites previous) result: tools: "match:arrayLength:1" tools: ["read_file"] # OVERWRITES above line! # WRONG - Invalid escaping in regex result: text: "match:regex:\d+" # Missing double backslash # WRONG - Mixing patterns in same object result: tools: "match:arrayLength:1" match:extractField: "tools.*.name" # Can't mix in same object ``` ### ✅ Correct Best Practices ```yaml # CORRECT - Separate pattern validations into different tests - it: "should have exactly one tool" expect: response: result: tools: "match:arrayLength:1" - it: "should extract correct tool name" expect: response: result: match:extractField: "tools.*.name" value: ["read_file"] # CORRECT - Proper regex escaping in YAML result: text: "match:regex:\\d{4}-\\d{2}-\\d{2}" # Double backslashes # CORRECT - Flexible arrayElements with partial matching result: tools: match:arrayElements: match:partial: # 🔥 Only validate what matters, ignore extra fields name: "match:regex:^[a-z][a-z0-9_]*$" description: "match:regex:.{10,}" ``` ### Real-World Test Examples #### Tool Discovery Test ```yaml - it: "should list available tools with correct structure" request: jsonrpc: "2.0" id: "list-1" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "list-1" result: tools: match:arrayElements: match:partial: # Flexible - recommended approach name: "match:regex:^[a-z][a-z0-9_]*$" # snake_case description: "match:regex:.{10,}" # min 10 chars ``` #### Tool Execution Test ```yaml - it: "should execute tool successfully" request: jsonrpc: "2.0" id: "exec-1" method: "tools/call" params: name: "read_file" arguments: path: "test.txt" expect: response: jsonrpc: "2.0" id: "exec-1" result: content: - type: "text" text: "match:contains:expected content" isError: false stderr: "toBeEmpty" ``` #### Error Handling Test ```yaml - it: "should handle invalid file gracefully" request: jsonrpc: "2.0" id: "error-1" method: "tools/call" params: name: "read_file" arguments: path: "nonexistent.txt" expect: response: jsonrpc: "2.0" id: "error-1" result: content: - type: "text" text: "match:contains:not found" isError: true ``` #### Performance Testing with SLA Validation ```yaml - it: "should meet performance SLA" request: jsonrpc: "2.0" id: "perf-1" method: "tools/call" params: name: "search_tools" arguments: category: "documentation" expect: response: result: tools: match:arrayElements: match:partial: name: "match:regex:^[a-z][a-z0-9_]*$" description: "match:regex:.{10,}" count: "match:type:number" performance: maxResponseTime: "1500ms" # SLA requirement stderr: "toBeEmpty" ``` ## CLI Commands & Interactive Testing ### Test Execution ```bash # Basic testing npx aegis "tests/**/*.test.mcp.yml" --config "config.json" # Debug modes & filtering npx aegis "tests/*.yml" --config "config.json" --verbose --debug --timing npx aegis "tests/*.yml" --config "config.json" --errors-only --filter "tools" npx aegis "tests/*.yml" --config "config.json" --filter "should validate" --json ``` ### Interactive Tool Testing (Dual Format Support) ```bash # List all tools npx aegis query --config "config.json" # Pipe format (recommended) - ALWAYS test success AND failure npx aegis query read_file 'path:test.txt' --config "config.json" # Success case npx aegis query read_file 'path:nonexistent.txt' --config "config.json" # Failure case # JSON format for complex structures npx aegis query complex_tool '{"config": {"host": "localhost"}, "data": [1,2,3]}' --config "config.json" # Discovery examples for edge cases npx aegis query search_tool 'query:' --config "config.json" # Empty input npx aegis query search_tool 'query:zzznothingfound' --config "config.json" # No results ``` ### Performance Testing Guidelines | Operation Type | Recommended Timeout | Use Case | |----------------|-------------------|----------| | Tool Listing | `200-500ms` | Metadata operations | | Simple File Ops | `1000ms` | Basic I/O | | Complex Operations | `2000ms` | Search, computation | | Error Responses | `800ms` | Should be faster than success | | Heavy Operations | `5000ms` | Database, large files | **Always use `--timing` flag** to see actual response times and adjust expectations. ## Pattern Selection Guide & Advanced Combinations ### When to Use Each Pattern - **Deep Equality**: Exact value matching (default) - **Type Validation**: Verify data types (`match:type:`) - **String Patterns**: Text validation (`contains`, `startsWith`, `endsWith`, `regex`, `stringLength`) - **Array Patterns**: Array validation (`arrayLength`, `arrayContains`, `arrayElements`) - **Field Extraction**: Extract nested values (`match:extractField`) - **Numeric**: Math comparisons (`greaterThan`, `approximately`, `decimalPlaces`) - **Date/Time**: Date validation (`dateValid`, `dateAfter`, `dateAge`) - **Cross-Field**: Compare fields (`match:crossField`) - **Negation**: Exclude patterns (`match:not:*`) - **Partial**: Subset validation (`match:partial`) - **🔥 Combined arrayElements + partial**: Validate specific fields across ALL array elements while ignoring others - extremely powerful for flexible schema validation! ### Multi-Step Validation Pattern (Recommended) ```yaml # Test 1: Basic structure - it: "should return array of tools" expect: response: result: tools: "match:type:array" # Test 2: Array length - it: "should have expected number of tools" expect: response: result: tools: "match:arrayLength:3" # Test 3: Extract and validate specific fields - it: "should have correct tool names" expect: response: result: match:extractField: "tools.*.name" value: ["read_file", "write_file", "list_files"] ``` ### Comprehensive Tool Validation (Best Practice) ```yaml - it: "should validate tools with flexible schema handling" expect: response: result: tools: match:arrayElements: match:partial: # 🔥 RECOMMENDED: Combines power with flexibility name: "match:regex:^[a-z][a-z0-9_]*$" # snake_case names description: "match:regex:.{10,200}" # 10-200 chars inputSchema: type: "object" properties: "match:type:object" ``` ## MCP Protocol Basics ### Standard JSON-RPC 2.0 Methods #### Initialize Request (Required for handshake) ```yaml request: jsonrpc: "2.0" id: "init-1" method: "initialize" params: protocolVersion: "2025-06-18" capabilities: {"tools": {}} clientInfo: {"name": "MCP Aegis", "version": "1.0.0"} ``` #### Tools List Request ```yaml request: jsonrpc: "2.0" id: "list-1" method: "tools/list" params: {} ``` #### Tool Call Request ```yaml request: jsonrpc: "2.0" id: "call-1" method: "tools/call" params: name: "tool_name" arguments: key: "value" ``` ### Standard Response Structure ```yaml expect: response: jsonrpc: "2.0" id: "matching-request-id" result: # For successful responses # Response data # OR for errors: error: code: -32601 # Standard JSON-RPC error codes message: "Method not found" stderr: "toBeEmpty" # Optional stderr validation ``` ## MCP Protocol Basics & Troubleshooting ### Standard JSON-RPC 2.0 Methods ```yaml # Initialize Request (Required for handshake) request: jsonrpc: "2.0" id: "init-1" method: "initialize" params: protocolVersion: "2025-06-18" capabilities: {"tools": {}} clientInfo: {"name": "MCP Aegis", "version": "1.0.0"} # Tools List Request request: jsonrpc: "2.0" id: "list-1" method: "tools/list" params: {} # Tool Call Request request: jsonrpc: "2.0" id: "call-1" method: "tools/call" params: name: "tool_name" arguments: key: "value" # Standard Response Structure expect: response: jsonrpc: "2.0" id: "matching-request-id" result: {} # For successful responses # OR for errors: error: code: -32601 # Standard JSON-RPC error codes message: "Method not found" stderr: "toBeEmpty" # Optional stderr validation ``` ## Quick Debugging Workflow ### 🥇 Discovery-First Development (Essential) **Before ANY test writing, run discovery commands:** ```bash # Success case npx aegis query [tool_name] '[valid_params]' --config "config.json" # Failure case npx aegis query [tool_name] '[invalid_params]' --config "config.json" ``` ### Common Issues & Solutions #### Server Won't Start ```bash npx aegis "test.yml" --config "config.json" --debug # Check startup issues # Increase startupTimeout if server is slow # Verify command/args in config are correct ``` #### Pattern Not Matching ```yaml # ❌ WRONG - Assuming structure without discovery - it: "should return tools object" expect: response: result: tools: "match:arrayLength:3" # Assumes 'tools' field exists count: "match:type:number" # Assumes 'count' field exists # ✅ CORRECT - Based on actual npx aegis query discovery # First run: npx aegis query list_tools --config "config.json" # Result: ["tool1", "tool2", "tool3"] # Simple array, no wrapper object! - it: "should return tools array" expect: response: result: text: "match:regex:\\[[\\s\\S]*\\]" # Match JSON array format text: "match:contains:tool1" # Check content exists ``` #### Regex Patterns Failing ```yaml # YAML requires double escaping backslashes text: "match:regex:\\d+" # ✅ Correct text: "match:regex:\d+" # ❌ Wrong ``` #### Array Pattern Issues ```yaml # ❌ Duplicate YAML keys (overwrites previous) result: tools: "match:arrayLength:1" tools: ["exact_tool"] # OVERWRITES above! # ✅ Use flexible arrayElements with partial matching instead result: tools: match:arrayElements: match:partial: # Only validate what you care about name: "match:type:string" description: "match:type:string" ``` ### 2. Test Filtering for Focus Development ```bash # Filter by suite/test description (case-insensitive) npx aegis "tests/**/*.yml" --config "config.json" --filter "Tools validation" npx aegis "tests/**/*.yml" --config "config.json" --filter "should handle errors" # Use regex patterns for advanced filtering npx aegis "tests/**/*.yml" --config "config.json" --filter "/should (read|write|validate)/" # Combine with debugging options npx aegis "tests/**/*.yml" --config "config.json" --filter "tools" --errors-only --timing ``` ### 3. Pipe Format Quick Reference ```bash # Basic syntax: key:value|other:123 'path:test.txt|encoding:utf8' # Nested objects: config.host:localhost|config.port:8080 'database.host:localhost|database.port:5432|cache.enabled:true' # Auto data types: text:hello|count:42|enabled:true|data:null 'operation:add|a:5|b:3|precise:true' # JSON values within pipe: simple:value|complex:{"nested":"object"} 'metadata:{"version":"1.0"}|tags:["test","demo"]|count:5' ``` ### 3. Pipe Format Quick Reference ```bash # Basic: key:value|other:123 'path:test.txt|encoding:utf8' # Discovery examples: 'query:catalog|limit:10' # Success case 'query:zzznothingfound' # No results 'query:' # Validation error 'fileName:nonexistent.txt' # File not found ``` ### 4. Performance Testing Guidelines - **Tool Listing**: 200-500ms (metadata - should be fast) - **Simple File Ops**: 1000ms (basic I/O operations) - **Complex Operations**: 2000ms (search, computation, API calls) - **Error Responses**: 800ms (often faster than successful operations) - **Heavy Operations**: 5000ms (database queries, large file processing) Use `--timing` flag to see actual response times and adjust expectations based on your environment. ## Complete Pattern Reference (35+ Patterns) ```yaml # STRING PATTERNS "match:contains:substring" "match:startsWith:prefix" "match:endsWith:suffix" "match:containsIgnoreCase:TEXT" "match:equalsIgnoreCase:value" "match:regex:pattern" "match:stringLength:10" "match:stringLengthGreaterThan:5" "match:stringLengthLessThan:100" "match:stringLengthBetween:10:200" "match:stringEmpty|stringNotEmpty" # TYPE & STRUCTURE "match:type:string|number|boolean|object|array" "match:exists" "match:length:5" # NUMERIC PATTERNS "match:greaterThan:10" "match:lessThanOrEqual:50" "match:between:10:90" "match:equals:42" "match:notEquals:0" "match:approximately:3.14:0.01" "match:multipleOf:5" "match:decimalPlaces:2" # ARRAY PATTERNS "match:arrayLength:3" "match:arrayContains:value" match:arrayElements: field: "match:type:string" # DATE PATTERNS "match:dateValid" "match:dateAfter:2023-01-01" "match:dateBetween:2023-01-01:2024-12-31" "match:dateAge:1d" # ADVANCED PATTERNS "match:crossField": "field1 > field2" "match:not:pattern" # Negate any pattern match:extractField: "path.*.field" match:partial: # Check subset of fields match:arrayElements: # Validate ALL array elements match:partial: # 🔥 POWERFUL COMBO field: "pattern" ``` ### Installation & Getting Started ```bash npm install -g mcp-aegis npx aegis init # Create sample config and tests npx aegis "tests/*.yml" --config "config.json" ``` ``` -------------------------------------------------------------------------------- /docs/dw_customer/ProductList.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.customer # Class ProductList ## Inheritance Hierarchy - Object - dw.object.PersistentObject - dw.object.ExtensibleObject - dw.customer.ProductList ## Description Represents a list of products (and optionally a gift certificate) that is typically maintained by a customer. This class can be used to implement a number of different storefront features, e.g. shopping list, wish list and gift registry. A product list is always owned by a customer. The owner can be anonymous or a registered customer. The owner can be the person for which items from that list will be purchased (wish list). Or it can be a person who maintains the list, for example a gift registry, on behalf of the bridal couple. Each product list can have a registrant and a co-registrant. A registrant is typically associated with an event related product list such as a gift registry. It holds information about a person associated with the event such as a bride or groom. A shipping address can be associated with this product list to ship the items, e.g. to an event location. A post-event shipping address can be associated to ship items to which could not be delivered on event date. The product list can also hold information about the event date and event location. ## Constants ### EXPORT_STATUS_EXPORTED **Type:** Number = 1 Constant for when Export Status is Exported ### EXPORT_STATUS_NOTEXPORTED **Type:** Number = 0 Constant for when Export Status is Not Exported ### TYPE_CUSTOM_1 **Type:** Number = 100 Constant representing a custom list type attribute. ### TYPE_CUSTOM_2 **Type:** Number = 101 Constant representing a custom list type attribute. ### TYPE_CUSTOM_3 **Type:** Number = 102 Constant representing a custom list type attribute. ### TYPE_GIFT_REGISTRY **Type:** Number = 11 Constant representing the gift registry type attribute. ### TYPE_SHOPPING_LIST **Type:** Number = 12 Constant representing the shopping list type attribute. ### TYPE_WISH_LIST **Type:** Number = 10 Constant representing the wish list registry type attribute. ## Properties ### anonymous **Type:** boolean (Read Only) Returns true if this product list is owned by an anonymous customer. ### coRegistrant **Type:** ProductListRegistrant (Read Only) The ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. ### currentShippingAddress **Type:** CustomerAddress (Read Only) This is a helper method typically used with an event related list. It provides the appropriate shipping address based on the eventDate. If the current date is after the eventDate, then the postEventShippingAddress is returned, otherwise the shippingAddress is returned. If the eventDate is null, then null is returned. ### description **Type:** String A description text that, for example, explains the purpose of this product list. ### eventCity **Type:** String For event related uses (e.g. gift registry), this holds the event city. ### eventCountry **Type:** String For event related uses (e.g. gift registry), this holds the event country. ### eventDate **Type:** Date For event related uses (e.g. gift registry), this holds the date of the event. ### eventState **Type:** String For event related uses (e.g. gift registry), this holds the event state. ### eventType **Type:** String For event related uses (e.g. gift registry), this holds the type of event, e.g. Wedding, Baby Shower. ### exportStatus **Type:** EnumValue (Read Only) The export status of the product list. Possible values are: EXPORT_STATUS_NOTEXPORTED, EXPORT_STATUS_EXPORTED. ### giftCertificateItem **Type:** ProductListItem (Read Only) The item in the list that represents a gift certificate. ### ID **Type:** String (Read Only) The unique system generated ID of the object. ### items **Type:** Collection (Read Only) A collection containing all items in the list. ### lastExportTime **Type:** Date (Read Only) The date where this product list has been exported successfully the last time. ### name **Type:** String The name of this product list given by its owner. ### owner **Type:** Customer (Read Only) The customer that created and owns the product list. ### postEventShippingAddress **Type:** CustomerAddress The shipping address for purchases made after the event date. ### productItems **Type:** Collection (Read Only) A collection containing all items in the list that reference products. ### public **Type:** boolean A flag, typically used to determine if the object is searchable by other customers. ### publicItems **Type:** Collection (Read Only) A collection containing all items in the list that are flagged as public. ### purchases **Type:** Collection (Read Only) The aggregated purchases from all the individual items. ### registrant **Type:** ProductListRegistrant (Read Only) The ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. ### shippingAddress **Type:** CustomerAddress Return the address that should be used as the shipping address for purchases made from the list. ### type **Type:** Number (Read Only) An int representing the type of object (e.g. wish list, gift registry). This is set at object creation time. ## Constructor Summary ## Method Summary ### createCoRegistrant **Signature:** `createCoRegistrant() : ProductListRegistrant` Create a ProductListRegistrant and assign it to the coRegistrant attribute of the list. ### createGiftCertificateItem **Signature:** `createGiftCertificateItem() : ProductListItem` Create an item in the list that represents a gift certificate. ### createProductItem **Signature:** `createProductItem(product : Product) : ProductListItem` Create an item in the list that references the specified product. ### createRegistrant **Signature:** `createRegistrant() : ProductListRegistrant` Create a ProductListRegistrant and assign it to the registrant attribute of the list. ### getCoRegistrant **Signature:** `getCoRegistrant() : ProductListRegistrant` Returns the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. ### getCurrentShippingAddress **Signature:** `getCurrentShippingAddress() : CustomerAddress` This is a helper method typically used with an event related list. ### getDescription **Signature:** `getDescription() : String` Returns a description text that, for example, explains the purpose of this product list. ### getEventCity **Signature:** `getEventCity() : String` For event related uses (e.g. ### getEventCountry **Signature:** `getEventCountry() : String` For event related uses (e.g. ### getEventDate **Signature:** `getEventDate() : Date` For event related uses (e.g. ### getEventState **Signature:** `getEventState() : String` For event related uses (e.g. ### getEventType **Signature:** `getEventType() : String` For event related uses (e.g. ### getExportStatus **Signature:** `getExportStatus() : EnumValue` Returns the export status of the product list. Possible values are: EXPORT_STATUS_NOTEXPORTED, EXPORT_STATUS_EXPORTED. ### getGiftCertificateItem **Signature:** `getGiftCertificateItem() : ProductListItem` Returns the item in the list that represents a gift certificate. ### getID **Signature:** `getID() : String` Returns the unique system generated ID of the object. ### getItem **Signature:** `getItem(ID : String) : ProductListItem` Returns the item from the list that has the specified ID. ### getItems **Signature:** `getItems() : Collection` Returns a collection containing all items in the list. ### getLastExportTime **Signature:** `getLastExportTime() : Date` Returns the date where this product list has been exported successfully the last time. ### getName **Signature:** `getName() : String` Returns the name of this product list given by its owner. ### getOwner **Signature:** `getOwner() : Customer` Returns the customer that created and owns the product list. ### getPostEventShippingAddress **Signature:** `getPostEventShippingAddress() : CustomerAddress` Returns the shipping address for purchases made after the event date. ### getProductItems **Signature:** `getProductItems() : Collection` Returns a collection containing all items in the list that reference products. ### getPublicItems **Signature:** `getPublicItems() : Collection` Returns a collection containing all items in the list that are flagged as public. ### getPurchases **Signature:** `getPurchases() : Collection` Returns the aggregated purchases from all the individual items. ### getRegistrant **Signature:** `getRegistrant() : ProductListRegistrant` Returns the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. ### getShippingAddress **Signature:** `getShippingAddress() : CustomerAddress` Return the address that should be used as the shipping address for purchases made from the list. ### getType **Signature:** `getType() : Number` Returns an int representing the type of object (e.g. ### isAnonymous **Signature:** `isAnonymous() : boolean` Returns true if this product list is owned by an anonymous customer. ### isPublic **Signature:** `isPublic() : boolean` A flag, typically used to determine if the object is searchable by other customers. ### removeCoRegistrant **Signature:** `removeCoRegistrant() : void` Removes the ProductListRegistrant assigned to the coRegistrant attribute. ### removeItem **Signature:** `removeItem(item : ProductListItem) : void` Removes the specified item from the list. ### removeRegistrant **Signature:** `removeRegistrant() : void` Removes the ProductListRegistrant assigned to the registrant attribute. ### setDescription **Signature:** `setDescription(description : String) : void` Set the description of this product list. ### setEventCity **Signature:** `setEventCity(eventCity : String) : void` Set the event city to which this product list is related. ### setEventCountry **Signature:** `setEventCountry(eventCountry : String) : void` Set the event country to which this product list is related. ### setEventDate **Signature:** `setEventDate(eventDate : Date) : void` Set the date of the event to which this product list is related. ### setEventState **Signature:** `setEventState(eventState : String) : void` Set the event state to which this product list is related. ### setEventType **Signature:** `setEventType(eventType : String) : void` Set the event type for which this product list was created by the owner. ### setName **Signature:** `setName(name : String) : void` Set the name of this product list. ### setPostEventShippingAddress **Signature:** `setPostEventShippingAddress(address : CustomerAddress) : void` This is typically used by an event related list (e.g. ### setPublic **Signature:** `setPublic(flag : boolean) : void` Makes this product list visible to other customers or hides it. ### setShippingAddress **Signature:** `setShippingAddress(address : CustomerAddress) : void` Associate an address, used as the shipping address for purchases made from the list. ## Method Detail ## Method Details ### createCoRegistrant **Signature:** `createCoRegistrant() : ProductListRegistrant` **Description:** Create a ProductListRegistrant and assign it to the coRegistrant attribute of the list. An exception is thrown if the list already has a coRegistrant assigned to it. **Returns:** the created ProductListRegistrant instance. **Throws:** CreateException - if one already exists --- ### createGiftCertificateItem **Signature:** `createGiftCertificateItem() : ProductListItem` **Description:** Create an item in the list that represents a gift certificate. A list may only contain a single gift certificate, so an exception is thrown if one already exists in the list. **Returns:** the created item. **Throws:** CreateException - if a gift certificate item already exists in the list. --- ### createProductItem **Signature:** `createProductItem(product : Product) : ProductListItem` **Description:** Create an item in the list that references the specified product. **Parameters:** - `product`: the product to use to create the list item. **Returns:** the created item. --- ### createRegistrant **Signature:** `createRegistrant() : ProductListRegistrant` **Description:** Create a ProductListRegistrant and assign it to the registrant attribute of the list. An exception is thrown if the list already has a registrant assigned to it. **Returns:** the created ProductListRegistrant instance. **Throws:** CreateException - if one already exists --- ### getCoRegistrant **Signature:** `getCoRegistrant() : ProductListRegistrant` **Description:** Returns the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. **Returns:** the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. --- ### getCurrentShippingAddress **Signature:** `getCurrentShippingAddress() : CustomerAddress` **Description:** This is a helper method typically used with an event related list. It provides the appropriate shipping address based on the eventDate. If the current date is after the eventDate, then the postEventShippingAddress is returned, otherwise the shippingAddress is returned. If the eventDate is null, then null is returned. **Returns:** the appropriate address, as described above. --- ### getDescription **Signature:** `getDescription() : String` **Description:** Returns a description text that, for example, explains the purpose of this product list. **Returns:** a description text explaining the purpose of this product list. Returns an empty string if the description is not set. --- ### getEventCity **Signature:** `getEventCity() : String` **Description:** For event related uses (e.g. gift registry), this holds the event city. **Returns:** the event city. The event city or an empty string if no event city is set. --- ### getEventCountry **Signature:** `getEventCountry() : String` **Description:** For event related uses (e.g. gift registry), this holds the event country. **Returns:** the event country. The event country or an empty string if no event country is set. --- ### getEventDate **Signature:** `getEventDate() : Date` **Description:** For event related uses (e.g. gift registry), this holds the date of the event. **Returns:** the date of the event. --- ### getEventState **Signature:** `getEventState() : String` **Description:** For event related uses (e.g. gift registry), this holds the event state. **Returns:** the event state. The event state or an empty string if no event state is set. --- ### getEventType **Signature:** `getEventType() : String` **Description:** For event related uses (e.g. gift registry), this holds the type of event, e.g. Wedding, Baby Shower. **Returns:** the type of event. Returns an empty string, if not set. --- ### getExportStatus **Signature:** `getExportStatus() : EnumValue` **Description:** Returns the export status of the product list. Possible values are: EXPORT_STATUS_NOTEXPORTED, EXPORT_STATUS_EXPORTED. **Returns:** Product list export status --- ### getGiftCertificateItem **Signature:** `getGiftCertificateItem() : ProductListItem` **Description:** Returns the item in the list that represents a gift certificate. **Returns:** the gift certificate item, or null if it doesn't exist. --- ### getID **Signature:** `getID() : String` **Description:** Returns the unique system generated ID of the object. **Returns:** the ID of object. --- ### getItem **Signature:** `getItem(ID : String) : ProductListItem` **Description:** Returns the item from the list that has the specified ID. **Parameters:** - `ID`: the product list item identifier. **Returns:** the specified item, or null if it's not found in the list. --- ### getItems **Signature:** `getItems() : Collection` **Description:** Returns a collection containing all items in the list. **Returns:** all items. --- ### getLastExportTime **Signature:** `getLastExportTime() : Date` **Description:** Returns the date where this product list has been exported successfully the last time. **Returns:** The time of the last successful export or null if this product list was not exported yet. --- ### getName **Signature:** `getName() : String` **Description:** Returns the name of this product list given by its owner. **Returns:** the name of this product list. Returns an empty string if the name is not set. --- ### getOwner **Signature:** `getOwner() : Customer` **Description:** Returns the customer that created and owns the product list. **Returns:** Owning customer --- ### getPostEventShippingAddress **Signature:** `getPostEventShippingAddress() : CustomerAddress` **Description:** Returns the shipping address for purchases made after the event date. **Returns:** the shipping address for purchases made after the event date. Returns null if no post-event shipping address is associated. --- ### getProductItems **Signature:** `getProductItems() : Collection` **Description:** Returns a collection containing all items in the list that reference products. **Returns:** all product items. --- ### getPublicItems **Signature:** `getPublicItems() : Collection` **Description:** Returns a collection containing all items in the list that are flagged as public. **Returns:** all public items. --- ### getPurchases **Signature:** `getPurchases() : Collection` **Description:** Returns the aggregated purchases from all the individual items. **Returns:** purchases --- ### getRegistrant **Signature:** `getRegistrant() : ProductListRegistrant` **Description:** Returns the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. **Returns:** the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. --- ### getShippingAddress **Signature:** `getShippingAddress() : CustomerAddress` **Description:** Return the address that should be used as the shipping address for purchases made from the list. **Returns:** the shipping address. The shipping address of this list or null if no address is associated. --- ### getType **Signature:** `getType() : Number` **Description:** Returns an int representing the type of object (e.g. wish list, gift registry). This is set at object creation time. **Returns:** the type of object. --- ### isAnonymous **Signature:** `isAnonymous() : boolean` **Description:** Returns true if this product list is owned by an anonymous customer. **Returns:** true if the owner of this product list is anonymous, false otherwise. --- ### isPublic **Signature:** `isPublic() : boolean` **Description:** A flag, typically used to determine if the object is searchable by other customers. **Returns:** true if the product list is public. False otherwise. --- ### removeCoRegistrant **Signature:** `removeCoRegistrant() : void` **Description:** Removes the ProductListRegistrant assigned to the coRegistrant attribute. --- ### removeItem **Signature:** `removeItem(item : ProductListItem) : void` **Description:** Removes the specified item from the list. This will also cause all purchase information associated with that item to be removed. **Parameters:** - `item`: The item to remove. --- ### removeRegistrant **Signature:** `removeRegistrant() : void` **Description:** Removes the ProductListRegistrant assigned to the registrant attribute. --- ### setDescription **Signature:** `setDescription(description : String) : void` **Description:** Set the description of this product list. **Parameters:** - `description`: The description of this product list. The description can have up to 256 characters, longer descriptions get truncated. If an empty string is provided, the description gets set to null. --- ### setEventCity **Signature:** `setEventCity(eventCity : String) : void` **Description:** Set the event city to which this product list is related. **Parameters:** - `eventCity`: The event city can have up to 256 characters, longer event city get truncated. If an empty string is provided, the event city gets set to null. --- ### setEventCountry **Signature:** `setEventCountry(eventCountry : String) : void` **Description:** Set the event country to which this product list is related. **Parameters:** - `eventCountry`: The event country can have up to 256 characters, longer event country get truncated. If an empty string is provided, the event country gets set to null. --- ### setEventDate **Signature:** `setEventDate(eventDate : Date) : void` **Description:** Set the date of the event to which this product list is related. **Parameters:** - `eventDate`: The event date or null if no event date should be available. --- ### setEventState **Signature:** `setEventState(eventState : String) : void` **Description:** Set the event state to which this product list is related. **Parameters:** - `eventState`: The event state can have up to 256 characters, longer event state get truncated. If an empty string is provided, the event state gets set to null. --- ### setEventType **Signature:** `setEventType(eventType : String) : void` **Description:** Set the event type for which this product list was created by the owner. **Parameters:** - `eventType`: The event type can have up to 256 characters, longer event type get truncated. If an empty string is provided, the event type gets set to null. --- ### setName **Signature:** `setName(name : String) : void` **Description:** Set the name of this product list. **Parameters:** - `name`: The name of this product list. The name can have up to 256 characters, longer names get truncated. If an empty string is provided, the name gets set to null. --- ### setPostEventShippingAddress **Signature:** `setPostEventShippingAddress(address : CustomerAddress) : void` **Description:** This is typically used by an event related list (e.g. gift registry) to specify a shipping address for purchases made after the event date. **Parameters:** - `address`: The shipping address. --- ### setPublic **Signature:** `setPublic(flag : boolean) : void` **Description:** Makes this product list visible to other customers or hides it. **Parameters:** - `flag`: If true, this product list becomes visible to other customers. If false, this product list can only be seen and searched by its owner. --- ### setShippingAddress **Signature:** `setShippingAddress(address : CustomerAddress) : void` **Description:** Associate an address, used as the shipping address for purchases made from the list. **Parameters:** - `address`: The shipping address. --- ``` -------------------------------------------------------------------------------- /docs/TopLevel/Date.md: -------------------------------------------------------------------------------- ```markdown ## Package: TopLevel # Class Date ## Inheritance Hierarchy - Object - Date ## Description A Date object contains a number indicating a particular instant in time to within a millisecond. The number may also be NaN, indicating that the Date object does not represent a specific instant of time. ## Constructor Summary Date() Constructs the Date instance using the current date and time. Date(millis : Number) Constructs the Date instance using the specified milliseconds. Date(year : Number, month : Number, args : Number...) Constructs the Date instance using the specified year and month. Date(dateString : String) Constructs the Date instance by parsing the specified String. ## Method Summary ### getDate **Signature:** `getDate() : Number` Returns the day of the month where the value is a Number from 1 to 31. ### getDay **Signature:** `getDay() : Number` Returns the day of the week where the value is a Number from 0 to 6. ### getFullYear **Signature:** `getFullYear() : Number` Returns the year of the Date in four-digit format. ### getHours **Signature:** `getHours() : Number` Return the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). ### getMilliseconds **Signature:** `getMilliseconds() : Number` Returns the milliseconds field of the Date. ### getMinutes **Signature:** `getMinutes() : Number` Return the minutes field of the Date where the value is a Number from 0 to 59. ### getMonth **Signature:** `getMonth() : Number` Returns the month of the year as a value between 0 and 11. ### getSeconds **Signature:** `getSeconds() : Number` Return the seconds field of the Date where the value is a Number from 0 to 59. ### getTime **Signature:** `getTime() : Number` Returns the internal, millisecond representation of the Date object. ### getTimezoneOffset **Signature:** `getTimezoneOffset() : Number` Returns the difference between local time and Greenwich Mean Time (GMT) in minutes. ### getUTCDate **Signature:** `getUTCDate() : Number` Returns the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. ### getUTCDay **Signature:** `getUTCDay() : Number` Returns the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. ### getUTCFullYear **Signature:** `getUTCFullYear() : Number` Returns the year when the Date is expressed in universal time. ### getUTCHours **Signature:** `getUTCHours() : Number` Return the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). ### getUTCMilliseconds **Signature:** `getUTCMilliseconds() : Number` Returns the milliseconds field, expressed in universal time, of the Date. ### getUTCMinutes **Signature:** `getUTCMinutes() : Number` Return the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. ### getUTCMonth **Signature:** `getUTCMonth() : Number` Returns the month of the year that results when the Date is expressed in universal time. ### getUTCSeconds **Signature:** `getUTCSeconds() : Number` Return the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. ### now **Signature:** `static now() : Number` Returns the number of milliseconds since midnight of January 1, 1970 up until now. ### parse **Signature:** `static parse(dateString : String) : Number` Takes a date string and returns the number of milliseconds since midnight of January 1, 1970. ### setDate **Signature:** `setDate(date : Number) : Number` Sets the day of the month where the value is a Number from 1 to 31. ### setFullYear **Signature:** `setFullYear(year : Number, args : Number...) : Number` Sets the full year of Date where the value must be a four-digit Number. ### setHours **Signature:** `setHours(hours : Number, args : Number...) : Number` Sets the hours field of this Date instance. ### setMilliseconds **Signature:** `setMilliseconds(millis : Number) : Number` Sets the milliseconds field of this Date instance. ### setMinutes **Signature:** `setMinutes(minutes : Number, args : Number...) : Number` Sets the minutes field of this Date instance. ### setMonth **Signature:** `setMonth(month : Number, date : Number...) : Number` Sets the month of the year where the value is a Number from 0 to 11. ### setSeconds **Signature:** `setSeconds(seconds : Number, millis : Number...) : Number` Sets the seconds field of this Date instance. ### setTime **Signature:** `setTime(millis : Number) : Number` Sets the number of milliseconds between the desired date and time and January 1, 1970. ### setUTCDate **Signature:** `setUTCDate(date : Number) : Number` Sets the day of the month, expressed in universal time, where the value is a Number from 1 to 31. ### setUTCFullYear **Signature:** `setUTCFullYear(year : Number, args : Number...) : Number` Sets the full year, expressed in universal time, of Date where the value must be a four-digit Number. ### setUTCHours **Signature:** `setUTCHours(hours : Number, args : Number...) : Number` Sets the hours field, expressed in universal time, of this Date instance. ### setUTCMilliseconds **Signature:** `setUTCMilliseconds(millis : Number) : Number` Sets the milliseconds field, expressed in universal time, of this Date instance. ### setUTCMinutes **Signature:** `setUTCMinutes(minutes : Number, args : Number...) : Number` Sets the minutes field, expressed in universal time, of this Date instance. ### setUTCMonth **Signature:** `setUTCMonth(month : Number, date : Number...) : Number` Sets the month of the year, expressed in universal time, where the value is a Number from 0 to 11. ### setUTCSeconds **Signature:** `setUTCSeconds(seconds : Number, millis : Number...) : Number` Sets the seconds field, expressed in universal time, of this Date instance. ### toDateString **Signature:** `toDateString() : String` Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). ### toISOString **Signature:** `toISOString() : String` This function returns a string value represent the instance in time represented by this Date object. ### toJSON **Signature:** `toJSON(key : String) : Object` This function returns the same string as Date.prototype.toISOString(). ### toLocaleDateString **Signature:** `toLocaleDateString() : String` Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). ### toLocaleString **Signature:** `toLocaleString() : String` Returns the Date as a String using the default locale (en_US). ### toLocaleTimeString **Signature:** `toLocaleTimeString() : String` Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). ### toTimeString **Signature:** `toTimeString() : String` Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). ### toUTCString **Signature:** `toUTCString() : String` Returns a String representation of this Date, expressed in universal time. ### UTC **Signature:** `static UTC(year : Number, month : Number, args : Number...) : Number` Returns the number of milliseconds since midnight of January 1, 1970 according to universal time. ### valueOf **Signature:** `valueOf() : Object` Returns the value of this Date represented in milliseconds. ## Constructor Detail ## Method Detail ## Method Details ### getDate **Signature:** `getDate() : Number` **Description:** Returns the day of the month where the value is a Number from 1 to 31. **Returns:** the day of the month where the value is a Number from 1 to 31. --- ### getDay **Signature:** `getDay() : Number` **Description:** Returns the day of the week where the value is a Number from 0 to 6. **Returns:** the day of the month where the value is a Number from 0 to 6. --- ### getFullYear **Signature:** `getFullYear() : Number` **Description:** Returns the year of the Date in four-digit format. **Returns:** the year of the Date in four-digit format. --- ### getHours **Signature:** `getHours() : Number` **Description:** Return the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). **Returns:** the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). --- ### getMilliseconds **Signature:** `getMilliseconds() : Number` **Description:** Returns the milliseconds field of the Date. **Returns:** the milliseconds field of the Date. --- ### getMinutes **Signature:** `getMinutes() : Number` **Description:** Return the minutes field of the Date where the value is a Number from 0 to 59. **Returns:** the minutes field of the Date where the value is a Number from 0 to 59. --- ### getMonth **Signature:** `getMonth() : Number` **Description:** Returns the month of the year as a value between 0 and 11. **Returns:** the month of the year as a value between 0 and 11. --- ### getSeconds **Signature:** `getSeconds() : Number` **Description:** Return the seconds field of the Date where the value is a Number from 0 to 59. **Returns:** the seconds field of the Date where the value is a Number from 0 to 59. --- ### getTime **Signature:** `getTime() : Number` **Description:** Returns the internal, millisecond representation of the Date object. This value is independent of time zone. **Returns:** the internal, millisecond representation of the Date object. --- ### getTimezoneOffset **Signature:** `getTimezoneOffset() : Number` **Description:** Returns the difference between local time and Greenwich Mean Time (GMT) in minutes. **Returns:** the difference between local time and Greenwich Mean Time (GMT) in minutes. --- ### getUTCDate **Signature:** `getUTCDate() : Number` **Description:** Returns the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. **Returns:** the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. --- ### getUTCDay **Signature:** `getUTCDay() : Number` **Description:** Returns the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. **Returns:** the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. --- ### getUTCFullYear **Signature:** `getUTCFullYear() : Number` **Description:** Returns the year when the Date is expressed in universal time. The return value is a four-digit format. **Returns:** the year of the Date in four-digit form. --- ### getUTCHours **Signature:** `getUTCHours() : Number` **Description:** Return the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). **Returns:** the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). --- ### getUTCMilliseconds **Signature:** `getUTCMilliseconds() : Number` **Description:** Returns the milliseconds field, expressed in universal time, of the Date. **Returns:** the milliseconds field, expressed in universal time, of the Date. --- ### getUTCMinutes **Signature:** `getUTCMinutes() : Number` **Description:** Return the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. **Returns:** the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. --- ### getUTCMonth **Signature:** `getUTCMonth() : Number` **Description:** Returns the month of the year that results when the Date is expressed in universal time. The return value is a Number betwee 0 and 11. **Returns:** the month of the year as a value between 0 and 11. --- ### getUTCSeconds **Signature:** `getUTCSeconds() : Number` **Description:** Return the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. **Returns:** the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. --- ### now **Signature:** `static now() : Number` **Description:** Returns the number of milliseconds since midnight of January 1, 1970 up until now. **Returns:** the number of milliseconds since midnight of January 1, 1970. --- ### parse **Signature:** `static parse(dateString : String) : Number` **Description:** Takes a date string and returns the number of milliseconds since midnight of January 1, 1970. Supports: RFC2822 date strings strings matching the exact ISO 8601 format 'YYYY-MM-DDTHH:mm:ss.sssZ' **Parameters:** - `dateString`: represents a Date in a valid date format. **Returns:** the number of milliseconds since midnight of January 1, 1970 or NaN if no date could be recognized. --- ### setDate **Signature:** `setDate(date : Number) : Number` **Description:** Sets the day of the month where the value is a Number from 1 to 31. **Parameters:** - `date`: the day of the month. **Returns:** the millisecond representation of the adjusted date. --- ### setFullYear **Signature:** `setFullYear(year : Number, args : Number...) : Number` **Description:** Sets the full year of Date where the value must be a four-digit Number. Optionally, you can set the month and date. **Parameters:** - `year`: the year as a four-digit Number. - `args`: the month and day of the month. **Returns:** the millisecond representation of the adjusted date. --- ### setHours **Signature:** `setHours(hours : Number, args : Number...) : Number` **Description:** Sets the hours field of this Date instance. The minutes value should be a Number from 0 to 23. Optionally, hours, seconds and milliseconds can also be provided. **Parameters:** - `hours`: the minutes field of this Date instance. - `args`: the hours, seconds and milliseconds values for this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setMilliseconds **Signature:** `setMilliseconds(millis : Number) : Number` **Description:** Sets the milliseconds field of this Date instance. **Parameters:** - `millis`: the milliseconds field of this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setMinutes **Signature:** `setMinutes(minutes : Number, args : Number...) : Number` **Description:** Sets the minutes field of this Date instance. The minutes value should be a Number from 0 to 59. Optionally, seconds and milliseconds can also be provided. **Parameters:** - `minutes`: the minutes field of this Date instance. - `args`: the seconds and milliseconds value for this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setMonth **Signature:** `setMonth(month : Number, date : Number...) : Number` **Description:** Sets the month of the year where the value is a Number from 0 to 11. Optionally, you can set the day of the month. **Parameters:** - `month`: the month of the year. - `date`: the day of the month. **Returns:** the millisecond representation of the adjusted date. --- ### setSeconds **Signature:** `setSeconds(seconds : Number, millis : Number...) : Number` **Description:** Sets the seconds field of this Date instance. The seconds value should be a Number from 0 to 59. Optionally, milliseconds can also be provided. **Parameters:** - `seconds`: the seconds field of this Date instance. - `millis`: the milliseconds field of this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setTime **Signature:** `setTime(millis : Number) : Number` **Description:** Sets the number of milliseconds between the desired date and time and January 1, 1970. **Parameters:** - `millis`: the number of milliseconds between the desired date and time and January 1, 1970. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCDate **Signature:** `setUTCDate(date : Number) : Number` **Description:** Sets the day of the month, expressed in universal time, where the value is a Number from 1 to 31. **Parameters:** - `date`: the day of the month, expressed in universal time. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCFullYear **Signature:** `setUTCFullYear(year : Number, args : Number...) : Number` **Description:** Sets the full year, expressed in universal time, of Date where the value must be a four-digit Number. Optionally, you can set the month and date. **Parameters:** - `year`: the year as a four-digit Number, expressed in universal time. - `args`: the month and day of the month. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCHours **Signature:** `setUTCHours(hours : Number, args : Number...) : Number` **Description:** Sets the hours field, expressed in universal time, of this Date instance. The minutes value should be a Number from 0 to 23. Optionally, seconds and milliseconds can also be provided. **Parameters:** - `hours`: the minutes field, expressed in universal time, of this Date instance. - `args`: the seconds and milliseconds value, expressed in universal time, for this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCMilliseconds **Signature:** `setUTCMilliseconds(millis : Number) : Number` **Description:** Sets the milliseconds field, expressed in universal time, of this Date instance. **Parameters:** - `millis`: the milliseconds field, expressed in universal time, of this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCMinutes **Signature:** `setUTCMinutes(minutes : Number, args : Number...) : Number` **Description:** Sets the minutes field, expressed in universal time, of this Date instance. The minutes value should be a Number from 0 to 59. Optionally, seconds and milliseconds can also be provided. **Parameters:** - `minutes`: the minutes field, expressed in universal time, of this Date instance. - `args`: the seconds and milliseconds values, expressed in universal time, for this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCMonth **Signature:** `setUTCMonth(month : Number, date : Number...) : Number` **Description:** Sets the month of the year, expressed in universal time, where the value is a Number from 0 to 11. Optionally, you can set the day of the month. **Parameters:** - `month`: the month of the year, expressed in universal time. - `date`: the day of the month. **Returns:** the millisecond representation of the adjusted date. --- ### setUTCSeconds **Signature:** `setUTCSeconds(seconds : Number, millis : Number...) : Number` **Description:** Sets the seconds field, expressed in universal time, of this Date instance. The seconds value should be a Number from 0 to 59. Optionally, milliseconds can also be provided. **Parameters:** - `seconds`: the seconds field, expressed in universal time, of this Date instance. - `millis`: the milliseconds field, expressed in universal time, of this Date instance. **Returns:** the millisecond representation of the adjusted date. --- ### toDateString **Signature:** `toDateString() : String` **Description:** Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. **Returns:** the Date as a String value. --- ### toISOString **Signature:** `toISOString() : String` **Description:** This function returns a string value represent the instance in time represented by this Date object. The date is formatted with the Simplified ISO 8601 format as follows: YYYY-MM-DDTHH:mm:ss.sssTZ. The time zone is always UTC, denoted by the suffix Z. **Returns:** string representation of this date --- ### toJSON **Signature:** `toJSON(key : String) : Object` **Description:** This function returns the same string as Date.prototype.toISOString(). The method is called when a Date object is stringified. **Parameters:** - `key`: the name of the key, which is stringified **Returns:** JSON string representation of this date --- ### toLocaleDateString **Signature:** `toLocaleDateString() : String` **Description:** Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. **Returns:** returns the date portion of the Date as a String. --- ### toLocaleString **Signature:** `toLocaleString() : String` **Description:** Returns the Date as a String using the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. **Returns:** the Date as a String using the default locale en_US --- ### toLocaleTimeString **Signature:** `toLocaleTimeString() : String` **Description:** Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. **Returns:** returns the time time's portion of the Date as a String. --- ### toTimeString **Signature:** `toTimeString() : String` **Description:** Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. **Returns:** the Date's time. --- ### toUTCString **Signature:** `toUTCString() : String` **Description:** Returns a String representation of this Date, expressed in universal time. **Returns:** a String representation of this Date, expressed in universal time. --- ### UTC **Signature:** `static UTC(year : Number, month : Number, args : Number...) : Number` **Description:** Returns the number of milliseconds since midnight of January 1, 1970 according to universal time. Optionally, you can pass up to five additional arguments representing date, hours, minutes, seconds, and milliseconds. **Parameters:** - `year`: a number representing the year. - `month`: a number representing the month. - `args`: a set of numbers representing the date, hours, minutes, seconds, and milliseconds. **Returns:** the number of milliseconds since midnight of January 1, 1970 according to universal time. --- ### valueOf **Signature:** `valueOf() : Object` **Description:** Returns the value of this Date represented in milliseconds. **Returns:** the value of this Date represented in milliseconds. --- ``` -------------------------------------------------------------------------------- /tests/oauth-token.test.ts: -------------------------------------------------------------------------------- ```typescript import { TokenManager } from '../src/clients/base/oauth-token.js'; import { OAuthTokenResponse } from '../src/types/types'; describe('TokenManager', () => { let tokenManager: TokenManager; const testHostname = 'test-instance.demandware.net'; const testClientId = 'test-client-id'; const testClientId2 = 'test-client-id-2'; const testHostname2 = 'test-instance-2.demandware.net'; beforeEach(() => { // Get a fresh instance and clear all tokens tokenManager = TokenManager.getInstance(); tokenManager.clearAllTokens(); }); afterEach(() => { // Clean up after each test tokenManager.clearAllTokens(); }); describe('Singleton pattern', () => { it('should return the same instance when called multiple times', () => { const instance1 = TokenManager.getInstance(); const instance2 = TokenManager.getInstance(); const instance3 = TokenManager.getInstance(); expect(instance1).toBe(instance2); expect(instance2).toBe(instance3); expect(instance1).toBe(tokenManager); }); it('should maintain state across getInstance calls', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'test-token', token_type: 'bearer', expires_in: 3600, }; const instance1 = TokenManager.getInstance(); instance1.storeToken(testHostname, testClientId, tokenResponse); const instance2 = TokenManager.getInstance(); expect(instance2.getValidToken(testHostname, testClientId)).toBe('test-token'); }); }); describe('storeToken()', () => { it('should store a token correctly', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'stored-token', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('stored-token'); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); }); it('should calculate expiration time correctly', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'expiration-test-token', token_type: 'bearer', expires_in: 3600, // 1 hour }; const beforeStore = Date.now(); tokenManager.storeToken(testHostname, testClientId, tokenResponse); const afterStore = Date.now(); const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); expect(expiration).toBeInstanceOf(Date); if (expiration) { const expirationTime = expiration.getTime(); const expectedMinExpiration = beforeStore + (3600 * 1000); const expectedMaxExpiration = afterStore + (3600 * 1000); expect(expirationTime).toBeGreaterThanOrEqual(expectedMinExpiration); expect(expirationTime).toBeLessThanOrEqual(expectedMaxExpiration); } }); it('should overwrite existing tokens for the same hostname/clientId', () => { const firstToken: OAuthTokenResponse = { access_token: 'first-token', token_type: 'bearer', expires_in: 3600, }; const secondToken: OAuthTokenResponse = { access_token: 'second-token', token_type: 'bearer', expires_in: 7200, }; tokenManager.storeToken(testHostname, testClientId, firstToken); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('first-token'); tokenManager.storeToken(testHostname, testClientId, secondToken); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('second-token'); }); it('should store different tokens for different hostname/clientId combinations', () => { const token1: OAuthTokenResponse = { access_token: 'token-1', token_type: 'bearer', expires_in: 3600, }; const token2: OAuthTokenResponse = { access_token: 'token-2', token_type: 'bearer', expires_in: 3600, }; const token3: OAuthTokenResponse = { access_token: 'token-3', token_type: 'bearer', expires_in: 3600, }; // Same hostname, different clientId tokenManager.storeToken(testHostname, testClientId, token1); tokenManager.storeToken(testHostname, testClientId2, token2); // Different hostname, same clientId tokenManager.storeToken(testHostname2, testClientId, token3); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); expect(tokenManager.getValidToken(testHostname2, testClientId)).toBe('token-3'); }); }); describe('getValidToken()', () => { it('should return null for non-existent tokens', () => { expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); expect(tokenManager.getValidToken('nonexistent.host', 'nonexistent-client')).toBeNull(); }); it('should return valid tokens', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'valid-token', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('valid-token'); }); it('should return null for expired tokens', async () => { const shortLivedToken: OAuthTokenResponse = { access_token: 'short-lived-token', token_type: 'bearer', expires_in: 0.001, // Very short expiration (1ms) }; tokenManager.storeToken(testHostname, testClientId, shortLivedToken); // Wait for token to expire await new Promise(resolve => setTimeout(resolve, 100)); expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); }); it('should return null for tokens that expire within 60-second buffer', () => { // Create a token that expires in 30 seconds (within the 60-second buffer) const soonToExpireToken: OAuthTokenResponse = { access_token: 'soon-to-expire-token', token_type: 'bearer', expires_in: 30, // 30 seconds }; tokenManager.storeToken(testHostname, testClientId, soonToExpireToken); // Should return null because it's within the 60-second buffer expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); }); it('should return tokens that expire beyond the 60-second buffer', () => { // Create a token that expires in 120 seconds (beyond the 60-second buffer) const validToken: OAuthTokenResponse = { access_token: 'valid-token-beyond-buffer', token_type: 'bearer', expires_in: 120, // 2 minutes }; tokenManager.storeToken(testHostname, testClientId, validToken); // Should return the token because it expires beyond the 60-second buffer expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('valid-token-beyond-buffer'); }); }); describe('isTokenValid()', () => { it('should return false for non-existent tokens', () => { expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); expect(tokenManager.isTokenValid('nonexistent.host', 'nonexistent-client')).toBe(false); }); it('should return true for valid tokens', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'valid-token', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); }); it('should return false for expired tokens', async () => { const shortLivedToken: OAuthTokenResponse = { access_token: 'expired-token', token_type: 'bearer', expires_in: 0.001, // Very short expiration }; tokenManager.storeToken(testHostname, testClientId, shortLivedToken); // Wait for expiration await new Promise(resolve => setTimeout(resolve, 100)); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); }); it('should return false for tokens within 60-second expiration buffer', () => { const soonToExpireToken: OAuthTokenResponse = { access_token: 'buffer-test-token', token_type: 'bearer', expires_in: 30, // 30 seconds - within buffer }; tokenManager.storeToken(testHostname, testClientId, soonToExpireToken); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); }); it('should return true for tokens beyond 60-second expiration buffer', () => { const validToken: OAuthTokenResponse = { access_token: 'buffer-safe-token', token_type: 'bearer', expires_in: 120, // 2 minutes - beyond buffer }; tokenManager.storeToken(testHostname, testClientId, validToken); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); }); it('should handle edge case of exactly 60-second expiration', () => { const exactBufferToken: OAuthTokenResponse = { access_token: 'exact-buffer-token', token_type: 'bearer', expires_in: 60, // Exactly 60 seconds }; tokenManager.storeToken(testHostname, testClientId, exactBufferToken); // Should be false because 60 seconds is not > 60 seconds (buffer) expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); }); it('should handle edge case just beyond 60-second buffer', () => { const justBeyondBufferToken: OAuthTokenResponse = { access_token: 'just-beyond-buffer-token', token_type: 'bearer', expires_in: 61, // 61 seconds - just beyond buffer }; tokenManager.storeToken(testHostname, testClientId, justBeyondBufferToken); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); }); }); describe('clearToken()', () => { it('should remove specific tokens', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'to-be-cleared', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('to-be-cleared'); tokenManager.clearToken(testHostname, testClientId); expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); }); it('should not affect other tokens when clearing specific token', () => { const token1: OAuthTokenResponse = { access_token: 'token-1', token_type: 'bearer', expires_in: 3600, }; const token2: OAuthTokenResponse = { access_token: 'token-2', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, token1); tokenManager.storeToken(testHostname, testClientId2, token2); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); tokenManager.clearToken(testHostname, testClientId); expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); }); it('should handle clearing non-existent tokens gracefully', () => { // Should not throw when clearing non-existent token expect(() => { tokenManager.clearToken(testHostname, testClientId); }).not.toThrow(); expect(() => { tokenManager.clearToken('nonexistent.host', 'nonexistent-client'); }).not.toThrow(); }); }); describe('clearAllTokens()', () => { it('should remove all stored tokens', () => { const token1: OAuthTokenResponse = { access_token: 'token-1', token_type: 'bearer', expires_in: 3600, }; const token2: OAuthTokenResponse = { access_token: 'token-2', token_type: 'bearer', expires_in: 3600, }; const token3: OAuthTokenResponse = { access_token: 'token-3', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, token1); tokenManager.storeToken(testHostname, testClientId2, token2); tokenManager.storeToken(testHostname2, testClientId, token3); // Verify all tokens are stored expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); expect(tokenManager.getValidToken(testHostname2, testClientId)).toBe('token-3'); tokenManager.clearAllTokens(); // Verify all tokens are cleared expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); expect(tokenManager.getValidToken(testHostname, testClientId2)).toBeNull(); expect(tokenManager.getValidToken(testHostname2, testClientId)).toBeNull(); }); it('should handle clearing when no tokens exist', () => { expect(() => { tokenManager.clearAllTokens(); }).not.toThrow(); // Should still work normally after clearing empty storage const tokenResponse: OAuthTokenResponse = { access_token: 'after-clear-all', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('after-clear-all'); }); }); describe('getTokenExpiration()', () => { it('should return null for non-existent tokens', () => { expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeNull(); expect(tokenManager.getTokenExpiration('nonexistent.host', 'nonexistent-client')).toBeNull(); }); it('should return correct expiration date for existing tokens', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'expiration-date-token', token_type: 'bearer', expires_in: 3600, // 1 hour }; const beforeStore = Date.now(); tokenManager.storeToken(testHostname, testClientId, tokenResponse); const afterStore = Date.now(); const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); expect(expiration).toBeInstanceOf(Date); if (expiration) { const expirationTime = expiration.getTime(); const expectedMinExpiration = beforeStore + (3600 * 1000); const expectedMaxExpiration = afterStore + (3600 * 1000); expect(expirationTime).toBeGreaterThanOrEqual(expectedMinExpiration); expect(expirationTime).toBeLessThanOrEqual(expectedMaxExpiration); } }); it('should return different expiration times for different tokens', () => { const shortToken: OAuthTokenResponse = { access_token: 'short-token', token_type: 'bearer', expires_in: 1800, // 30 minutes }; const longToken: OAuthTokenResponse = { access_token: 'long-token', token_type: 'bearer', expires_in: 7200, // 2 hours }; tokenManager.storeToken(testHostname, testClientId, shortToken); tokenManager.storeToken(testHostname, testClientId2, longToken); const shortExpiration = tokenManager.getTokenExpiration(testHostname, testClientId); const longExpiration = tokenManager.getTokenExpiration(testHostname, testClientId2); expect(shortExpiration).toBeInstanceOf(Date); expect(longExpiration).toBeInstanceOf(Date); if (shortExpiration && longExpiration) { expect(longExpiration.getTime()).toBeGreaterThan(shortExpiration.getTime()); } }); it('should return null after token is cleared', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'cleared-token', token_type: 'bearer', expires_in: 3600, }; tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeInstanceOf(Date); tokenManager.clearToken(testHostname, testClientId); expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeNull(); }); }); describe('Token key generation', () => { it('should create unique keys for different hostname/clientId combinations', () => { // We can't directly test the private getTokenKey method, but we can test its behavior const token1: OAuthTokenResponse = { access_token: 'unique-test-1', token_type: 'bearer', expires_in: 3600, }; const token2: OAuthTokenResponse = { access_token: 'unique-test-2', token_type: 'bearer', expires_in: 3600, }; // Store tokens with similar but different keys tokenManager.storeToken('host1.com', 'client1', token1); tokenManager.storeToken('host1.com', 'client2', token2); expect(tokenManager.getValidToken('host1.com', 'client1')).toBe('unique-test-1'); expect(tokenManager.getValidToken('host1.com', 'client2')).toBe('unique-test-2'); // Different hostname, same client tokenManager.storeToken('host2.com', 'client1', token1); expect(tokenManager.getValidToken('host2.com', 'client1')).toBe('unique-test-1'); expect(tokenManager.getValidToken('host1.com', 'client1')).toBe('unique-test-1'); // Should still exist }); it('should handle special characters in hostname and clientId', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'special-chars-token', token_type: 'bearer', expires_in: 3600, }; const specialHostname = 'test-instance_with.special-chars.demandware.net'; const specialClientId = 'client-id_with.special:chars'; tokenManager.storeToken(specialHostname, specialClientId, tokenResponse); expect(tokenManager.getValidToken(specialHostname, specialClientId)).toBe('special-chars-token'); expect(tokenManager.isTokenValid(specialHostname, specialClientId)).toBe(true); }); }); describe('Edge cases and error handling', () => { it('should handle zero expiration time', () => { const zeroExpirationToken: OAuthTokenResponse = { access_token: 'zero-expiration-token', token_type: 'bearer', expires_in: 0, }; tokenManager.storeToken(testHostname, testClientId, zeroExpirationToken); // Should be invalid immediately due to buffer expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); }); it('should handle negative expiration time', () => { const negativeExpirationToken: OAuthTokenResponse = { access_token: 'negative-expiration-token', token_type: 'bearer', expires_in: -100, }; tokenManager.storeToken(testHostname, testClientId, negativeExpirationToken); // Should be invalid immediately expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); }); it('should handle very large expiration times', () => { const largeExpirationToken: OAuthTokenResponse = { access_token: 'large-expiration-token', token_type: 'bearer', expires_in: 86400 * 365, // 1 year in seconds }; tokenManager.storeToken(testHostname, testClientId, largeExpirationToken); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('large-expiration-token'); const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); expect(expiration).toBeInstanceOf(Date); }); it('should handle empty strings for hostname and clientId', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'empty-string-token', token_type: 'bearer', expires_in: 3600, }; // Should not throw errors with empty strings expect(() => { tokenManager.storeToken('', '', tokenResponse); }).not.toThrow(); expect(() => { tokenManager.getValidToken('', ''); }).not.toThrow(); expect(() => { tokenManager.isTokenValid('', ''); }).not.toThrow(); expect(() => { tokenManager.clearToken('', ''); }).not.toThrow(); expect(() => { tokenManager.getTokenExpiration('', ''); }).not.toThrow(); }); it('should handle fractional expiration times', () => { const fractionalExpirationToken: OAuthTokenResponse = { access_token: 'fractional-expiration-token', token_type: 'bearer', expires_in: 3600.5, // 1 hour and 30 minutes }; tokenManager.storeToken(testHostname, testClientId, fractionalExpirationToken); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('fractional-expiration-token'); }); }); describe('Concurrency and state consistency', () => { it('should maintain consistent state with rapid operations', () => { const tokenResponse: OAuthTokenResponse = { access_token: 'rapid-ops-token', token_type: 'bearer', expires_in: 3600, }; // Rapid store/clear/check operations for (let i = 0; i < 100; i++) { tokenManager.storeToken(testHostname, testClientId, tokenResponse); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); tokenManager.clearToken(testHostname, testClientId); expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); } }); it('should handle multiple token storage operations correctly', () => { const baseToken: OAuthTokenResponse = { access_token: '', token_type: 'bearer', expires_in: 3600, }; // Store multiple tokens rapidly for (let i = 0; i < 50; i++) { const token = { ...baseToken, access_token: `token-${i}` }; tokenManager.storeToken(testHostname, `client-${i}`, token); } // Verify all tokens are stored correctly for (let i = 0; i < 50; i++) { expect(tokenManager.getValidToken(testHostname, `client-${i}`)).toBe(`token-${i}`); } // Clear all and verify tokenManager.clearAllTokens(); for (let i = 0; i < 50; i++) { expect(tokenManager.getValidToken(testHostname, `client-${i}`)).toBeNull(); } }); }); }); ```