@whooshbang/contract-mock 2.0.0-rc.4 → 2.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ recorded here.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 2.0.0-rc.5
9
+
10
+ ### Changed
11
+
12
+ - Route a message the way the service does: a chosen `connection_id` is honored, and with none the subscriber's preferred channel, else the default connection when connected there, else the most recently connected one. A chosen connection that is unknown, archived or paused is `request_invalid` at `/connection_id`; one the subscriber never connected through is `subscriber_not_connected` (FXO-2865).
13
+
8
14
  ## 2.0.0-rc.4
9
15
 
10
16
  ### Changed
package/dist/core.js CHANGED
@@ -1413,6 +1413,10 @@ function problem(code, status, diagnosticId, overrides = {}) {
1413
1413
  ...(overrides.provider_failure === undefined
1414
1414
  ? {}
1415
1415
  : { provider_failure: overrides.provider_failure }),
1416
+ ...(overrides.channel === undefined ? {} : { channel: overrides.channel }),
1417
+ ...(overrides.subscriber_id === undefined
1418
+ ? {}
1419
+ : { subscriber_id: overrides.subscriber_id }),
1416
1420
  };
1417
1421
  if (!validateProblem(value)) {
1418
1422
  throw new Error("Internal contract-mock Problem Details violation.");
@@ -2032,6 +2036,8 @@ function normalizeMessageRequest(request) {
2032
2036
  };
2033
2037
  }
