Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions compatibility-tests/sdk-test-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>marketplacedeployment</artifactId>
</dependency>
<!-- AWS Marketplace Discovery client -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>marketplacediscovery</artifactId>
</dependency>
<!-- IAM Access Analyzer client -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import software.amazon.awssdk.services.marketplaceentitlement.MarketplaceEntitlementClient;
import software.amazon.awssdk.services.verifiedpermissions.VerifiedPermissionsClient;
import software.amazon.awssdk.services.marketplacedeployment.MarketplaceDeploymentClient;
import software.amazon.awssdk.services.marketplacediscovery.MarketplaceDiscoveryClient;
import software.amazon.awssdk.services.inspector2.Inspector2Client;
import software.amazon.awssdk.services.securityhub.SecurityHubClient;
import software.amazon.awssdk.services.detective.DetectiveClient;
Expand Down Expand Up @@ -464,6 +465,14 @@ public static MarketplaceDeploymentClient marketplaceDeploymentClient() {
.build();
}

public static MarketplaceDiscoveryClient marketplaceDiscoveryClient() {
return MarketplaceDiscoveryClient.builder()
.endpointOverride(ENDPOINT)
.region(REGION)
.credentialsProvider(CREDENTIALS)
.build();
}

public static DetectiveClient detectiveClient(String accountId) {
return DetectiveClient.builder()
.endpointOverride(ENDPOINT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.floci.test;

import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.marketplacediscovery.MarketplaceDiscoveryClient;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MarketplaceDiscoveryTest {

@Test
void usesAwsSdkWireContract() {
try (MarketplaceDiscoveryClient client = TestFixtures.marketplaceDiscoveryClient()) {
var response = client.searchListings(r -> r.searchText("sdk-compat"));
assertTrue(response.totalResults() >= 0);
assertNotNull(response.listingSummaries());
}
}
}
13 changes: 13 additions & 0 deletions docs/services/marketplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ Floci emulates AWS Marketplace APIs under the shared `aws-marketplace` SigV4 sig
| `UpdatePurchaseOrders` | Updates purchase orders for an agreement |
| `GetEntitlements` | - |
| `PutDeploymentParameter` | Creates or updates an AWS Marketplace deployment parameter |
| `GetListing` | Returns full buyer-facing listing details |
| `GetOffer` | Returns an offer and its associated product information |
| `GetOfferSet` | Returns an offer set and its associated offers and products |
| `GetOfferTerms` | Returns the terms attached to an offer with pagination |
| `GetProduct` | Returns Marketplace product details |
| `ListFulfillmentOptions` | Lists fulfillment options available for a product |
| `ListPurchaseOptions` | Lists buyer purchase options with filtering and pagination |
| `SearchFacets` | Returns paginated facet values for matching listings |
| `SearchListings` | Searches listings with filtering, sorting, and pagination |
<!-- floci:actions:end -->

## Marketplace Catalog
Expand All @@ -73,6 +82,10 @@ AWS exposes this service as read-only: `GetEntitlements` is the only public oper

Deployment parameters and idempotency records are persisted through `StorageFactory` and isolated by AWS account.

## Marketplace Discovery

Marketplace Discovery reads the shared Marketplace Catalog entity backend and exposes listing, offer, purchase-option, facet, filtering, sorting, and pagination behavior across the supported Discovery regions.



## Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import io.github.hectorvent.floci.services.efs.EfsController;
import io.github.hectorvent.floci.services.marketplace.MarketplaceCatalogController;
import io.github.hectorvent.floci.services.marketplace.MarketplaceDeploymentController;
import io.github.hectorvent.floci.services.marketplace.MarketplaceDiscoveryController;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

Expand Down Expand Up @@ -658,7 +659,7 @@ public ResolvedServiceCatalog(EmulatorConfig config) {
"marketplace", config.storage().mode(), 5000L, null, ServiceProtocol.REST_JSON,
protocols(ServiceProtocol.REST_JSON, ServiceProtocol.JSON, ServiceProtocol.CBOR),
Set.of("AWSMPCommerceService_v20200301.", "AWSMPEntitlementService."), Set.of("aws-marketplace"), Set.of("AWS Marketplace Entitlement Service"),
Set.of(MarketplaceCatalogController.class, MarketplaceDeploymentController.class))
Set.of(MarketplaceCatalogController.class, MarketplaceDeploymentController.class, MarketplaceDiscoveryController.class))
));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.github.hectorvent.floci.services.marketplace;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import io.github.hectorvent.floci.core.common.AwsException;
import io.github.hectorvent.floci.core.common.RequestContext;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/2026-02-05")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MarketplaceDiscoveryController {
private final MarketplaceDiscoveryService service;
private final ObjectReader strictReader;
private final RequestContext context;

@Inject
public MarketplaceDiscoveryController(MarketplaceDiscoveryService service, ObjectMapper mapper, RequestContext context) {
this.service = service;
this.strictReader = mapper.reader().with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
this.context = context;
}
@POST
@Path("/getListing")
public Response getListing(String body) { return ok(service.getListing(parse(body), region())); }

@POST
@Path("/getOffer")
public Response getOffer(String body) { return ok(service.getOffer(parse(body), region())); }

@POST
@Path("/getOfferSet")
public Response getOfferSet(String body) { return ok(service.getOfferSet(parse(body), region())); }

@POST
@Path("/getOfferTerms")
public Response getOfferTerms(String body) { return ok(service.getOfferTerms(parse(body), region())); }

@POST
@Path("/getProduct")
public Response getProduct(String body) { return ok(service.getProduct(parse(body), region())); }

@POST
@Path("/listFulfillmentOptions")
public Response listFulfillmentOptions(String body) { return ok(service.listFulfillmentOptions(parse(body), region())); }

@POST
@Path("/listPurchaseOptions")
public Response listPurchaseOptions(String body) { return ok(service.listPurchaseOptions(parse(body), region())); }

@POST
@Path("/searchFacets")
public Response searchFacets(String body) { return ok(service.searchFacets(parse(body), region())); }

@POST
@Path("/searchListings")
public Response searchListings(String body) { return ok(service.searchListings(parse(body), region())); }

private JsonNode parse(String body) {
try {
JsonNode node = strictReader.readTree(body == null || body.isBlank() ? "{}" : body);
if (node == null || !node.isObject()) {
throw validation("Request body must be a JSON object.");
}
return node;
} catch (AwsException e) { throw e; }
catch (Exception e) { throw validation("Request body is not valid JSON."); }
}
private String region() { return context.getRegion() == null ? "us-east-1" : context.getRegion(); }
private Response ok(JsonNode node) { return Response.ok(node).build(); }
private static AwsException validation(String message) { return new AwsException("ValidationException", message, 400); }
}
Loading
Loading