sondahub

sondahub / SOAP test server

A SOAP test server with a WSDL for every API

Every mock API here is also a SOAP service: seven WSDLs, document/literal over SOAP 1.1 and 1.2, five operations per collection over thousands of related records, faults by the book, WS-Addressing, and a secure twin behind WS-Security. Writes are validated like a real server’s and are there when you read them back. Free, no key.

The services

Each of the seven APIs is a SOAP service with five operations per collection — List, Get, Create, Update, Delete — over the same data and the same rules as the REST API: a transfer checks the funds, an order refuses an impossible status, a booking takes a free seat.

ServiceWSDLOperations
Storehttps://api.sondahub.com/soap/store?wsdl45
Fleethttps://api.sondahub.com/soap/fleet?wsdl25
Bankhttps://api.sondahub.com/soap/bank?wsdl30
Socialhttps://api.sondahub.com/soap/social?wsdl25
Helpdeskhttps://api.sondahub.com/soap/helpdesk?wsdl25
Flightshttps://api.sondahub.com/soap/flights?wsdl20
Identityhttps://api.sondahub.com/soap/identity?wsdl15
Endpoint
https://api.sondahub.com/soap/{api}
WSDL
https://api.sondahub.com/soap/{api}?wsdl
SOAP 1.1
Content-Type: text/xml — SOAPAction: "https://sondahub.com/soap/store/{Operation}"
SOAP 1.2
Content-Type: application/soap+xml; action="…"
Namespace
https://sondahub.com/soap/{api}
Secure
https://api.sondahub.com/soap/secure/{api} — WS-Security UsernameToken or HTTP Basic, sonda / probe

Try it here

The examples on this page share one session: run them in order and each sees what the one before it wrote.
GetProduct, SOAP 1.1
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/GetProduct"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Body>
    <s:GetProduct><s:id>1</s:id></s:GetProduct>
  </soap:Body>
</soap:Envelope>'
ListProducts: the five dearest under $30
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/ListProducts"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Body>
    <s:ListProducts>
      <s:limit>5</s:limit>
      <s:sort>-price</s:sort>
      <s:filter><s:field>price_lt</s:field><s:value>30</s:value></s:filter>
    </s:ListProducts>
  </soap:Body>
</soap:Envelope>'
CreateCategory: a write, kept in the session
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/CreateCategory"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Body>
    <s:CreateCategory>
      <s:category><s:name>Garden</s:name><s:slug>garden</s:slug></s:category>
    </s:CreateCategory>
  </soap:Body>
</soap:Envelope>'
GetCategory 9: the one you just made
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/GetCategory"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Body>
    <s:GetCategory><s:id>9</s:id></s:GetCategory>
  </soap:Body>
</soap:Envelope>'
GetProduct, SOAP 1.2
curl https://api.sondahub.com/soap/store \
  -H 'Content-Type: application/soap+xml; charset=utf-8; action="https://sondahub.com/soap/store/GetProduct"' \
  -d '<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:s="https://sondahub.com/soap/store">
  <env:Body>
    <s:GetProduct><s:id>2</s:id></s:GetProduct>
  </env:Body>
</env:Envelope>'
A fault: no such product
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/GetProduct"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Body>
    <s:GetProduct><s:id>999999</s:id></s:GetProduct>
  </soap:Body>
</soap:Envelope>'
A fault: a mandatory header nobody understands
curl https://api.sondahub.com/soap/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/GetProduct"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Header>
    <t:Trace xmlns:t="urn:example:trace" soap:mustUnderstand="1">abc</t:Trace>
  </soap:Header>
  <soap:Body>
    <s:GetProduct><s:id>1</s:id></s:GetProduct>
  </soap:Body>
</soap:Envelope>'
The secure endpoint, with a UsernameToken
curl https://api.sondahub.com/soap/secure/store \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "https://sondahub.com/soap/store/GetProduct"' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:s="https://sondahub.com/soap/store">
  <soap:Header>
    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1">
      <wsse:UsernameToken>
        <wsse:Username>sonda</wsse:Username>
        <wsse:Password>probe</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>
  <soap:Body>
    <s:GetProduct><s:id>1</s:id></s:GetProduct>
  </soap:Body>
</soap:Envelope>'

From the shell, keep the token yourself: the answer’s Session header (or curl -i’s X-Sondahub-Session) goes back as <s:Session>…</s:Session> in the request’s Header, or as the HTTP header.

From code

Any client that reads WSDL builds the calls from the document. With Python’s zeep, which was run against these services — both bindings of all seven, the session, and the secure endpoint with both kinds of password:

