mock-sap 0.1.0__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY mocksap ./mocksap
4
+ COPY pyproject.toml README.md LICENSE ./
5
+ RUN pip install --no-cache-dir .
6
+ EXPOSE 8000
7
+ ENTRYPOINT ["mock-sap", "--host", "0.0.0.0", "--port", "8000"]
mock_sap-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mock-sap contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE README.md Dockerfile
2
+ recursive-include examples *.py *.sh
3
+ recursive-include tests *.py
@@ -0,0 +1,369 @@
1
+ Metadata-Version: 2.4
2
+ Name: mock-sap
3
+ Version: 0.1.0
4
+ Summary: A black-box mock SAP endpoint: OData V2, BAPI/RFC and IDoc shapes over SQLite, with zero dependencies.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/rseufert/mock-sap
7
+ Project-URL: Repository, https://github.com/rseufert/mock-sap
8
+ Project-URL: Issues, https://github.com/rseufert/mock-sap/issues
9
+ Project-URL: Changelog, https://github.com/rseufert/mock-sap/releases
10
+ Keywords: sap,odata,mock,bapi,rfc,idoc,s4hana,testing,integration
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Topic :: Software Development :: Testing :: Mocking
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Dynamic: license-file
30
+
31
+ # mock-sap
32
+
33
+ [![CI](https://github.com/rseufert/mock-sap/actions/workflows/ci.yml/badge.svg)](https://github.com/rseufert/mock-sap/actions/workflows/ci.yml)
34
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)
35
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
36
+ [![PyPI](https://img.shields.io/pypi/v/mock-sap)](https://pypi.org/project/mock-sap/)
37
+
38
+ A **black-box mock SAP endpoint**. It does not implement any SAP business logic —
39
+ it speaks the shapes: SAP Gateway **OData V2** services, **BAPI/RFC** calls over JSON
40
+ and SOAP, and **IDocs** in XML and EDI_DC40 flat-file form. Data is kept in SQLite.
41
+
42
+ Point your integration at it and develop, test and demo without an SAP system,
43
+ an SAP licence, or a VPN tunnel to someone's sandbox.
44
+
45
+ - **Zero dependencies.** Python 3.8+ standard library and SQLite, nothing else.
46
+ - **Real wire shapes.** `{"d":{"results":[…]}}`, `__metadata`, `__deferred`,
47
+ `/Date(1700000000000)/`, decimals as strings, EDMX `$metadata` with `sap:` annotations,
48
+ CSRF tokens, `$batch` with atomic changesets, BAPIRET2 return tables, EDI_DC40 control records.
49
+ - **Deterministic demo data.** Business partners, products, sales orders, purchase orders —
50
+ same data every run for the same `--seed`.
51
+ - **Failure modes on demand.** Latency, 500s, locked documents, "no work process available",
52
+ expired CSRF tokens — per request, or as programmable rules.
53
+
54
+ MIT licensed. SAP, S/4HANA, ABAP, NetWeaver, BAPI and IDoc are trademarks of SAP SE;
55
+ this project is not affiliated with, endorsed by, or connected to SAP SE, and imitates
56
+ publicly documented wire formats for testing purposes only.
57
+
58
+ ---
59
+
60
+ ## Quick start
61
+
62
+ ```bash
63
+ pip install mock-sap
64
+ mock-sap --port 8000
65
+ ```
66
+
67
+ ```bash
68
+ curl "http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?\$top=1&\$format=json"
69
+ ```
70
+
71
+ ```json
72
+ {
73
+ "d": {
74
+ "results": [
75
+ {
76
+ "__metadata": {
77
+ "id": "http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('0000004712')",
78
+ "uri": "http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('0000004712')",
79
+ "type": "API_SALES_ORDER_SRV.A_SalesOrderType"
80
+ },
81
+ "SalesOrder": "0000004712",
82
+ "SalesOrderType": "OR",
83
+ "SalesOrganization": "1710",
84
+ "SoldToParty": "1000004",
85
+ "TransactionCurrency": "EUR",
86
+ "TotalNetAmount": "123991.820",
87
+ "CreationDate": "/Date(1754611200000)/",
88
+ "to_Item": { "__deferred": { "uri": ".../A_SalesOrder('0000004712')/to_Item" } }
89
+ }
90
+ ]
91
+ }
92
+ }
93
+ ```
94
+
95
+ Or run it straight from a checkout, with no install at all, or in a container:
96
+
97
+ ```bash
98
+ python3 -m mocksap --port 8000
99
+ docker build -t mock-sap . && docker run -p 8000:8000 mock-sap
100
+ ```
101
+
102
+ A guided tour of every endpoint, in curl:
103
+
104
+ ```bash
105
+ bash examples/demo.sh
106
+ ```
107
+
108
+ ## What it serves
109
+
110
+ | Surface | Endpoint |
111
+ | --- | --- |
112
+ | Service catalog | `GET /sap/opu/odata/IWFND/CATALOGSERVICE;v=2/ServiceCollection` |
113
+ | Business Partner | `/sap/opu/odata/sap/API_BUSINESS_PARTNER_SRV` |
114
+ | Product | `/sap/opu/odata/sap/API_PRODUCT_SRV` |
115
+ | Sales Order | `/sap/opu/odata/sap/API_SALES_ORDER_SRV` |
116
+ | Purchase Order | `/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV` |
117
+ | BAPI over JSON | `POST /sap/bc/rfc/<FUNCTION_MODULE>` |
118
+ | BAPI over SOAP | `POST /sap/bc/srt/rfc/sap/<service>/<client>/<name>/<binding>` |
119
+ | IDoc inbound | `POST /sap/bc/idoc` (XML or flat file) |
120
+ | IDoc outbound | `POST /sap/bc/idoc/generate` → ORDERS05 |
121
+ | Mock control plane | `/_mock/health`, `/_mock/state`, `/_mock/requests`, `/_mock/faults`, `/_mock/reset` |
122
+
123
+ Entity sets carry the S/4HANA field names — `A_SalesOrder` with `SoldToParty`,
124
+ `TotalNetAmount`, `OverallSDProcessStatus`, `to_Item`; `A_BusinessPartner` with
125
+ `BusinessPartnerCategory`, `to_BusinessPartnerAddress`; and so on. Browse
126
+ `/sap/opu/odata/sap/<SERVICE>/$metadata` for the full picture, or open
127
+ `http://127.0.0.1:8000/` for an index page.
128
+
129
+ ## OData V2 support
130
+
131
+ | Feature | Notes |
132
+ | --- | --- |
133
+ | `$filter` | `eq ne gt ge lt le`, `and or not`, parentheses, arithmetic, `substringof`, `startswith`, `endswith`, `contains`, `tolower`, `toupper`, `trim`, `length`, `concat`, `substring`, `indexof`, `year`…`second`. Compiled to parameterised SQL. |
134
+ | `$select` `$expand` | `$expand` follows to-one and to-many navigations, nested paths included |
135
+ | `$orderby` `$top` `$skip` | |
136
+ | `$inlinecount=allpages`, `/$count` | |
137
+ | `$format=json`, Accept negotiation | XML/Atom for the service document and errors |
138
+ | `$metadata` | EDMX 1.0 with associations, referential constraints and `sap:label`/`sap:creatable`/`sap:updatable` |
139
+ | `$batch` | multipart/mixed, changesets execute atomically and roll back as a unit |
140
+ | Writes | `POST` (incl. deep insert), `PATCH`/`MERGE`, `PUT`, `DELETE`, `POST` to a navigation |
141
+ | Conventions | CSRF tokens, `sap-client`, `DataServiceVersion`, `Location` on create, SAP error envelope with `/IWBEP/CX_MGW_*` codes |
142
+
143
+ ### Writing requires a CSRF token
144
+
145
+ Exactly as against a real Gateway:
146
+
147
+ ```bash
148
+ TOKEN=$(curl -s -D - -o /dev/null -H 'X-CSRF-Token: Fetch' \
149
+ http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/ \
150
+ | awk 'tolower($1)=="x-csrf-token:"{print $2}' | tr -d '\r')
151
+
152
+ curl -X POST "http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?\$expand=to_Item" \
153
+ -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \
154
+ -d '{"SalesOrderType":"OR","SalesOrganization":"1710","DistributionChannel":"10",
155
+ "OrganizationDivision":"00","SoldToParty":"1000001","TransactionCurrency":"EUR",
156
+ "to_Item":[{"Material":"TG11","RequestedQuantity":"2","NetAmount":"1998.00"}]}'
157
+ ```
158
+
159
+ The document number comes from a number range, items are numbered `000010`, `000020`, …,
160
+ `TotalNetAmount` is recalculated from the items, administrative fields are filled in,
161
+ and unset fields come back as ABAP initial values (`""`, `0.000`) rather than `null`.
162
+ Run with `--no-csrf` to switch the check off.
163
+
164
+ ## BAPI / RFC
165
+
166
+ The same function modules are reachable over JSON and over SOAP. Parameter names are
167
+ matched case- and underscore-insensitively, so `ORDER_HEADER_IN` and `OrderHeaderIn`
168
+ both work.
169
+
170
+ ```bash
171
+ curl -X POST http://127.0.0.1:8000/sap/bc/rfc/BAPI_SALESORDER_CREATEFROMDAT2 \
172
+ -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \
173
+ -d '{"ORDER_HEADER_IN":{"DOC_TYPE":"OR","SALES_ORG":"1710","DISTR_CHAN":"10","DIVISION":"00"},
174
+ "ORDER_PARTNERS":[{"PARTN_ROLE":"AG","PARTN_NUMB":"0001000001"}],
175
+ "ORDER_ITEMS_IN":[{"ITM_NUMBER":"000010","MATERIAL":"TG11","REQ_QTY":"3","COND_VALUE":"1500.00"}]}'
176
+ ```
177
+
178
+ ```json
179
+ {
180
+ "SALESDOCUMENT": "0000004737",
181
+ "RETURN": [{
182
+ "TYPE": "S", "ID": "V1", "NUMBER": "311",
183
+ "MESSAGE": "Standard Order 4737 has been saved",
184
+ "MESSAGE_V1": "Standard Order", "MESSAGE_V2": "4737",
185
+ "LOG_NO": "", "LOG_MSG_NO": "000000", "PARAMETER": "", "ROW": 0, "FIELD": "",
186
+ "SYSTEM": "MCKCLNT100"
187
+ }]
188
+ }
189
+ ```
190
+
191
+ Errors arrive the way BAPIs report them — `TYPE: "E"` in the `RETURN` table with a
192
+ message number, not an HTTP error. Unknown material, unknown customer, missing
193
+ sold-to party and test runs (`TESTRUN: "X"`) are all modelled.
194
+
195
+ Available: `BAPI_SALESORDER_CREATEFROMDAT2`, `BAPI_SALESORDER_GETLIST`,
196
+ `BAPI_SALESORDER_GETSTATUS`, `BAPI_PO_CREATE1`, `BAPI_PO_GETDETAIL1`,
197
+ `BAPI_MATERIAL_GET_DETAIL`, `BAPI_BUSINESS_PARTNER_GETDETAIL`,
198
+ `BAPI_TRANSACTION_COMMIT`, `BAPI_TRANSACTION_ROLLBACK`, `RFC_PING`, `STFC_CONNECTION`.
199
+ `GET /_mock/services` lists them; `POST /sap/bc/rfc/` with no name does too.
200
+
201
+ SOAP uses the `urn:sap-com:document:sap:soap:functions:mc-style` namespace, returns
202
+ `<…Response>` envelopes, and answers unknown functions with a SOAP fault.
203
+
204
+ ## IDoc
205
+
206
+ ```bash
207
+ # outbound: render a stored sales order as ORDERS05
208
+ curl -X POST "http://127.0.0.1:8000/sap/bc/idoc/generate?format=xml" \
209
+ -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \
210
+ -d '{"SalesOrder":"0000004712"}'
211
+
212
+ # inbound: post an IDoc and get its status record back
213
+ curl -X POST http://127.0.0.1:8000/sap/bc/idoc \
214
+ -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/xml' -H 'Accept: application/json' \
215
+ --data-binary @order.xml
216
+ ```
217
+
218
+ Generated IDocs carry a full `EDI_DC40` control record plus `E1EDK01`, `E1EDK14`,
219
+ `E1EDK03`, `E1EDKA1`, `E1EDP01`/`E1EDP19` segments. Inbound IDocs are parsed (XML, or a
220
+ flat file read at the documented EDI_DC40 offsets), stored with a 16-digit IDoc number
221
+ and status `53`, and can be inspected or re-statused:
222
+
223
+ ```bash
224
+ curl http://127.0.0.1:8000/_mock/idocs
225
+ curl -X PUT http://127.0.0.1:8000/sap/bc/idoc/<DOCNUM>/status \
226
+ -H "X-CSRF-Token: $TOKEN" -d '{"status":"51"}'
227
+ ```
228
+
229
+ ## Simulating a bad day
230
+
231
+ Per request, with a header or a query parameter:
232
+
233
+ ```bash
234
+ curl -H 'sap-mock-scenario: busy' http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder
235
+ ```
236
+
237
+ | Scenario | Effect |
238
+ | --- | --- |
239
+ | `slow` | delays by `--slow-ms` (default 3s) |
240
+ | `timeout` | sleeps past any client timeout |
241
+ | `error` | 500, Gateway technical exception |
242
+ | `busy` | 503 with `Retry-After`, "no dialog work process available" |
243
+ | `auth` / `forbidden` | 401 with a NetWeaver realm / 403 missing authorization |
244
+ | `lock` | 423, document locked by another user |
245
+ | `csrf` | 403 with `x-csrf-token: Required` |
246
+ | `notfound` | 404 |
247
+
248
+ As rules, matched by regex on the path:
249
+
250
+ ```bash
251
+ curl -X POST http://127.0.0.1:8000/_mock/faults -H 'Content-Type: application/json' \
252
+ -d '{"match":"A_SalesOrder","method":"POST","status":500,"message":"Backend unreachable","count":1}'
253
+ curl -X DELETE http://127.0.0.1:8000/_mock/faults # clear all rules
254
+ ```
255
+
256
+ Or globally, at startup: `--latency-ms 250 --error-rate 0.05`.
257
+
258
+ ## Inspecting what your client did
259
+
260
+ Every request is recorded, which makes the mock useful as a contract check in CI:
261
+
262
+ ```bash
263
+ curl "http://127.0.0.1:8000/_mock/requests?limit=10" # method, path, query, status, duration
264
+ curl http://127.0.0.1:8000/_mock/rfc-log # which BAPIs were called
265
+ curl http://127.0.0.1:8000/_mock/state # row counts per entity
266
+ curl -X POST http://127.0.0.1:8000/_mock/reset \
267
+ -H 'Content-Type: application/json' -d '{"seed":7,"orders":50}'
268
+ ```
269
+
270
+ `POST /_mock/reset` restores a known dataset between test cases.
271
+
272
+ ## Command line
273
+
274
+ ```
275
+ mock-sap [--host 127.0.0.1] [--port 8000] [--db :memory:|path.db] [--client 100]
276
+ [--user MOCKUSER] [--auth USER:PASSWORD] [--no-csrf] [--seed 42]
277
+ [--latency-ms 0] [--error-rate 0.0] [--slow-ms 3000] [--no-request-log] [-q]
278
+ ```
279
+
280
+ `--db mock.db` keeps data across restarts; the default in-memory system starts fresh
281
+ every time. `--auth` turns on HTTP basic authentication with a NetWeaver realm.
282
+
283
+ ## Using it from tests
284
+
285
+ ```python
286
+ from mocksap import Config, make_server
287
+
288
+ httpd = make_server(Config(port=0, db_path=":memory:", csrf=False, quiet=True))
289
+ port = httpd.server_address[1]
290
+ threading.Thread(target=httpd.serve_forever, daemon=True).start()
291
+ # point the code under test at http://127.0.0.1:{port} ...
292
+ httpd.shutdown()
293
+ ```
294
+
295
+ `examples/client.py` is a dependency-free client showing the token/cookie flow.
296
+
297
+ ## Adding entity sets
298
+
299
+ Everything is generated from the declarations in
300
+ [`mocksap/schema.py`](mocksap/schema.py) — tables, `$metadata`, payload shapes and
301
+ key handling. Add an `EntityType` with its `Prop`s and `Nav`s, list it in a `Service`,
302
+ and it is fully queryable and writable:
303
+
304
+ ```python
305
+ _register(EntityType("A_BillingDocument", label="Billing Document", props=[
306
+ S("BillingDocument", key=True, nullable=False, max_length=10),
307
+ S("BillingDocumentType", max_length=4),
308
+ DEC("TotalNetAmount", precision=16, scale=3),
309
+ DT("BillingDocumentDate"),
310
+ ], navs=[Nav("to_Item", "A_BillingDocumentItem", "*", [("BillingDocument", "BillingDocument")])]))
311
+ ```
312
+
313
+ Seed data for it goes in `mocksap/db.py`, a BAPI wrapper (if you need one) in
314
+ `mocksap/bapi.py`.
315
+
316
+ ## Tests
317
+
318
+ ```bash
319
+ python3 -m unittest discover -s tests -v
320
+ ```
321
+
322
+ 31 tests, every one of them over real HTTP against a running mock: metadata,
323
+ query options, error envelopes, CSRF, deep insert, `$batch` rollback, BAPI JSON and
324
+ SOAP, IDoc round-trip, fault injection and authentication.
325
+
326
+ CI runs them on Python 3.8-3.13 across Linux, macOS and Windows, and additionally
327
+ checks that `examples/demo.sh`, the packaged wheel and the Docker image still work.
328
+
329
+ ## Releasing
330
+
331
+ Releases go to PyPI through [Trusted Publishing](https://docs.pypi.org/trusted-publishers/):
332
+ PyPI trusts this repository's `publish.yml` workflow directly, so no API token is
333
+ stored in the repository or on anyone's laptop. Publishing a GitHub Release runs the
334
+ test suite, builds the sdist and wheel, checks that the tag matches the version in
335
+ `pyproject.toml`, and uploads. Running the workflow manually publishes to TestPyPI
336
+ instead, to rehearse.
337
+
338
+ ```bash
339
+ # bump version in pyproject.toml and mocksap/__init__.py first
340
+ git tag v0.1.0 && git push origin v0.1.0
341
+ gh release create v0.1.0 --generate-notes
342
+ ```
343
+
344
+ ## Layout
345
+
346
+ ```
347
+ mocksap/schema.py entity types, navigations, services (add shapes here)
348
+ mocksap/db.py SQLite schema, number ranges, seed data
349
+ mocksap/odata.py $filter parser, key predicates, JSON shaping, error envelope
350
+ mocksap/metadata.py EDMX / service document
351
+ mocksap/store.py CRUD, deep insert, cascades, document defaults
352
+ mocksap/service.py OData request dispatcher
353
+ mocksap/batch.py $batch multipart and atomic changesets
354
+ mocksap/bapi.py BAPI/RFC functions, JSON and SOAP transports
355
+ mocksap/idoc.py IDoc inbox/outbox, ORDERS05 generation
356
+ mocksap/server.py HTTP front end, CSRF, auth, fault injection, /_mock API
357
+ ```
358
+
359
+ ## Scope
360
+
361
+ This is a black box. It stores what you send and returns it in SAP's shapes.
362
+ It does **not** do ATP checks, pricing procedures, output determination,
363
+ authorization objects, workflow or any other real SAP logic. What it is good for:
364
+ developing and testing integrations, contract tests in CI, demos, and load-testing
365
+ your side of the wire.
366
+
367
+ Not implemented yet, and the obvious next contributions: OData V4 services,
368
+ complex (structured) types, `$links`, ETags with `If-Match`, and OAuth/SAML instead
369
+ of basic authentication.