[CalendarServer-changes] [10133] CalDAVTester/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri Dec 7 09:50:06 PST 2012
Revision: 10133
http://trac.calendarserver.org//changeset/10133
Author: cdaboo at apple.com
Date: 2012-12-07 09:50:06 -0800 (Fri, 07 Dec 2012)
Log Message:
-----------
JSON pointer based verifier.
Added Paths:
-----------
CalDAVTester/trunk/src/jsonPointer.py
CalDAVTester/trunk/src/unittest/
CalDAVTester/trunk/src/unittest/__init__.py
CalDAVTester/trunk/src/unittest/test_jsonPointer.py
CalDAVTester/trunk/verifiers/jsonPointerMatch.py
Added: CalDAVTester/trunk/src/jsonPointer.py
===================================================================
--- CalDAVTester/trunk/src/jsonPointer.py (rev 0)
+++ CalDAVTester/trunk/src/jsonPointer.py 2012-12-07 17:50:06 UTC (rev 10133)
@@ -0,0 +1,171 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import json
+
+
+class JSONPointerMatchError(Exception):
+ """
+ Exception for failed pointer matches
+ """
+ pass
+
+
+
+class JSONPointer(object):
+ """
+ Represents a JSON Pointer that can match a specific JSON object.
+ """
+
+ def __init__(self, pointer):
+
+ if not pointer or pointer[0] != "/":
+ raise ValueError("Invalid JSON pointer: %s" % (pointer,))
+ self.segments = self._splitSegments(pointer)
+
+
+ def _splitSegments(self, pointer):
+ """
+ Split a pointer up into segments.
+
+ @param pointer: the pointer
+ @type pointer: C{str}
+
+ @return: list of segments
+ @rtype C{list}
+ """
+ splits = pointer[1:].split("/")
+ if splits == [""]:
+ return None
+ if any(map(lambda x: len(x) == 0, splits)):
+ raise TypeError("Pointer segment is empty: %s" % (pointer,))
+ return map(self._unescape, splits)
+
+
+ def _unescape(self, segment):
+ """
+ Unescape ~0 and ~1 in a path segment.
+
+ @param segment: the segment to unescape
+ @type segment: C{str}
+
+ @return: the unescaped segment
+ @rtype: C{str}
+ """
+ return segment.replace("~1", "/").replace("~0", "~")
+
+
+ def matchs(self, s):
+ """
+ Match this pointer against the string representation of a JSON object.
+
+ @param s: a string representation of a JSON object
+ @type s: C{str}
+ """
+
+ return self.match(json.loads(s))
+
+
+ def match(self, j):
+ """
+ Match this pointer against the JSON object.
+
+ @param j: a JSON object
+ @type j: C{dict} or C{list}
+ """
+
+ try:
+ return self.walk(j, self.segments)
+ except Exception:
+ raise JSONPointerMatchError
+
+
+ def walk(self, j, segments):
+ """
+ Recursively match the next portion of a pointer segment.
+
+ @param j: JSON object to match
+ @type j: C{dict} or C{list}
+ @param segments: list of pointer segments
+ @type segments: C{list}
+ """
+
+ if not segments:
+ return j
+
+ if isinstance(j, dict):
+ return self.walk(j[segments[0]], segments[1:])
+ elif isinstance(j, list):
+ index = -1 if segments[0] == "-" else int(segments[0])
+ return self.walk(j[index], segments[1:])
+ else:
+ raise JSONPointerMatchError
+
+
+
+class JSONMatcher(JSONPointer):
+ """
+ Represents a JSON pointer with syntax allowing a match against multiple JSON objects. If any
+ segment of a path is a single ".", then all object or array members are matched. The result of
+ the match is the array of objects that match. Missing keys and index past the end are ignored.
+ """
+
+ def __init__(self, pointer):
+
+ if not pointer or pointer[0] != "/":
+ raise ValueError("Invalid JSON pointer: %s" % (pointer,))
+ self.segments = self._splitSegments(pointer)
+
+
+ def walk(self, j, segments):
+ """
+ Recursively match the next portion of a pointer segment, talking wildcard "."
+ segment matching into account.
+
+ @param j: JSON object to match
+ @type j: C{dict} or C{list}
+ @param segments: list of pointer segments
+ @type segments: C{list}
+ """
+
+ if not segments:
+ return [j, ]
+
+ results = []
+ if isinstance(j, dict):
+ if segments[0] == ".":
+ keys = j.keys()
+ else:
+ keys = [segments[0]]
+ for k in keys:
+ try:
+ results.extend(self.walk(j[k], segments[1:]))
+ except KeyError:
+ pass
+ elif isinstance(j, list):
+ if segments[0] == ".":
+ r = range(len(j))
+ else:
+ r = [-1 if segments[0] == "-" else int(segments[0])]
+ for index in r:
+ try:
+ results.extend(self.walk(j[index], segments[1:]))
+ except IndexError:
+ pass
+ else:
+ raise JSONPointerMatchError
+
+ return results
Added: CalDAVTester/trunk/src/unittest/__init__.py
===================================================================
--- CalDAVTester/trunk/src/unittest/__init__.py (rev 0)
+++ CalDAVTester/trunk/src/unittest/__init__.py 2012-12-07 17:50:06 UTC (rev 10133)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
Added: CalDAVTester/trunk/src/unittest/test_jsonPointer.py
===================================================================
--- CalDAVTester/trunk/src/unittest/test_jsonPointer.py (rev 0)
+++ CalDAVTester/trunk/src/unittest/test_jsonPointer.py 2012-12-07 17:50:06 UTC (rev 10133)
@@ -0,0 +1,260 @@
+##
+# Copyright (c) 2012 Cyrus Daboo. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import unittest
+from src.jsonPointer import JSONPointer, JSONPointerMatchError, JSONMatcher
+
+class TestJSONPointer(unittest.TestCase):
+
+ def testValidPointers(self):
+ data = (
+ (None, False),
+ ("", False),
+ (1, False),
+ ("/", True),
+ ("//", False),
+ ("/abc", True),
+ ("/abc/", False),
+ )
+
+ for pointer, result in data:
+ try:
+ JSONPointer(pointer)
+ ok = True
+ except (ValueError, TypeError):
+ ok = False
+ self.assertEqual(ok, result, "Failed test: %s" % (pointer,))
+
+
+ def testUnescape(self):
+ data = (
+ ("/", None),
+ ("/~0", ["~", ]),
+ ("/abc~1def", ["abc/def", ]),
+ ("/abc/~0~1a", ["abc", "~/a", ]),
+ ("/~0ab~1c/~0~1a", ["~ab/c", "~/a", ]),
+ )
+
+ for pointer, result in data:
+ j = JSONPointer(pointer)
+ self.assertEqual(j.segments, result, "Failed test: %s" % (pointer,))
+
+
+ def testMatchOK(self):
+ data = (
+ # Objects
+ ("/", '{"1": "foo"}', {"1": "foo"}),
+ ("/1", '{"1": "foo", "2": "bar"}', "foo"),
+ ("/2", '{"1": "foo", "2": "bar"}', "bar"),
+ ("/1", '{"1": {"1.1": "foo"}}', {"1.1": "foo"}),
+ ("/1/1.1", '{"1": {"1.1": "foo", "1.2": "bar"}}', "foo"),
+ ("/1/1.2", '{"1": {"1.1": "foo", "1.2": "bar"}}', "bar"),
+
+ # Arrays
+ ("/", '["1", "2"]', ["1", "2"]),
+ ("/0", '["1", "2"]', "1"),
+ ("/1", '["1", "2"]', "2"),
+ ("/-", '["1", "2"]', "2"),
+ ("/0", '[["1", "2"]]', ["1", "2"]),
+ ("/-", '[["1", "2"]]', ["1", "2"]),
+ ("/0/0", '[["1", "2"]]', "1"),
+ ("/0/1", '[["1", "2"]]', "2"),
+ ("/0/-", '[["1", "2"]]', "2"),
+
+ # Both
+ ("/", '{"1": ["foo", "bar"]}', {"1": ["foo", "bar"]}),
+ ("/1", '{"1": ["foo", "bar"]}', ["foo", "bar"]),
+ ("/1/0", '{"1": ["foo", "bar"]}', "foo"),
+ ("/1/1", '{"1": ["foo", "bar"]}', "bar"),
+ ("/1/-", '{"1": ["foo", "bar"]}', "bar"),
+ ("/", '[{"1.1": "foo", "1.2": "bar"}]', [{"1.1": "foo", "1.2": "bar"}]),
+ ("/0", '[{"1.1": "foo", "1.2": "bar"}]', {"1.1": "foo", "1.2": "bar"}),
+ ("/-", '[{"1.1": "foo", "1.2": "bar"}]', {"1.1": "foo", "1.2": "bar"}),
+ ("/0/1.1", '[{"1.1": "foo", "1.2": "bar"}]', "foo"),
+ ("/0/1.2", '[{"1.1": "foo", "1.2": "bar"}]', "bar"),
+ )
+
+ for pointer, jobj, result in data:
+ j = JSONPointer(pointer)
+ self.assertEqual(j.matchs(jobj), result, "Failed test: %s" % (pointer,))
+
+
+ def testMatchBad(self):
+ data = (
+ # Objects
+ ("/3", '{"1": "foo", "2": "bar"}'),
+ ("/a", '{"1": "foo", "2": "bar"}'),
+ ("/-", '{"1": "foo", "2": "bar"}'),
+ ("/1/3", '{"1": {"1.1": "foo", "1.2": "bar"}}'),
+ ("/1/a", '{"1": {"1.1": "foo", "1.2": "bar"}}'),
+ ("/1/-", '{"1": {"1.1": "foo", "1.2": "bar"}}'),
+
+ # Arrays
+ ("/2", '["1", "2"]'),
+ ("/0/2", '[["1", "2"]]'),
+
+ # Both
+ ("/1/3", '{"1": ["foo", "bar"]}'),
+ ("/0/3", '[{"1.1": "foo", "1.2": "bar"}]'),
+ ("/0/a", '[{"1.1": "foo", "1.2": "bar"}]'),
+ ("/0/-", '[{"1.1": "foo", "1.2": "bar"}]'),
+
+ # Wrong Object
+ ("/1/0", '{"1": "foo"}'),
+ ("/1/0", '{"1": 1}'),
+ ("/1/0", '{"1": true}'),
+ ("/1/0", '{"1": null}'),
+ )
+
+ for pointer, jobj in data:
+ j = JSONPointer(pointer)
+ self.assertRaises(JSONPointerMatchError, j.matchs, jobj)
+
+
+
+class TestJSONMatcher(unittest.TestCase):
+
+ def testMatchOK(self):
+ data = (
+ # Objects
+ ("/", '{"1": "foo"}', [{"1": "foo"}]),
+ ("/1", '{"1": "foo", "2": "bar"}', ["foo"]),
+ ("/2", '{"1": "foo", "2": "bar"}', ["bar"]),
+ ("/1", '{"1": {"1.1": "foo"}}', [{"1.1": "foo"}]),
+ ("/1/1.1", '{"1": {"1.1": "foo", "1.2": "bar"}}', ["foo"]),
+ ("/1/1.2", '{"1": {"1.1": "foo", "1.2": "bar"}}', ["bar"]),
+
+ # Arrays
+ ("/", '["1", "2"]', [["1", "2"]]),
+ ("/0", '["1", "2"]', ["1"]),
+ ("/1", '["1", "2"]', ["2"]),
+ ("/-", '["1", "2"]', ["2"]),
+ ("/0", '[["1", "2"]]', [["1", "2"]]),
+ ("/-", '[["1", "2"]]', [["1", "2"]]),
+ ("/0/0", '[["1", "2"]]', ["1"]),
+ ("/0/1", '[["1", "2"]]', ["2"]),
+ ("/0/-", '[["1", "2"]]', ["2"]),
+
+ # Both
+ ("/", '{"1": ["foo", "bar"]}', [{"1": ["foo", "bar"]}]),
+ ("/1", '{"1": ["foo", "bar"]}', [["foo", "bar"]]),
+ ("/1/0", '{"1": ["foo", "bar"]}', ["foo"]),
+ ("/1/1", '{"1": ["foo", "bar"]}', ["bar"]),
+ ("/1/-", '{"1": ["foo", "bar"]}', ["bar"]),
+ ("/", '[{"1.1": "foo", "1.2": "bar"}]', [[{"1.1": "foo", "1.2": "bar"}]]),
+ ("/0", '[{"1.1": "foo", "1.2": "bar"}]', [{"1.1": "foo", "1.2": "bar"}]),
+ ("/-", '[{"1.1": "foo", "1.2": "bar"}]', [{"1.1": "foo", "1.2": "bar"}]),
+ ("/0/1.1", '[{"1.1": "foo", "1.2": "bar"}]', ["foo"]),
+ ("/0/1.2", '[{"1.1": "foo", "1.2": "bar"}]', ["bar"]),
+ )
+
+ for pointer, jobj, result in data:
+ j = JSONMatcher(pointer)
+ self.assertEqual(j.matchs(jobj), result, "Failed test: %s" % (pointer,))
+
+
+ def testMatchBad(self):
+ data = (
+ # Objects
+ ("/3", '{"1": "foo", "2": "bar"}', False),
+ ("/a", '{"1": "foo", "2": "bar"}', False),
+ ("/-", '{"1": "foo", "2": "bar"}', False),
+ ("/1/3", '{"1": {"1.1": "foo", "1.2": "bar"}}', False),
+ ("/1/a", '{"1": {"1.1": "foo", "1.2": "bar"}}', False),
+ ("/1/-", '{"1": {"1.1": "foo", "1.2": "bar"}}', False),
+
+ # Arrays
+ ("/2", '["1", "2"]', False),
+ ("/0/2", '[["1", "2"]]', False),
+
+ # Both
+ ("/1/3", '{"1": ["foo", "bar"]}', False),
+ ("/0/3", '[{"1.1": "foo", "1.2": "bar"}]', False),
+ ("/0/a", '[{"1.1": "foo", "1.2": "bar"}]', False),
+ ("/0/-", '[{"1.1": "foo", "1.2": "bar"}]', False),
+
+ # Wrong Object
+ ("/1/0", '{"1": "foo"}', True),
+ ("/1/0", '{"1": 1}', True),
+ ("/1/0", '{"1": true}', True),
+ ("/1/0", '{"1": null}', True),
+ )
+
+ for pointer, jobj, willRaise in data:
+ j = JSONMatcher(pointer)
+ if willRaise:
+ self.assertRaises(JSONPointerMatchError, j.matchs, jobj)
+ else:
+ self.assertEqual(j.matchs(jobj), [], "Failed test: %s" % (pointer,))
+
+
+ def testMatchingOK(self):
+ data = (
+ (
+ "/",
+ '{"1":"foo", "2": "bar"}',
+ [{"1":"foo", "2": "bar"}, ],
+ ),
+ (
+ "/.",
+ '{"1":"foo", "2": "bar"}',
+ ["foo", "bar", ],
+ ),
+ (
+ "/./0",
+ '{"1":["foo1", "foo2"], "2": ["bar1", "bar2"]}',
+ ["foo1", "bar1", ],
+ ),
+ (
+ "/./1",
+ '{"1":["foo1", "foo2"], "2": ["bar1", "bar2"]}',
+ ["foo2", "bar2", ],
+ ),
+ (
+ "/./-",
+ '{"1":["foo1", "foo2"], "2": ["bar1", "bar2"]}',
+ ["foo2", "bar2", ],
+ ),
+ (
+ "/./2",
+ '{"1":["foo1", "foo2"], "2": ["bar1", "bar2"]}',
+ [],
+ ),
+ (
+ "/./foo1",
+ '{"1":{"foo1": "bar1", "foo2": "bar2"}, "2": {"foo1": "bar3", "foo4": "bar4"}}',
+ ["bar1", "bar3", ],
+ ),
+ (
+ "/./foo2",
+ '{"1":{"foo1": "bar1", "foo2": "bar2"}, "2": {"foo1": "bar3", "foo4": "bar4"}}',
+ ["bar2", ],
+ ),
+ (
+ "/./foo4",
+ '{"1":{"foo1": "bar1", "foo2": "bar2"}, "2": {"foo1": "bar3", "foo4": "bar4"}}',
+ ["bar4", ],
+ ),
+ (
+ "/./foo3",
+ '{"1":{"foo1": "bar1", "foo2": "bar2"}, "2": {"foo1": "bar3", "foo4": "bar4"}}',
+ [],
+ ),
+ )
+
+ for pointer, jobj, result in data:
+ j = JSONMatcher(pointer)
+ self.assertEqual(j.matchs(jobj), result, "Failed test: %s" % (pointer,))
Added: CalDAVTester/trunk/verifiers/jsonPointerMatch.py
===================================================================
--- CalDAVTester/trunk/verifiers/jsonPointerMatch.py (rev 0)
+++ CalDAVTester/trunk/verifiers/jsonPointerMatch.py 2012-12-07 17:50:06 UTC (rev 10133)
@@ -0,0 +1,103 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+"""
+Verifier that matches JSON content using extended JSON pointer syntax.
+
+JSON pointer syntax is extended as follows:
+
+1) A ~$xxx at the end will result in a test for the string "xxx" in the matching JSON object.
+2) A "." as a path segment will match any JSON object member or array item.
+"""
+
+import json
+from src.jsonPointer import JSONMatcher, JSONPointerMatchError
+
+class Verifier(object):
+
+ def verify(self, manager, uri, response, respdata, args): #@UnusedVariable
+ # Get arguments
+ statusCodes = args.get("status", ["200", ])
+ exists = args.get("exists", [])
+ notexists = args.get("notexists", [])
+
+ # status code must match
+ if str(response.status) not in statusCodes:
+ return False, " HTTP Status Code Wrong: %d" % (response.status,)
+
+ # look for response data
+ if not respdata:
+ return False, " No response body"
+
+ # Must be application/json
+ ct = response.msg.getheaders("content-type")
+ if ct[0] != "application/json":
+ return False, " Wrong Content-Type: %s" % (ct,)
+
+ # Read in json
+ try:
+ j = json.loads(respdata)
+ except Exception, e:
+ return False, " Response data is not JSON data: %s" % (e,)
+
+ def _splitPathTests(path):
+ if '[' in path:
+ return path.split('[', 1)
+ else:
+ return path, None
+
+ result = True
+ resulttxt = ""
+ for jpath in exists:
+ if jpath.find("~$") != -1:
+ path, value = jpath.split("~$")
+ else:
+ path, value = jpath, None
+ try:
+ jp = JSONMatcher(path)
+ except Exception:
+ result = False
+ resulttxt += " Invalid JSON pointer for %s\n" % (path,)
+ else:
+ try:
+ jobjs = jp.match(j)
+ if not jobjs:
+ result = False
+ resulttxt += " Items not returned in JSON for %s\n" % (path,)
+ if value and value not in map(str, jobjs):
+ result = False
+ resulttxt += " Item values not returned in JSON for %s\n" % (jpath,)
+ except JSONPointerMatchError:
+ result = False
+ resulttxt += " Items not returned in JSON for %s\n" % (path,)
+
+ for jpath in notexists:
+ try:
+ jp = JSONMatcher(jpath)
+ except Exception:
+ result = False
+ resulttxt += " Invalid JSON pointer for %s\n" % (jpath,)
+ else:
+ try:
+ jp.match(j)
+ except JSONPointerMatchError:
+ pass
+ else:
+ resulttxt += " Items returned in JSON for %s\n" % (jpath,)
+ result = False
+
+ return result, resulttxt
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121207/544b2fa6/attachment-0001.html>
More information about the calendarserver-changes
mailing list