@toa.io/extensions.exposition 0.22.1 → 1.0.0-alpha.0

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.
Files changed (77) hide show
  1. package/components/identity.basic/source/authenticate.ts +3 -2
  2. package/components/identity.basic/source/transit.ts +4 -3
  3. package/components/octets.storage/manifest.toa.yaml +26 -0
  4. package/components/octets.storage/operations/delete.js +7 -0
  5. package/components/octets.storage/operations/fetch.js +46 -0
  6. package/components/octets.storage/operations/get.js +7 -0
  7. package/components/octets.storage/operations/list.js +7 -0
  8. package/components/octets.storage/operations/permute.js +7 -0
  9. package/components/octets.storage/operations/store.js +11 -0
  10. package/cucumber.js +0 -1
  11. package/documentation/access.md +2 -3
  12. package/documentation/cache.md +42 -0
  13. package/documentation/octets.md +196 -0
  14. package/documentation/protocol.md +49 -5
  15. package/documentation/tree.md +1 -5
  16. package/features/access.feature +1 -0
  17. package/features/cache.feature +160 -0
  18. package/features/errors.feature +18 -0
  19. package/features/identity.basic.feature +2 -0
  20. package/features/octets.feature +295 -0
  21. package/features/octets.workflows.feature +114 -0
  22. package/features/routes.feature +40 -0
  23. package/features/steps/HTTP.ts +56 -6
  24. package/features/steps/Parameters.ts +5 -2
  25. package/features/steps/Workspace.ts +8 -5
  26. package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
  27. package/features/steps/components/octets.tester/operations/bar.js +12 -0
  28. package/features/steps/components/octets.tester/operations/baz.js +11 -0
  29. package/features/steps/components/octets.tester/operations/diversify.js +14 -0
  30. package/features/steps/components/octets.tester/operations/err.js +16 -0
  31. package/features/steps/components/octets.tester/operations/foo.js +7 -0
  32. package/features/steps/components/octets.tester/operations/lenna.png +0 -0
  33. package/features/steps/components/pots/manifest.toa.yaml +1 -1
  34. package/features/streams.feature +5 -1
  35. package/package.json +16 -9
  36. package/readme.md +8 -5
  37. package/schemas/octets/context.cos.yaml +1 -0
  38. package/schemas/octets/delete.cos.yaml +1 -0
  39. package/schemas/octets/fetch.cos.yaml +3 -0
  40. package/schemas/octets/list.cos.yaml +1 -0
  41. package/schemas/octets/permute.cos.yaml +1 -0
  42. package/schemas/octets/store.cos.yaml +3 -0
  43. package/source/Gateway.ts +9 -4
  44. package/source/HTTP/Server.fixtures.ts +2 -6
  45. package/source/HTTP/Server.test.ts +9 -31
  46. package/source/HTTP/Server.ts +33 -19
  47. package/source/HTTP/exceptions.ts +2 -12
  48. package/source/HTTP/formats/index.ts +7 -4
  49. package/source/HTTP/formats/json.ts +3 -0
  50. package/source/HTTP/formats/msgpack.ts +3 -0
  51. package/source/HTTP/formats/text.ts +3 -0
  52. package/source/HTTP/formats/yaml.ts +3 -0
  53. package/source/HTTP/messages.test.ts +3 -49
  54. package/source/HTTP/messages.ts +60 -35
  55. package/source/RTD/Route.ts +1 -1
  56. package/source/RTD/segment.ts +2 -1
  57. package/source/RTD/syntax/parse.ts +2 -1
  58. package/source/Remotes.ts +8 -0
  59. package/source/Tenant.ts +5 -0
  60. package/source/directives/auth/Family.ts +26 -22
  61. package/source/directives/auth/Rule.ts +1 -1
  62. package/source/directives/cache/Control.ts +59 -0
  63. package/source/directives/cache/Exact.ts +7 -0
  64. package/source/directives/cache/Family.ts +36 -0
  65. package/source/directives/cache/index.ts +3 -0
  66. package/source/directives/cache/types.ts +9 -0
  67. package/source/directives/index.ts +3 -1
  68. package/source/directives/octets/Context.ts +18 -0
  69. package/source/directives/octets/Delete.ts +32 -0
  70. package/source/directives/octets/Family.ts +68 -0
  71. package/source/directives/octets/Fetch.ts +85 -0
  72. package/source/directives/octets/List.ts +32 -0
  73. package/source/directives/octets/Permute.ts +37 -0
  74. package/source/directives/octets/Store.ts +158 -0
  75. package/source/directives/octets/index.ts +3 -0
  76. package/source/directives/octets/schemas.ts +12 -0
  77. package/source/directives/octets/types.ts +13 -0
