blob: 298a94ec2de00e676b1b6b042631da47837a7ad9 [file] [log] [blame]
Maru Newbyb096d9f2015-03-09 18:54:54 +00001# 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
16
17from testtools import helpers
18
19
20class ExistsAllResponseHeaders(object):
21 """
22 Specific matcher to check the existence of Swift's response headers
23
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):
32 """
33 param: target Account/Container/Object
34 param: method PUT/GET/HEAD/DELETE/COPY/POST
35 """
36 self.target = target
37 self.method = method
38
39 def match(self, actual):
40 """
41 param: actual HTTP response headers
42 """
43 # Check common headers for all HTTP methods
44 if 'content-length' not in actual:
45 return NonExistentHeader('content-length')
46 if 'content-type' not in actual:
47 return NonExistentHeader('content-type')
48 if 'x-trans-id' not in actual:
49 return NonExistentHeader('x-trans-id')
50 if 'date' not in actual:
51 return NonExistentHeader('date')
52
53 # Check headers for a specific method or target
54 if self.method == 'GET' or self.method == 'HEAD':
55 if 'x-timestamp' not in actual:
56 return NonExistentHeader('x-timestamp')
57 if 'accept-ranges' not in actual:
58 return NonExistentHeader('accept-ranges')
59 if self.target == 'Account':
60 if 'x-account-bytes-used' not in actual:
61 return NonExistentHeader('x-account-bytes-used')
62 if 'x-account-container-count' not in actual:
63 return NonExistentHeader('x-account-container-count')
64 if 'x-account-object-count' not in actual:
65 return NonExistentHeader('x-account-object-count')
66 elif self.target == 'Container':
67 if 'x-container-bytes-used' not in actual:
68 return NonExistentHeader('x-container-bytes-used')
69 if 'x-container-object-count' not in actual:
70 return NonExistentHeader('x-container-object-count')
71 elif self.target == 'Object':
72 if 'etag' not in actual:
73 return NonExistentHeader('etag')
74 if 'last-modified' not in actual:
75 return NonExistentHeader('last-modified')
76 elif self.method == 'PUT':
77 if self.target == 'Object':
78 if 'etag' not in actual:
79 return NonExistentHeader('etag')
80 if 'last-modified' not in actual:
81 return NonExistentHeader('last-modified')
82 elif self.method == 'COPY':
83 if self.target == 'Object':
84 if 'etag' not in actual:
85 return NonExistentHeader('etag')
86 if 'last-modified' not in actual:
87 return NonExistentHeader('last-modified')
88 if 'x-copied-from' not in actual:
89 return NonExistentHeader('x-copied-from')
90 if 'x-copied-from-last-modified' not in actual:
91 return NonExistentHeader('x-copied-from-last-modified')
92
93 return None
94
95
96class NonExistentHeader(object):
97 """
98 Informs an error message for end users in the case of missing a
99 certain header in Swift's responses
100 """
101
102 def __init__(self, header):
103 self.header = header
104
105 def describe(self):
106 return "%s header does not exist" % self.header
107
108 def get_details(self):
109 return {}
110
111
112class AreAllWellFormatted(object):
113 """
114 Specific matcher to check the correctness of formats of values of Swift's
115 response headers
116
117 This matcher checks the format of values of response headers.
118 When checking the format of values of 'specific' headers such as
119 X-Account-Meta-* or X-Object-Manifest for example, those values must be
120 checked in each test code.
121 """
122
123 def match(self, actual):
124 for key, value in actual.iteritems():
125 if key in ('content-length', 'x-account-bytes-used',
126 'x-account-container-count', 'x-account-object-count',
127 'x-container-bytes-used', 'x-container-object-count')\
128 and not value.isdigit():
129 return InvalidFormat(key, value)
130 elif key in ('content-type', 'date', 'last-modified',
131 'x-copied-from-last-modified') and not value:
132 return InvalidFormat(key, value)
133 elif key == 'x-timestamp' and not re.match("^\d+\.?\d*\Z", value):
134 return InvalidFormat(key, value)
135 elif key == 'x-copied-from' and not re.match("\S+/\S+", value):
136 return InvalidFormat(key, value)
137 elif key == 'x-trans-id' and \
138 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
139 return InvalidFormat(key, value)
140 elif key == 'accept-ranges' and not value == 'bytes':
141 return InvalidFormat(key, value)
142 elif key == 'etag' and not value.isalnum():
143 return InvalidFormat(key, value)
144 elif key == 'transfer-encoding' and not value == 'chunked':
145 return InvalidFormat(key, value)
146
147 return None
148
149
150class InvalidFormat(object):
151 """
152 Informs an error message for end users if a format of a certain header
153 is invalid
154 """
155
156 def __init__(self, key, value):
157 self.key = key
158 self.value = value
159
160 def describe(self):
161 return "InvalidFormat (%s, %s)" % (self.key, self.value)
162
163 def get_details(self):
164 return {}
165
166
167class MatchesDictExceptForKeys(object):
168 """Matches two dictionaries. Verifies all items are equals except for those
169 identified by a list of keys.
170 """
171
172 def __init__(self, expected, excluded_keys=None):
173 self.expected = expected
174 self.excluded_keys = excluded_keys if excluded_keys is not None else []
175
176 def match(self, actual):
177 filtered_expected = helpers.dict_subtract(self.expected,
178 self.excluded_keys)
179 filtered_actual = helpers.dict_subtract(actual,
180 self.excluded_keys)
181 if filtered_actual != filtered_expected:
182 return DictMismatch(filtered_expected, filtered_actual)
183
184
185class DictMismatch(object):
186 """Mismatch between two dicts describes deltas"""
187
188 def __init__(self, expected, actual):
189 self.expected = expected
190 self.actual = actual
191 self.intersect = set(self.expected) & set(self.actual)
192 self.symmetric_diff = set(self.expected) ^ set(self.actual)
193
194 def _format_dict(self, dict_to_format):
195 # Ensure the error string dict is printed in a set order
196 # NOTE(mtreinish): needed to ensure a deterministic error msg for
197 # testing. Otherwise the error message will be dependent on the
198 # dict ordering.
199 dict_string = "{"
200 for key in sorted(dict_to_format):
201 dict_string += "'%s': %s, " % (key, dict_to_format[key])
202 dict_string = dict_string[:-2] + '}'
203 return dict_string
204
205 def describe(self):
206 msg = ""
207 if self.symmetric_diff:
208 only_expected = helpers.dict_subtract(self.expected, self.actual)
209 only_actual = helpers.dict_subtract(self.actual, self.expected)
210 if only_expected:
211 msg += "Only in expected:\n %s\n" % self._format_dict(
212 only_expected)
213 if only_actual:
214 msg += "Only in actual:\n %s\n" % self._format_dict(
215 only_actual)
216 diff_set = set(o for o in self.intersect if
217 self.expected[o] != self.actual[o])
218 if diff_set:
219 msg += "Differences:\n"
220 for o in diff_set:
221 msg += " %s: expected %s, actual %s\n" % (
222 o, self.expected[o], self.actual[o])
223 return msg
224
225 def get_details(self):
226 return {}