blob: b0bf5b200fb1d20af4efe9c8aff7efe0c60e7bff [file] [log] [blame]
Daisuke Morita8e1f8612013-11-26 15:43:21 +09001# Copyright 2013 NTT Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import re
Andrea Frittolib6e1a282014-08-05 20:08:27 +010016
17from testtools import helpers
Daisuke Morita8e1f8612013-11-26 15:43:21 +090018
19
20class ExistsAllResponseHeaders(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +000021 """Specific matcher to check the existence of Swift's response headers
Daisuke Morita8e1f8612013-11-26 15:43:21 +090022
23 This matcher checks the existence of common headers for each HTTP method
24 or the target, which means account, container or object.
25 When checking the existence of 'specific' headers such as
26 X-Account-Meta-* or X-Object-Manifest for example, those headers must be
27 checked in each test code.
28 """
29
Brian Obera212c4a2016-04-16 18:30:12 -050030 def __init__(self, target, method, policies=None):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +000031 """Initialization of ExistsAllResponseHeaders
32
Daisuke Morita8e1f8612013-11-26 15:43:21 +090033 param: target Account/Container/Object
34 param: method PUT/GET/HEAD/DELETE/COPY/POST
35 """
36 self.target = target
37 self.method = method
Brian Obera212c4a2016-04-16 18:30:12 -050038 self.policies = policies or []
Daisuke Morita8e1f8612013-11-26 15:43:21 +090039
Radoslaw Zarzynskic22ef482016-01-25 10:54:07 +010040 def _content_length_required(self, resp):
41 # Verify whether given HTTP response must contain content-length.
42 # Take into account the exceptions defined in RFC 7230.
43 if resp.status in range(100, 200) or resp.status == 204:
44 return False
45
46 return True
47
Daisuke Morita8e1f8612013-11-26 15:43:21 +090048 def match(self, actual):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +000049 """Check headers
50
Radoslaw Zarzynskic22ef482016-01-25 10:54:07 +010051 param: actual HTTP response object containing headers and status
Daisuke Morita8e1f8612013-11-26 15:43:21 +090052 """
Radoslaw Zarzynskic22ef482016-01-25 10:54:07 +010053 # Check common headers for all HTTP methods.
54 #
55 # Please note that for 1xx and 204 responses Content-Length presence
56 # is not checked intensionally. According to RFC 7230 a server MUST
57 # NOT send the header in such responses. Thus, clients should not
58 # depend on this header. However, the standard does not require them
59 # to validate the server's behavior. We leverage that to not refuse
60 # any implementation violating it like Swift [1] or some versions of
61 # Ceph RadosGW [2].
62 # [1] https://bugs.launchpad.net/swift/+bug/1537811
63 # [2] http://tracker.ceph.com/issues/13582
64 if ('content-length' not in actual and
Thomas Morin42e111c2020-06-17 18:08:49 +020065 'transfer-encoding' not in actual and
Radoslaw Zarzynskic22ef482016-01-25 10:54:07 +010066 self._content_length_required(actual)):
Thomas Morin42e111c2020-06-17 18:08:49 +020067 return NonExistentHeaders(['content-length', 'transfer-encoding'])
Daisuke Morita8e1f8612013-11-26 15:43:21 +090068 if 'content-type' not in actual:
69 return NonExistentHeader('content-type')
70 if 'x-trans-id' not in actual:
71 return NonExistentHeader('x-trans-id')
72 if 'date' not in actual:
73 return NonExistentHeader('date')
74
75 # Check headers for a specific method or target
76 if self.method == 'GET' or self.method == 'HEAD':
77 if 'x-timestamp' not in actual:
78 return NonExistentHeader('x-timestamp')
Daisuke Morita8e1f8612013-11-26 15:43:21 +090079 if self.target == 'Account':
80 if 'x-account-bytes-used' not in actual:
81 return NonExistentHeader('x-account-bytes-used')
82 if 'x-account-container-count' not in actual:
83 return NonExistentHeader('x-account-container-count')
84 if 'x-account-object-count' not in actual:
85 return NonExistentHeader('x-account-object-count')
Andrea Frittoli52d3ffa2016-12-13 18:17:45 +000086 if int(actual['x-account-container-count']) > 0:
Brian Obera212c4a2016-04-16 18:30:12 -050087 acct_header = "x-account-storage-policy-"
88 matched_policy_count = 0
89
90 # Loop through the policies and look for account
91 # usage data. There should be at least 1 set
92 for policy in self.policies:
93 front_header = acct_header + policy['name'].lower()
94
95 usage_policies = [
96 front_header + '-bytes-used',
97 front_header + '-object-count',
98 front_header + '-container-count'
99 ]
100
101 # There should be 3 usage values for a give storage
102 # policy in an account bytes, object count, and
103 # container count
104 policy_hdrs = sum(1 for use_hdr in usage_policies
105 if use_hdr in actual)
106
107 # If there are less than 3 headers here then 1 is
108 # missing, let's figure out which one and report
109 if policy_hdrs == 3:
110 matched_policy_count = matched_policy_count + 1
111 else:
112 if policy_hdrs > 0 and policy_hdrs < 3:
113 for use_hdr in usage_policies:
114 if use_hdr not in actual:
115 return NonExistentHeader(use_hdr)
116
117 # Only flag an error if actual policies have been read and
118 # no usage has been found
119 if self.policies and matched_policy_count == 0:
120 return GenericError("No storage policy usage headers")
121
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900122 elif self.target == 'Container':
123 if 'x-container-bytes-used' not in actual:
124 return NonExistentHeader('x-container-bytes-used')
125 if 'x-container-object-count' not in actual:
126 return NonExistentHeader('x-container-object-count')
Brian Obera212c4a2016-04-16 18:30:12 -0500127 if 'x-storage-policy' not in actual:
128 return NonExistentHeader('x-storage-policy')
129 else:
130 policy_name = actual['x-storage-policy']
131
132 # loop through the policies and ensure that
133 # the value in the container header matches
134 # one of the storage policies
135 for policy in self.policies:
136 if policy['name'] == policy_name:
137 break
138 else:
139 # Ensure that there are actual policies stored
140 if self.policies:
141 return InvalidHeaderValue('x-storage-policy',
142 policy_name)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900143 elif self.target == 'Object':
144 if 'etag' not in actual:
145 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +0900146 if 'last-modified' not in actual:
147 return NonExistentHeader('last-modified')
148 elif self.method == 'PUT':
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900149 if self.target == 'Object':
150 if 'etag' not in actual:
151 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +0900152 if 'last-modified' not in actual:
153 return NonExistentHeader('last-modified')
154 elif self.method == 'COPY':
155 if self.target == 'Object':
156 if 'etag' not in actual:
157 return NonExistentHeader('etag')
158 if 'last-modified' not in actual:
159 return NonExistentHeader('last-modified')
160 if 'x-copied-from' not in actual:
161 return NonExistentHeader('x-copied-from')
162 if 'x-copied-from-last-modified' not in actual:
163 return NonExistentHeader('x-copied-from-last-modified')
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900164
165 return None
166
167
Brian Obera212c4a2016-04-16 18:30:12 -0500168class GenericError(object):
169 """Informs an error message of a generic error during header evaluation"""
170
171 def __init__(self, body):
172 self.body = body
173
174 def describe(self):
175 return "%s" % self.body
176
177 def get_details(self):
178 return {}
179
180
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900181class NonExistentHeader(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000182 """Informs an error message in the case of missing a certain header"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900183
184 def __init__(self, header):
185 self.header = header
186
187 def describe(self):
188 return "%s header does not exist" % self.header
189
190 def get_details(self):
191 return {}
192
193
Thomas Morin42e111c2020-06-17 18:08:49 +0200194class NonExistentHeaders(object):
195 """Informs an error message in the case of missing certain headers"""
196
197 def __init__(self, headers):
198 self.headers = headers
199
200 def describe(self):
201 return "none of these headers exist: %s" % self.headers
202
203 def get_details(self):
204 return {}
205
206
Brian Obera212c4a2016-04-16 18:30:12 -0500207class InvalidHeaderValue(object):
208 """Informs an error message when a header contains a bad value"""
209
210 def __init__(self, header, value):
211 self.header = header
212 self.value = value
213
214 def describe(self):
215 return "InvalidValue (%s, %s)" % (self.header, self.value)
216
217 def get_details(self):
218 return {}
219
220
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900221class AreAllWellFormatted(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000222 """Specific matcher to check the correctness of formats of values
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900223
224 This matcher checks the format of values of response headers.
225 When checking the format of values of 'specific' headers such as
226 X-Account-Meta-* or X-Object-Manifest for example, those values must be
227 checked in each test code.
228 """
229
230 def match(self, actual):
guo yunxian7bbbec12016-08-21 20:03:10 +0800231 for key, value in actual.items():
Abhishek Chandaf4c97ee2014-12-12 03:14:43 +0530232 if key in ('content-length', 'x-account-bytes-used',
233 'x-account-container-count', 'x-account-object-count',
234 'x-container-bytes-used', 'x-container-object-count')\
235 and not value.isdigit():
236 return InvalidFormat(key, value)
237 elif key in ('content-type', 'date', 'last-modified',
238 'x-copied-from-last-modified') and not value:
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900239 return InvalidFormat(key, value)
Stephen Finucane7f4a6212018-07-06 13:58:21 +0100240 elif key == 'x-timestamp' and not re.match(r"^\d+\.?\d*\Z", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900241 return InvalidFormat(key, value)
Stephen Finucane7f4a6212018-07-06 13:58:21 +0100242 elif key == 'x-copied-from' and not re.match(r"\S+/\S+", value):
Daisuke Morita397f5892014-03-20 14:33:46 +0900243 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900244 elif key == 'x-trans-id' and \
Christian Schwede44dcb302013-12-19 07:52:35 +0000245 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900246 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900247 elif key == 'accept-ranges' and not value == 'bytes':
248 return InvalidFormat(key, value)
249 elif key == 'etag' and not value.isalnum():
250 return InvalidFormat(key, value)
Daisuke Morita499bba32013-11-28 18:44:49 +0900251 elif key == 'transfer-encoding' and not value == 'chunked':
252 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900253
254 return None
255
256
257class InvalidFormat(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000258 """Informs an error message if a format of a certain header is invalid"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900259
260 def __init__(self, key, value):
261 self.key = key
262 self.value = value
263
264 def describe(self):
265 return "InvalidFormat (%s, %s)" % (self.key, self.value)
266
267 def get_details(self):
268 return {}
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100269
270
271class MatchesDictExceptForKeys(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000272 """Matches two dictionaries.
273
274 Verifies all items are equals except for those identified by a list of keys
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100275 """
276
277 def __init__(self, expected, excluded_keys=None):
278 self.expected = expected
279 self.excluded_keys = excluded_keys if excluded_keys is not None else []
280
281 def match(self, actual):
282 filtered_expected = helpers.dict_subtract(self.expected,
283 self.excluded_keys)
284 filtered_actual = helpers.dict_subtract(actual,
285 self.excluded_keys)
286 if filtered_actual != filtered_expected:
287 return DictMismatch(filtered_expected, filtered_actual)
288
289
290class DictMismatch(object):
291 """Mismatch between two dicts describes deltas"""
292
293 def __init__(self, expected, actual):
294 self.expected = expected
295 self.actual = actual
296 self.intersect = set(self.expected) & set(self.actual)
297 self.symmetric_diff = set(self.expected) ^ set(self.actual)
298
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400299 def _format_dict(self, dict_to_format):
300 # Ensure the error string dict is printed in a set order
301 # NOTE(mtreinish): needed to ensure a deterministic error msg for
302 # testing. Otherwise the error message will be dependent on the
303 # dict ordering.
304 dict_string = "{"
305 for key in sorted(dict_to_format):
306 dict_string += "'%s': %s, " % (key, dict_to_format[key])
307 dict_string = dict_string[:-2] + '}'
308 return dict_string
309
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100310 def describe(self):
311 msg = ""
312 if self.symmetric_diff:
313 only_expected = helpers.dict_subtract(self.expected, self.actual)
314 only_actual = helpers.dict_subtract(self.actual, self.expected)
315 if only_expected:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400316 msg += "Only in expected:\n %s\n" % self._format_dict(
317 only_expected)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100318 if only_actual:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400319 msg += "Only in actual:\n %s\n" % self._format_dict(
320 only_actual)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100321 diff_set = set(o for o in self.intersect if
322 self.expected[o] != self.actual[o])
323 if diff_set:
324 msg += "Differences:\n"
Martin Pavlasek659e2db2014-09-04 16:43:21 +0200325 for o in diff_set:
326 msg += " %s: expected %s, actual %s\n" % (
327 o, self.expected[o], self.actual[o])
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100328 return msg
329
330 def get_details(self):
331 return {}