blob: c702d88963dac463ff1c60bb94322662d559acad [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
65 self._content_length_required(actual)):
Daisuke Morita8e1f8612013-11-26 15:43:21 +090066 return NonExistentHeader('content-length')
67 if 'content-type' not in actual:
68 return NonExistentHeader('content-type')
69 if 'x-trans-id' not in actual:
70 return NonExistentHeader('x-trans-id')
71 if 'date' not in actual:
72 return NonExistentHeader('date')
73
74 # Check headers for a specific method or target
75 if self.method == 'GET' or self.method == 'HEAD':
76 if 'x-timestamp' not in actual:
77 return NonExistentHeader('x-timestamp')
78 if 'accept-ranges' not in actual:
79 return NonExistentHeader('accept-ranges')
80 if self.target == 'Account':
81 if 'x-account-bytes-used' not in actual:
82 return NonExistentHeader('x-account-bytes-used')
83 if 'x-account-container-count' not in actual:
84 return NonExistentHeader('x-account-container-count')
85 if 'x-account-object-count' not in actual:
86 return NonExistentHeader('x-account-object-count')
Andrea Frittoli52d3ffa2016-12-13 18:17:45 +000087 if int(actual['x-account-container-count']) > 0:
Brian Obera212c4a2016-04-16 18:30:12 -050088 acct_header = "x-account-storage-policy-"
89 matched_policy_count = 0
90
91 # Loop through the policies and look for account
92 # usage data. There should be at least 1 set
93 for policy in self.policies:
94 front_header = acct_header + policy['name'].lower()
95
96 usage_policies = [
97 front_header + '-bytes-used',
98 front_header + '-object-count',
99 front_header + '-container-count'
100 ]
101
102 # There should be 3 usage values for a give storage
103 # policy in an account bytes, object count, and
104 # container count
105 policy_hdrs = sum(1 for use_hdr in usage_policies
106 if use_hdr in actual)
107
108 # If there are less than 3 headers here then 1 is
109 # missing, let's figure out which one and report
110 if policy_hdrs == 3:
111 matched_policy_count = matched_policy_count + 1
112 else:
113 if policy_hdrs > 0 and policy_hdrs < 3:
114 for use_hdr in usage_policies:
115 if use_hdr not in actual:
116 return NonExistentHeader(use_hdr)
117
118 # Only flag an error if actual policies have been read and
119 # no usage has been found
120 if self.policies and matched_policy_count == 0:
121 return GenericError("No storage policy usage headers")
122
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900123 elif self.target == 'Container':
124 if 'x-container-bytes-used' not in actual:
125 return NonExistentHeader('x-container-bytes-used')
126 if 'x-container-object-count' not in actual:
127 return NonExistentHeader('x-container-object-count')
Brian Obera212c4a2016-04-16 18:30:12 -0500128 if 'x-storage-policy' not in actual:
129 return NonExistentHeader('x-storage-policy')
130 else:
131 policy_name = actual['x-storage-policy']
132
133 # loop through the policies and ensure that
134 # the value in the container header matches
135 # one of the storage policies
136 for policy in self.policies:
137 if policy['name'] == policy_name:
138 break
139 else:
140 # Ensure that there are actual policies stored
141 if self.policies:
142 return InvalidHeaderValue('x-storage-policy',
143 policy_name)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900144 elif self.target == 'Object':
145 if 'etag' not in actual:
146 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +0900147 if 'last-modified' not in actual:
148 return NonExistentHeader('last-modified')
149 elif self.method == 'PUT':
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900150 if self.target == 'Object':
151 if 'etag' not in actual:
152 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +0900153 if 'last-modified' not in actual:
154 return NonExistentHeader('last-modified')
155 elif self.method == 'COPY':
156 if self.target == 'Object':
157 if 'etag' not in actual:
158 return NonExistentHeader('etag')
159 if 'last-modified' not in actual:
160 return NonExistentHeader('last-modified')
161 if 'x-copied-from' not in actual:
162 return NonExistentHeader('x-copied-from')
163 if 'x-copied-from-last-modified' not in actual:
164 return NonExistentHeader('x-copied-from-last-modified')
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900165
166 return None
167
168
Brian Obera212c4a2016-04-16 18:30:12 -0500169class GenericError(object):
170 """Informs an error message of a generic error during header evaluation"""
171
172 def __init__(self, body):
173 self.body = body
174
175 def describe(self):
176 return "%s" % self.body
177
178 def get_details(self):
179 return {}
180
181
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900182class NonExistentHeader(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000183 """Informs an error message in the case of missing a certain header"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900184
185 def __init__(self, header):
186 self.header = header
187
188 def describe(self):
189 return "%s header does not exist" % self.header
190
191 def get_details(self):
192 return {}
193
194
Brian Obera212c4a2016-04-16 18:30:12 -0500195class InvalidHeaderValue(object):
196 """Informs an error message when a header contains a bad value"""
197
198 def __init__(self, header, value):
199 self.header = header
200 self.value = value
201
202 def describe(self):
203 return "InvalidValue (%s, %s)" % (self.header, self.value)
204
205 def get_details(self):
206 return {}
207
208
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900209class AreAllWellFormatted(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000210 """Specific matcher to check the correctness of formats of values
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900211
212 This matcher checks the format of values of response headers.
213 When checking the format of values of 'specific' headers such as
214 X-Account-Meta-* or X-Object-Manifest for example, those values must be
215 checked in each test code.
216 """
217
218 def match(self, actual):
guo yunxian7bbbec12016-08-21 20:03:10 +0800219 for key, value in actual.items():
Abhishek Chandaf4c97ee2014-12-12 03:14:43 +0530220 if key in ('content-length', 'x-account-bytes-used',
221 'x-account-container-count', 'x-account-object-count',
222 'x-container-bytes-used', 'x-container-object-count')\
223 and not value.isdigit():
224 return InvalidFormat(key, value)
225 elif key in ('content-type', 'date', 'last-modified',
226 'x-copied-from-last-modified') and not value:
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900227 return InvalidFormat(key, value)
Stephen Finucane7f4a6212018-07-06 13:58:21 +0100228 elif key == 'x-timestamp' and not re.match(r"^\d+\.?\d*\Z", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900229 return InvalidFormat(key, value)
Stephen Finucane7f4a6212018-07-06 13:58:21 +0100230 elif key == 'x-copied-from' and not re.match(r"\S+/\S+", value):
Daisuke Morita397f5892014-03-20 14:33:46 +0900231 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900232 elif key == 'x-trans-id' and \
Christian Schwede44dcb302013-12-19 07:52:35 +0000233 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900234 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900235 elif key == 'accept-ranges' and not value == 'bytes':
236 return InvalidFormat(key, value)
237 elif key == 'etag' and not value.isalnum():
238 return InvalidFormat(key, value)
Daisuke Morita499bba32013-11-28 18:44:49 +0900239 elif key == 'transfer-encoding' and not value == 'chunked':
240 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900241
242 return None
243
244
245class InvalidFormat(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000246 """Informs an error message if a format of a certain header is invalid"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900247
248 def __init__(self, key, value):
249 self.key = key
250 self.value = value
251
252 def describe(self):
253 return "InvalidFormat (%s, %s)" % (self.key, self.value)
254
255 def get_details(self):
256 return {}
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100257
258
259class MatchesDictExceptForKeys(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000260 """Matches two dictionaries.
261
262 Verifies all items are equals except for those identified by a list of keys
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100263 """
264
265 def __init__(self, expected, excluded_keys=None):
266 self.expected = expected
267 self.excluded_keys = excluded_keys if excluded_keys is not None else []
268
269 def match(self, actual):
270 filtered_expected = helpers.dict_subtract(self.expected,
271 self.excluded_keys)
272 filtered_actual = helpers.dict_subtract(actual,
273 self.excluded_keys)
274 if filtered_actual != filtered_expected:
275 return DictMismatch(filtered_expected, filtered_actual)
276
277
278class DictMismatch(object):
279 """Mismatch between two dicts describes deltas"""
280
281 def __init__(self, expected, actual):
282 self.expected = expected
283 self.actual = actual
284 self.intersect = set(self.expected) & set(self.actual)
285 self.symmetric_diff = set(self.expected) ^ set(self.actual)
286
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400287 def _format_dict(self, dict_to_format):
288 # Ensure the error string dict is printed in a set order
289 # NOTE(mtreinish): needed to ensure a deterministic error msg for
290 # testing. Otherwise the error message will be dependent on the
291 # dict ordering.
292 dict_string = "{"
293 for key in sorted(dict_to_format):
294 dict_string += "'%s': %s, " % (key, dict_to_format[key])
295 dict_string = dict_string[:-2] + '}'
296 return dict_string
297
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100298 def describe(self):
299 msg = ""
300 if self.symmetric_diff:
301 only_expected = helpers.dict_subtract(self.expected, self.actual)
302 only_actual = helpers.dict_subtract(self.actual, self.expected)
303 if only_expected:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400304 msg += "Only in expected:\n %s\n" % self._format_dict(
305 only_expected)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100306 if only_actual:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400307 msg += "Only in actual:\n %s\n" % self._format_dict(
308 only_actual)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100309 diff_set = set(o for o in self.intersect if
310 self.expected[o] != self.actual[o])
311 if diff_set:
312 msg += "Differences:\n"
Martin Pavlasek659e2db2014-09-04 16:43:21 +0200313 for o in diff_set:
314 msg += " %s: expected %s, actual %s\n" % (
315 o, self.expected[o], self.actual[o])
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100316 return msg
317
318 def get_details(self):
319 return {}