robotframework-openapitools 1.0.0b5__py3-none-any.whl → 1.0.1__py3-none-any.whl

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,891 @@
1
+ GENERAL_INTRODUCTION = """
2
+ My RoboCon 2022 talk about OpenApiDriver and OpenApiLibCore can be found
3
+ <a href="https://www.youtube.com/watch?v=7YWZEHxk9Ps" target="_blank">here</a>.
4
+
5
+ For more information about Robot Framework, see http://robotframework.org.
6
+
7
+ <hr>
8
+
9
+ <h2>Installation</h2>
10
+
11
+ If you already have Python >= 3.10 with pip installed, you can simply run:
12
+
13
+ <div class="code-block"><pre><code class="language-shell">pip install --upgrade robotframework-openapitools</code></pre></div>
14
+
15
+ <hr>
16
+
17
+ <h2>OpenAPI (aka Swagger)</h2>
18
+
19
+ The OpenAPI Specification (OAS) defines a standard, language-agnostic interface
20
+ to RESTful APIs, see https://www.openapis.org/
21
+
22
+ The OpenApiTools libraries implement a number of Robot Framework keywords that make it
23
+ easy to interact with an OpenAPI implementation by using the information in the
24
+ OpenAPI document (previously: Swagger file), for examply by automatic generation of
25
+ valid values for requests based on the schema information in the document.
26
+
27
+ <blockquote><i>
28
+ Note: The OpenApiTools libraries are designed for APIs based on the OAS v3.
29
+ The libraries have not been tested for APIs based on the OAS v2.
30
+ </i></blockquote>
31
+
32
+ <hr>
33
+
34
+ <h2>Getting started</h2>
35
+
36
+ Before trying to use the keywords exposed by any of the libraries from OpenApiTools on
37
+ the target API, it's recommended to first ensure that the OpenAPI document for the API
38
+ is valid under the OpenAPI Specification.
39
+
40
+ This can be done using the command line interface of the <code>prance</code> package
41
+ that is installed as a prerequisite for OpenApiTools.
42
+ Both a local openapi.json or openapi.yaml file or one hosted by the API server
43
+ can be checked using the <code>prance validate <reference_to_file></code> shell command:
44
+
45
+ <div class="code-block"><pre><code class="language-shell">
46
+ (.venv) prance validate --backend=openapi-spec-validator http://localhost:8000/openapi.json
47
+
48
+ Processing "http://localhost:8000/openapi.json"...
49
+ -> Resolving external references.
50
+ Validates OK as OpenAPI 3.0.2!
51
+
52
+ (.venv) prance validate --backend=openapi-spec-validator /tests/files/petstore_openapi.yaml
53
+
54
+ Processing "/tests/files/petstore_openapi.yaml"...
55
+ -> Resolving external references.
56
+ Validates OK as OpenAPI 3.0.2!
57
+
58
+ </code></pre></div>
59
+
60
+ You'll have to change the url or file reference to the location of the openapi
61
+ document for your API.
62
+
63
+ <blockquote><i>
64
+ Note: Although recursion is technically allowed under the OAS, tool
65
+ support is limited and changing the OAS to not use recursion is recommended.
66
+ </i></blockquote>
67
+
68
+ Support for parsing OpenAPI documents with recursion in them is limited.
69
+ See the <code>recursion_limit</code> and <code>recursion_default</code> library
70
+ parameters for the available options.
71
+
72
+ If the OpenAPI document passes this validation, the next step is trying to do a test
73
+ run with a minimal test suite.
74
+ The examples below can be used with <code>source</code>, <code>origin</code>
75
+ and <code>path</code> altered to fit your situation.
76
+
77
+ <div class="code-block"><pre><code class="language-robotframework">
78
+ *** Settings ***
79
+ Library OpenApiDriver
80
+ ... source=http://localhost:8000/openapi.json
81
+ ... origin=http://localhost:8000
82
+ Test Template Validate Using Test Endpoint Keyword
83
+
84
+ *** Test Cases ***
85
+ Test Endpoint for ${method} on ${path} where ${status_code} is expected
86
+
87
+ *** Keywords ***
88
+ Validate Using Test Endpoint Keyword
89
+ [Arguments] ${path} ${method} ${status_code}
90
+ Test Endpoint
91
+ ... path=${path} method=${method} status_code=${status_code}
92
+
93
+ </code></pre></div>
94
+
95
+ <div class="code-block"><pre><code class="language-robotframework">
96
+ *** Settings ***
97
+ Library OpenApiLibCore
98
+ ... source=http://localhost:8000/openapi.json
99
+ ... origin=http://localhost:8000
100
+
101
+ *** Test Cases ***
102
+ Getting Started
103
+ ${url}= Get Valid Url path=/employees/{employee_id}
104
+
105
+ </code></pre></div>
106
+
107
+ Running the above tests for the first time may result in an error / failed test.
108
+ You should look at the Robot Framework <code>log.html</code> to determine the reasons
109
+ for the failing tests.
110
+ Depending on the reasons for the failures, different solutions are possible.
111
+
112
+ The OpenApiTools libraries also support handling of relations between resources within
113
+ the scope of the API being validated as well as handling dependencies on resources
114
+ outside the scope of the API. In addition there is support for handling restrictions
115
+ on the values of parameters and properties.
116
+ This is supported by the <code>mappings_path</code> library parameter.
117
+ Details about the <code>mappings_path</code> parameter usage can be
118
+ found under the Advanced Use tab.
119
+
120
+ <hr>
121
+
122
+ <h2>Limitations</h2>
123
+
124
+ There are currently a number of limitations to supported API structures, supported data
125
+ types and properties.
126
+ The following list details the most important ones.
127
+ - Only JSON request and response bodies are supported.
128
+ - No support for per-path authorization levels (only simple 401 / 403 validation).
129
+
130
+ For all open issues please visit the OpenApiTools <a href="https://github.com/MarketSquare/robotframework-openapitools" target="_blank">GitHub page</a>.
131
+ """
132
+
133
+ SHARED_PARAMETERS = """
134
+ <h2>Base parameters</h2>
135
+
136
+ <h3>source</h3>
137
+ An absolute path to an openapi.json or openapi.yaml file or an url to such a file.
138
+
139
+ <h3>origin</h3>
140
+ The server (and port) of the target server. E.g. <code>https://localhost:8000</code>
141
+
142
+ <h3>base_path</h3>
143
+ The routing between <code>origin</code> and the paths as found in the <code>paths</code>
144
+ section in the openapi document.
145
+ E.g. <code>/petshop/v2</code>.
146
+
147
+ <h2>Test case execution</h2>
148
+
149
+ <h3>response_validation</h3>
150
+ By default, a <code>WARN</code> is logged when the Response received after a Request
151
+ does not comply with the schema as defined in the OpenAPI document for the given operation.
152
+ The following values are supported:
153
+ <ul>
154
+ <li><code>DISABLED</code>: All Response validation errors will be ignored</li>
155
+ <li><code>INFO</code>: Any Response validation erros will be logged at <code>INFO</code> level</li>
156
+ <li><code>WARN</code>: Any Response validation erros will be logged at <code>WARN</code> level</li>
157
+ <li><code>STRICT</code>: The Test Case will fail on any Response validation errors</li>
158
+ </ul>
159
+
160
+ <h3>disable_server_validation</h3>
161
+ If enabled by setting this parameter to <code class="language-robotframework">${TRUE}</code>,
162
+ the Response validation will also include possible errors for Requests made to a server
163
+ address that is not defined in the list of servers in the OpenAPI document.
164
+ This generally means that if there is a mismatch, every Test Case will raise this error.
165
+ Note that <code>localhost</code> and <code>127.0.0.1</code> are not considered the same
166
+ by Response validation.
167
+
168
+ <h2>API-specific configurations</h2>
169
+
170
+ <h3>mappings_path</h3>
171
+ See the Advanced Use tab for an in-depth explanation.
172
+
173
+ <h3>invalid_property_default_response</h3>
174
+ The default response code for requests with a JSON body that does not comply
175
+ with the schema.
176
+ Example: a value outside the specified range or a string value
177
+ for a property defined as integer in the schema.
178
+
179
+ <h3>default_id_property_name</h3>
180
+ The default name for the property that identifies a resource (i.e. a unique
181
+ entity) within the API.
182
+ The default value for this property name is <code>id</code>.
183
+ If the target API uses a different name for all the resources within the API,
184
+ you can configure it globally using this property.
185
+
186
+ If different property names are used for the unique identifier for different
187
+ types of resources, an <code>ID_MAPPING</code> can be implemented using the <code>mappings_path</code>.
188
+
189
+ <h3>faker_locale</h3>
190
+ A locale string or list of locale strings to pass to the Faker library to be
191
+ used in generation of string data for supported format types.
192
+
193
+ <h3>require_body_for_invalid_url</h3>
194
+ When a request is made against an invalid url, this usually is because of a "404" request;
195
+ a request for a resource that does not exist. Depending on API implementation, when a
196
+ request with a missing or invalid request body is made on a non-existent resource,
197
+ either a 404 or a 422 or 400 Response is normally returned. If the API being tested
198
+ processes the request body before checking if the requested resource exists, set
199
+ this parameter to True.
200
+
201
+ <h2>Parsing parameters</h2>
202
+
203
+ <h3>recursion_limit</h3>
204
+ The recursion depth to which to fully parse recursive references before the
205
+ <code>recursion_default</code> is used to end the recursion.
206
+
207
+ <h3>recursion_default</h3>
208
+ The value that is used instead of the referenced schema when the
209
+ <code>recursion_limit</code> has been reached.
210
+ The default <code class="language-python">{}</code> represents an empty object in JSON.
211
+ Depending on schema definitions, this may cause schema validation errors.
212
+ If this is the case <code class="language-robotframework">${NONE}</code> or an empty list
213
+ can be tried as an alternative.
214
+
215
+ <h2>Security-related parameters</h2>
216
+ <blockquote><i>Note: these parameters are equivalent to those in the <code>requests</code> library.</i></blockquote>
217
+
218
+ <h3>username</h3>
219
+ The username to be used for Basic Authentication.
220
+
221
+ <h3>password</h3>
222
+ The password to be used for Basic Authentication.
223
+
224
+ <h3>security_token</h3>
225
+ The token to be used for token based security using the <code>Authorization</code> header.
226
+
227
+ <h3>auth</h3>
228
+ A <a href="https://requests.readthedocs.io/en/latest/api/#authentication" target="_blank">requests <code>AuthBase</code> instance</a> to be used for authentication instead of the <code>username</code> and <code>password</code>.
229
+
230
+ <h3>cert</h3>
231
+ The SSL certificate to use with all requests.
232
+ If string: the path to ssl client cert file (<code>.pem</code>).
233
+ If tuple: the <code class="language-python">("cert", "key")</code> pair.
234
+
235
+ <h3>verify_tls</h3>
236
+ Whether or not to verify the TLS / SSL certificate of the server.
237
+ If boolean: whether or not to verify the server TLS certificate.
238
+ If string: path to a CA bundle to use for verification.
239
+
240
+ <h3>extra_headers</h3>
241
+ A dictionary with extra / custom headers that will be send with every request.
242
+ This parameter can be used to send headers that are not documented in the
243
+ openapi document or to provide an API-key.
244
+
245
+ <h3>cookies</h3>
246
+ A dictionary or
247
+ <a href="https://docs.python.org/3/library/http.cookiejar.html#http.cookiejar.CookieJar" target="_blank"><code>CookieJar</code> object</a>
248
+ to send with all requests.
249
+
250
+ <h3>proxies</h3>
251
+ A dictionary of <code>"protocol": "proxy url"</code> to use for all requests.
252
+ """
253
+
254
+ OPENAPILIBCORE_DOCUMENTATION = """
255
+ <h1>OpenApiLibCore for Robot Framework</h1>
256
+
257
+ The OpenApiLibCore library is a utility library that is meant to simplify creation
258
+ of other Robot Framework libraries for API testing based on the information in
259
+ an OpenAPI document.
260
+ Both OpenApiDriver and libraries generated using OpenApiLibGen rely on OpenApiLibCore
261
+ and its keywords for their functionality.
262
+ This document explains how to use the OpenApiLibCore library.
263
+
264
+ <blockquote>
265
+ Keyword documentation for OpenApiLibCore can be found <a href="./openapi_libcore.html" target="_blank">here</a>.
266
+ </blockquote>
267
+
268
+ <h2>Introduction</h2>
269
+ {general_introduction}
270
+
271
+ """.format(general_introduction=GENERAL_INTRODUCTION)
272
+
273
+ OPENAPILIBCORE_LIBRARY_DOCSTRING = """
274
+ The OpenApiLibCore library provides the keywords and core logic to interact with an OpenAPI server.
275
+
276
+ Visit the <a href="./index.html" target="_blank">OpenApiTools documentation</a> for an introduction.
277
+ """
278
+
279
+ OPENAPILIBCORE_INIT_DOCSTRING = SHARED_PARAMETERS
280
+
281
+ OPENAPIDRIVER_DOCUMENTATION = """
282
+ <h1>OpenApiDriver for Robot Framework</h1>
283
+
284
+ OpenApiDriver is an extension of the Robot Framework DataDriver library that allows
285
+ for generation and execution of test cases based on the information in an OpenAPI document.
286
+
287
+ For more information about the DataDriver library, see
288
+ https://github.com/Snooz82/robotframework-datadriver.
289
+
290
+ <blockquote>
291
+ Keyword documentation for OpenApiDriver can be found <a href="./openapidriver.html" target="_blank">here</a>.
292
+ </blockquote>
293
+
294
+ <h2>Introduction</h2>
295
+ {general_introduction}
296
+
297
+ """.format(general_introduction=GENERAL_INTRODUCTION)
298
+
299
+ OPENAPIDRIVER_LIBRARY_DOCSTRING = """
300
+ The OpenApiDriver library provides the keywords and logic for execution of generated test cases based on an OpenAPI document.
301
+
302
+ Visit the <a href="./index.html" target="_blank">OpenApiTools documentation</a> for an introduction.
303
+ """
304
+
305
+ OPENAPIDRIVER_INIT_DOCSTRING = """
306
+ <h2>Test case generation and execution</h2>
307
+
308
+ <h3>included_paths</h3>
309
+ A list of paths that will be included when generating the test cases.
310
+ The <code>*</code> character can be used at the end of a partial path to include all paths
311
+ starting with the partial path (wildcard include).
312
+
313
+ <h3>ignored_paths</h3>
314
+ A list of paths that will be ignored when generating the test cases.
315
+ The <code>*</code> character can be used at the end of a partial path to ignore all paths
316
+ starting with the partial path (wildcard ignore).
317
+
318
+ <h3>ignored_responses</h3>
319
+ A list of responses that will be ignored when generating the test cases.
320
+
321
+ <h3>ignored_testcases</h3>
322
+ A list of specific test cases that, if it would be generated, will be ignored.
323
+ Specific test cases to ignore must be specified as a <code>tuple</code> or
324
+ <code>list</code> of <code>path</code>, <code>method</code> and <code>response</code>.
325
+
326
+ {shared_parameters}
327
+ """.format(shared_parameters=SHARED_PARAMETERS)
328
+
329
+ OPENAPILIBGEN_DOCUMENTATION = """
330
+ <h1>OpenApiLibGen for Robot Framework</h1>
331
+
332
+ OpenApiLibGen is a command-line tool that can be used to generate a Robot Framework
333
+ library based on an OpenAPI document.
334
+
335
+ <hr>
336
+
337
+ In a (virtual) environment where <code>robotframework-openapitools</code> is installed
338
+ the <code>generate-library</code> shell command is availabe:
339
+
340
+ <div class="code-block"><pre><code class="language-shell">
341
+ $ generate-library -h
342
+ usage: openapi_libgen [-h] [-s SOURCE] [-d DESTINATION] [-n NAME] [--recursion-limit RECURSION_LIMIT]
343
+ [--recursion-default RECURSION_DEFAULT]
344
+
345
+ The OpenApiTools library generator
346
+
347
+ options:
348
+ -h, --help show this help message and exit
349
+ -s SOURCE, --source SOURCE
350
+ -d DESTINATION, --destination DESTINATION
351
+ -n NAME, --name NAME
352
+ --recursion-limit RECURSION_LIMIT
353
+ --recursion-default RECURSION_DEFAULT
354
+
355
+ Inspired by roboswag. Thank you Bartlomiej and Mateusz for your work!
356
+ </code></pre></div>
357
+
358
+ Generation of your library is an interactive process with a few steps:
359
+
360
+ <div class="code-block"><pre><code class="language-shell">
361
+ $ generate-library
362
+ Please provide a source for the generation:
363
+ $ http://127.0.0.1:8000/openapi.json
364
+ Please provide a path to where the library will be generated:
365
+ $ ./generated
366
+ Please provide a name for the library [default: FastAPI]:
367
+ $ My Generated Library
368
+ generated/MyGeneratedLibrary/__init__.py created
369
+ generated/MyGeneratedLibrary/my_generated_library.py created
370
+ </code></pre></div>
371
+
372
+ <div><codeblock>
373
+ Tip: The name for the library that you provide (or that's taken from the OpenAPI document)
374
+ will be transformed into a Python-legal module name and a Python-legal class name.
375
+ This means special characters like <code>-</code> will be removed or replaced.
376
+ If you used spaces in the desired name, they will be removed from the Library (class) name
377
+ and the next character will be capitalized while the spaces will be replaced with an
378
+ underscore in the module name as can be seen in the example above.
379
+ </codeblock></div>
380
+
381
+ <div><pre><codeblock><i>
382
+ Note: There's currently 2 environment variables that are taken into account by the generator:
383
+ <code>USE_SUMMARY_AS_KEYWORD_NAME</code> can result in nicer keyword names (but: uniqueness is not guaranteed, so you might need to rename some of the generated keywords manually)
384
+ <code>EXPAND_BODY_ARGUMENTS</code> changes how body arguments are provided to keywords (either a single body dict or key-value pairs for the body parameters)
385
+ </i><pre></codeblock></div>
386
+
387
+ <hr>
388
+
389
+ If the location where the library is located is in your Python search path, you'll be able
390
+ to use it like a regular Robot Framework library (in fact, it's a drop-in replacement for
391
+ OpenApiLibCore).
392
+
393
+ The generated library has a keyword for every endpoint in the OpenAPI document used to generated it.
394
+ In addition, all the keywords from OpenApiLibCore are available.
395
+ <blockquote>
396
+ Keyword documentation for OpenApiLibCore can be found <a href="./openapi_libcore.html" target="_blank">here</a>.
397
+ </blockquote>
398
+
399
+ The generated library can be used as shown below:
400
+
401
+ <div class="code-block"><pre><code class="language-robotframework">
402
+ *** Settings ***
403
+ Library MyGeneratedLibrary
404
+ ... source=${ORIGIN}/openapi.json
405
+ ... origin=${ORIGIN}
406
+ ... base_path=${EMPTY}
407
+ ... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py
408
+ Variables ${ROOT}/tests/variables.py
409
+
410
+
411
+ *** Variables ***
412
+ ${ORIGIN}= http://localhost:8000
413
+
414
+
415
+ *** Test Cases ***
416
+ Test Generated Keywords: Get Employees
417
+ ${response}= Get Employees
418
+ Should Be Equal As Integers ${response.status_code} 200
419
+
420
+ Test Generated Keywords: Post Employee
421
+ VAR &{body} name=Robin the Robot
422
+ ${response}= Post Employee body=${body}
423
+ # ${response}= Post Employee name=Robin the Robot
424
+ Should Be Equal As Integers ${response.status_code} 201
425
+ Should Be Equal ${response.json()}[name] Robin the Robot
426
+
427
+ Test Generated Keywords: Get Employee
428
+ ${response}= Get Employee
429
+ Should Be Equal As Integers ${response.status_code} 200
430
+
431
+ Test Generated Keywords: Patch Employee
432
+ ${employee_id}= Get Valid Id For Path path=/employees/{employee_id}
433
+ VAR &{body} date_of_birth=2021-12-31
434
+ ${response}= Patch Employee employee_id=${employee_id} body=${body}
435
+ # ${response}= Patch Employee employee_id=${employee_id} date_of_birth=2021-12-31
436
+ Should Be Equal As Integers ${response.status_code} 403
437
+
438
+ Test Generated Keywords: Post WageGroup
439
+ VAR &{body} hourly_rate=99.99
440
+ ${response}= Post Wagegroup body=${body}
441
+ # ${response}= Post Wagegroup hourly_rate=99.99
442
+ Should Be Equal As Integers ${response.status_code} 201
443
+ Should Be Equal As Numbers ${response.json()}[hourly-rate] 99.99
444
+
445
+ Test Generated Keywords: Get Energy Label
446
+ ${response}= Get Energy Label
447
+ ... zipcode=1111AA
448
+ ... home_number=10
449
+ ... extension=too long to be acceptable
450
+ ... validate_against_schema=${FALSE}
451
+ Should Be Equal As Integers ${response.status_code} 422
452
+
453
+ VAR @{omit} extension
454
+ ${response}= Get Energy Label
455
+ ... zipcode=1111AA
456
+ ... home_number=10
457
+ ... extension=too long to be acceptable
458
+ ... omit_parameters=${omit}
459
+ Should Be Equal As Integers ${response.status_code} 200
460
+ </code></pre></div>
461
+ """
462
+
463
+ ADVANCED_USE_DOCUMENTATION = """
464
+ <h1>Advanced use scenario's: using the mappings_path</h1>
465
+
466
+ <h2>Introduction</h2>
467
+ When working with APIs, there are often relations between resources or constraints on values.
468
+ The property on one resource may refer to the <code>id</code> of another resource.
469
+ The value for a certain property may have to be unique within a certain scope.
470
+ Perhaps an url contains parameters that must match values that are defined outside the API itself.
471
+
472
+ These types of relations and limitations cannot be described / modeled within the openapi document.
473
+ To support automatic validation of API endpoints where such relations apply, OpenApiLibCore supports the usage of a custom mappings file.
474
+
475
+ <h2>Taking a custom mappings file into use</h2>
476
+ To take a custom mappings file into use, the absolute path to it has to be passed to OpenApiLibCore as the <code>mappings_path</code> parameter:
477
+
478
+ <div class="code-block"><pre><code class="language-robotframework">
479
+ *** Settings ***
480
+ Library OpenApiLibCore
481
+ ... source=http://localhost:8000/openapi.json
482
+ ... origin=http://localhost:8000
483
+ ... mappings_path=${ROOT}/tests/custom_user_mappings.py
484
+ ...
485
+
486
+ </code></pre></div>
487
+
488
+ <blockquote><i>
489
+ Note: An absolute path is required.
490
+ In the example above, <code>${ROOT}</code> is a global variable that holds the absolute path to the repository root.
491
+ </i></blockquote>
492
+
493
+ <h2>The custom mappings file</h2>
494
+ Just like custom Robot Framework libraries, the mappings file has to be implemented in Python.
495
+ Since this Python file is imported by the OpenApiLibCore, it has to follow a fixed format (more technically, implement a certain interface).
496
+ The (almost) bare minimum implementation of a mappings.py file looks like this:
497
+
498
+ <div class="code-block"><pre><code class="language-python">
499
+ from OpenApiLibCore import (
500
+ IGNORE,
501
+ Dto,
502
+ IdDependency,
503
+ IdReference,
504
+ PathPropertiesConstraint,
505
+ PropertyValueConstraint,
506
+ UniquePropertyValueConstraint,
507
+ )
508
+
509
+
510
+ ID_MAPPING = {
511
+ "/myspecialpath", "special_thing_id",
512
+ }
513
+
514
+
515
+ class MyDtoThatDoesNothing(Dto):
516
+ @staticmethod
517
+ def get_relations():
518
+ relations = []
519
+ return relations
520
+
521
+ @staticmethod
522
+ def get_path_relations():
523
+ relations = []
524
+ return relations
525
+
526
+ @staticmethod
527
+ def get_parameter_relations():
528
+ relations = []
529
+ return relations
530
+
531
+
532
+ DTO_MAPPING = {
533
+ ("/myspecialpath", "post"): MyDtoThatDoesNothing
534
+ }
535
+
536
+
537
+ PATH_MAPPING = {
538
+ "/mypathwithexternalid/{external_id}": MyDtoThatDoesNothing
539
+ }
540
+
541
+ </code></pre></div>
542
+
543
+ There are 5 main parts in this mappings file:
544
+
545
+ <ol type="1">
546
+ <li>The import section.
547
+ Here the classes needed to implement custom mappings are imported.
548
+ This section can just be copied without changes.</li>
549
+ <li>The <code class="language-python">ID_MAPPING</code> "constant" definition / assignment.</li>
550
+ <li>The section defining the mapping Dtos. More on this later.</li>
551
+ <li>The <code class="language-python">DTO_MAPPING</code> "constant" definition / assignment.</li>
552
+ <li>The <code class="language-python">PATH_MAPPING</code> "constant" definition / assignment.</li>
553
+ </ol>
554
+
555
+ <h2>The ID_MAPPING, DTO_MAPPING and PATH_MAPPING</h2>
556
+ When a custom mappings file is used, the OpenApiLibCore will attempt to import it and then import <code>DTO_MAPPING</code>, <code>PATH_MAPPING</code> and <code>ID_MAPPING</code> from it.
557
+ For this reason, the exact same name must be used in a custom mappings file (capitilization matters).
558
+
559
+ <h3>The ID_MAPPING</h3>
560
+ The <code>ID_MAPPING</code> is a dictionary with a string as its key and either a string or a tuple of string and a callable as its value. The callable must take a string as its argument and return a string.
561
+
562
+ The <code>ID_MAPPING</code> is used to specify what the unique identifier property of a resource at the given path is, if different from the <code>default_id_property_name</code> (see library parameters).
563
+
564
+ In some situations, the identifier of the resource is not url-safe (e.g. containing a <code class="language-python">/</code>).
565
+ To support this type of resource identifier, a transformer can be provided:
566
+
567
+ <div class="code-block"><pre><code class="language-python">
568
+ def my_transformer(identifier_name: str) -> str:
569
+ return identifier_name.replace("/", "_")
570
+
571
+
572
+ ID_MAPPING = {
573
+ "/myspecialpath": ("special_thing_id", my_transformer),
574
+ }
575
+
576
+ </code></pre></div>
577
+
578
+ <h3>The DTO_MAPPING</h3>
579
+ The <code>DTO_MAPPING</code> is a dictionary with a tuple as its key and a mappings Dto as its value.
580
+ The tuple must be in the form <code class="language-python">("path_from_the_paths_section", "method_supported_by_the_path")</code>.
581
+ The <code class="language-python">path_from_the_paths_section</code> must be exactly as found in the openapi document.
582
+ The <code class="language-python">method_supported_by_the_path</code> must be one of the methods supported by the path and must be in lowercase.
583
+
584
+ <h3>The PATH_MAPPING</h3>
585
+ The <code>PATH_MAPPING</code> is a dictionary with a <code>"path_from_the_paths_section"</code> as its key and a mappings Dto as its value.
586
+ The <code>path_from_the_paths_section</code> must be exactly as found in the openapi document.
587
+
588
+
589
+ <h2>Dto mapping classes</h2>
590
+ As can be seen from the import section above, a number of classes are available to deal with relations between resources and / or constraints on properties.
591
+ Each of these classes is designed to handle a relation or constraint commonly seen in REST APIs.
592
+
593
+ To explain the different mapping classes, we'll use the following example:
594
+
595
+ <blockquote>
596
+ Imagine we have an API path <code>/employees</code> where we can create (<code>post</code>) a new Employee resource.
597
+ The Employee has a number of required properties; name, employee_number, wagegroup_id, and date_of_birth.
598
+
599
+ There is also the the <code>/wagegroups</code> path where a Wagegroup resource can be created.
600
+ This Wagegroup also has a number of required properties: <code>name</code> and <code>hourly rate</code>.
601
+ </blockquote>
602
+
603
+ <hr>
604
+
605
+ <h3><code>IdReference</code></h3>
606
+ <blockquote><u>The value for this propery must the the <code>id</code> of another resource</u></blockquote>
607
+
608
+ To add an Employee, a <code>wagegroup_id</code> is required, the <code>id</code> of a Wagegroup resource that is already present in the system.
609
+
610
+ Since a typical REST API generates this <code>id</code> for a new resource and returns that <code>id</code> as part of the <code>post</code> response, the required <code>wagegroup_id</code> can be obtained by posting a new Wagegroup.
611
+ This relation can be implemented as follows:
612
+
613
+ <div class="code-block"><pre><code class="language-python">
614
+ class EmployeeDto(Dto):
615
+ @staticmethod
616
+ def get_relations():
617
+ relations = [
618
+ IdDependency(
619
+ property_name="wagegroup_id",
620
+ get_path="/wagegroups",
621
+ error_code=451,
622
+ ),
623
+ ]
624
+ return relations
625
+
626
+ DTO_MAPPING = {
627
+ ("/employees", "post"): EmployeeDto
628
+ }
629
+
630
+ </code></pre></div>
631
+
632
+ Notice that the <code>get_path</code> of the <code>IdDependency</code> is not named <code>post_path</code> as one might expect.
633
+ This is deliberate for two reasons:
634
+
635
+ 1. The purpose is getting an <code>id</code>
636
+ 2. If the <code>post</code> operation is not supported on the provided path, a <code>get</code> operation is performed instead.
637
+ It is assumed that such a <code>get</code> will yield a list of resources and that each of these resources has an <code>id</code> that is valid for the desired <code>post</code> operation.
638
+
639
+ Also note the <code>error_code</code> of the <code>IdDependency</code>.
640
+ If a <code>post</code> is attempted with a value for the <code>wagegroup_id</code> that does not exist, the API should return an <code>error_code</code> response.
641
+ This <code>error_code</code> should be described as one of the <code>responses</code> in the openapi document for the <code>post</code> operation of the <code>/employees</code> path.
642
+
643
+ <hr>
644
+
645
+ <h3><code>IdDependency</code></h3>
646
+ <blockquote><u>This resource may not be DELETED if another resource refers to it</u></blockquote>
647
+
648
+ If an Employee has been added to the system, this Employee refers to the <code>id</code> of a Wagegroup for its required <code>employee_number</code> property.
649
+
650
+ Now let's say there is also the <code>/wagegroups/${wagegroup_id}</code> path that supports the <code>delete</code> operation.
651
+ If the Wagegroup refered to the Employee would be deleted, the Employee would be left with an invalid reference for one of its required properties.
652
+ To prevent this, an API typically returns an <code>error_code</code> when such a <code>delete</code> operation is attempted on a resource that is refered to in this fashion.
653
+ This <code>error_code</code> should be described as one of the <code>responses</code> in the openapi document for the <code>delete</code> operation of the <code>/wagegroups/${wagegroup_id}</code> path.
654
+
655
+ To verify that the specified <code>error_code</code> indeed occurs when attempting to <code>delete</code> the Wagegroup, we can implement the following dependency:
656
+
657
+ <div class="code-block"><pre><code class="language-python">
658
+ class WagegroupDto(Dto):
659
+ @staticmethod
660
+ def get_relations():
661
+ relations = [
662
+ IdReference(
663
+ property_name="wagegroup_id",
664
+ post_path="/employees",
665
+ error_code=406,
666
+ ),
667
+ ]
668
+ return relations
669
+
670
+ DTO_MAPPING = {
671
+ ("/wagegroups/{wagegroup_id}", "delete"): WagegroupDto
672
+ }
673
+
674
+ </code></pre></div>
675
+
676
+ <hr>
677
+
678
+ <h3><code>UniquePropertyValueConstraint</code></h3>
679
+ <blockquote><u>The value of this property must be unique within its scope</u></blockquote>
680
+
681
+ In a lot of systems, there is data that should be unique; an employee number, the email address of an employee, the domain name for the employee, etc.
682
+ Often those values are automatically generated based on other data, but for some data, an "available value" must be chosen by hand.
683
+
684
+ In our example, the required <code>employee_number</code> must be chosen from the "free" numbers.
685
+ When a number is chosen that is already in use, the API should return the <code>error_code</code> specified in the openapi document for the operation (typically <code>post</code>, <code>put</code> and <code>patch</code>) on the endpoint.
686
+
687
+ To verify that the specified <code>error_code</code> occurs when attempting to <code>post</code> an Employee with an <code>employee_number</code> that is already in use, we can implement the following dependency:
688
+
689
+ <div class="code-block"><pre><code class="language-python">
690
+ class EmployeeDto(Dto):
691
+ @staticmethod
692
+ def get_relations():
693
+ relations = [
694
+ UniquePropertyValueConstraint(
695
+ property_name="employee_number",
696
+ value=42,
697
+ error_code=422,
698
+ ),
699
+ ]
700
+ return relations
701
+
702
+ DTO_MAPPING = {
703
+ ("/employees", "post"): EmployeeDto,
704
+ ("/employees/${employee_id}", "put"): EmployeeDto,
705
+ ("/employees/${employee_id}", "patch"): EmployeeDto,
706
+ }
707
+
708
+ </code></pre></div>
709
+
710
+ Note how this example reuses the <code>EmployeeDto</code> to model the uniqueness constraint for all the operations (<code>post</code>, <code>put</code> and <code>patch</code>) that all relate to the same <code>employee_number</code>.
711
+
712
+ <hr>
713
+
714
+ <h3><code>PropertyValueConstraint</code></h3>
715
+ <blockquote><u>Use one of these values for this property</u></blockquote>
716
+
717
+ The OpenApiLibCore uses the <code>type</code> information in the openapi document to generate random data of the correct type to perform the operations that need it.
718
+ While this works in many situations (e.g. a random <code>string</code> for a <code>name</code>), there can be additional restrictions to a value that cannot be specified in an openapi document.
719
+
720
+ In our example, the <code>date_of_birth</code> must be a string in a specific format, e.g. 1995-03-27.
721
+ This type of constraint can be modeled as follows:
722
+
723
+ <div class="code-block"><pre><code class="language-python">
724
+ class EmployeeDto(Dto):
725
+ @staticmethod
726
+ def get_relations():
727
+ relations = [
728
+ PropertyValueConstraint(
729
+ property_name="date_of_birth",
730
+ values=["1995-03-27", "1980-10-02"],
731
+ error_code=422,
732
+ ),
733
+ ]
734
+ return relations
735
+
736
+ DTO_MAPPING = {
737
+ ("/employees", "post"): EmployeeDto,
738
+ ("/employees/${employee_id}", "put"): EmployeeDto,
739
+ ("/employees/${employee_id}", "patch"): EmployeeDto,
740
+ }
741
+
742
+ </code></pre></div>
743
+
744
+ Now in addition, there could also be the restriction that the Employee must be 18 years or older.
745
+ To support additional restrictions like these, the <code class="language-python">PropertyValueConstraint</code> supports two additional properties: <code class="language-python">error_value</code> and <code class="language-python">invalid_value_error_code</code>:
746
+
747
+ <div class="code-block"><pre><code class="language-python">
748
+ class EmployeeDto(Dto):
749
+ @staticmethod
750
+ def get_relations():
751
+ relations = [
752
+ PropertyValueConstraint(
753
+ property_name="date_of_birth",
754
+ values=["1995-03-27", "1980-10-02"],
755
+ error_code=422,
756
+ invalid_value="2020-02-20",
757
+ invalid_value_error_code=403,
758
+ ),
759
+ ]
760
+ return relations
761
+
762
+ DTO_MAPPING = {
763
+ ("/employees", "post"): EmployeeDto,
764
+ ("/employees/${employee_id}", "put"): EmployeeDto,
765
+ ("/employees/${employee_id}", "patch"): EmployeeDto,
766
+ }
767
+
768
+ </code></pre></div>
769
+
770
+ So now if an incorrectly formatted date is send a 422 response is expected, but when <code>2020-02-20</code> is send the expected repsonse is 403.
771
+
772
+ In some API implementations, there may be a property that will always return a specific error code if it's value is not valid.
773
+ This means that sending e.g. an invalid type of value will not result in the default error code for the API (typically 422 or 400).
774
+ This situation can be handled by use of the special <code class="language-python">IGNORE</code> value (see below for other uses):
775
+
776
+ <div class="code-block"><pre><code class="language-python">
777
+ class EmployeeDto(Dto):
778
+ @staticmethod
779
+ def get_relations():
780
+ relations = [
781
+ PropertyValueConstraint(
782
+ property_name="date_of_birth",
783
+ values=["1995-03-27", "1980-10-02"],
784
+ error_code=403,
785
+ invalid_value=IGNORE,
786
+ invalid_value_error_code=422,
787
+ ),
788
+ ]
789
+ return relations
790
+
791
+ DTO_MAPPING = {
792
+ ("/employees", "post"): EmployeeDto,
793
+ ("/employees/${employee_id}", "put"): EmployeeDto,
794
+ ("/employees/${employee_id}", "patch"): EmployeeDto,
795
+ }
796
+
797
+ </code></pre></div>
798
+
799
+ Note that while this configuration will prevent failing test cases generated by OpenApiDriver, it does not explicitly check for business logic errors anymore (younger than 18 in this example).
800
+
801
+ <hr>
802
+
803
+ <h3><code>PathPropertiesConstraint</code></h3>
804
+ <blockquote><u>Just use this for the <code>path</code></u></blockquote>
805
+
806
+ <blockquote><i>
807
+ Note: The <code class="language-python">PathPropertiesConstraint</code> is only applicable to the <code class="language-python">get_path_relations</code> in a <code class="language-python">Dto</code> and only the <code class="language-python">PATH_MAPPING</code> uses the <code class="language-python">get_path_relations</code>.
808
+ </i></blockquote>
809
+
810
+ To be able to automatically perform endpoint validations, the OpenApiLibCore has to construct the <code>url</code> for the resource from the <code>path</code> as found in the openapi document.
811
+ Often, such a <code>path</code> contains a reference to a resource id, e.g. <code class="language-python">"/employees/${employee_id}"</code>.
812
+ When such an <code>id</code> is needed, the OpenApiLibCore tries to obtain a valid <code>id</code> by taking these steps:
813
+
814
+ 1. Attempt a <code>post</code> on the "parent path" and extract the <code>id</code> from the response.
815
+ In our example: perform a <code>post</code> request on the <code>/employees</code> path and get the <code>id</code> from the response.
816
+ 2. If 1. fails, perform a <code>get</code> request on the <code>/employees</code> path. It is assumed that this will return a list of Employee objects with an <code>id</code>.
817
+ One item from the returned list is picked at rondom and its <code>id</code> is used.
818
+
819
+ This mechanism relies on the standard REST structure and patterns.
820
+
821
+ Unfortunately, this structure / pattern does not apply to every endpoint; not every path parameter refers to a resource id.
822
+ Imagine we want to extend the API from our example with an endpoint that returns all the Employees that have their birthday at a given date:
823
+ <code class="language-python">"/birthdays/${month}/${date}"</code>.
824
+ It should be clear that the OpenApiLibCore won't be able to acquire a valid <code>month</code> and <code>date</code>. The <code class="language-python">PathPropertiesConstraint</code> can be used in this case:
825
+
826
+ <div class="code-block"><pre><code class="language-python">
827
+ class BirthdaysDto(Dto):
828
+ @staticmethod
829
+ def get_path_relations():
830
+ relations = [
831
+ PathPropertiesConstraint(path="/birthdays/03/27"),
832
+ ]
833
+ return relations
834
+
835
+ PATH_MAPPING = {
836
+ "/birthdays/{month}/{date}": BirthdaysDto
837
+ }
838
+
839
+ </code></pre></div>
840
+
841
+ <hr>
842
+
843
+ <h3><code>IGNORE</code></h3>
844
+ <blockquote><u>Never send this query parameter as part of a request</u></blockquote>
845
+
846
+ Some optional query parameters have a range of valid values that depend on one or more path parameters.
847
+ Since path parameters are part of an url, they cannot be optional or empty so to extend the path parameters with optional parameters, query parameters can be used.
848
+
849
+ To illustrate this, let's imagine an API where the energy label for a building can be requested: <code class="language-python">"/energylabel/${zipcode}/${home_number}"</code>.
850
+ Some addresses however have an address extension, e.g. 1234AB 42 <sup>2.C</sup>.
851
+ The extension may not be limited to a fixed pattern / range and if an address has an extension, in many cases the address without an extension part is invalid.
852
+
853
+ To prevent OpenApiLibCore from generating invalid combinations of path and query parameters in this type of endpoint, the <code class="language-python">IGNORE</code> special value can be used to ensure the related query parameter is never send in a request.
854
+
855
+ <div class="code-block"><pre><code class="language-python">
856
+ class EnergyLabelDto(Dto):
857
+ @staticmethod
858
+ def get_parameter_relations():
859
+ relations = [
860
+ PropertyValueConstraint(
861
+ property_name="address_extension",
862
+ values=[IGNORE],
863
+ error_code=422,
864
+ ),
865
+ ]
866
+ return relations
867
+
868
+ @staticmethod
869
+ def get_relations(:
870
+ relations = [
871
+ PathPropertiesConstraint(path="/energy_label/1111AA/10"),
872
+ ]
873
+ return relations
874
+
875
+ DTO_MAPPING = {
876
+ ("/energy_label/{zipcode}/{home_number}", "get"): EnergyLabelDto
877
+ }
878
+
879
+ </code></pre></div>
880
+
881
+ Note that in this example, the <code class="language-python">get_parameter_relations()</code> method is implemented.
882
+ This method works mostly the same as the <code class="language-python">get_relations()</code> method but applies to headers and query parameters.
883
+
884
+ <hr>
885
+
886
+ <h2>Type annotations</h2>
887
+
888
+ An additional import to support type annotations is also available: <code class="language-python">Relation</code>.
889
+ A fully typed example can be found
890
+ <a href="https://github.com/MarketSquare/robotframework-openapitools/blob/main/tests/user_implemented/custom_user_mappings.py" target="_blank">here</a>.
891
+ """