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