Merge pull request #150 from DaveGamble/json-patch-tests

Add json-patch/json-patch-tests for testing JSON patch implementation
This commit is contained in:
Max Bruckner 2017-04-12 20:54:18 +02:00 committed by GitHub
commit b759ff38b8
12 changed files with 1181 additions and 39 deletions

10
cJSON.c
View File

@ -2585,3 +2585,13 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons
return false;
}
}
CJSON_PUBLIC(void *) cJSON_malloc(size_t size)
{
return global_hooks.allocate(size);
}
CJSON_PUBLIC(void) cJSON_free(void *object)
{
global_hooks.deallocate(object);
}

View File

@ -241,6 +241,10 @@ CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
/* Macro for iterating over an array */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif

View File

@ -194,6 +194,46 @@ CJSON_PUBLIC(char *) cJSONUtils_FindPointerFromObjectTo(cJSON *object, cJSON *ta
return NULL;
}
/* non broken version of cJSON_GetArrayItem */
static cJSON *get_array_item(const cJSON *array, size_t item)
{
cJSON *child = array ? array->child : NULL;
while ((child != NULL) && (item > 0))
{
item--;
child = child->next;
}
return child;
}
static cJSON_bool decode_array_index_from_pointer(const unsigned char * const pointer, size_t * const index)
{
size_t parsed_index = 0;
size_t position = 0;
if ((pointer[0] == '0') && ((pointer[1] != '\0') && (pointer[1] != '/')))
{
/* leading zeroes are not permitted */
return 0;
}
for (position = 0; (pointer[position] >= '0') && (*pointer <= '9'); position++)
{
parsed_index = (10 * parsed_index) + (size_t)(pointer[position] - '0');
}
if ((pointer[position] != '\0') && (pointer[position] != '/'))
{
return 0;
}
*index = parsed_index;
return 1;
}
CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON *object, const char *pointer)
{
/* follow path of the pointer */
@ -201,22 +241,13 @@ CJSON_PUBLIC(cJSON *) cJSONUtils_GetPointer(cJSON *object, const char *pointer)
{
if (cJSON_IsArray(object))
{
size_t which = 0;
/* parse array index */
while ((*pointer >= '0') && (*pointer <= '9'))
{
which = (10 * which) + (size_t)(*pointer++ - '0');
}
if (*pointer && (*pointer != '/'))
{
/* not end of string or new path token */
return NULL;
}
if (which > INT_MAX)
size_t index = 0;
if (!decode_array_index_from_pointer((const unsigned char*)pointer, &index))
{
return NULL;
}
object = cJSON_GetArrayItem(object, (int)which);
object = get_array_item(object, index);
}
else if (cJSON_IsObject(object))
{
@ -262,6 +293,39 @@ static void cJSONUtils_InplaceDecodePointerString(unsigned char *string)
*s2 = '\0';
}
/* non-broken cJSON_DetachItemFromArray */
static cJSON *detach_item_from_array(cJSON *array, size_t which)
{
cJSON *c = array->child;
while (c && (which > 0))
{
c = c->next;
which--;
}
if (!c)
{
/* item doesn't exist */
return NULL;
}
if (c->prev)
{
/* not the first element */
c->prev->next = c->next;
}
if (c->next)
{
c->next->prev = c->prev;
}
if (c==array->child)
{
array->child = c->next;
}
/* make sure the detached item doesn't point anywhere anymore */
c->prev = c->next = NULL;
return c;
}
static cJSON *cJSONUtils_PatchDetach(cJSON *object, const unsigned char *path)
{
unsigned char *parentptr = NULL;
@ -294,7 +358,13 @@ static cJSON *cJSONUtils_PatchDetach(cJSON *object, const unsigned char *path)
}
else if (cJSON_IsArray(parent))
{
ret = cJSON_DetachItemFromArray(parent, atoi((char*)childptr));
size_t index = 0;
if (!decode_array_index_from_pointer(childptr, &index))
{
free(parentptr);
return NULL;
}
ret = detach_item_from_array(parent, index);
}
else if (cJSON_IsObject(parent))
{
@ -364,19 +434,59 @@ static int cJSONUtils_Compare(cJSON *a, cJSON *b)
return 0;
}
/* non broken version of cJSON_InsertItemInArray */
static cJSON_bool insert_item_in_array(cJSON *array, size_t which, cJSON *newitem)
{
cJSON *child = array->child;
while (child && (which > 0))
{
child = child->next;
which--;
}
if (which > 0)
{
/* item is after the end of the array */
return 0;
}
if (child == NULL)
{
cJSON_AddItemToArray(array, newitem);
return 1;
}
/* insert into the linked list */
newitem->next = child;
newitem->prev = child->prev;
child->prev = newitem;
/* was it at the beginning */
if (child == array->child)
{
array->child = newitem;
}
else
{
newitem->prev->next = newitem;
}
return 1;
}
enum patch_operation { INVALID, ADD, REMOVE, REPLACE, MOVE, COPY, TEST };
static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
{
cJSON *op = NULL;
cJSON *path = NULL;
cJSON *value = NULL;
cJSON *parent = NULL;
int opcode = 0;
enum patch_operation opcode = INVALID;
unsigned char *parentptr = NULL;
unsigned char *childptr = NULL;
op = cJSON_GetObjectItem(patch, "op");
path = cJSON_GetObjectItem(patch, "path");
if (!op || !path)
if (!cJSON_IsString(op) || !cJSON_IsString(path))
{
/* malformed patch. */
return 2;
@ -385,23 +495,23 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
/* decode operation */
if (!strcmp(op->valuestring, "add"))
{
opcode = 0;
opcode = ADD;
}
else if (!strcmp(op->valuestring, "remove"))
{
opcode = 1;
opcode = REMOVE;
}
else if (!strcmp(op->valuestring, "replace"))
{
opcode = 2;
opcode = REPLACE;
}
else if (!strcmp(op->valuestring, "move"))
{
opcode = 3;
opcode = MOVE;
}
else if (!strcmp(op->valuestring, "copy"))
{
opcode = 4;
opcode = COPY;
}
else if (!strcmp(op->valuestring, "test"))
{
@ -414,20 +524,99 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
return 3;
}
/* Remove/Replace */
if ((opcode == 1) || (opcode == 2))
/* special case for replacing the root */
if (path->valuestring[0] == '\0')
{
if (opcode == REMOVE)
{
/* remove possible children */
if (object->child != NULL)
{
cJSON_Delete(object->child);
}
/* remove other allocated resources */
if (object->string != NULL)
{
cJSON_free(object->string);
}
if (object->valuestring != NULL)
{
cJSON_free(object->valuestring);
}
/* make it invalid */
memset(object, '\0', sizeof(cJSON));
return 0;
}
if ((opcode == REPLACE) || (opcode == ADD))
{
/* remove possible children */
if (object->child != NULL)
{
cJSON_Delete(object->child);
}
/* remove other allocated resources */
if (object->string != NULL)
{
cJSON_free(object->string);
}
if (object->valuestring != NULL)
{
cJSON_free(object->valuestring);
}
value = cJSON_GetObjectItem(patch, "value");
if (value == NULL)
{
/* missing "value" for add/replace. */
return 7;
}
value = cJSON_Duplicate(value, 1);
if (value == NULL)
{
/* out of memory for add/replace. */
return 8;
}
/* the string "value" isn't needed */
if (value->string != NULL)
{
cJSON_free(value->string);
value->string = NULL;
}
/* copy over the value object */
memcpy(object, value, sizeof(cJSON));
/* delete the duplicated value */
cJSON_free(value);
return 0;
}
}
if ((opcode == REMOVE) || (opcode == REPLACE))
{
/* Get rid of old. */
cJSON_Delete(cJSONUtils_PatchDetach(object, (unsigned char*)path->valuestring));
if (opcode == 1)
cJSON *old_item = cJSONUtils_PatchDetach(object, (unsigned char*)path->valuestring);
if (old_item == NULL)
{
/* For Remove, this is job done. */
return 13;
}
cJSON_Delete(old_item);
if (opcode == REMOVE)
{
/* For Remove, this job is done. */
return 0;
}
}
/* Copy/Move uses "from". */
if ((opcode == 3) || (opcode == 4))
if ((opcode == MOVE) || (opcode == COPY))
{
cJSON *from = cJSON_GetObjectItem(patch, "from");
if (!from)
@ -436,14 +625,12 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
return 4;
}
if (opcode == 3)
if (opcode == MOVE)
{
/* move */
value = cJSONUtils_PatchDetach(object, (unsigned char*)from->valuestring);
}
if (opcode == 4)
if (opcode == COPY)
{
/* copy */
value = cJSONUtils_GetPointer(object, from->valuestring);
}
if (!value)
@ -451,7 +638,7 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
/* missing "from" for copy/move. */
return 5;
}
if (opcode == 4)
if (opcode == COPY)
{
value = cJSON_Duplicate(value, 1);
}
@ -505,7 +692,20 @@ static int cJSONUtils_ApplyPatch(cJSON *object, cJSON *patch)
}
else
{
cJSON_InsertItemInArray(parent, atoi((char*)childptr), value);
size_t index = 0;
if (!decode_array_index_from_pointer(childptr, &index))
{
free(parentptr);
cJSON_Delete(value);
return 11;
}
if (!insert_item_in_array(parent, index, value))
{
free(parentptr);
cJSON_Delete(value);
return 10;
}
}
}
else if (cJSON_IsObject(parent))
@ -526,12 +726,7 @@ CJSON_PUBLIC(int) cJSONUtils_ApplyPatches(cJSON *object, cJSON *patches)
{
int err = 0;
if (patches == NULL)
{
return 1;
}
if (cJSON_IsArray(patches))
if (!cJSON_IsArray(patches))
{
/* malformed patches. */
return 1;

View File

@ -75,4 +75,28 @@ if(ENABLE_CJSON_TEST)
endforeach()
add_dependencies(check ${unity_tests})
if (ENABLE_CJSON_UTILS)
#copy test files
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/json-patch-tests")
file(GLOB test_files "json-patch-tests/*")
file(COPY ${test_files} DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/json-patch-tests/")
set (cjson_utils_tests
json_patch_tests)
foreach (cjson_utils_test ${cjson_utils_tests})
add_executable("${cjson_utils_test}" "${cjson_utils_test}.c")
target_link_libraries("${cjson_utils_test}" "${CJSON_LIB}" "${CJSON_UTILS_LIB}" unity test-common)
if(MEMORYCHECK_COMMAND)
add_test(NAME "${cjson_utils_test}"
COMMAND "${MEMORYCHECK_COMMAND}" ${MEMORYCHECK_COMMAND_OPTIONS} "${CMAKE_CURRENT_BINARY_DIR}/${cjson_utils_test}")
else()
add_test(NAME "${cjson_utils_test}"
COMMAND "./${cjson_utils_test}")
endif()
endforeach()
add_dependencies(check ${cjson_utils_tests})
endif()
endif()

View File

@ -0,0 +1,10 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space

4
tests/json-patch-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*~
\#*
!.editorconfig
!.gitignore

View File

@ -0,0 +1,2 @@
.editorconfig
.gitignore

View File

@ -0,0 +1,75 @@
JSON Patch Tests
================
These are test cases for implementations of [IETF JSON Patch (RFC6902)](http://tools.ietf.org/html/rfc6902).
Some implementations can be found at [jsonpatch.com](http://jsonpatch.com).
Test Format
-----------
Each test file is a JSON document that contains an array of test records. A
test record is an object with the following members:
- doc: The JSON document to test against
- patch: The patch(es) to apply
- expected: The expected resulting document, OR
- error: A string describing an expected error
- comment: A string describing the test
- disabled: True if the test should be skipped
All fields except 'doc' and 'patch' are optional. Test records consisting only
of a comment are also OK.
Files
-----
- tests.json: the main test file
- spec_tests.json: tests from the RFC6902 spec
Writing Tests
-------------
All tests should have a descriptive comment. Tests should be as
simple as possible - just what's required to test a specific piece of
behavior. If you want to test interacting behaviors, create tests for
each behavior as well as the interaction.
If an 'error' member is specified, the error text should describe the
error the implementation should raise - *not* what's being tested.
Implementation error strings will vary, but the suggested error should
be easily matched to the implementation error string. Try to avoid
creating error tests that might pass because an incorrect error was
reported.
Please feel free to contribute!
Credits
-------
The seed test set was adapted from Byron Ruth's
[jsonpatch-js](https://github.com/bruth/jsonpatch-js/blob/master/test.js) and
extended by [Mike McCabe](https://github.com/mikemccabe).
License
-------
Copyright 2014 The Authors
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.

View File

@ -0,0 +1,15 @@
{
"name": "json-patch-test-suite",
"version": "1.1.0",
"description": "JSON Patch RFC 6902 test suite",
"repository": "github:json-patch/json-patch-tests",
"homepage": "https://github.com/json-patch/json-patch-tests",
"bugs": "https://github.com/json-patch/json-patch-tests/issues",
"keywords": [
"JSON",
"Patch",
"test",
"suite"
],
"license": "Apache-2.0"
}

View File

@ -0,0 +1,233 @@
[
{
"comment": "4.1. add with missing object",
"doc": { "q": { "bar": 2 } },
"patch": [ {"op": "add", "path": "/a/b", "value": 1} ],
"error":
"path /a does not exist -- missing objects are not created recursively"
},
{
"comment": "A.1. Adding an Object Member",
"doc": {
"foo": "bar"
},
"patch": [
{ "op": "add", "path": "/baz", "value": "qux" }
],
"expected": {
"baz": "qux",
"foo": "bar"
}
},
{
"comment": "A.2. Adding an Array Element",
"doc": {
"foo": [ "bar", "baz" ]
},
"patch": [
{ "op": "add", "path": "/foo/1", "value": "qux" }
],
"expected": {
"foo": [ "bar", "qux", "baz" ]
}
},
{
"comment": "A.3. Removing an Object Member",
"doc": {
"baz": "qux",
"foo": "bar"
},
"patch": [
{ "op": "remove", "path": "/baz" }
],
"expected": {
"foo": "bar"
}
},
{
"comment": "A.4. Removing an Array Element",
"doc": {
"foo": [ "bar", "qux", "baz" ]
},
"patch": [
{ "op": "remove", "path": "/foo/1" }
],
"expected": {
"foo": [ "bar", "baz" ]
}
},
{
"comment": "A.5. Replacing a Value",
"doc": {
"baz": "qux",
"foo": "bar"
},
"patch": [
{ "op": "replace", "path": "/baz", "value": "boo" }
],
"expected": {
"baz": "boo",
"foo": "bar"
}
},
{
"comment": "A.6. Moving a Value",
"doc": {
"foo": {
"bar": "baz",
"waldo": "fred"
},
"qux": {
"corge": "grault"
}
},
"patch": [
{ "op": "move", "from": "/foo/waldo", "path": "/qux/thud" }
],
"expected": {
"foo": {
"bar": "baz"
},
"qux": {
"corge": "grault",
"thud": "fred"
}
}
},
{
"comment": "A.7. Moving an Array Element",
"doc": {
"foo": [ "all", "grass", "cows", "eat" ]
},
"patch": [
{ "op": "move", "from": "/foo/1", "path": "/foo/3" }
],
"expected": {
"foo": [ "all", "cows", "eat", "grass" ]
}
},
{
"comment": "A.8. Testing a Value: Success",
"doc": {
"baz": "qux",
"foo": [ "a", 2, "c" ]
},
"patch": [
{ "op": "test", "path": "/baz", "value": "qux" },
{ "op": "test", "path": "/foo/1", "value": 2 }
],
"expected": {
"baz": "qux",
"foo": [ "a", 2, "c" ]
}
},
{
"comment": "A.9. Testing a Value: Error",
"doc": {
"baz": "qux"
},
"patch": [
{ "op": "test", "path": "/baz", "value": "bar" }
],
"error": "string not equivalent"
},
{
"comment": "A.10. Adding a nested Member Object",
"doc": {
"foo": "bar"
},
"patch": [
{ "op": "add", "path": "/child", "value": { "grandchild": { } } }
],
"expected": {
"foo": "bar",
"child": {
"grandchild": {
}
}
}
},
{
"comment": "A.11. Ignoring Unrecognized Elements",
"doc": {
"foo":"bar"
},
"patch": [
{ "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
],
"expected": {
"foo":"bar",
"baz":"qux"
}
},
{
"comment": "A.12. Adding to a Non-existent Target",
"doc": {
"foo": "bar"
},
"patch": [
{ "op": "add", "path": "/baz/bat", "value": "qux" }
],
"error": "add to a non-existent target"
},
{
"comment": "A.13 Invalid JSON Patch Document",
"doc": {
"foo": "bar"
},
"patch": [
{ "op": "add", "path": "/baz", "value": "qux", "op": "remove" }
],
"error": "operation has two 'op' members",
"disabled": true
},
{
"comment": "A.14. ~ Escape Ordering",
"doc": {
"/": 9,
"~1": 10
},
"patch": [{"op": "test", "path": "/~01", "value": 10}],
"expected": {
"/": 9,
"~1": 10
}
},
{
"comment": "A.15. Comparing Strings and Numbers",
"doc": {
"/": 9,
"~1": 10
},
"patch": [{"op": "test", "path": "/~01", "value": "10"}],
"error": "number is not equal to string"
},
{
"comment": "A.16. Adding an Array Value",
"doc": {
"foo": ["bar"]
},
"patch": [{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }],
"expected": {
"foo": ["bar", ["abc", "def"]]
}
}
]

View File

@ -0,0 +1,408 @@
[
{ "comment": "empty list, empty docs",
"doc": {},
"patch": [],
"expected": {} },
{ "comment": "empty patch list",
"doc": {"foo": 1},
"patch": [],
"expected": {"foo": 1} },
{ "comment": "rearrangements OK?",
"doc": {"foo": 1, "bar": 2},
"patch": [],
"expected": {"bar":2, "foo": 1} },
{ "comment": "rearrangements OK? How about one level down ... array",
"doc": [{"foo": 1, "bar": 2}],
"patch": [],
"expected": [{"bar":2, "foo": 1}] },
{ "comment": "rearrangements OK? How about one level down...",
"doc": {"foo":{"foo": 1, "bar": 2}},
"patch": [],
"expected": {"foo":{"bar":2, "foo": 1}} },
{ "comment": "add replaces any existing field",
"doc": {"foo": null},
"patch": [{"op": "add", "path": "/foo", "value":1}],
"expected": {"foo": 1} },
{ "comment": "toplevel array",
"doc": [],
"patch": [{"op": "add", "path": "/0", "value": "foo"}],
"expected": ["foo"] },
{ "comment": "toplevel array, no change",
"doc": ["foo"],
"patch": [],
"expected": ["foo"] },
{ "comment": "toplevel object, numeric string",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": "1"}],
"expected": {"foo":"1"} },
{ "comment": "toplevel object, integer",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": 1}],
"expected": {"foo":1} },
{ "comment": "Toplevel scalar values OK?",
"doc": "foo",
"patch": [{"op": "replace", "path": "", "value": "bar"}],
"expected": "bar",
"disabled": true },
{ "comment": "Add, / target",
"doc": {},
"patch": [ {"op": "add", "path": "/", "value":1 } ],
"expected": {"":1} },
{ "comment": "Add, /foo/ deep target (trailing slash)",
"doc": {"foo": {}},
"patch": [ {"op": "add", "path": "/foo/", "value":1 } ],
"expected": {"foo":{"": 1}} },
{ "comment": "Add composite value at top level",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": [1, 2]}],
"expected": {"foo": 1, "bar": [1, 2]} },
{ "comment": "Add into composite value",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}],
"expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/8", "value": "5"}],
"error": "Out of bounds (upper)" },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/-1", "value": "5"}],
"error": "Out of bounds (lower)" },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": true}],
"expected": {"foo": 1, "bar": true} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": false}],
"expected": {"foo": 1, "bar": false} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": null}],
"expected": {"foo": 1, "bar": null} },
{ "comment": "0 can be an array index or object element name",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": {"foo": 1, "0": "bar" } },
{ "doc": ["foo"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar", "sil"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": ["bar", "foo", "sil"] },
{ "comment": "push item to array via last index + 1",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/2", "value": "bar"}],
"expected": ["foo", "sil", "bar"] },
{ "comment": "add item to array at index > length should fail",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/3", "value": "bar"}],
"error": "index is greater than number of items in array" },
{ "comment": "test against implementation-specific numeric parsing",
"doc": {"1e0": "foo"},
"patch": [{"op": "test", "path": "/1e0", "value": "foo"}],
"expected": {"1e0": "foo"} },
{ "comment": "test with bad number should fail",
"doc": ["foo", "bar"],
"patch": [{"op": "test", "path": "/1e0", "value": "bar"}],
"error": "test op shouldn't get array element 1" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/bar", "value": 42}],
"error": "Object operation on array target" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"], "sil"],
"comment": "value in array add not flattened" },
{ "doc": {"foo": 1, "bar": [1, 2, 3, 4]},
"patch": [{"op": "remove", "path": "/bar"}],
"expected": {"foo": 1} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/0/qux"}],
"expected": {"foo": 1, "baz": [{}]} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} },
{ "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} },
{ "doc": ["foo"],
"patch": [{"op": "replace", "path": "/0", "value": "bar"}],
"expected": ["bar"] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": 0}],
"expected": [0] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": true}],
"expected": [true] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": false}],
"expected": [false] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": null}],
"expected": [null] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"]],
"comment": "value in array replace not flattened" },
{ "comment": "replace whole document",
"doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}],
"expected": {"baz": "qux"} },
{ "comment": "spurious patch properties",
"doc": {"foo": 1},
"patch": [{"op": "test", "path": "/foo", "value": 1, "spurious": 1}],
"expected": {"foo": 1} },
{ "doc": {"foo": null},
"patch": [{"op": "test", "path": "/foo", "value": null}],
"comment": "null value should be valid obj property" },
{ "doc": {"foo": null},
"patch": [{"op": "replace", "path": "/foo", "value": "truthy"}],
"expected": {"foo": "truthy"},
"comment": "null value should be valid obj property to be replaced with something truthy" },
{ "doc": {"foo": null},
"patch": [{"op": "move", "from": "/foo", "path": "/bar"}],
"expected": {"bar": null},
"comment": "null value should be valid obj property to be moved" },
{ "doc": {"foo": null},
"patch": [{"op": "copy", "from": "/foo", "path": "/bar"}],
"expected": {"foo": null, "bar": null},
"comment": "null value should be valid obj property to be copied" },
{ "doc": {"foo": null},
"patch": [{"op": "remove", "path": "/foo"}],
"expected": {},
"comment": "null value should be valid obj property to be removed" },
{ "doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "/foo", "value": null}],
"expected": {"foo": null},
"comment": "null value should still be valid obj property replace other value" },
{ "doc": {"foo": {"foo": 1, "bar": 2}},
"patch": [{"op": "test", "path": "/foo", "value": {"bar": 2, "foo": 1}}],
"comment": "test should pass despite rearrangement" },
{ "doc": {"foo": [{"foo": 1, "bar": 2}]},
"patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}],
"comment": "test should pass despite (nested) rearrangement" },
{ "doc": {"foo": {"bar": [1, 2, 5, 4]}},
"patch": [{"op": "test", "path": "/foo", "value": {"bar": [1, 2, 5, 4]}}],
"comment": "test should pass - no error" },
{ "doc": {"foo": {"bar": [1, 2, 5, 4]}},
"patch": [{"op": "test", "path": "/foo", "value": [1, 2]}],
"error": "test op should fail" },
{ "comment": "Whole document",
"doc": { "foo": 1 },
"patch": [{"op": "test", "path": "", "value": {"foo": 1}}],
"disabled": true },
{ "comment": "Empty-string element",
"doc": { "": 1 },
"patch": [{"op": "test", "path": "/", "value": 1}] },
{ "doc": {
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8
},
"patch": [{"op": "test", "path": "/foo", "value": ["bar", "baz"]},
{"op": "test", "path": "/foo/0", "value": "bar"},
{"op": "test", "path": "/", "value": 0},
{"op": "test", "path": "/a~1b", "value": 1},
{"op": "test", "path": "/c%d", "value": 2},
{"op": "test", "path": "/e^f", "value": 3},
{"op": "test", "path": "/g|h", "value": 4},
{"op": "test", "path": "/i\\j", "value": 5},
{"op": "test", "path": "/k\"l", "value": 6},
{"op": "test", "path": "/ ", "value": 7},
{"op": "test", "path": "/m~0n", "value": 8}] },
{ "comment": "Move to same location has no effect",
"doc": {"foo": 1},
"patch": [{"op": "move", "from": "/foo", "path": "/foo"}],
"expected": {"foo": 1} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "move", "from": "/foo", "path": "/bar"}],
"expected": {"baz": [{"qux": "hello"}], "bar": 1} },
{ "doc": {"baz": [{"qux": "hello"}], "bar": 1},
"patch": [{"op": "move", "from": "/baz/0/qux", "path": "/baz/1"}],
"expected": {"baz": [{}, "hello"], "bar": 1} },
{ "doc": {"baz": [{"qux": "hello"}], "bar": 1},
"patch": [{"op": "copy", "from": "/baz/0", "path": "/boo"}],
"expected": {"baz":[{"qux":"hello"}],"bar":1,"boo":{"qux":"hello"}} },
{ "comment": "replacing the root of the document is possible with add",
"doc": {"foo": "bar"},
"patch": [{"op": "add", "path": "", "value": {"baz": "qux"}}],
"expected": {"baz":"qux"}},
{ "comment": "Adding to \"/-\" adds to the end of the array",
"doc": [ 1, 2 ],
"patch": [ { "op": "add", "path": "/-", "value": { "foo": [ "bar", "baz" ] } } ],
"expected": [ 1, 2, { "foo": [ "bar", "baz" ] } ]},
{ "comment": "Adding to \"/-\" adds to the end of the array, even n levels down",
"doc": [ 1, 2, [ 3, [ 4, 5 ] ] ],
"patch": [ { "op": "add", "path": "/2/1/-", "value": { "foo": [ "bar", "baz" ] } } ],
"expected": [ 1, 2, [ 3, [ 4, 5, { "foo": [ "bar", "baz" ] } ] ] ]},
{ "comment": "test remove with bad number should fail",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/1e0/qux"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test remove on array",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/0"}],
"expected": [2, 3, 4] },
{ "comment": "test repeated removes",
"doc": [1, 2, 3, 4],
"patch": [{ "op": "remove", "path": "/1" },
{ "op": "remove", "path": "/2" }],
"expected": [1, 3] },
{ "comment": "test remove with bad index should fail",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/1e0"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test replace with bad number should fail",
"doc": [""],
"patch": [{"op": "replace", "path": "/1e0", "value": false}],
"error": "replace op shouldn't replace in array with bad number" },
{ "comment": "test copy with bad number should fail",
"doc": {"baz": [1,2,3], "bar": 1},
"patch": [{"op": "copy", "from": "/baz/1e0", "path": "/boo"}],
"error": "copy op shouldn't work with bad number" },
{ "comment": "test move with bad number should fail",
"doc": {"foo": 1, "baz": [1,2,3,4]},
"patch": [{"op": "move", "from": "/baz/1e0", "path": "/foo"}],
"error": "move op shouldn't work with bad number" },
{ "comment": "test add with bad number should fail",
"doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1e0", "value": "bar"}],
"error": "add op shouldn't add to array with bad number" },
{ "comment": "missing 'value' parameter to add",
"doc": [ 1 ],
"patch": [ { "op": "add", "path": "/-" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing 'value' parameter to replace",
"doc": [ 1 ],
"patch": [ { "op": "replace", "path": "/0" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing 'value' parameter to test",
"doc": [ null ],
"patch": [ { "op": "test", "path": "/0" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing value parameter to test - where undef is falsy",
"doc": [ false ],
"patch": [ { "op": "test", "path": "/0" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing from parameter to copy",
"doc": [ 1 ],
"patch": [ { "op": "copy", "path": "/-" } ],
"error": "missing 'from' parameter" },
{ "comment": "missing from parameter to move",
"doc": { "foo": 1 },
"patch": [ { "op": "move", "path": "" } ],
"error": "missing 'from' parameter" },
{ "comment": "duplicate ops",
"doc": { "foo": "bar" },
"patch": [ { "op": "add", "path": "/baz", "value": "qux",
"op": "move", "from":"/foo" } ],
"error": "patch has two 'op' members",
"disabled": true },
{ "comment": "unrecognized op should fail",
"doc": {"foo": 1},
"patch": [{"op": "spam", "path": "/foo", "value": 1}],
"error": "Unrecognized op 'spam'" },
{ "comment": "test with bad array number that has leading zeros",
"doc": ["foo", "bar"],
"patch": [{"op": "test", "path": "/00", "value": "foo"}],
"error": "test op should reject the array value, it has leading zeros" },
{ "comment": "test with bad array number that has leading zeros",
"doc": ["foo", "bar"],
"patch": [{"op": "test", "path": "/01", "value": "bar"}],
"error": "test op should reject the array value, it has leading zeros" },
{ "comment": "Removing nonexistent field",
"doc": {"foo" : "bar"},
"patch": [{"op": "remove", "path": "/baz"}],
"error": "removing a nonexistent field should fail" },
{ "comment": "Removing nonexistent index",
"doc": ["foo", "bar"],
"patch": [{"op": "remove", "path": "/2"}],
"error": "removing a nonexistent index should fail" }
]

162
tests/json_patch_tests.c Normal file
View File

@ -0,0 +1,162 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "unity/examples/unity_config.h"
#include "unity/src/unity.h"
#include "common.h"
#include "../cJSON_Utils.h"
static cJSON *parse_test_file(const char * const filename)
{
char *file = NULL;
cJSON *json = NULL;
file = read_file(filename);
TEST_ASSERT_NOT_NULL_MESSAGE(file, "Failed to read file.");
json = cJSON_Parse(file);
TEST_ASSERT_NOT_NULL_MESSAGE(json, "Failed to parse test json.");
TEST_ASSERT_TRUE_MESSAGE(cJSON_IsArray(json), "Json is not an array.");
free(file);
return json;
}
static cJSON_bool test_apply_patch(const cJSON * const test)
{
cJSON *doc = NULL;
cJSON *patch = NULL;
cJSON *expected = NULL;
cJSON *error_element = NULL;
cJSON *comment = NULL;
cJSON *disabled = NULL;
cJSON *object = NULL;
cJSON_bool successful = false;
/* extract all the data out of the test */
comment = cJSON_GetObjectItem(test, "comment");
if (cJSON_IsString(comment))
{
printf("Testing \"%s\"\n", comment->valuestring);
}
else
{
printf("Testing unkown\n");
}
disabled = cJSON_GetObjectItem(test, "disabled");
if (cJSON_IsTrue(disabled))
{
printf("SKIPPED\n");
return true;
}
doc = cJSON_GetObjectItem(test, "doc");
TEST_ASSERT_NOT_NULL_MESSAGE(doc, "No \"doc\" in the test.");
patch = cJSON_GetObjectItem(test, "patch");
TEST_ASSERT_NOT_NULL_MESSAGE(patch, "No \"patch\"in the test.");
/* Make a working copy of 'doc' */
object = cJSON_Duplicate(doc, true);
TEST_ASSERT_NOT_NULL(object);
expected = cJSON_GetObjectItem(test, "expected");
error_element = cJSON_GetObjectItem(test, "error");
if (error_element != NULL)
{
/* excepting an error */
TEST_ASSERT_TRUE_MESSAGE(0 != cJSONUtils_ApplyPatches(object, patch), "Test didn't fail as it's supposed to.");
successful = true;
}
else
{
/* apply the patch */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, cJSONUtils_ApplyPatches(object, patch), "Failed to apply patches.");
successful = true;
if (expected != NULL)
{
successful = cJSON_Compare(object, expected, true);
}
}
cJSON_Delete(object);
if (successful)
{
printf("OK\n");
}
else
{
printf("FAILED\n");
}
return successful;
}
static void cjson_utils_should_pass_json_patch_test_tests(void)
{
cJSON *tests = parse_test_file("json-patch-tests/tests.json");
cJSON *test = NULL;
cJSON_bool failed = false;
cJSON_ArrayForEach(test, tests)
{
failed |= !test_apply_patch(test);
}
cJSON_Delete(tests);
TEST_ASSERT_FALSE_MESSAGE(failed, "Some tests failed.");
}
static void cjson_utils_should_pass_json_patch_test_spec_tests(void)
{
cJSON *tests = parse_test_file("json-patch-tests/spec_tests.json");
cJSON *test = NULL;
cJSON_bool failed = false;
cJSON_ArrayForEach(test, tests)
{
failed |= !test_apply_patch(test);
}
cJSON_Delete(tests);
TEST_ASSERT_FALSE_MESSAGE(failed, "Some tests failed.");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(cjson_utils_should_pass_json_patch_test_tests);
RUN_TEST(cjson_utils_should_pass_json_patch_test_spec_tests);
return UNITY_END();
}