blob: 298a94ec2de00e676b1b6b042631da47837a7ad9 [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():
Abhishek Chandaf4c97ee2014-12-12 03:14:43 +0530125 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:
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900132 return InvalidFormat(key, value)
133 elif key == 'x-timestamp' and not re.match("^\d+\.?\d*\Z", value):
134 return InvalidFormat(key, value)
Daisuke Morita397f5892014-03-20 14:33:46 +0900135 elif key == 'x-copied-from' and not re.match("\S+/\S+", value):
136 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900137 elif key == 'x-trans-id' and \
Christian Schwede44dcb302013-12-19 07:52:35 +0000138 not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900139 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900140 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)
Daisuke Morita499bba32013-11-28 18:44:49 +0900144 elif key == 'transfer-encoding' and not value == 'chunked':
145 return InvalidFormat(key, value)
Daisuke Morita8e1f8612013-11-26 15:43:21 +0900146
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 {}
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100165
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
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400194 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
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100205 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:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400211 msg += "Only in expected:\n %s\n" % self._format_dict(
212 only_expected)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100213 if only_actual:
Matthew Treinish6bbc8742014-08-25 18:28:15 -0400214 msg += "Only in actual:\n %s\n" % self._format_dict(
215 only_actual)
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100216 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"
Martin Pavlasek659e2db2014-09-04 16:43:21 +0200220 for o in diff_set:
221 msg += " %s: expected %s, actual %s\n" % (
222 o, self.expected[o], self.actual[o])
Andrea Frittolib6e1a282014-08-05 20:08:27 +0100223 return msg
224
225 def get_details(self):
226 return {}