blob: 39e3a67c0c41d5a02795ae0e6aa6187eb26a0dcc [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):
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')
Daisuke Morita397f5892014-03-20 14:33:46 +090074 if 'last-modified' not in actual:
75 return NonExistentHeader('last-modified')
76 elif self.method == 'PUT':
Daisuke Morita8e1f8612013-11-26 15:43:21 +090077 if self.target == 'Object':
78 if 'etag' not in actual:
79 return NonExistentHeader('etag')
Daisuke Morita397f5892014-03-20 14:33:46 +090080 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')
Daisuke Morita8e1f8612013-11-26 15:43:21 +090092
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 == 'content-length' and not value.isdigit():
126 return InvalidFormat(key, value)
127 elif key == 'x-timestamp' and not re.match("^\d+\.?\d*\Z", value):
128 return InvalidFormat(key, value)
129 elif key == 'x-account-bytes-used' and not value.isdigit():
130 return InvalidFormat(key, value)
131 elif key == 'x-account-container-count' and not value.isdigit():
132 return InvalidFormat(key, value)
133 elif key == 'x-account-object-count' and not value.isdigit():
134 return InvalidFormat(key, value)
135 elif key == 'x-container-bytes-used' and not value.isdigit():
136 return InvalidFormat(key, value)
137 elif key == 'x-container-object-count' and not value.isdigit():
138 return InvalidFormat(key, value)
139 elif key == 'content-type' and not value:
140 return InvalidFormat(key, value)
Daisuke Morita397f5892014-03-20 14:33:46 +0900141 elif key == 'x-copied-from' and not re.match("\S+/\S+", value):
142 return InvalidFormat(key, value)
143 elif key == 'x-copied-from-last-modified' and not value:
144 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900145 elif key == 'x-trans-id' and \
Christian Schwede44dcb302013-12-19 07:52:35 +0000146 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900147 return InvalidFormat(key, value)
148 elif key == 'date' and not value:
149 return InvalidFormat(key, value)
Daisuke Morita397f5892014-03-20 14:33:46 +0900150 elif key == 'last-modified' and not value:
151 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900152 elif key == 'accept-ranges' and not value == 'bytes':
153 return InvalidFormat(key, value)
154 elif key == 'etag' and not value.isalnum():
155 return InvalidFormat(key, value)
Daisuke Morita499bba32013-11-28 18:44:49 +0900156 elif key == 'transfer-encoding' and not value == 'chunked':
157 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900158
159 return None
160
161
162class InvalidFormat(object):
163 """
164 Informs an error message for end users if a format of a certain header
165 is invalid
166 """
167
168 def __init__(self, key, value):
169 self.key = key
170 self.value = value
171
172 def describe(self):
173 return "InvalidFormat (%s, %s)" % (self.key, self.value)
174
175 def get_details(self):
176 return {}
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100177
178
179class MatchesDictExceptForKeys(object):
180 """Matches two dictionaries. Verifies all items are equals except for those
181 identified by a list of keys.
182 """
183
184 def __init__(self, expected, excluded_keys=None):
185 self.expected = expected
186 self.excluded_keys = excluded_keys if excluded_keys is not None else []
187
188 def match(self, actual):
189 filtered_expected = helpers.dict_subtract(self.expected,
190 self.excluded_keys)
191 filtered_actual = helpers.dict_subtract(actual,
192 self.excluded_keys)
193 if filtered_actual != filtered_expected:
194 return DictMismatch(filtered_expected, filtered_actual)
195
196
197class DictMismatch(object):
198 """Mismatch between two dicts describes deltas"""
199
200 def __init__(self, expected, actual):
201 self.expected = expected
202 self.actual = actual
203 self.intersect = set(self.expected) & set(self.actual)
204 self.symmetric_diff = set(self.expected) ^ set(self.actual)
205
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400206 def _format_dict(self, dict_to_format):
207 # Ensure the error string dict is printed in a set order
208 # NOTE(mtreinish): needed to ensure a deterministic error msg for
209 # testing. Otherwise the error message will be dependent on the
210 # dict ordering.
211 dict_string = "{"
212 for key in sorted(dict_to_format):
213 dict_string += "'%s': %s, " % (key, dict_to_format[key])
214 dict_string = dict_string[:-2] + '}'
215 return dict_string
216
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100217 def describe(self):
218 msg = ""
219 if self.symmetric_diff:
220 only_expected = helpers.dict_subtract(self.expected, self.actual)
221 only_actual = helpers.dict_subtract(self.actual, self.expected)
222 if only_expected:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400223 msg += "Only in expected:\n %s\n" % self._format_dict(
224 only_expected)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100225 if only_actual:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400226 msg += "Only in actual:\n %s\n" % self._format_dict(
227 only_actual)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100228 diff_set = set(o for o in self.intersect if
229 self.expected[o] != self.actual[o])
230 if diff_set:
231 msg += "Differences:\n"
Martin Pavlasek659e2db2014-09-04 16:43:21 +0200232 for o in diff_set:
233 msg += " %s: expected %s, actual %s\n" % (
234 o, self.expected[o], self.actual[o])
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100235 return msg
236
237 def get_details(self):
238 return {}