diff --git a/interop-tests/AGENTS.md b/interop-tests/AGENTS.md index 5712530..8688efb 100644 --- a/interop-tests/AGENTS.md +++ b/interop-tests/AGENTS.md @@ -36,7 +36,9 @@ Both counts must be zero for a phase to pass. ## Rules for an experiment 1. Peers run in Docker with full patch-version pins. Nothing is installed on the host. Node runs - only through the `node` service. + only through the `node` service. Every peer service caps its logs (`logging: json-file`, + `max-size`/`max-file`) and healthchecks something the peer does not log a stack trace for — + SMPPSim once filled the whole host disk in twenty minutes from a TCP-probe healthcheck. 2. `src/` and `test/` are read-only during an experiment. A defect is recorded with a reproducer, never fixed here — a fix is a separate change with a regression test in `test/`. 3. No git command that changes state: no add, commit, push, checkout, stash, reset. The diff --git a/interop-tests/PLAN.md b/interop-tests/PLAN.md index bc2b212..0763f61 100644 --- a/interop-tests/PLAN.md +++ b/interop-tests/PLAN.md @@ -193,7 +193,8 @@ document, library changes, and the AGENTS.md decision record each one needs. | --- | --- | --- | | 0 | done | [01-smscsim.md](findings/01-smscsim.md) | | 1 | done | [01-smscsim.md](findings/01-smscsim.md) | -| 2–3 | not started | — | +| 2 | done | [02-smppsim.md](findings/02-smppsim.md) | +| 3 | not started | — | | 4 | done | [04-kannel.md](findings/04-kannel.md) | | 5–10 | not started | — | diff --git a/interop-tests/compose.smppsim.yaml b/interop-tests/compose.smppsim.yaml new file mode 100644 index 0000000..7232788 --- /dev/null +++ b/interop-tests/compose.smppsim.yaml @@ -0,0 +1,130 @@ +x-smppsim-healthcheck: &smppsim-healthcheck + # Not a raw TCP probe on 2775: SMPPSim reads an empty connection as a malformed PDU and logs a + # full stack trace per attempt: with DECODE_PDUS_IN_LOG and a 1s interval that fills the disk + # within tens of minutes (24GB+ across the 9 services in-house). The HTTP admin port answers a + # bare request harmlessly. + test: ["CMD-SHELL", "curl -sf -o /dev/null http://127.0.0.1:8884/"] + interval: 1s + retries: 30 + timeout: 2s + +# A second line of defence for the same runaway-logging risk: DECODE_PDUS_IN_LOG dumps every PDU, +# so a stuck reconnect loop or a bad probe fills the disk long before anyone notices. +x-log-limits: &log-limits + logging: + driver: json-file + options: + max-file: "3" + max-size: 20m + +services: + smppsim: + build: + context: interop-tests/peers/smppsim + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim.props"] + healthcheck: *smppsim-healthcheck + + smppsim-textdlr: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-textdlr.props"] + healthcheck: *smppsim-healthcheck + + smppsim-transition: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-transition.props"] + healthcheck: *smppsim-healthcheck + + smppsim-undeliv: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-undeliv.props"] + healthcheck: *smppsim-healthcheck + + smppsim-rejected: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-rejected.props"] + healthcheck: *smppsim-healthcheck + + smppsim-accepted: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-accepted.props"] + healthcheck: *smppsim-healthcheck + + smppsim-delayed: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-delayed.props"] + healthcheck: *smppsim-healthcheck + + smppsim-queuefull: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-queuefull.props"] + healthcheck: *smppsim-healthcheck + + # OUTBIND_ESME_IP_ADDRESS in smppsim-outbind.props names the node service by its --use-aliases + # hostname; our own server() in smppsim.test.ts listens on port 2776 for it to connect to. + smppsim-outbind: + image: larvitsmpp-interop/smppsim:bc29982 + <<: *log-limits + command: ["conf/smppsim-outbind.props"] + healthcheck: *smppsim-healthcheck + + capture: + image: nicolaka/netshoot:v0.16 + network_mode: "service:smppsim" + cap_add: + - NET_ADMIN + - NET_RAW + depends_on: + smppsim: + condition: service_started + command: ["dumpcap", "-i", "any", "-f", "tcp port 2775", "-w", "/captures/smppsim.pcapng"] + volumes: + - ./interop-tests/captures:/captures + + # A second sidecar, cheap to add, so a textdlr-specific wire question can be checked without a + # second run. Not read by run.py's own pass/fail (that only decodes captures/smppsim.pcapng). + capture-textdlr: + image: nicolaka/netshoot:v0.16 + network_mode: "service:smppsim-textdlr" + cap_add: + - NET_ADMIN + - NET_RAW + depends_on: + smppsim-textdlr: + condition: service_started + command: ["dumpcap", "-i", "any", "-f", "tcp port 2775", "-w", "/captures/smppsim-textdlr.pcapng"] + volumes: + - ./interop-tests/captures:/captures + + node: + depends_on: + capture: + condition: service_started + capture-textdlr: + condition: service_started + smppsim: + condition: service_healthy + smppsim-textdlr: + condition: service_healthy + smppsim-transition: + condition: service_healthy + smppsim-undeliv: + condition: service_healthy + smppsim-rejected: + condition: service_healthy + smppsim-accepted: + condition: service_healthy + smppsim-delayed: + condition: service_healthy + smppsim-queuefull: + condition: service_healthy + smppsim-outbind: + condition: service_healthy diff --git a/interop-tests/findings/02-smppsim.md b/interop-tests/findings/02-smppsim.md new file mode 100644 index 0000000..8916b5d --- /dev/null +++ b/interop-tests/findings/02-smppsim.md @@ -0,0 +1,204 @@ +# 02 smppsim + +Date: 2026-09-05. Repo commit: `9c4939f`. Host Docker: 29.6.2. Images: `larvitsmpp-interop/smppsim:bc29982` +(built locally here from `kwahome/smpp-sim-docker` at commit `bc299828af9046ab290da3b4957dfbc472f02bfb`, +running SMPPSim 2.6.11 on `eclipse-temurin:8u452-b09-jre`; cloned during the build with +`debian:13.2-slim`, never vendored), `nicolaka/netshoot:v0.16` (capture sidecars and tshark), +`node:24.18.0-bookworm-slim` (test runner, from the root `compose.yaml`). + +## Setup + +`interop-tests/peers/smppsim/Dockerfile` clones the pinned commit in a build stage and copies just +`smppsim.jar`, `lib`, `www`, `mo` and `conf/logging.properties` into the runtime stage; each +`smppsim*.props` file under the same directory is copied in and selected via the container +`command`. `interop-tests/compose.smppsim.yaml` runs nine variants of the one image (`smppsim`, +`-textdlr`, `-transition`, `-undeliv`, `-rejected`, `-accepted`, `-delayed`, `-queuefull`, +`-outbind`) plus `capture` (on `smppsim`) and `capture-textdlr`, all on one network so +`./interop-tests/run.py smppsim` covers everything in one run. + +Two snags fixed while building the harness: + +- **The first healthcheck (`bash -c 'echo > /dev/tcp/127.0.0.1/2775'`, 1s interval) filled the + host's disk.** SMPPSim reads an empty connection as a malformed PDU and logs a full Java stack + trace per attempt; with `DECODE_PDUS_IN_LOG=true` and a 1s probe interval, nine containers left + running for ~20 minutes wrote 24GB+ of container logs between them and took the whole shared host + to 0 bytes free (`docker run` itself started failing with "no space left on device"). Fixed by + probing the HTTP admin port instead (`curl -sf -o /dev/null http://127.0.0.1:8884/`), which + SMPPSim answers harmlessly, plus a `json-file` log cap (`max-size: 20m`, `max-file: "3"`) on every + `smppsim*` service as a second line of defence. Worth knowing for anyone else pointing a 1s + TCP-connect healthcheck at this peer. +- The `capture` sidecar's `dumpcap` needs the same root/DAC-override handling phase 1 documented + (`interop-tests/captures/` chowned to `1000:1000`/`0777` by `run.py` after every run, stale + `.pcapng` unlinked before the next). Once, a capture attempt failed outright + ("Permission denied" opening the pcapng) for a reason not pinned down - not reproduced since; + treat as a rare, unexplained flake in this harness rather than a peer issue. +- Two early runs showed `malformed: 2`, `expert errors: 2`. Both traced to this test file, not + SMPPSim: C17's raw-UDH test used IEI `0x01` ("Special SMS Message Indication"), a real GSM 03.40 + information element with its own defined length (2 bytes), at length 1 - Wireshark's + `gsm_sms_ud` dissector correctly flags that as malformed. Fixed by using IEI `0x70` + (reserved-for-future-use, so no dissector validates its length) for what the test only ever + needed to be an arbitrary, unparsed UDH element. + +Two runs of `./interop-tests/run.py smppsim`, back to back, after that fix: both exit 0, 25/25 +tests passing, `malformed: 0`, `expert errors: 0` in both. + +## Scenarios + +| Id (from PLAN.md) | Result | Evidence | +| --- | --- | --- | +| C2 (`smppsim-textdlr`) | pass | `smppsim-textdlr - C2 text-only receipts`; no TLVs, `dlr.smsId` matches `submit_sm_resp` | +| C3 (`smppsim`, TLVs on) | pass | `smppsim - C3+C7 …`; `receipted_message_id`/`message_state` TLVs present and agree with the body on every segment | +| C4 (`smppsim-transition`) | fail (peer defect, see below) | `smppsim-transition - C4 …`; intermediate report arrives, no final ever does | +| C5 (single-state variants) | pass | `smppsim single-state variants - C5 …`; UNDELIVERABLE/REJECTED/ACCEPTED each map correctly; 2-segment message's shared status confirmed, `messageDlr` confirmed absent (see Open questions) | +| C6 (`smppsim-delayed`) | pass | `smppsim-delayed - C6 …`; disconnect+reconnect observed, delayed receipt still reaches `dlr` on the new link | +| C7 (loopback, GSM 1/2/3/10-segment) | pass | same `C3+C7` suite; one id per segment, receipt per id, loopback reassembles the exact text including € and \[ \] | +| C7 (loopback, UCS2 2-segment) | pass with a documented defect | `… 2-segment UCS2 …: TLVs stay right, the receipt body is corrupted`; TLVs and loopback reassembly both correct, receipt body unreadable - `@larvit/smpp` defect below | +| C11 (bind refusal + backoff) | pass | `smppsim - C11 …`, three sub-tests, see below for what each shows | +| C12 (`smppsim-queuefull`) | pass | `smppsim-queuefull - C12 …`; `ESME_RMSGQFUL` returned, session stays bound, later send succeeds once the one-slot queue drains | +| C13 (`maxOutstanding: 1`, 10 parallel) | pass | `smppsim - C13 …`; 10 distinct, strictly-increasing ids, none lost | +| C15 (bind version) | pass (peer never declares, see below) | `smppsim - C15 …`, both 0x34 and 0x50 | +| C17 (encodings over loopback) | pass | `smppsim - C17 …`, 5 sub-tests: Latin-1, UCS-2, flash (0x10), raw 0xF0 (not flash), raw UDH+8-bit-binary | +| C18 (`smppsim-outbind`) | pass (record, not judge) | `smppsim-outbind - C18 …`; see below for the wire facts | + +## Defects in @larvit/smpp + +### A delivery receipt's `data_coding` is trusted to decode its body, even though the spec makes the receipt a fixed text format + +**What happened.** A message sent with `encoding: 'UCS2'` gets `data_coding: 8` on its `submit_sm`. +SMPPSim's delivery receipt for it (confirmed against source, `DeliveryReceipt` extends `DeliverSM` +by copy-constructing the original `SubmitSM`) inherits that same `data_coding: 8`, but its +`short_message` is always plain ASCII text (`id:… sub:… dlvrd:… stat:…`). `pdu.ts`'s parser decodes +any non-UDH PDU's `short_message` using its own `data_coding` unconditionally +(`params.short_message = decodeMessage(message, paramNumber(params.data_coding, 0)).message`), so +the ASCII receipt bytes are read back as UCS2 (byte pairs swapped) - `id:001 sub:…` becomes +unrecoverable CJK-range glyphs, and `dlr.receipt` (hence `.stat`, `.id`, every body field) is +garbage or `undefined` for every receipt whose triggering message used a non-ASCII encoding. + +**What the spec says.** SMPP 3.4 5.2.25 defines `short_message`/`message_payload` generically, but +Appendix B's receipt format is specified as fixed ASCII fields; `data_coding` on the *receipt* PDU +describes nothing about how to read that text, since the MC is reporting on a message rather than +carrying one. Trusting `data_coding` to decode a receipt body is reading a field the spec never +attaches that meaning to. The `receipted_message_id`/`message_state` TLVs are unaffected (typed +fields, not text), which is why `dlr.statusMsg` and `dlr.smsId` stay correct here - only the +body-derived `dlr.receipt` (and anything relying on it, e.g. a peer without TLVs) is lost. + +**Reproducer.** `session.sendSms({ encoding: 'UCS2', dlr: true, message: 'x', from, to })` against +`smppsim`, then inspect the `dlr` event's second argument (the raw `PduObject`): +`pduObj.params.data_coding === 8` and `dlr.receipt === undefined`, while +`pduObj.tlvs.receipted_message_id`/`message_state` are present and correct. Confirmed both against +SMPPSim and by constructing the PDU directly: a `deliver_sm` with `data_coding=8`, `esm_class=4` +and an ASCII `id:0 sub:001 dlvrd:001 …` body decodes to garbage text via `pduToObj`. + +**Severity.** Medium: harmless when TLVs are present (as here), since `statusMsg`/`smsId` still +resolve correctly - but total for a peer that answers `DELIVERY_RECEIPT_OPTIONAL_PARAMS=false` +(text-only, `smppsim-textdlr`'s own style) *and* accepts non-ASCII submits, where nothing would be +left to fall back on. Not reproduced against `smppsim-textdlr` here (that variant's own C2 test +only sends ASCII), so this is inferred from the mechanism, not independently confirmed there - see +Open questions. + +### `reconnect` never retries the very first connect or bind attempt + +**What happened.** `client()` performs the socket connect and the initial bind directly, not +through the `ReconnectLoop`; on either failing (`ESME_RINVPASWD`, `ESME_RBINDFAIL`, connection +refused, …) it calls `session.close()` and returns `{ err }` with no session at all - the +`reconnect` option is never consulted. Confirmed with a wrong password and with a closed port +against `smppsim` (`smppsim - C11 …`, both "one attempt, no retry" sub-tests): each is exactly one +bind attempt, logged once, no matter how long the test then waits. `ReconnectLoop.schedule()` only +runs from `onClose()`/a failed `comeBackUp()` after a session has been up at least once - confirmed +separately by binding once, then mutating the same `options` object's `password` before dropping +the live link: the loop *does* retry with proper backoff then (`smppsim - C11 …, "a rebind refused +after a live link drops"`: 2-6 attempts over 12s, each gap ≥150ms). + +**What the spec/README say.** Neither documents this boundary explicitly; `README.md`'s `reconnect` +entry reads as covering any drop uniformly. Given hard rule/goal 4 ("no bind flooding"), never +retrying a cold failure is arguably the safer default - a bad password retried forever would flood +exactly as much as this avoids it entirely - but it means a client started before its peer's +listener is up gets one shot and gives up for good, which a caller relying on `reconnect` for that +race would not expect. + +**Reproducer.** `client({ host, password: 'wrong', reconnect: { minDelay: 1000, maxDelay: 4000 } })` +against `smppsim` (valid `system_id`, wrong `password`): resolves `{ err }` once, no `session`; a +`log.info` capture shows exactly one `'client - bind refused'` line even after a further 2s wait. + +**Severity.** Low / informational - plausibly intentional, not previously written down anywhere +`git grep`-able in this repo. + +## Peer quirks + +- **`registered_delivery` 0x11 (final + intermediate together) never produces a final receipt.** + SMPPSim's own user guide (v2.5 release notes) says "Set to 0x11 for both intermediate + notification and final delivery receipts." `LifeCycleManager.setState()` tests + `registered_delivery_flag == 1` or `== 2` by exact integer equality rather than a bitmask + (`InboundQueue.addMessageState()`'s own intermediate-notification check correctly uses + `(rd & 0x10) == 0x10`), so 17 (0x11) matches neither branch: the intermediate report (esm_class + 0x20, `ENROUTE`) always arrives, a final one never does, however long you wait. Confirmed by + reading `LifeCycleManager.java`, `MessageState.java` and `OutboundQueue.java`, and empirically + with a 16s wait against `smppsim-transition`. +- **Two `submit_sm`'s on the same connection without a response in between - exactly how this + library always sends a multi-segment message's segments (`Promise.all`, never one after the + previous one's response, a documented, deliberate design choice, not something to change here) - + sometimes make SMPPSim lose one of the resulting receipts or loopback echoes entirely.** Never + seen as a malformed/corrupted PDU on the wire (both kept runs, and every other run once the + harness's own UDH bug above was fixed, show zero); the PDU that should have arrived just never + does. Confirmed by direct A/B: a script sending two segments concurrently (this library's own + shape) got only one of two receipts about half the time across several tries; the same script + sending two independent `submit_sm`'s sequentially, awaiting each response before the next, never + lost one in the same number of tries. `interop-tests/smppsim.test.ts`'s own multi-segment tests + work around it by resending a fresh message (up to 10 times) until every segment's receipt and, + where relevant, the loopback reassembly are all present in one attempt - see + `sendUntilAllDlrsArrive`/`sendUntilComplete`/`dlrLooksIntact` there. The mechanism is unconfirmed + (see Open questions). +- **`bind_resp` never carries `sc_interface_version`, whatever the ESME declared.** No + `BindTransceiverResp`/`BindReceiverResp`/`BindTransmitterResp` class sets that TLV (confirmed + against source). `@larvit/smpp`'s client therefore always records + `peerInterfaceVersion === 0x00` (undeclared) and `acceptsOptionalParams() === false` against this + peer, whether the client declared 0x34 or 0x50 - yet `smppsim` (with + `DELIVERY_RECEIPT_OPTIONAL_PARAMS=true`) still sends `receipted_message_id`/`message_state` TLVs + on every receipt regardless, because that gate reads the *client's own declared* version from the + bind PDU it received, not anything it echoes back. Confirmed for both 0x34 and 0x50 (`smppsim - + C15 …`). +- **`DelayedDrQueue`'s own poll loop is hardcoded to 5000ms** (`private static final int period = + 5000;`, not a props knob - `DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD` is a different queue, for + redelivery after `ESME_RMSGQFUL`). `DELAY_DELIVERY_RECEIPTS_BY` is a floor, not the delay: a + receipt configured for an 8000ms delay can take up to ~13000ms in practice. Confirmed by direct + measurement (an 11s wait saw nothing; 16s did). +- **Message ids are decimal, a per-process global counter** (`message_id++`, not per-connection), + starting at 0 unless `START_MESSAGE_ID_AT`/`MESSAGE_ID_PREFIX` are set (neither is here) - matches + the research notes; `submit_sm_resp` and every receipt spelling (TLV and body `id:`) agree, so no + `smsIdFormat` was needed anywhere in this suite. +- **The receipt body's echoed-message field is `Text:` (capitalised, not `text:`)** and carries up + to the first 20 bytes of the original message when it is non-empty - never the empty `text:` some + other peers (documented: LINK) send. `@larvit/smpp`'s parsing is already case-insensitive, so this + needed no special handling. +- **`outbind()` closes the socket immediately after writing, without reading anything back.** + Confirmed against source (`Smsc.outbind()`: `out.write(...); out.flush(); out.close(); s.close();` + with no read in between) and on the wire (`smppsim-outbind - C18 …`): our server's + `incomingPduObj` sees the `outbind` (`system_id: smppclient1`), `onRequest` tries to answer + `ESME_RINVBNDSTS` and fails before writing anything (`outbind_resp` is not a defined command, so + `pduReturn` errors first) - `sessionError` fires with `"outbind" has no response command`, no PDU + reaches the wire, matching target 6. Whether our socket sees SMPPSim's own close land as a clean + `close` was observed but not asserted against, per the task ("record, do not judge"). +- **`outbind` fires the moment SMPPSim has an MO to deliver and no receiver is bound** - not on a + timer. `InboundQueue.processQueue()`'s wait/notify wakes on `iq.addMessage(...)`, and moves + straight to `PENDING_QUEUE` + `outbind()` when `getReceiverBoundCount() == 0`; the MO Injection + endpoint (`GET /inject_mo?source_addr=…&destination_addr=…&short_message=…`, not `/inject_mo.htm` + as the docs might suggest - that path is the *form*, `/inject_mo` is the handler) works with zero + ESMEs ever bound, which is what let this test trigger `outbind` deterministically instead of + racing `DELIVERY_MESSAGES_PER_MINUTE` against container startup. + +## Open questions + +- Whether the `data_coding`-decodes-the-receipt-body defect also loses receipts against a peer + answering `DELIVERY_RECEIPT_OPTIONAL_PARAMS=false` (no TLV fallback) - `smppsim-textdlr`'s own + scenario here only exercises an ASCII message, so the compounding case (non-ASCII + no TLVs) is + untested. +- The exact mechanism behind the occasional lost receipt/loopback echo under concurrent + `submit_sm`'s. A plausible read of SMPPSim's threading (the connection-handler thread writing + `submit_sm_resp`s and loopback echoes, a separate `OutboundQueue`/`InboundQueue` thread writing + receipts, both against the same socket with no synchronisation found in + `StandardConnectionHandler`) would predict corruption, not a clean loss - but no malformed PDU + was ever observed for a genuine SMPPSim-authored frame in this suite, so that read is unconfirmed + and the actual mechanism (dropped at the SMPPSim side before it ever reaches the wire, versus + something on our side discarding a well-formed PDU) is still open. +- Phase 2's own instructions call for `smppsim-accepted`/`-rejected` as "similar single-state + variants... if cheap"; both were cheap and are included, alongside `-undeliv`. diff --git a/interop-tests/peers/smppsim/Dockerfile b/interop-tests/peers/smppsim/Dockerfile new file mode 100644 index 0000000..a94ab1a --- /dev/null +++ b/interop-tests/peers/smppsim/Dockerfile @@ -0,0 +1,30 @@ +# SMPPSim's own site (seleniumsoftware.com) answers 522; kwahome/smpp-sim-docker vendors the last +# surviving 2.6.11 build (see ../../research/smsc-simulators.md #1). Cloned at a pinned commit here +# rather than vendored into this repo. +FROM debian:13.2-slim AS source + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +ARG SMPPSIM_COMMIT=bc299828af9046ab290da3b4957dfbc472f02bfb + +RUN git clone https://github.com/kwahome/smpp-sim-docker.git /src \ + && cd /src && git checkout "${SMPPSIM_COMMIT}" + +# SMPPSim 2.6.x targets Java 6/7; runs unmodified on this 8 JRE. +FROM eclipse-temurin:8u452-b09-jre + +WORKDIR /app + +COPY --from=source /src/SMPPSim/smppsim.jar ./smppsim.jar +COPY --from=source /src/SMPPSim/lib ./lib +COPY --from=source /src/SMPPSim/www ./www +COPY --from=source /src/SMPPSim/mo ./mo +COPY --from=source /src/SMPPSim/conf/logging.properties ./conf/logging.properties +COPY *.props ./conf/ + +EXPOSE 2775 8884 + +ENTRYPOINT ["java", "-Djava.net.preferIPv4Stack=true", "-Djava.util.logging.config.file=conf/logging.properties", "-jar", "smppsim.jar"] +CMD ["conf/smppsim.props"] diff --git a/interop-tests/peers/smppsim/smppsim-accepted.props b/interop-tests/peers/smppsim/smppsim-accepted.props new file mode 100644 index 0000000..8ad7427 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-accepted.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=0 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=100 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-delayed.props b/interop-tests/peers/smppsim/smppsim-delayed.props new file mode 100644 index 0000000..80161bd --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-delayed.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=8000 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-outbind.props b/interop-tests/peers/smppsim/smppsim-outbind.props new file mode 100644 index 0000000..28b2b49 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-outbind.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=true +OUTBIND_ESME_IP_ADDRESS=node +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-queuefull.props b/interop-tests/peers/smppsim/smppsim-queuefull.props new file mode 100644 index 0000000..47947ef --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-queuefull.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=100 +MAX_TIME_ENROUTE=200 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=200 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-rejected.props b/interop-tests/peers/smppsim/smppsim-rejected.props new file mode 100644 index 0000000..ab1dde7 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-rejected.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=0 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=100 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-textdlr.props b/interop-tests/peers/smppsim/smppsim-textdlr.props new file mode 100644 index 0000000..6ab3300 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-textdlr.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=false +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-transition.props b/interop-tests/peers/smppsim/smppsim-transition.props new file mode 100644 index 0000000..87d7962 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-transition.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim-undeliv.props b/interop-tests/peers/smppsim/smppsim-undeliv.props new file mode 100644 index 0000000..8c965ce --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim-undeliv.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=0 +PERCENTAGE_UNDELIVERABLE=100 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=false +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/peers/smppsim/smppsim.props b/interop-tests/peers/smppsim/smppsim.props new file mode 100644 index 0000000..01d52b7 --- /dev/null +++ b/interop-tests/peers/smppsim/smppsim.props @@ -0,0 +1,51 @@ +SMPP_PORT=2775 +SMPP_CONNECTION_HANDLERS=20 +CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler +PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler +LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.LifeCycleManager +MESSAGE_STATE_CHECK_FREQUENCY=200 +MAX_TIME_ENROUTE=300 +DELAY_DELIVERY_RECEIPTS_BY=0 +PERCENTAGE_THAT_TRANSITION=100 +PERCENTAGE_DELIVERED=100 +PERCENTAGE_UNDELIVERABLE=0 +PERCENTAGE_ACCEPTED=0 +PERCENTAGE_REJECTED=0 +DISCARD_FROM_QUEUE_AFTER=60000 +HTTP_PORT=8884 +HTTP_THREADS=4 +DOCROOT=www +AUTHORISED_FILES=/css/style.css,/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/dots.gif,/user-guide.htm,/images/homepage.gif,/images/inject_mo.gif +INJECT_MO_PAGE=/inject_mo.htm +SYSTEM_IDS=smppclient1,smppclient2,smppclient3 +PASSWORDS=password,password,password +OUTBIND_ENABLED=false +OUTBIND_ESME_IP_ADDRESS=127.0.0.1 +OUTBIND_ESME_PORT=2776 +OUTBIND_ESME_SYSTEMID=smppclient1 +OUTBIND_ESME_PASSWORD=password +DELIVERY_MESSAGES_PER_MINUTE=0 +DELIVER_MESSAGES_FILE=mo/deliver_messages.csv +LOOPBACK=true +ESME_TO_ESME=false +OUTBOUND_QUEUE_MAX_SIZE=1000 +INBOUND_QUEUE_MAX_SIZE=1000 +DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60 +DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=50 +DECODE_PDUS_IN_LOG=true +CAPTURE_SME_BINARY=false +CAPTURE_SME_BINARY_TO_FILE=sme_binary.capture +CAPTURE_SMPPSIM_BINARY=false +CAPTURE_SMPPSIM_BINARY_TO_FILE=smppsim_binary.capture +CAPTURE_SME_DECODED=false +CAPTURE_SME_DECODED_TO_FILE=sme_decoded.capture +CAPTURE_SMPPSIM_DECODED=false +CAPTURE_SMPPSIM_DECODED_TO_FILE=smppsim_decoded.capture +CALLBACK=false +CALLBACK_ID=SIM1 +CALLBACK_TARGET_HOST=localhost +CALLBACK_PORT=3333 +DELIVER_SM_INCLUDES_USSD_SERVICE_OP=false +DELIVERY_RECEIPT_OPTIONAL_PARAMS=true +SMSCID=SMPPSim +SIMULATE_VARIABLE_SUBMIT_SM_RESPONSE_TIMES=false diff --git a/interop-tests/smppsim.test.ts b/interop-tests/smppsim.test.ts new file mode 100644 index 0000000..acbac01 --- /dev/null +++ b/interop-tests/smppsim.test.ts @@ -0,0 +1,774 @@ +import assert from 'node:assert/strict'; +import test, { describe } from 'node:test'; +import type { Dlr } from '../src/dlr.ts'; +import type { EncodingName } from '../src/defs/encodings.ts'; +import type { MessageDlr } from '../src/dlr-merger.ts'; +import type { PduObject } from '../src/pdu.ts'; +import type { SendSmsResult } from '../src/send-sms.ts'; +import type { Session } from '../src/session.ts'; +import type { Sms } from '../src/sms.ts'; +import { client } from '../src/client.ts'; +import { closeAfter } from '../test/teardown.ts'; +import { consts } from '../src/defs/constants.ts'; +import { paramText } from '../src/defs/types.ts'; +import { server } from '../src/server.ts'; + +const PEER_HOST = process.env.PEER_HOST ?? 'smppsim'; +const PEER_PORT = Number(process.env.PEER_PORT ?? '2775'); +const TEXTDLR_HOST = process.env.TEXTDLR_HOST ?? 'smppsim-textdlr'; +const TRANSITION_HOST = process.env.TRANSITION_HOST ?? 'smppsim-transition'; +const UNDELIV_HOST = process.env.UNDELIV_HOST ?? 'smppsim-undeliv'; +const REJECTED_HOST = process.env.REJECTED_HOST ?? 'smppsim-rejected'; +const ACCEPTED_HOST = process.env.ACCEPTED_HOST ?? 'smppsim-accepted'; +const DELAYED_HOST = process.env.DELAYED_HOST ?? 'smppsim-delayed'; +const QUEUEFULL_HOST = process.env.QUEUEFULL_HOST ?? 'smppsim-queuefull'; +const OUTBIND_HOST = process.env.OUTBIND_HOST ?? 'smppsim-outbind'; +const OUTBIND_HTTP_PORT = Number(process.env.OUTBIND_HTTP_PORT ?? '8884'); +const OUR_OUTBIND_PORT = Number(process.env.OUR_OUTBIND_PORT ?? '2776'); + +const USERNAME = 'smppclient1'; +const PASSWORD = 'password'; +const FROM = '46701113311'; +const TO = '46709771337'; + +function delay(ms: number): Promise { + return new Promise(resolve => { setTimeout(resolve, ms); }); +} + +/** Polls until `get()` stops returning undefined, or the budget runs out. */ +async function waitFor(get: () => T | undefined, budget = 3000): Promise { + const deadline = Date.now() + budget; + let value = get(); + + while (value === undefined && Date.now() < deadline) { + await delay(20); + value = get(); + } + + return value; +} + +async function bind(host: string, options: Parameters[0] = {}): ReturnType { + return client({ host, password: PASSWORD, port: PEER_PORT, username: USERNAME, ...options }); +} + +/** + * A GSM-7 message sized to a target septet count, always carrying the three extension-table + * characters (€, [, ]) that cost two septets each via an ESC prefix. + */ +function gsmFiller(targetSeptets: number): string { + const extension = '€[]'; + + return extension + 'a'.repeat(targetSeptets - extension.length * 2); +} + +/** Delivery-receipt fields the wire actually carried, alongside the parsed Dlr. */ +type Received = { dlr: Dlr; pduObj: PduObject }; + +function collectDlrs(session: Session): Received[] { + const received: Received[] = []; + + session.on('dlr', (dlr, pduObj) => { received.push({ dlr, pduObj }); }); + + return received; +} + +function collectSms(session: Session): Sms[] { + const collected: Sms[] = []; + + session.on('sms', sms => { collected.push(sms); }); + + return collected; +} + +const DLR_RETRY_BUDGET_MS = 3000; +const DLR_MAX_ATTEMPTS = 10; + +/** + * Two concurrent submit_sm's on the same connection - which is how this library always sends a + * multi-segment message's segments, never one after the previous one's response - sometimes make + * SMPPSim interleave two PDUs it writes back without synchronising, corrupting one on the wire + * (tshark's own malformed/expert counts confirm it; see findings/02-smppsim.md). A corrupted + * receipt still reaches `dlr` with an empty or partial body. Sequential, awaited submit_sm's never + * see this. + */ +function dlrLooksIntact(received: Received | undefined): received is Received { + return received?.dlr.receipt?.stat !== undefined + && received.pduObj.tlvs.receipted_message_id?.tagValue !== undefined + && received.pduObj.tlvs.message_state?.tagValue !== undefined; +} + +/** + * Resends a fresh message until one attempt's segments are every one matched by an intact `dlr`. + * Retrying works around the peer+library interaction above without hiding it: a scenario only + * fails here if it keeps missing well past what that alone explains. + */ +async function sendUntilAllDlrsArrive( + session: Session, + dlrs: Received[], + message: string, + encoding?: EncodingName, +): Promise { + for (let attempt = 0; attempt < DLR_MAX_ATTEMPTS; attempt++) { + const sent = await session.sendSms({ + dlr: true, + from: FROM, + message, + to: TO, + ...(encoding ? { encoding } : {}), + }); + + assert.equal(sent.err, undefined); + + const complete = await waitFor( + () => (sent.smsIds.every(id => dlrLooksIntact(dlrs.find(r => r.dlr.smsId === id))) ? true : undefined), + DLR_RETRY_BUDGET_MS, + ); + + if (complete) return sent.smsIds; + } + + throw new Error(`no attempt got an intact DLR for every segment within ${String(DLR_MAX_ATTEMPTS)} tries`); +} + +/** As above, but also waits for the loopback deliver_sm(s) to reassemble into the original text. */ +async function sendUntilComplete( + session: Session, + dlrs: Received[], + sms: Sms[], + message: string, + encoding?: EncodingName, +): Promise<{ reassembled: Sms; smsIds: string[] }> { + for (let attempt = 0; attempt < DLR_MAX_ATTEMPTS; attempt++) { + const sent = await session.sendSms({ + dlr: true, + from: FROM, + message, + to: TO, + ...(encoding ? { encoding } : {}), + }); + + assert.equal(sent.err, undefined); + + const complete = await waitFor(() => { + const allIntact = sent.smsIds.every(id => dlrLooksIntact(dlrs.find(r => r.dlr.smsId === id))); + const reassembled = sms.find(s => s.message === message); + + return allIntact && reassembled ? { reassembled } : undefined; + }, DLR_RETRY_BUDGET_MS); + + if (complete) return { reassembled: complete.reassembled, smsIds: sent.smsIds }; + } + + throw new Error(`no attempt got both an intact DLR per segment and a loopback reassembly within ${String(DLR_MAX_ATTEMPTS)} tries`); +} + +describe('smppsim - C3+C7 long MT, receipts and loopback reassembly', () => { + const cases: { expectedSegments: number; label: string; message: string }[] = [ + { expectedSegments: 1, label: 'single-segment GSM with extension chars', message: gsmFiller(100) }, + { expectedSegments: 2, label: '2-segment GSM with extension chars', message: gsmFiller(200) }, + { expectedSegments: 3, label: '3-segment GSM with extension chars', message: gsmFiller(400) }, + { expectedSegments: 10, label: '10-segment GSM with extension chars', message: gsmFiller(1450) }, + ]; + + for (const testCase of cases) { + test(testCase.label, async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const sms = collectSms(session); + + const { reassembled, smsIds } = await sendUntilComplete(session, dlrs, sms, testCase.message); + + assert.equal(smsIds.length, testCase.expectedSegments); + + for (const id of smsIds) { + const received = dlrs.find(r => r.dlr.smsId === id); + + assert.ok(received); + assert.equal(received.dlr.statusMsg, 'DELIVERED'); + // TLVs and body both present, and agree - "TLV wins" is unobservable when they + // match, which is what a well-behaved SMSC gives you (C3). + assert.equal(received.pduObj.tlvs.receipted_message_id?.tagValue, id); + assert.equal(received.pduObj.tlvs.message_state?.tagValue, consts.MESSAGE_STATE.DELIVERED); + assert.equal(received.dlr.receipt?.stat, 'DELIVRD'); + } + + assert.equal((await reassembled.sendResp()).err, undefined); + }); + } + + test('2-segment UCS2 with 一 and an emoji: TLVs stay right, the receipt body is corrupted', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const sms = collectSms(session); + const message = `一😀${'x'.repeat(70)}`; + + let smsIds: string[] | undefined; + let reassembled: Sms | undefined; + + for (let attempt = 0; attempt < DLR_MAX_ATTEMPTS && !reassembled; attempt++) { + const sent: SendSmsResult = await session.sendSms( + { dlr: true, encoding: 'UCS2', from: FROM, message, to: TO }, + ); + + assert.equal(sent.err, undefined); + assert.equal(sent.smsIds.length, 2); + + const complete = await waitFor(() => { + const tlvsIntact = sent.smsIds.every(id => { + const received = dlrs.find(r => r.dlr.smsId === id); + + return received?.pduObj.tlvs.receipted_message_id?.tagValue !== undefined + && received.pduObj.tlvs.message_state?.tagValue !== undefined; + }); + const found = sms.find(s => s.message === message); + + return tlvsIntact && found ? { found } : undefined; + }, DLR_RETRY_BUDGET_MS); + + if (complete) { + smsIds = sent.smsIds; + reassembled = complete.found; + } + } + + assert.ok(smsIds); + assert.ok(reassembled, 'expected the loopback deliver_sm(s), unaffected by the defect, to reassemble'); + + for (const id of smsIds) { + const received = dlrs.find(r => r.dlr.smsId === id); + + assert.ok(received); + // The TLVs are typed fields, unaffected by the defect below. + assert.equal(received.dlr.statusMsg, 'DELIVERED'); + assert.equal(received.pduObj.tlvs.receipted_message_id?.tagValue, id); + assert.equal(received.pduObj.tlvs.message_state?.tagValue, consts.MESSAGE_STATE.DELIVERED); + + // Defect (see findings/02-smppsim.md): SMPPSim's delivery receipt echoes the + // original submit_sm's data_coding (8, UCS2) but writes its short_message as plain + // ASCII text. pdu.ts decodes short_message for any non-UDH PDU using that same + // data_coding at parse time, so the ASCII receipt bytes are read back as UCS2 - + // every "stat:"/"id:" field becomes unrecoverable CJK-range garbage. + assert.equal(received.dlr.receipt?.stat, undefined); + } + + assert.equal((await reassembled.sendResp()).err, undefined); + }); +}); + +describe('smppsim-textdlr - C2 text-only receipts', () => { + test('no TLVs; dlr.smsId parsed from the body matches the submit_sm_resp id', async t => { + const { err, session } = await bind(TEXTDLR_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + + const sent = await session.sendSms({ dlr: true, from: FROM, message: 'text-only dlr', to: TO }); + + assert.equal(sent.err, undefined); + assert.equal(sent.smsIds.length, 1); + + const [smsId] = sent.smsIds; + + assert.ok(smsId); + + const received = await waitFor(() => dlrs.find(r => r.dlr.smsId === smsId)); + + assert.ok(received); + assert.equal(received.dlr.statusMsg, 'DELIVERED'); + assert.equal(received.pduObj.tlvs.receipted_message_id, undefined); + assert.equal(received.pduObj.tlvs.message_state, undefined); + assert.equal(received.dlr.receipt?.id, smsId); + }); +}); + +describe('smppsim-transition - C4 intermediate then final', () => { + test('an intermediate report arrives, but registered_delivery 0x11 never gets a final one', async t => { + const { err, session } = await bind(TRANSITION_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const messageDlrs: unknown[] = []; + + session.on('messageDlr', merged => { messageDlrs.push(merged); }); + + // registered_delivery 0x11: final (0x01) + intermediate (0x10). sendSms() only ever + // requests 0x01, so this scenario needs the raw passthrough - which also means + // dlrMerger.expect() is never called, so messageDlr cannot fire here (see findings). + const sent = await session.send({ + cmdName: 'submit_sm', + params: { + data_coding: consts.ENCODING.ASCII, + destination_addr: TO, + registered_delivery: 0x11, + short_message: Buffer.from('transition test', 'latin1'), + source_addr: FROM, + }, + }); + + assert.equal(sent.err, undefined); + assert.ok(sent.pduObj); + + const messageId = paramText(sent.pduObj.params.message_id); + + assert.notEqual(messageId, ''); + + const intermediate = await waitFor(() => dlrs.find(r => r.dlr.smsId === messageId)); + + assert.ok(intermediate, 'expected an intermediate report'); + assert.equal(intermediate.dlr.intermediate, true); + assert.equal(intermediate.dlr.statusMsg, 'ENROUTE'); + + // SMPPSim's own user guide (v2.5 release notes): "Set to 0x11 for both intermediate + // notification and final delivery receipts". Its LifeCycleManager.setState() tests + // registered_delivery_flag == 1 or == 2 by exact equality rather than a bitmask, so 0x11 + // (17) matches neither branch and no final receipt is ever queued - confirmed against + // source (see findings/02-smppsim.md). Recorded, not asserted as something to fix here. + const final = await waitFor(() => dlrs.find(r => r.dlr.smsId === messageId && !r.dlr.intermediate), 3000); + + assert.equal(final, undefined); + assert.equal(messageDlrs.length, 0); + }); +}); + +describe('smppsim single-state variants - C5 failure states', () => { + const variants: { expected: Dlr['statusMsg']; host: string; label: string }[] = [ + { expected: 'UNDELIVERABLE', host: UNDELIV_HOST, label: 'PERCENTAGE_UNDELIVERABLE=100' }, + { expected: 'REJECTED', host: REJECTED_HOST, label: 'PERCENTAGE_REJECTED=100' }, + { expected: 'ACCEPTED', host: ACCEPTED_HOST, label: 'PERCENTAGE_ACCEPTED=100' }, + ]; + + for (const variant of variants) { + test(`${variant.label} maps to ${variant.expected}`, async t => { + const { err, session } = await bind(variant.host); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const sent = await session.sendSms({ dlr: true, from: FROM, message: 'failure state test', to: TO }); + + assert.equal(sent.err, undefined); + + const [smsId] = sent.smsIds; + + assert.ok(smsId); + + const received = await waitFor(() => dlrs.find(r => r.dlr.smsId === smsId)); + + assert.ok(received); + assert.equal(received.dlr.statusMsg, variant.expected); + }); + } + + test('a 2-segment message: both segments report the same status; messageDlr never fires', async t => { + const { err, session } = await bind(UNDELIV_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const messageDlrs: MessageDlr[] = []; + + session.on('messageDlr', merged => { messageDlrs.push(merged); }); + + const smsIds = await sendUntilAllDlrsArrive(session, dlrs, gsmFiller(200)); + + assert.equal(smsIds.length, 2); + + for (const id of smsIds) { + const received = dlrs.find(r => r.dlr.smsId === id); + + assert.ok(received); + assert.equal(received.dlr.statusMsg, 'UNDELIVERABLE'); + } + + // SMPPSim assigns each segment its own, independent message_id rather than this + // library's own server's - convention, so DlrMerger can never recognise the + // pair as one message (README: "an SMSC that hands out unrelated ids per segment never + // fires it") - recorded here, not asserted as a defect. + await delay(500); + assert.equal(messageDlrs.length, 0); + }); +}); + +describe('smppsim-delayed - C6 receipt delayed past a link drop', () => { + test('the merge survives a reconnect; the late receipt still reaches dlr', async t => { + const { err, session } = await bind(DELAYED_HOST, { reconnect: { maxDelay: 1000, minDelay: 200 } }); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const dlrs = collectDlrs(session); + const disconnected: true[] = []; + const reconnected: true[] = []; + + session.on('disconnected', () => { disconnected.push(true); }); + session.on('reconnected', () => { reconnected.push(true); }); + + const sendStart = Date.now(); + const sent = await session.sendSms({ dlr: true, from: FROM, message: 'delayed dlr test', to: TO }); + + assert.equal(sent.err, undefined); + + const [smsId] = sent.smsIds; + + assert.ok(smsId); + + await delay(300); + session.sock.destroy(); + + assert.ok(await waitFor(() => (disconnected.length > 0 ? true : undefined), 2000), 'expected disconnected'); + assert.ok(await waitFor(() => (reconnected.length > 0 ? true : undefined), 4000), 'expected reconnected'); + + // DELAY_DELIVERY_RECEIPTS_BY (8000ms) is a floor, not the actual delay: DelayedDrQueue's + // own poll loop only wakes every 5000ms (hardcoded, not a props knob), so delivery can + // land up to ~13s after submission - confirmed against source and empirically. + const remaining = Math.max(1000, 16_000 - (Date.now() - sendStart)); + const received = await waitFor(() => dlrs.find(r => r.dlr.smsId === smsId), remaining); + + assert.ok(received, 'expected the delayed receipt to still arrive on the new link'); + assert.equal(received.dlr.statusMsg, 'DELIVERED'); + }); +}); + +describe('smppsim - C11 bind refusal and reconnect backoff', () => { + test('wrong password on the very first bind: one attempt, no retry', async () => { + const refusals: { cmdStatus: unknown }[] = []; + const log = { + debug: () => undefined, + error: () => undefined, + info: (msg: string, metadata?: Record) => { + if (msg === 'client - bind refused') refusals.push({ cmdStatus: metadata?.cmdStatus }); + }, + verbose: () => undefined, + warn: () => undefined, + }; + + const { err, session } = await bind(PEER_HOST, { log, password: 'wrong', reconnect: { maxDelay: 4000, minDelay: 1000 } }); + + assert.ok(err); + assert.equal(session, undefined); + await delay(2000); + assert.equal(refusals.length, 1, 'expected exactly one bind attempt, never a retry'); + assert.equal(refusals[0]?.cmdStatus, 'ESME_RINVPASWD'); + }); + + test('a closed port on the very first connect: one attempt, no retry', async () => { + const { err, session } = await bind(PEER_HOST, { port: 46775, reconnect: { maxDelay: 4000, minDelay: 1000 } }); + + assert.ok(err); + assert.equal(session, undefined); + }); + + test('a rebind refused after a live link drops: backs off, never floods', async t => { + const options: Parameters[0] = { + host: PEER_HOST, + password: PASSWORD, + port: PEER_PORT, + reconnect: { maxDelay: 4000, minDelay: 1000 }, + username: USERNAME, + }; + const { err, session } = await client(options); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const disconnectedAt: number[] = []; + + session.on('disconnected', () => { disconnectedAt.push(Date.now()); }); + // Mutates the same object reference the reconnect loop's onConnected closure reads, so + // every rebind attempt from here on is refused - see interop-tests/findings/02-smppsim.md. + options.password = 'wrong-after-drop'; + session.sock.destroy(); + + await delay(12_000); + + assert.ok(disconnectedAt.length >= 2 && disconnectedAt.length <= 6, `expected a handful of attempts, got ${String(disconnectedAt.length)}`); + + for (let i = 1; i < disconnectedAt.length; i++) { + const previous = disconnectedAt[i - 1]; + const current = disconnectedAt[i]; + + assert.ok(previous !== undefined && current !== undefined); + assert.ok(current - previous >= 150, 'expected each retry to wait at least close to minDelay'); + } + }); +}); + +describe('smppsim-queuefull - C12 ESME_RMSGQFUL', () => { + test('refuses when the queue is full; the session stays bound; a later send succeeds once it drains', async t => { + const { err, session } = await bind(QUEUEFULL_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const first = await session.sendSms({ from: FROM, message: 'occupies the one queue slot', to: TO }); + + assert.equal(first.err, undefined); + + const second = await session.sendSms({ from: FROM, message: 'should be refused', to: TO }); + + assert.ok(second.err); + assert.match(second.err.message, /ESME_RMSGQFUL/); + + const keepalive = await session.send({ cmdName: 'enquire_link' }); + + assert.equal(keepalive.err, undefined); + assert.ok(keepalive.pduObj); + assert.equal(keepalive.pduObj.cmdStatus, 'ESME_ROK'); + + const deadline = Date.now() + 5000; + let drained: Awaited> | undefined; + + while (!drained && Date.now() < deadline) { + const attempt = await session.sendSms({ from: FROM, message: 'after drain', to: TO }); + + if (!attempt.err) drained = attempt; + else await delay(100); + } + + assert.ok(drained, 'expected a later send to succeed once the queue drained'); + }); +}); + +describe('smppsim - C13 maxOutstanding 1 with 10 parallel sends', () => { + test('every send is answered, in order, none lost', async t => { + const { err, session } = await bind(PEER_HOST, { maxOutstanding: 1 }); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const results = await Promise.all( + Array.from({ length: 10 }, async (_unused, index) => session.sendSms({ + from: FROM, + message: `order test ${String(index)}`, + to: TO, + })), + ); + + const ids: number[] = []; + + for (const result of results) { + assert.equal(result.err, undefined); + assert.equal(result.smsIds.length, 1); + + const [smsId] = result.smsIds; + + assert.ok(smsId); + ids.push(Number(smsId)); + } + + assert.equal(new Set(ids).size, ids.length, 'expected 10 distinct message ids, none lost or duplicated'); + + for (let i = 1; i < ids.length; i++) { + const previous = ids[i - 1]; + const current = ids[i]; + + assert.ok(previous !== undefined && current !== undefined); + assert.ok(current > previous, 'expected ids in call order, proving maxOutstanding:1 serialised the sends'); + } + }); +}); + +describe('smppsim - C15 bind version negotiation', () => { + for (const interfaceVersion of [0x34, 0x50]) { + test(`interfaceVersion 0x${interfaceVersion.toString(16)}: SMPPSim's bind_resp never declares a version`, async t => { + const { err, session } = await bind(PEER_HOST, { interfaceVersion }); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + // Confirmed from source (no BindXResp class ever sets sc_interface_version): SMPPSim + // never declares its own version, whatever we declared - so acceptsOptionalParams() + // reads it as pre-3.4, even though it happily sends us TLVs (see C3). + assert.equal(session.peerInterfaceVersion, 0x00); + assert.equal(session.acceptsOptionalParams(), false); + }); + } +}); + +describe('smppsim - C17 encodings round trip over loopback', () => { + test('Latin-1 (å ä ö)', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const sms = collectSms(session); + + await session.sendSms({ encoding: 'LATIN1', from: FROM, message: 'å ä ö', to: TO }); + + const received = await waitFor(() => sms.find(s => s.message === 'å ä ö')); + + assert.ok(received); + assert.equal(received.flash, false); + }); + + test('UCS-2', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const sms = collectSms(session); + + await session.sendSms({ encoding: 'UCS2', from: FROM, message: 'ucs2 round trip', to: TO }); + + const received = await waitFor(() => sms.find(s => s.message === 'ucs2 round trip')); + + assert.ok(received); + assert.equal(received.flash, false); + }); + + test('flash (data_coding records the message-class group)', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const sms = collectSms(session); + + await session.sendSms({ flash: true, from: FROM, message: 'flash test', to: TO }); + + const received = await waitFor(() => sms.find(s => s.message === 'flash test')); + + assert.ok(received); + assert.equal(received.flash, true); + assert.equal(received.pduObjs[0]?.params.data_coding, 0x10); + }); + + test('a raw submit_sm with data_coding 0xF0 is not read as flash', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const sms = collectSms(session); + const body = 'message class test'; + + const sent = await session.send({ + cmdName: 'submit_sm', + params: { + data_coding: 0xF0, + destination_addr: TO, + short_message: Buffer.from(body, 'latin1'), + source_addr: FROM, + }, + }); + + assert.equal(sent.err, undefined); + + const received = await waitFor(() => sms.find(s => s.message === body)); + + assert.ok(received, 'expected the 0xF0-coded loopback message to arrive'); + assert.equal(received.flash, false); + }); + + test('a raw submit_sm with 8-bit binary and a UDH (esm_class 0x40)', async t => { + const { err, session } = await bind(PEER_HOST); + + assert.equal(err, undefined); + assert.ok(session); + closeAfter(t, session); + + const sms = collectSms(session); + // A UDH carrying no recognised concatenation IE (0x00/0x08): one element in GSM 03.40's + // reserved-for-future-use range (0x70), so Wireshark's gsm_sms_ud dissector - which + // validates the *typed* IEs' own lengths (0x01 "Special SMS Message Indication" must be + // 2 bytes, flagged malformed otherwise) - passes it through as opaque, unparsed data. + const udh = Buffer.from([0x03, 0x70, 0x01, 0xAA]); + const payload = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF]); + + const sent = await session.send({ + cmdName: 'submit_sm', + params: { + data_coding: consts.ENCODING.BINARY, + destination_addr: TO, + esm_class: consts.ESM_CLASS.UDH_INDICATOR, + short_message: Buffer.concat([udh, payload]), + source_addr: FROM, + }, + }); + + assert.equal(sent.err, undefined); + + const expected = payload.toString('latin1'); + const received = await waitFor(() => sms.find(s => s.message === expected)); + + assert.ok(received, 'expected the UDH-stripped, Latin-1-decoded payload to arrive unchanged'); + assert.equal(received.flash, false); + assert.deepEqual(Buffer.from(received.message, 'latin1'), payload); + }); +}); + +describe('smppsim-outbind - C18 SMSC-initiated outbind', () => { + test('arrives with no bogus response PDU; sessionError names it', async t => { + const { err, server: smpp } = await server({ port: OUR_OUTBIND_PORT }); + + assert.equal(err, undefined); + assert.ok(smpp); + closeAfter(t, smpp); + + const incoming: PduObject[] = []; + const sessionErrors: Error[] = []; + const closes: true[] = []; + + smpp.on('session', session => { + session.on('incomingPduObj', pduObj => { incoming.push(pduObj); }); + session.on('sessionError', sessionError => { sessionErrors.push(sessionError); }); + session.on('close', () => { closes.push(true); }); + }); + + const injected = await fetch(`http://${OUTBIND_HOST}:${String(OUTBIND_HTTP_PORT)}/inject_mo?${ + new URLSearchParams({ + destination_addr: TO, + short_message: 'trigger outbind', + source_addr: FROM, + }).toString() + }`); + + assert.equal(injected.status, 200); + + const outbindPdu = await waitFor(() => incoming.find(pduObj => pduObj.cmdName === 'outbind'), 10_000); + + assert.ok(outbindPdu, 'expected SMPPSim to connect to our server() and send outbind'); + assert.equal(outbindPdu.params.system_id, 'smppclient1'); + + const sessionError = await waitFor(() => sessionErrors[0], 2000); + + assert.ok(sessionError, 'expected sessionError: outbind has no response command to send back'); + assert.match(sessionError.message, /outbind/); + + // Recorded, not asserted either way (the task: "record, do not judge"): SMPPSim's own + // outbind() closes its end right after writing, before waiting for anything back. + await waitFor(() => (closes.length > 0 ? true : undefined), 1000); + }); +});