blob: 998612bd45d135cf954941975f78abd9e9c54c71 [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
Matthew Treinish71426682015-04-23 11:19:38 -040017import six
Andrea Frittolib6e1a282014-08-05 20:08:27 +010018from testtools import helpers
Daisuke Morita8e1f8612013-11-26 15:43:21 +090019
20
21class ExistsAllResponseHeaders(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +000022 """Specific matcher to check the existence of Swift's response headers
Daisuke Morita8e1f8612013-11-26 15:43:21 +090023
24 This matcher checks the existence of common headers for each HTTP method
25 or the target, which means account, container or object.
26 When checking the existence of 'specific' headers such as
27 X-Account-Meta-* or X-Object-Manifest for example, those headers must be
28 checked in each test code.
29 """
30
31 def __init__(self, target, method):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +000032 """Initialization of ExistsAllResponseHeaders
33
Daisuke Morita8e1f8612013-11-26 15:43:21 +090034 param: target Account/Container/Object
35 param: method PUT/GET/HEAD/DELETE/COPY/POST
36 """
37 self.target = target
38 self.method = method
39
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')
87 elif self.target == 'Container':
88 if 'x-container-bytes-used' not in actual:
89 return NonExistentHeader('x-container-bytes-used')
90 if 'x-container-object-count' not in actual:
91 return NonExistentHeader('x-container-object-count')
92 elif self.target == 'Object':
93 if 'etag' not in actual:
94 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +090095 if 'last-modified' not in actual:
96 return NonExistentHeader('last-modified')
97 elif self.method == 'PUT':
Daisuke Morita8e1f8612013-11-26 15:43:21 +090098 if self.target == 'Object':
99 if 'etag' not in actual:
100 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +0900101 if 'last-modified' not in actual:
102 return NonExistentHeader('last-modified')
103 elif self.method == 'COPY':
104 if self.target == 'Object':
105 if 'etag' not in actual:
106 return NonExistentHeader('etag')
107 if 'last-modified' not in actual:
108 return NonExistentHeader('last-modified')
109 if 'x-copied-from' not in actual:
110 return NonExistentHeader('x-copied-from')
111 if 'x-copied-from-last-modified' not in actual:
112 return NonExistentHeader('x-copied-from-last-modified')
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900113
114 return None
115
116
117class NonExistentHeader(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000118 """Informs an error message in the case of missing a certain header"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900119
120 def __init__(self, header):
121 self.header = header
122
123 def describe(self):
124 return "%s header does not exist" % self.header
125
126 def get_details(self):
127 return {}
128
129
130class AreAllWellFormatted(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000131 """Specific matcher to check the correctness of formats of values
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900132
133 This matcher checks the format of values of response headers.
134 When checking the format of values of 'specific' headers such as
135 X-Account-Meta-* or X-Object-Manifest for example, those values must be
136 checked in each test code.
137 """
138
139 def match(self, actual):
Matthew Treinish71426682015-04-23 11:19:38 -0400140 for key, value in six.iteritems(actual):
Abhishek Chandaf4c97ee2014-12-12 03:14:43 +0530141 if key in ('content-length', 'x-account-bytes-used',
142 'x-account-container-count', 'x-account-object-count',
143 'x-container-bytes-used', 'x-container-object-count')\
144 and not value.isdigit():
145 return InvalidFormat(key, value)
146 elif key in ('content-type', 'date', 'last-modified',
147 'x-copied-from-last-modified') and not value:
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900148 return InvalidFormat(key, value)
149 elif key == 'x-timestamp' and not re.match("^\d+\.?\d*\Z", value):
150 return InvalidFormat(key, value)
Daisuke Morita397f5892014-03-20 14:33:46 +0900151 elif key == 'x-copied-from' and not re.match("\S+/\S+", value):
152 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900153 elif key == 'x-trans-id' and \
Christian Schwede44dcb302013-12-19 07:52:35 +0000154 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900155 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900156 elif key == 'accept-ranges' and not value == 'bytes':
157 return InvalidFormat(key, value)
158 elif key == 'etag' and not value.isalnum():
159 return InvalidFormat(key, value)
Daisuke Morita499bba32013-11-28 18:44:49 +0900160 elif key == 'transfer-encoding' and not value == 'chunked':
161 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900162
163 return None
164
165
166class InvalidFormat(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000167 """Informs an error message if a format of a certain header is invalid"""
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900168
169 def __init__(self, key, value):
170 self.key = key
171 self.value = value
172
173 def describe(self):
174 return "InvalidFormat (%s, %s)" % (self.key, self.value)
175
176 def get_details(self):
177 return {}
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100178
179
180class MatchesDictExceptForKeys(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000181 """Matches two dictionaries.
182
183 Verifies all items are equals except for those identified by a list of keys
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100184 """
185
186 def __init__(self, expected, excluded_keys=None):
187 self.expected = expected
188 self.excluded_keys = excluded_keys if excluded_keys is not None else []
189
190 def match(self, actual):
191 filtered_expected = helpers.dict_subtract(self.expected,
192 self.excluded_keys)
193 filtered_actual = helpers.dict_subtract(actual,
194 self.excluded_keys)
195 if filtered_actual != filtered_expected:
196 return DictMismatch(filtered_expected, filtered_actual)
197
198
199class DictMismatch(object):
200 """Mismatch between two dicts describes deltas"""
201
202 def __init__(self, expected, actual):
203 self.expected = expected
204 self.actual = actual
205 self.intersect = set(self.expected) & set(self.actual)
206 self.symmetric_diff = set(self.expected) ^ set(self.actual)
207
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400208 def _format_dict(self, dict_to_format):
209 # Ensure the error string dict is printed in a set order
210 # NOTE(mtreinish): needed to ensure a deterministic error msg for
211 # testing. Otherwise the error message will be dependent on the
212 # dict ordering.
213 dict_string = "{"
214 for key in sorted(dict_to_format):
215 dict_string += "'%s': %s, " % (key, dict_to_format[key])
216 dict_string = dict_string[:-2] + '}'
217 return dict_string
218
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100219 def describe(self):
220 msg = ""
221 if self.symmetric_diff:
222 only_expected = helpers.dict_subtract(self.expected, self.actual)
223 only_actual = helpers.dict_subtract(self.actual, self.expected)
224 if only_expected:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400225 msg += "Only in expected:\n %s\n" % self._format_dict(
226 only_expected)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100227 if only_actual:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400228 msg += "Only in actual:\n %s\n" % self._format_dict(
229 only_actual)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100230 diff_set = set(o for o in self.intersect if
231 self.expected[o] != self.actual[o])
232 if diff_set:
233 msg += "Differences:\n"
Martin Pavlasek659e2db2014-09-04 16:43:21 +0200234 for o in diff_set:
235 msg += " %s: expected %s, actual %s\n" % (
236 o, self.expected[o], self.actual[o])
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100237 return msg
238
239 def get_details(self):
240 return {}