diff --git a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
index d5a21899..3167038c 100644
--- a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
+++ b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
@@ -25,8 +25,7 @@ Upon user selecting a filter to use the list query string has the regular paging
LIST FILTERING TODO
-
-Add test for fetching widget filter options (localized, so have two users test each for expected response)
+ - DONE test for fetching widget filter options (localized, so have two users test each for expected response)
Add test for excercising all of DataFilterController route including rights to non personal alternative owner etc
Add test for correctly validated Widget datafilter when saved or updated via datafiltercontroller (sanity check of the filter settings and options IFilterableObject implemented)
diff --git a/server/AyaNova/biz/FilterComparisonOperator.cs b/server/AyaNova/biz/FilterComparisonOperator.cs
new file mode 100644
index 00000000..b433b2bb
--- /dev/null
+++ b/server/AyaNova/biz/FilterComparisonOperator.cs
@@ -0,0 +1,18 @@
+namespace AyaNova.Biz
+{
+ public static class FilterComparisonOperator
+ {
+ public const string Equality = "=";
+ public const string GreaterThan = ">";
+ public const string GreaterThanOrEqualTo = ">=";
+ public const string LessThan = "<";
+ public const string LessThanOrEqualTo = "<=";
+ public const string NotEqual = "!=";
+ public const string NotLike = "!%";
+ public const string StartsWith = "%-";
+ public const string EndsWith = "-%";
+ public const string Contains = "-%-";
+ public const string NotContains = "!-%-";
+
+ }
+}
diff --git a/test/raven-integration/DataFilter/DataFilterCrud.cs b/test/raven-integration/DataFilter/DataFilterCrud.cs
new file mode 100644
index 00000000..2a8f99be
--- /dev/null
+++ b/test/raven-integration/DataFilter/DataFilterCrud.cs
@@ -0,0 +1,114 @@
+using System;
+using Xunit;
+using Newtonsoft.Json.Linq;
+using FluentAssertions;
+using System.Collections.Generic;
+using System.Collections.Concurrent;
+
+namespace raven_integration
+{
+
+ public class DataFilterCrud
+ {
+
+ ///
+ /// Test all CRUD routes
+ ///
+ [Fact]
+ public async void CRUD()
+ {
+ /*
+ {
+ "id": 0,
+ "concurrencyToken": 0,
+ "ownerId": 0,
+ "name": "string",
+ "public": true,
+ "listKey": "string",
+ "filter": "string"
+}
+ */
+
+ //CREATE
+ dynamic d = new JObject();
+ d.name = Util.Uniquify("Test DataFilter");
+ d.ownerId = 1L;
+ d.listKey="Widget";
+ d.filter="";
+
+ ApiResponse a = await Util.PostAsync("Tag", await Util.GetTokenAsync("BizAdminFull"), d.ToString());
+ Util.ValidateDataReturnResponseOk(a);
+ long tagId = a.ObjectResponse["data"]["id"].Value();
+ string tagName = a.ObjectResponse["data"]["name"].Value();
+ tagName.Should().StartWith("testtag");
+
+
+ //RETRIEVE
+ /*
+ {
+ "data": {
+ "id": 24,
+ "created": "2018-03-28T21:07:41.9703503Z",
+ "concurrencyToken": 9502,
+ "ownerId": 1,
+ "name": "یونیکُد چیست؟"
+ }
+ }
+ */
+ //Get one
+ a = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"));
+ Util.ValidateDataReturnResponseOk(a);
+ a.ObjectResponse["data"]["name"].Value().Should().StartWith("testtag");
+
+ //UPDATE
+
+ //PUT
+ d.Id = tagId;
+ d.name = Util.Uniquify("PutTestTag");
+ d.created = DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture);
+ d.concurrencyToken = a.ObjectResponse["data"]["concurrencyToken"].Value();
+ d.OwnerId = 1L;
+
+
+ ApiResponse PUTTestResponse = await Util.PutAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"), d.ToString());
+ Util.ValidateHTTPStatusCode(PUTTestResponse, 200);
+
+ //check PUT worked
+ ApiResponse checkPUTWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"));
+ Util.ValidateNoErrorInResponse(checkPUTWorked);
+ checkPUTWorked.ObjectResponse["data"]["name"].Value().Should().Be(d.name.ToString().ToLowerInvariant().Replace(" ", "-"));
+ uint concurrencyToken = PUTTestResponse.ObjectResponse["data"]["concurrencyToken"].Value();
+
+ //PATCH
+ var newName = Util.Uniquify("PatchUpdate");
+ string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]";
+ ApiResponse PATCHTestResponse = await Util.PatchAsync("Tag/" + tagId.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("BizAdminFull"), patchJson);
+ Util.ValidateHTTPStatusCode(PATCHTestResponse, 200);
+
+ //check PATCH worked
+ ApiResponse checkPATCHWorked = await Util.GetAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"));
+ Util.ValidateNoErrorInResponse(checkPATCHWorked);
+ checkPATCHWorked.ObjectResponse["data"]["name"].Value().Should().Be(newName.ToLowerInvariant().Replace(" ", "-"));
+
+ // //DELETE
+ ApiResponse DELETETestResponse = await Util.DeleteAsync("Tag/" + tagId.ToString(), await Util.GetTokenAsync("BizAdminFull"));
+ Util.ValidateHTTPStatusCode(DELETETestResponse, 204);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ //==================================================
+
+ }//eoc
+}//eons