@@ -1,6 +1,7 @@
1
1
  import { atob } from 'buffer'
2
2
  import { compare } from 'bcryptjs'
3
3
  import { type Query, type Maybe } from '@toa.io/types'
4
+ import { Err } from 'error-value'
4
5
  import { type Context } from './types'
5
6
 
6
7
  export async function computation (input: string, context: Context): Promise<Maybe<Output>> {
@@ -21,8 +22,8 @@ export async function computation (input: string, context: Context): Promise<May
21
22
  else return ERR_PASSWORD_MISMATCH
22
23
  }
23
24
 
24
- const ERR_NOT_FOUND = new Error('NOT_FOUND')
25
- const ERR_PASSWORD_MISMATCH = new Error('PASSWORD_MISMATCH')
25
+ const ERR_NOT_FOUND = Err('NOT_FOUND')
26
+ const ERR_PASSWORD_MISMATCH = Err('PASSWORD_MISMATCH')
26
27
 
27
28
  interface Output {
28
29
  identity: {
@@ -1,5 +1,6 @@
1
1
  import { genSalt, hash } from 'bcryptjs'
2
2
  import { type Maybe, type Operation } from '@toa.io/types'
3
+ import { Err } from 'error-value'
3
4
  import { type Context, type Entity, type TransitInput, type TransitOutput } from './types'
4
5
 
5
6
  export class Transition implements Operation {
@@ -60,8 +61,8 @@ function invalid (value: string, expressions: RegExp[]): boolean {
60
61
  return expressions.some((expression) => !expression.test(value))
61
62
  }
62
63
 
63
- const ERR_PRINCIPAL_LOCKED = new Error('PRINCIPAL_LOCKED')
64
- const ERR_INVALID_USERNAME = new Error('INVALID_USERNAME')
65
- const ERR_INVALID_PASSWORD = new Error('INVALID_PASSWORD')
64
+ const ERR_PRINCIPAL_LOCKED = Err('PRINCIPAL_LOCKED', 'Principal username cannot be changed.')
65
+ const ERR_INVALID_USERNAME = Err('INVALID_USERNAME', 'Username is not meeting the requirements.')
66
+ const ERR_INVALID_PASSWORD = Err('INVALID_PASSWORD', 'Password is not meeting the requirements.')
66
67
 
67
68
  type Tokens = Context['remote']['identity']['tokens']
@@ -0,0 +1,26 @@
1
+ namespace: octets
2
+ name: storage
3
+
4
+ storages: ~
5
+
6
+ operations:
7
+ store:
8
+ bindings: ~
9
+ input:
10
+ storage*: string
11
+ request*: ~
12
+ accept: string
13
+ fetch: &simple
14
+ bindings: ~
15
+ input:
16
+ storage*: string
17
+ path*: string
18
+ get: *simple
19
+ list: *simple
20
+ delete: *simple
21
+ permute:
22
+ bindings: ~
23
+ input:
24
+ storage*: string
25
+ path*: string
26
+ list*: [string]
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function del (input, context) {
4
+ return context.storages[input.storage].delete(input.path)
5
+ }
6
+
7
+ exports.effect = del
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+
3
+ const { posix } = require('node:path')
4
+ const { Err } = require('error-value')
5
+
6
+ async function fetch (input, context) {
7
+ const storage = context.storages[input.storage]
8
+ const basename = posix.basename(input.path)
9
+ const path = posix.dirname(input.path)
10
+ const [id, suffix] = split(basename)
11
+ const entry = await storage.get(posix.join(path, id))
12
+
13
+ if (entry instanceof Error)
14
+ return entry
15
+
16
+ let variant
17
+
18
+ if (suffix !== undefined) {
19
+ variant = entry.variants.find((variant) => variant.name === suffix)
20
+
21
+ if (variant === undefined)
22
+ return NOT_FOUND
23
+ }
24
+
25
+ const stream = await storage.fetch(input.path)
26
+
27
+ if (stream instanceof Error)
28
+ return stream
29
+
30
+ const { type, size } = variant ?? entry
31
+
32
+ return { stream, checksum: entry.id, type, size }
33
+ }
34
+
35
+ function split (basename) {
36
+ const dot = basename.indexOf('.')
37
+
38
+ if (dot === -1)
39
+ return [basename, undefined]
40
+ else
41
+ return [basename.slice(0, dot), basename.slice(dot + 1)]
42
+ }
43
+
44
+ const NOT_FOUND = Err('NOT_FOUND')
45
+
46
+ exports.effect = fetch
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function get (input, context) {
4
+ return context.storages[input.storage].get(input.path)
5
+ }
6
+
7
+ exports.computation = get
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function list (input, context) {
4
+ return context.storages[input.storage].list(input.path)
5
+ }
6
+
7
+ exports.effect = list
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function permute (input, context) {
4
+ return context.storages[input.storage].permute(input.path, input.list)
5
+ }
6
+
7
+ exports.effect = permute
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ function store (input, context) {
4
+ const { storage, request } = input
5
+ const path = request.path
6
+ const claim = request.headers['content-type']
7
+
8
+ return context.storages[storage].put(path, request, { claim, accept: input.accept })
9
+ }
10
+
11
+ exports.effect = store
package/cucumber.js CHANGED
@@ -3,7 +3,6 @@ module.exports = {
3
3
  paths: ['features/**/*.feature'],
4
4
  requireModule: ['ts-node/register'],
5
5
  require: ['./features/**/*.ts'],
6
- publishQuiet: true,
7
6
  failFast: true
8
7
  }
9
8
  }
@@ -13,9 +13,8 @@
13
13
 
14
14
  The Authorization is implemented as a set of [RTD Directives](tree.md#directives).
15
15
 
16
- Directives are executed in a predetermined order until one of them grants access to a resource. If
17
- none of the
18
- directives grants access, then the Authorization interrupts request processing and responds with an
16
+ Directives are executed in a predetermined order until one of them grants access to a resource.
17
+ If none of the directives grants access, then the Authorization interrupts request processing and responds with an
19
18
  authorization error.
20
19
 
21
20
  > The Authorization directive provider is named `authorization`,
@@ -0,0 +1,42 @@
1
+ # Caching
2
+
3
+ Directive family `cache` implements the
4
+ HTTP [Cache-Control](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9).
5
+
6
+ ## `cache:control`
7
+
8
+ Sets the value of the `Cache-Control` header
9
+ for [successful responses](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2)
10
+ to [safe HTTP methods](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP).
11
+
12
+ ```yaml
13
+ /:
14
+ GET:
15
+ cache:control: max-age=60000
16
+ ```
17
+
18
+ ### Implicit modifications
19
+
20
+ In terms of security, the following implicit modifications are made to the `Cache-Control` header:
21
+
22
+ - If it contains the `public` directive without `no-cache` and the request is authenticated,
23
+ the `no-cache` directive is added.
24
+ This is done to prevent the storage of authentication tokens in shared caches.
25
+ - If it does not contain the `private` directive and the request is authenticated, the `private`
26
+ directive is added.
27
+ This is to prevent the storage of private data in shared caches.
28
+
29
+ ## `cache:exact`
30
+
31
+ Same as `cache:control` without implicit modifications.
32
+
33
+ ```yaml
34
+ /:
35
+ GET:
36
+ cache:exact: public, max-age=60000
37
+ ```
38
+
39
+ ## References
40
+
41
+ - HTTP 14.9.1 [What is cacheable](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9.1)
42
+ - See also [features](/extensions/exposition/features/cache.feature)
@@ -0,0 +1,196 @@
1
+ # BLOBs
2
+
3
+ The `octets` directive family implements operations with BLOBs, using
4
+ the [Storages extention](/extensions/storages).
5
+ The most common use case is to handle file uploads, downloads, and processing.
6
+
7
+ ## `octets:context`
8
+
9
+ Sets the [storage name](/extensions/storages/readme.md#annotation) to be used for the `octets`
10
+ directives under the current RTD Node.
11
+
12
+ ```yaml
13
+ /images:
14
+ octets:context: images
15
+ ```
16
+
17
+ ## `octets:store`
18
+
19
+ Stores the content of the request body into a storage, under the request path with
20
+ specified `content-type`.
21
+
22
+ If request's `content-type` is not acceptable, or if the request body does not pass
23
+ the [validation](/extensions/storages/readme.md#async-putpath-string-stream-readable-type-typecontrol-maybeentry),
24
+ the request is rejected with a `415 Unsupported Media Type` response.
25
+
26
+ The value of the directive is an object with the following properties:
27
+
28
+ - `accept`: a media type or an array of media types that are acceptable.
29
+ If the `accept` property is not specified, any media type is acceptable (which is the default).
30
+ - `workflow`: [workflow](#workflows) to be executed once the content is successfully stored.
31
+
32
+ ```yaml
33
+ /images:
34
+ octets:context: images
35
+ POST:
36
+ octets:store:
37
+ accept:
38
+ - image/jpeg
39
+ - image/png
40
+ - video/*
41
+ workflow:
42
+ resize: images.resize
43
+ analyze: images.analyze
44
+ ```
45
+
46
+ ### Workflows
47
+
48
+ A workflow is a list of endpoints to be called.
49
+ The following input will be passed to each endpoint:
50
+
51
+ ```yaml
52
+ storage: string
53
+ path: string
54
+ entry: Entry
55
+ ```
56
+
57
+ See [Entry](/extensions/storages/readme.md#entry) and an
58
+ example [workflow step processor](../features/steps/components/octets.tester).
59
+
60
+ A _workflow unit_ is an object with keys referencing the workflow step identifier, and an endpoint
61
+ as value.
62
+ Steps within a workflow unit are executed in parallel.
63
+
64
+ ```yaml
65
+ octets:store:
66
+ workflow:
67
+ resize: images.resize
68
+ analyze: images.analyze
69
+ ```
70
+
71
+ A workflow can be a single unit, or an array of units.
72
+ If it's an array, the workflow units are executed in sequence.
73
+
74
+ ```yaml
75
+ octets:store:
76
+ workflow:
77
+ - optimize: images.optimize # executed first
78
+ - resize: images.resize # executed second
79
+ analyze: images.analyze # executed in parallel with `resize`
80
+ ```
81
+
82
+ If one of the workflow units returns an error, the execution of the workflow is interrupted.
83
+
84
+ ### Response
85
+
86
+ The response of the `octets:store` directive is the created Entry.
87
+
88
+ ```
89
+ 201 Created
90
+ content-type: application/yaml
91
+
92
+ id: eecd837c
93
+ type: image/jpeg
94
+ created: 1698004822358
95
+ ```
96
+
97
+ If the `octets:store` directive contains a `workflow`, the response
98
+ is [multipart](protocol.md#multipart-types).
99
+ The first part represents the created Entry, which is sent immediately after the BLOB is stored,
100
+ while subsequent parts are results from the workflow endpoints, sent as soon as they are available.
101
+
102
+ In case a workflow endpoint returns an `Error`, the error part is sent, and the response is closed.
103
+ Error's properties are added to the error part, among with the `step` identifier.
104
+
105
+ ```
106
+ 201 Created
107
+ content-type: multipart/yaml; boundary=cut
108
+
109
+ --cut
110
+ id: eecd837c
111
+ type: image/jpeg
112
+ created: 1698004822358
113
+ --cut
114
+ optimize: null
115
+ --cut
116
+ error:
117
+ step: resize
118
+ code: TOO_SMALL
119
+ message: Image is too small
120
+ --cut--
121
+ ```
122
+
123
+ ## `octets:fetch`
124
+
125
+ Fetches the content of a stored BLOB corresponding to the request path, and returns it as the
126
+ response body with the corresponding `content-type`, `content-length`
127
+ and `etag` ([conditional GET](https://datatracker.ietf.org/doc/html/rfc2616#section-9.3) is
128
+ also supported).
129
+ The `accept` request header is disregarded.
130
+
131
+ The value of the directive is an object with the following properties:
132
+
133
+ - `meta`: `boolean` indicating whether an Entry is accessible.
134
+ Defaults to `false`.
135
+ - `blob`: `boolean` indicating whether the original BLOB is accessible,
136
+ [BLOB variant](/extensions/storages/readme.md#async-fetchpath-string-maybereadable) must be
137
+ specified in the path otherwise.
138
+ Defaults to `true`.
139
+
140
+ ```yaml
141
+ /images:
142
+ octets:context: images
143
+ /*:
144
+ GET:
145
+ octets:fetch:
146
+ blob: false # prevent access to the original BLOB
147
+ meta: true # allow access to an Entry
148
+ ```
149
+
150
+ To access an Entry, the request path must be suffixed with `:meta`:
151
+
152
+ ```http
153
+ GET /images/eecd837c:meta HTTP/1.1
154
+ ```
155
+
156
+ The `octets:fetch: ~` declaration is equivalent to defaults.
157
+
158
+ ## `octets:list`
159
+
160
+ Lists the entries stored under the request path.
161
+
162
+ ```yaml
163
+ /images:
164
+ octets:context: images
165
+ GET:
166
+ octets:list: ~
167
+ ```
168
+
169
+ Responds with a list of entry identifiers.
170
+
171
+ ## `octets:delete`
172
+
173
+ Delete the entry corresponding to the request path.
174
+
175
+ ```yaml
176
+ /images:
177
+ octets:context: images
178
+ DELETE:
179
+ octets:delete: ~
180
+ ```
181
+
182
+ ## `octets:permute`
183
+
184
+ Performs
185
+ a [permutation](/extensions/storages/readme.md#async-permutepath-string-ids-string-maybevoid) on the
186
+ entries
187
+ under the request path.
188
+
189
+ ```yaml
190
+ /images:
191
+ octets:context: images
192
+ PUT:
193
+ octets:permute: ~
194
+ ```
195
+
196
+ The request body must be a list of entry identifiers.
@@ -4,15 +4,59 @@
4
4
 
5
5
  The following media types are supported for both requests and responses:
6
6
 
7
- - `application/json`
8
- - `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
9
7
  - `application/msgpack` using [msgpackr](https://github.com/kriszyp/msgpackr)
8
+ - `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
9
+ - `application/json`
10
10
  - `text/plain`
11
11
 
12
12
  The response format is determined by content negotiation
13
13
  using [negotiator](https://github.com/jshttp/negotiator).
14
14
 
15
- ## Streams
15
+ ```http
16
+ GET / HTTP/1.1
17
+ accept: application/yaml
18
+ ```
19
+
20
+ ```
21
+ 200 OK
22
+ content-type: application/yaml
23
+
24
+ foo: bar
25
+ ```
26
+
27
+ ### Multipart types
28
+
29
+ Multipart responses are endoded using content negotiation,
30
+ and the `content-type` of the response is set to one of the custom `multipart/` subtypes, corresponding to the type of
31
+ the parts:
32
+
33
+ | Response type | Part type |
34
+ |---------------------|-----------------------|
35
+ | `multipart/msgpack` | `application/msgpack` |
36
+ | `multipart/yaml` | `application/yaml` |
37
+ | `multipart/json` | `application/json` |
38
+ | `multipart/text` | `text/plain` |
39
+
40
+ Example:
41
+
42
+ ```
43
+ GET /stream/ HTTP/1.1
44
+ accept: application/yaml
45
+ ```
46
+
47
+ ```
48
+ 200 OK
49
+ content-type: multipart/yaml; boundary=cut
50
+
51
+ --cut
52
+ foo: bar
53
+ --cut
54
+ baz: qux
55
+ --cut--
56
+ ```
57
+
58
+ See also:
16
59
 
17
- Reply streams are transmitted
18
- using [chunked transfer encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#directives).
60
+ - [Multipart Content-Type](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) at W3C
61
+ - [Content-Type: multipart](https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa493937(v=exchg.140))
62
+ at Microsoft
@@ -124,7 +124,7 @@ Intermediate Nodes must not have Methods as they are unreachable.
124
124
 
125
125
  ## Directives
126
126
 
127
- RTD Directives are declared using RTD node or Method keys following the `{provider}:{directive}` pattern and can be used
127
+ RTD Directives are declared using RTD node or Method keys following the `{family}:{directive}` pattern and can be used
128
128
  to add or modify the behavior of request processing. Directive declarations are applied to the RTD node where they are
129
129
  declared and to all nested nodes.
130
130
 
@@ -151,10 +151,6 @@ When it is necessary to avoid directive nesting, a Route can be declared adjacen
151
151
  In this example, the Route `/posts/:user-id/:post-id/` has only the `authorization:role` directive
152
152
  applied.
153
153
 
154
- > Directives can be declared without the `{provider}:` prefix unless there are multiple directives
155
- > with the same name
156
- > across different providers.
157
-
158
154
  Another way to avoid nesting is to declare an _isolated_ Node as follows:
159
155
 
160
156
  ```yaml
@@ -38,6 +38,7 @@ Feature: Access authorization
38
38
  When the following request is received:
39
39
  """
40
40
  GET / HTTP/1.1
41
+ accept: application/yaml
41
42
  """
42
43
  Then the following reply is sent:
43
44
  """
@@ -0,0 +1,160 @@
1
+ Feature: Caching
2
+
3
+ Background:
4
+ Given the `identity.basic` database contains:
5
+ # developer:secret
6
+ # user:12345
7
+ | _id | username | password |
8
+ | b70a7dbca6b14a2eaac8a9eb4b2ff4db | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
9
+ Given the `identity.roles` database contains:
10
+ | _id | identity | role |
11
+ | 775a648d054e4ce1a65f8f17e5b51803 | b70a7dbca6b14a2eaac8a9eb4b2ff4db | developer |
12
+
13
+ Scenario: Caching successful response
14
+ Given the annotation:
15
+ """yaml
16
+ /:
17
+ anonymous: true
18
+ GET:
19
+ cache:control: max-age=60000
20
+ dev:stub: hello
21
+ """
22
+ When the following request is received:
23
+ """
24
+ GET / HTTP/1.1
25
+ accept: text/plain
26
+ """
27
+ Then the following reply is sent:
28
+ """
29
+ 200 OK
30
+ content-type: text/plain
31
+ cache-control: max-age=60000
32
+
33
+ hello
34
+ """
35
+
36
+ Scenario: Nested cache directives
37
+ Given the annotation:
38
+ """yaml
39
+ /:
40
+ cache:control: max-age=30000
41
+ GET:
42
+ anonymous: true
43
+ dev:stub: hello
44
+ /foo:
45
+ auth:role: developer
46
+ GET:
47
+ dev:stub: hello
48
+ /bar:
49
+ auth:role: developer
50
+ cache:control: max-age=60000, public
51
+ GET:
52
+ dev:stub: hello
53
+ """
54
+ When the following request is received:
55
+ """
56
+ GET / HTTP/1.1
57
+ accept: text/plain
58
+ """
59
+ Then the following reply is sent:
60
+ """
61
+ 200 OK
62
+ content-type: text/plain
63
+ cache-control: max-age=30000
64
+
65
+ hello
66
+ """
67
+ When the following request is received:
68
+ """
69
+ GET /foo/ HTTP/1.1
70
+ accept: text/plain
71
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
72
+ """
73
+ Then the following reply is sent:
74
+ """
75
+ 200 OK
76
+ content-type: text/plain
77
+ cache-control: private, max-age=30000
78
+
79
+ hello
80
+ """
81
+ When the following request is received:
82
+ """
83
+ GET /bar/ HTTP/1.1
84
+ accept: text/plain
85
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
86
+ """
87
+ Then the following reply is sent:
88
+ """
89
+ 200 OK
90
+ content-type: text/plain
91
+ cache-control: no-cache, max-age=60000, public
92
+
93
+ hello
94
+ """
95
+ And the reply does not contain:
96
+ """
97
+ cache-control: private, max-age=30000
98
+ """
99
+
100
+ Scenario: Cache-control is not added when request is unsafe
101
+ Given the annotation:
102
+ """yaml
103
+ /:
104
+ anonymous: true
105
+ cache:control: max-age=60000
106
+ POST:
107
+ dev:stub: hello
108
+ """
109
+ When the following request is received:
110
+ """
111
+ POST / HTTP/1.1
112
+ accept: application/yaml
113
+ """
114
+ Then the reply does not contain:
115
+ """
116
+ cache-control: max-age=60000
117
+ """
118
+
119
+ Scenario: Cache-control is added without implicit modifications
120
+ Given the annotation:
121
+ """yaml
122
+ /:
123
+ auth:role: developer
124
+ cache:exact: max-age=60000, public
125
+ GET:
126
+ dev:stub: hello
127
+ """
128
+ When the following request is received:
129
+ """
130
+ GET / HTTP/1.1
131
+ authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
132
+ accept: text/plain
133
+
134
+ """
135
+ Then the following reply is sent:
136
+ """
137
+ 200 OK
138
+ content-type: text/plain
139
+ cache-control: max-age=60000, public
140
+
141
+ hello
142
+ """
143
+
144
+ Scenario: Response without caching
145
+ Given the annotation:
146
+ """yaml
147
+ /:
148
+ anonymous: true
149
+ GET:
150
+ dev:stub: hello
151
+ """
152
+ When the following request is received:
153
+ """
154
+ GET / HTTP/1.1
155
+ accept: text/plain
156
+ """
157
+ Then the reply does not contain:
158
+ """
159
+ cache-control:
160
+ """