seed-skills/soap-api-testing/SKILL.md
SOAP web service testing including WSDL validation, XML schema testing, WS-Security, and SOAP fault handling verification.
npx skillsauth add PramodDutta/qaskills SOAP API TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes the agent test SOAP/XML web services the way they actually break: malformed envelopes, schema-invalid payloads, WS-Security signature failures, and SOAP Faults that return HTTP 200 with an error body. Trigger it whenever you see a .wsdl, an <soap:Envelope>, a ?wsdl URL, WS-Security/UsernameToken, or libraries like zeep, JAX-WS, Apache CXF, or System.ServiceModel.
<soap:Fault> element with faultcode/faultstring (SOAP 1.1) or <Code>/<Reason> (SOAP 1.2). A "successful" HTTP response can carry a business error.SOAPAction headers, mustUnderstand attributes, and header blocks — not only in the business body.Python (recommended for fast, readable SOAP tests):
pip install "zeep==4.2.1" "requests==2.32.3" "lxml==5.2.2" "pytest==8.2.0" "xmlsec==1.3.14"
Java (Maven, for JAX-WS / CXF shops):
<!-- pom.xml -->
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>4.0.5</version>
</dependency>
<!-- Generate the client stubs from the WSDL at build time -->
<!-- mvn org.apache.cxf:cxf-codegen-plugin:wsdl2java -->
zeep parses the WSDL, builds typed operations, and validates input against the XSD automatically.
import pytest
from zeep import Client, Settings
from zeep.exceptions import Fault, ValidationError
WSDL = "http://www.dneonline.org/Calculator/calculator.wsdl"
@pytest.fixture(scope="module")
def client():
settings = Settings(strict=True, xml_huge_tree=True)
return Client(wsdl=WSDL, settings=settings)
def test_add_returns_sum(client):
result = client.service.Add(intA=7, intB=5)
assert result == 12
def test_missing_required_arg_is_rejected_locally(client):
# zeep validates against the XSD before the request leaves the machine
with pytest.raises(ValidationError):
client.service.Add(intA=7) # intB is required by the schema
The service may return a Fault for business errors. zeep raises zeep.exceptions.Fault; assert on its contents rather than letting the test crash.
def test_divide_by_zero_raises_soap_fault(client):
with pytest.raises(Fault) as exc_info:
client.service.Divide(intA=10, intB=0)
fault = exc_info.value
assert "DivideByZero" in fault.message or "zero" in fault.message.lower()
# SOAP 1.1 faultcode lives in fault.code; detail carries app data
assert fault.detail is not None
A generated client cannot send a broken envelope. Build the SOAP request by hand and POST it, then assert the server rejects it with a Fault (a robust service must not 500 or accept garbage).
import requests
from lxml import etree
ENDPOINT = "http://www.dneonline.org/calculator.asmx"
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"tns": "http://tempuri.org/",
}
def post_soap(body: str, action: str) -> requests.Response:
envelope = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://tempuri.org/">
<soap:Body>{body}</soap:Body>
</soap:Envelope>"""
return requests.post(
ENDPOINT,
data=envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": action},
timeout=10,
)
def test_non_numeric_input_returns_fault():
body = "<tns:Add><tns:intA>not_a_number</tns:intA><tns:intB>2</tns:intB></tns:Add>"
resp = post_soap(body, '"http://tempuri.org/Add"')
# SOAP Faults come back as HTTP 500 on .NET ASMX or 200 elsewhere — inspect the body
root = etree.fromstring(resp.content)
fault = root.find(".//soap:Fault", NS)
assert fault is not None, f"Expected a SOAP Fault, got: {resp.text[:300]}"
faultstring = fault.findtext("faultstring")
assert faultstring and faultstring.strip() != ""
def test_missing_soapaction_header_is_rejected():
body = "<tns:Add><tns:intA>1</tns:intA><tns:intB>2</tns:intB></tns:Add>"
resp = post_soap(body, "") # empty SOAPAction
assert resp.status_code in (400, 415, 500)
Pull the schema out of the WSDL (or a standalone .xsd) and validate live responses against it to catch contract drift.
from lxml import etree
def test_response_validates_against_schema():
schema_doc = etree.parse("schemas/calculator_types.xsd")
schema = etree.XMLSchema(schema_doc)
body = "<tns:Add><tns:intA>4</tns:intA><tns:intB>6</tns:intB></tns:Add>"
resp = post_soap(body, '"http://tempuri.org/Add"')
root = etree.fromstring(resp.content)
payload = root.find(".//{http://tempuri.org/}AddResponse")
assert payload is not None
# raises DocumentInvalid with a precise line/element on drift
schema.assertValid(payload)
Attach a signed/plaintext UsernameToken and assert that valid creds succeed and bad creds Fault.
from zeep import Client
from zeep.wsse.username import UsernameToken
def test_valid_username_token_authenticates():
client = Client(
WSDL,
wsse=UsernameToken("svc_user", "s3cret", use_digest=True),
)
assert client.service.Add(intA=1, intB=1) == 2
def test_invalid_credentials_raise_fault():
client = Client(WSDL, wsse=UsernameToken("svc_user", "WRONG"))
with pytest.raises(Fault) as exc:
client.service.Add(intA=1, intB=1)
assert "auth" in str(exc.value).lower() or "security" in str(exc.value).lower()
For JVM teams, generate stubs with wsdl2java and assert on the typed exception that maps to the Fault.
import jakarta.xml.ws.soap.SOAPFaultException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorSoapTest {
private final CalculatorSoap port =
new Calculator().getCalculatorSoap(); // generated from WSDL
@Test
void add_returnsSum() {
assertEquals(12, port.add(7, 5));
}
@Test
void divideByZero_throwsSoapFault() {
SOAPFaultException ex = assertThrows(
SOAPFaultException.class,
() -> port.divide(10, 0));
assertNotNull(ex.getFault().getFaultCode());
assertTrue(ex.getFault().getFaultString().toLowerCase().contains("zero"));
}
}
<detail> block carries the machine-readable error code consumers branch on.mustUnderstand headers — send an unknown header with mustUnderstand="1" and confirm the service Faults rather than ignoring it.status_code == 200 and calling it green. A SOAP Fault rides inside a 200 on many stacks — you've just shipped a passing test for a broken call.root.find(".//Fault") returns None even when a Fault exists because the element is namespaced. Always bind soap:.SOAPAction. Many services route on the SOAPAction header; omitting it produces confusing 415/500s that look like server bugs.?wsdl URL or .wsdl filezeep, JAX-WS, Apache CXF, wsimport, wsdl2java, System.ServiceModel, WSDL, SOAPAction, or WS-Security / UsernameTokendevelopment
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.