import requests
from zeep import Client
from zeep.transports import Transport
from zeep.wsse.username import UsernameToken

# carry the session (your writes) from each answer to the next request
http = requests.Session()
def keep(response, *args, **kwargs):
    if 'X-Sondahub-Session' in response.headers:
        http.headers['X-Sondahub-Session'] = response.headers['X-Sondahub-Session']
http.hooks['response'].append(keep)

client = Client('https://api.sondahub.com/soap/store?wsdl', transport=Transport(session=http))
product = client.service.GetProduct(id=1)
cheap = client.service.ListProducts(limit=5, filter=[{'field': 'price_lt', 'value': '30'}])
made = client.service.CreateCategory(category={'name': 'Garden', 'slug': 'garden'})
print(client.service.GetCategory(id=made.id).name)    # Garden: the session carried it

# the same service behind WS-Security
secure = Client('https://api.sondahub.com/soap/secure/store?wsdl', wsse=UsernameToken('sonda', 'probe', use_digest=True))
print(secure.service.GetProduct(id=1).name)

SOAP as it is written

  • Faults in each version’s shape. SOAP 1.1: faultcode soap:Client or soap:Server, HTTP 500. SOAP 1.2: env:Sender (HTTP 400) or env:Receiver (500) with a subcode — InvalidParams, NotFound, Conflict, Precondition, UnknownOperation, ActionMismatch. The detail is a ServiceFault, declared in the WSDL: a code, the message and, when a record was refused, an error per field.
  • The action has to match. A SOAPAction (or 1.2 action) that names another operation than the body is a fault; an empty one is fine, as the spec allows.
  • Headers. A header block marked mustUnderstand that the service doesn’t understand is a MustUnderstand fault (SOAP 1.2 lists it in NotUnderstood); one not so marked is ignored; blocks for another actor or role are left alone. WS-Addressing is understood: the answer carries wsa:Action and wsa:RelatesTo your MessageID.
  • Versions and media types. An envelope in an unknown namespace is a VersionMismatch with an Upgrade header listing the two it speaks; a SOAP 1.1 envelope sent as application/soap+xml (or the reverse) is a fault; anything that isn’t text/xml or application/soap+xml is a 415.
  • XML from the open internet. No DOCTYPE (so no entity expansion), only legal characters, and limits on size, depth and element counts.

WS-Security

/soap/secure/{api} is the same service behind the OASIS UsernameToken profile: a wsse:Security header with a UsernameToken — PasswordText, or PasswordDigest (Base64 of SHA-1 over the nonce, the wsu:Created time and the password) — for sonda / probe. A Created more than five minutes old is wsse:MessageExpired, as is a wsu:Timestamp past its Expires; a wrong password is wsse:FailedAuthentication; no header at all is wsse:InvalidSecurity; an X.509 or SAML token is wsse:UnsupportedSecurityToken. HTTP Basic with the same credentials works too, for the clients that secure SOAP that way. The open endpoints ignore a Security header.

Questions

Which SOAP does it speak?

SOAP 1.1 (text/xml with a SOAPAction header) and SOAP 1.2 (application/soap+xml, the action as a parameter of the content type), both document/literal wrapped — the style .NET, JAX-WS, zeep and SoapUI generate by default. Each WSDL has a port for each version. RPC/encoded is not offered.

Do writes stick?

Yes, for you: a create, update or delete is validated and run through the same rules as the REST API, and the answer carries the session token — in the X-Sondahub-Session HTTP header and in a Session SOAP header. Send either back and the next call sees your change. Nothing is stored on the server. How sessions work.

How are lists and JSON fields typed?

Every field has its XML Schema type — xs:long ids, xs:double prices, xs:dateTime timestamps, an enumeration for each status. A field that holds a list of words (tags, skills) is a list of <item> elements; a field that holds a structured value (an address, a passenger) travels as JSON text, as the WSDL documents per field. Every answer validates against the WSDL’s own schema.

Can I import it into an API client?

Point a WSDL-reading client at https://api.sondahub.com/soap/{api}?wsdl: it builds a request per operation with the envelope filled in. The credentials for the secure endpoint are sonda / probe, published on purpose like every playground credential here.

What is not modelled?

Message signing and encryption (XML Signature and Encryption inside WS-Security), X.509 and SAML tokens, MTOM and attachments, WS-ReliableMessaging and WS-AtomicTransaction. A Nonce is checked as part of a digest but not remembered, so a replayed one is not caught — there is no storage to remember it in.