2034
2038
  function messageDeliveryTarget(request, environment) {
2039
+ if (request.connection_id !== undefined)
2040
+ return "connection";
2035
2041
  return (request.delivery_target ??
2036
2042
  (environment === "test" &&
2037
2043
  SIMULATOR_DESTINATIONS.has(request.to.subscriber_id)
@@ -2055,21 +2061,20 @@ function simulatorBinding(state, credential, subscriberId, notifierId) {
2055
2061
  return binding;
2056
2062
  }
2057
2063
  /**
2058
- * Resolves one exact provider binding without sorting across connections.
2064
+ * Resolves the one binding a send reaches, by the service's rule (FXO-2865).
2059
2065
  *
2060
- * The notifier route selects the connection that may deliver. Later consent
2061
- * on another connection remains valid but is never selected by sorting. A
2062
- * converted-v1 binding remains eligible only when the subscriber has no
2063
- * connection-bound candidate.
2066
+ * A named connection is the only one used. Otherwise the recipient's
2067
+ * preference is the only one used; otherwise the default connection when the
2068
+ * subscriber is connected there; otherwise the connection they connected
2069
+ * through most recently, which here is the last one recorded. A converted-v1
2070
+ * binding remains eligible only when the subscriber has no connection-bound
2071
+ * candidate.
2064
2072
  */
2065
2073
  function connectionBinding(state, credential, subscriberId, notifierId, connectionId) {
2066
2074
  const routeKey = notifierConnectionKey(credential.projectId, credential.environment, notifierId);
2067
2075
  const defaultConnectionId = state.notifierDefaultConnections.get(routeKey);
2068
2076
  const isExecutable = (binding) => {
2069
- if (binding.connection_id === undefined ||
2070
- binding.connection_id !== (connectionId ?? defaultConnectionId) ||
2071
- !(state.notifierConnections.get(routeKey)?.has(binding.connection_id) ??
2072
- false)) {
2077
+ if (binding.connection_id === undefined) {
2073
2078
  return false;
2074
2079
  }
2075
2080
  const record = state.channelConnections.get(binding.connection_id);
@@ -2107,9 +2112,13 @@ function connectionBinding(state, credential, subscriberId, notifierId, connecti
2107
2112
  state.recipientConnectionIds.has(subscriberGroupKey(credential.projectId, credential.environment, preference.connection_id)));
2108
2113
  }
2109
2114
  const connectionBound = candidates.filter((binding) => binding.connection_id !== undefined);
2110
- const defaultBindings = connectionBound.filter((binding) => isExecutable(binding));
2111
- if (defaultBindings.length === 1) {
2112
- return defaultBindings[0];
2115
+ const executable = connectionBound.filter((binding) => isExecutable(binding));
2116
+ const onDefault = executable.find((binding) => binding.connection_id === defaultConnectionId);
2117
+ if (onDefault !== undefined) {
2118
+ return onDefault;
2119
+ }
2120
+ if (executable.length > 0) {
2121
+ return executable[executable.length - 1];
2113
2122
  }
2114
2123
  if (connectionBound.length > 0) {
2115
2124
  return undefined;
@@ -6022,8 +6031,52 @@ function addPublicRoutes(app, getState, options) {
6022
6031
  }
6023
6032
  const matchingBinding = deliveryTarget === "simulator"
6024
6033
  ? simulatorBinding(state, credential, normalized.to.subscriber_id, notifierId)
6025
- : connectionBinding(state, credential, normalized.to.subscriber_id, notifierId, exactConnectionTest ? normalized.connection_id : undefined);
6034
+ : connectionBinding(state, credential, normalized.to.subscriber_id, notifierId, normalized.connection_id);
6026
6035
  if (matchingBinding === undefined) {
6036
+ // A chosen connection is the sender's, not the recipient's: say which
6037
+ // one the subscriber is not connected through (FXO-2865).
6038
+ const chosen = normalized.connection_id === undefined || exactConnectionTest
6039
+ ? undefined
6040
+ : state.channelConnections.get(normalized.connection_id);
6041
+ // Not connected anywhere is still subscriber_unbound below, as on the
6042
+ // service; subscriber_not_connected names a subscriber connected
6043
+ // somewhere else.
6044
+ const connectedElsewhere = normalized.connection_id !== undefined &&
6045
+ !exactConnectionTest &&
6046
+ connectionBinding(state, credential, normalized.to.subscriber_id, notifierId) !== undefined;
6047
+ if (normalized.connection_id !== undefined && !exactConnectionTest) {
6048
+ if (chosen === undefined ||
6049
+ chosen.ownerOrganizationId !== credential.organizationId ||
6050
+ chosen.connection.project_id !== credential.projectId ||
6051
+ chosen.connection.status === "archived")
6052
+ return problemResponse({
6053
+ ...problem("request_invalid", 400, state.idGenerator("diagnostic")),
6054
+ field_errors: [
6055
+ {
6056
+ code: "connection_not_found",
6057
+ detail: "No connection with this ID exists in this environment.",
6058
+ field: "/connection_id",
6059
+ },
6060
+ ],
6061
+ });
6062
+ if (chosen.connection.status !== "active" &&
6063
+ chosen.connection.status !== "degraded")
6064
+ return problemResponse({
6065
+ ...problem("request_invalid", 400, state.idGenerator("diagnostic")),
6066
+ field_errors: [
6067
+ {
6068
+ code: "connection_not_sending",
6069
+ detail: "This connection is paused or not finished setting up, so it cannot send.",
6070
+ field: "/connection_id",
6071
+ },
6072
+ ],
6073
+ });
6074
+ if (connectedElsewhere)
6075
+ return problemResponse(problem("subscriber_not_connected", 422, state.idGenerator("diagnostic"), {
6076
+ channel: chosen.connection.provider,
6077
+ subscriber_id: normalized.to.subscriber_id,
6078
+ }));
6079
+ }
6027
6080
  if (state.preferences.has(recipientPreferenceKey(credential.projectId, credential.environment, normalized.to.subscriber_id, notifierId))) {
6028
6081
  return problemResponse(problem("preferred_binding_unavailable", 422, state.idGenerator("diagnostic")));
6029
6082
  }
@@ -7263,11 +7316,10 @@ function addPublicRoutes(app, getState, options) {
7263
7316
  record.binding.connection_id === connection.connection.id &&
7264
7317
  record.binding.status === "active")?.binding;
7265
7318
  if (!binding)
7266
- return problemResponse({
7267
- ...problem("subscriber_not_connected", 422, state.idGenerator("diagnostic")),
7319
+ return problemResponse(problem("subscriber_not_connected", 422, state.idGenerator("diagnostic"), {
7268
7320
  channel: connection.connection.provider,
7269
7321
  subscriber_id: input.to.subscriber_id,
7270
- });
7322
+ }));
7271
7323
  // A connection test is authorized by connections:write. The internal
7272
7324
  // message admission carries the same tenant and exact bound connection;
7273
7325
  // it does not grant a reusable messages:write credential to the caller.