C# Client Library
A C# Client Library for the AnalyzeRe REST API
Loading...
Searching...
No Matches
GenericTest.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.Linq;
5using System.Net;
6using System.Reflection;
7using System.Runtime.Serialization;
8
9using AnalyzeRe;
12using AnalyzeRe.Rest;
14
15#if MSTEST
16using Microsoft.VisualStudio.TestTools.UnitTesting;
17#elif NUNIT
18using NUnit.Framework;
19#endif
20
22{
25 public static class GenericTest
26 {
27 #region GET Generic Tests
31 public static void GET_Collection<T>(IResourceCollection<T> collection_source)
33 {
35 Assert.Inconclusive("RUN_OFFLINE = true");
37 {
38 Assert.IsNotNull(response.meta);
39 Assert.AreEqual(0, response.meta.offset);
40 Debug.WriteLine("List contains " + response.meta.total_count + " items.");
41 Assert.IsNotNull(response.items);
42 Assert.IsFalse(response.items.Any(), "Expected no items to be returned.");
43 }
44
45 // Test the GET on the collection works with a page size of zero.
47
48 // Test retrieval using the IResourceCollection works.
49 ValidateResponse(collection_source.List(0, 0));
50 }
51
66 {
68 Assert.Inconclusive("RANDOMIZED_TESTS_ENABLED = false");
70 Assert.Inconclusive("RUN_OFFLINE = true");
71 string generation_error = null;
72 // Try to generate a POSTable resource of this type.
73 return Reflection.LimitAttempts(() =>
74 {
75 try
76 {
77 generation_error = null;
78 T randomizedResource = (T)reflection.CreateRandomizedInstance(subtype);
80 Console.WriteLine($"Posted a random new {randomizedResource.GetType().NiceTypeName()} " +
81 $"with id {randomizedResource.id}");
82 return reflection.FinalizePostedResource(randomizedResource);
83 }
84 catch (APIRequestException ex) { generation_error = ex.ToString(); }
85 return default(T);
86 }, arg => !Equals(arg, default(T)), () => generation_error, max_attempts: attempts);
87 }
88
103 IResourceCollection<T> collection_source,
105 long offset = 0, long limit = 100,
108 int? timeout = null)
110 {
112 Assert.Inconclusive("RANDOMIZED_TESTS_ENABLED = false");
114 Assert.Inconclusive("RUN_OFFLINE = true");
115
116 // Attempt to POST one new resource of every sub-type
119 .ToList();
120 foreach (T item in posted)
121 Assert.IsFalse(String.IsNullOrWhiteSpace(item.id), "The " +
122 item.GetType().NiceTypeName() + " just posted didn't return a valid id ");
123 List<string> ids = posted.Select(i => i.id).ToList();
124
125 RequestParameters parameters =
126 API.Parameters.Order<StoredAPIResource>(res => res.created, false)
127 .AddParameters(API.Parameters.Page(offset, limit))
128 // Add in any additional parameters from the caller.
129 .AddParameters(additionalParameters);
130
131 // Get the collection in each supported way
133 collection_source.GetItems(ids, parameters, timeout);
136
137 // Set up the routine that will validate our response
139 {
140 Assert.IsNotNull(response);
141 Assert.IsNotNull(response.meta);
142 // Assert that either the max page size was reached, or that
143 // the collection returned as many or more items than we posted.
144 long total_count = response.meta.total_count;
145 int result_count = response.items.Count();
146
147 Assert.AreEqual(limit, response.meta.limit, "Limit was not respected by the server.");
148 Assert.AreEqual(offset, response.meta.offset, "Offset was not respected by the server.");
149 if (total_count - offset >= limit)
150 Assert.AreEqual(limit, result_count, $"Expected a full page of {limit} " +
151 $"results to be returned, but only {result_count} items were.");
152 else
153 {
154 Assert.IsTrue(total_count >= posted.Count, $"Expected the collection to contain " +
155 $"at least as many items as were posted ({posted.Count}), but the server " +
156 $"only reports having {total_count} items.");
157 Assert.AreEqual(total_count - offset, result_count,
158 $"Expected all {(total_count - offset)} items in the collection after " +
159 $"offset {offset} to be in our results list, but only {result_count} items " +
160 $"were returned. Server total is {total_count}");
161 }
162
163 // Check if any specific items are missing from the list
164 // (only applies if the offset wasn't set)
165 if (offset == 0)
166 {
168 item => response.items.All(res => res.id != item.id)).ToList();
169 Assert.AreEqual(0, missingFromList.Count,
170 "One or more items that were successfully posted " +
171 $"weren't found in the list of {total_count} {typeof(T).NiceTypeName()} " +
172 $"objects retrieved with offset {offset} and limit {limit}:\n" +
173 String.Join("\n", missingFromList.Select(
174 r => $"{r.GetType().NiceTypeName()} with id {r.id}")));
175 }
176
177 // Perform any additional caller actions.
179 }
180
181 // Validate both responses
184 }
185
191 {
193 Assert.Inconclusive("RUN_OFFLINE = true");
195 AssertApi.AllPropertiesEqual(existingResource, getResult);
196 }
197
200 {
202 Assert.Inconclusive("RUN_OFFLINE = true");
206 Help_Test_Resource_GET_AllPropertiesRecognized(
208
209 // Give an error for any unrecognized properties
210 if (unrecognizedProperties.Count > 0)
211 {
212 Assert.Inconclusive("The following properties were in the JSON response, " +
213 "but were ignored because they had no corresponding property " +
214 "to deserialize to in the .NET object model:\n " +
215 String.Join("\n ", unrecognizedProperties.Select(kvp =>
216 "\"" + kvp.Key + "\": " + (kvp.Value?.ToString() ?? "(null)"))));
217 }
218 }
219
220 private static void Help_Test_Resource_GET_AllPropertiesRecognized(
223 {
224 if (String.IsNullOrEmpty(prefix))
225 prefix = toCheck.NiceTypeName() + ".";
227
228 // Include all user-facing properties that aren't marked as ignored by the serializer.
229 foreach (PropertyInfo property in toCheck.GetUserFacingProperties(excludeNotSerialized:true))
230 {
232 property.GetCustomAttributeFast<DataMemberAttribute>();
233 // Only properties that have a DataMember attribute count,
234 // otherwise the property risks being ignored by the serializer.
235 if (dataMember == null) continue;
236 // The property is inserted with its DataMemberAttribute name if available
237 string propertyName = dataMember.Name ?? property.Name;
239 }
240 // Note: Newtonsoft can serialize to private properties as well, so if we can't
241 // find a property, we will check this list.
243 BindingFlags.Public | BindingFlags.NonPublic |
244 BindingFlags.Instance | BindingFlags.FlattenHierarchy).ToList();
245
247 {
248 Type expectedType = resultTyped.GetType();
249 object jsonValue = jsonProperty.Value;
250
251 // If the strongly typed property (from the object model) is a dictionary
252 // or list of objects, then it can contain anything it wants, so no need
253 // to recurse and check all items in the dictionary returned by the JSON
254 if (expectedType.IsGenericType &&
255 expectedType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
256 return;
257 // Otherwise, check that all properties in the json are in the object.
258 // If the property is missing, add it to the list of unrecognized properties
259 if (!serializedProperties.ContainsKey(jsonProperty.Key))
260 {
261 // See if any non-user-facing properties have a matching DataMemberAttribute
263 .Where(pi => pi.IsAttributeDefinedFast<DataMemberAttribute>())
265 (pi.GetCustomAttributeFast<DataMemberAttribute>().Name ?? pi.Name));
266 // If not, see if any properties have a name that would match
267 if (ignored == null)
268 ignored = allProperties.FirstOrDefault(pi => jsonProperty.Key == pi.Name);
269 // If there was no matching property, add it to the list of failed properties.
270 if (ignored == null)
271 {
273 continue;
274 }
275 // If there was a matching property on the object, just not a "user-facing"
276 // property, make a note of it in the test output, but don't necessarily fail the test.
277 string message = $"The response contained a property \"{jsonProperty.Key}\" " +
278 "that does not correspond to a user-facing deserialized data member, but " +
279 $"which corresponds to the {ignored.ReflectedType.NiceTypeName()} " +
280 $"property {ignored.PropertyType.NiceTypeName()} " +
281 $"{ignored.DeclaringType.NiceTypeName()}.{ignored.Name} ";
282 if (ignored.IsAttributeDefinedFast<ObsoleteAttribute>())
283 Console.WriteLine($"{message} - but this property is marked as Obsolete, " +
284 "so it is safe to assume it is intentionally being ignored.");
285 else if (ignored.IsAttributeDefinedFast<InternalMemberAttribute>())
286 Console.WriteLine($"{message} - but this property is marked as Internal, " +
287 "so it is safe to assume it is being surfaced to the user via " +
288 "another property or is intentionally not user-facing.");
289 else
290 {
291 Console.WriteLine($"{message} - but this property is not-user facing " +
292 "for reasons that don't seem deliberate. This should be clarified.");
294 }
295 continue;
296 }
297
299 object propertyValue = property.GetValue(resultTyped, null);
300 // If the property is not missing, check its sub-properties if any are recognized.
302 {
303 Type typeToCompare = propertyValue.GetType();
305 // If the property we're getting back from the server is a resolved reference,
306 // we should instead be comparing it to the reference type.
308 !jsonDictionary.ContainsKey(nameof(IReference.ref_id)))
309 {
310 typeToCompare = typeToCompare.GetGenericArguments()[0];
312 }
313
314 // Recurse and test the properties of any nested objects.
315 Help_Test_Resource_GET_AllPropertiesRecognized(unrecognizedProperties,
317 prefix + jsonProperty.Key + ".");
318 }
319 // Otherwise, check that we retrieved its value correctly
320 else if (property.PropertyType.IsPrimitive || property.PropertyType == typeof(string))
321 {
322 // JSON may interpret a numeric value as a being type other than the target
323 // property type (resulting from our custom deserialization). We should attempt
324 // to convert that value to the desired type before doing an equivalency test.
325 string if_error_message = $"We have a property to capture \"{jsonProperty.Key}\"" +
326 $", but the value didn't match. JSON property value: {jsonProperty.Value} " +
327 $"Deserialized Value {propertyValue}";
328 if (jsonValue != null && jsonValue.GetType() != property.PropertyType)
329 {
330 if_error_message += ". Note that the native JSON interpretation of the " +
331 $"value was as the type {jsonValue.GetType()}. This value was converted " +
332 $"to the type {property.PropertyType} to perform this equivalency test.";
333 jsonValue = Convert.ChangeType(jsonValue, property.PropertyType);
334 }
336 }
337 }
338 }
339 #endregion GET Generic Tests
340
341 #region POST Generic Tests and Assertions
346 {
348 Assert.Inconclusive("RANDOMIZED_TESTS_ENABLED = false");
350 {
352 Debug.WriteLine("Successfully posted a new " + typeof(T).NiceTypeName() +
353 " instance of type " + subType.NiceTypeName() + " with the id: " + posted.id);
354 }
355 }
356
360 {
362 Assert.Inconclusive("RUN_OFFLINE = true");
363 T posted = default(T);
364
365 AssertApi.MethodIsAllowed(() => posted = toPost.Post(), "POST", true);
366 Console.WriteLine($"Posted a new {posted.GetType().NiceTypeName()} with id {posted.id}");
367 Assert.IsFalse(String.IsNullOrWhiteSpace(posted.id),
368 "No id returned after posting the " + posted.GetType().NiceTypeName());
369 AssertApi.PostResponseMatches(toPost, posted);
370 return posted;
371 }
372
376 public static T POST_ValidResourceWithData<T>(T toPost, string data)
378 {
379 return UploadData(POST_ValidResource(toPost), data);
380 }
381
387 public static T UploadData<T>(T resource, string data)
389 {
391 resource.data.UploadString(data, Samples.UploadParams), "Data Upload", true);
392 // If uploading data changed the posted object, we need to get it again
393 return resource.Get();
394 }
395
409
425
431
443
453
466
467 #region Private Helper Methods
472 {
474 "The POST that was successfully before performing additional actions was:\n" +
475 $"Request: {unposted.Serialize()}\nResponse: {posted.Serialize()}");
476 }
477
478 private static void Perform_InvalidPost<T, TException>(
481 where TException : Exception
482 {
483 if (EnvironmentSettings.RUN_OFFLINE)
484 Assert.Inconclusive("RUN_OFFLINE = true");
485 long numBefore = 0;
486 if (!EnvironmentSettings.EXECUTING_IN_PARALLEL)
487 {
488 // Track the number of resources on the server before attempting the post.
489 numBefore = API.GetResourceList<T>(API.Parameters.Page(0, 0)).meta.total_count;
490 }
491 // Flag determines whether exception was thrown after during POST or data PUT
492 bool noExceptionOnPost = false;
493
494 // Post the invalid resource and expect to catch an exception
495 AssertApi.ExceptionThrown(() =>
496 {
497 T posted = toPost.Post();
498 noExceptionOnPost = true;
499 // Regardless of whether the Post was meant to succeed or not, if there is data to
500 // be uploaded, do it now to avoid leaving an broken resource on the server
502 asWithData.data.UploadString(data, Samples.UploadParams);
503 else if (data != null)
504 throw new ArgumentException($"Data was provided for upload, but the " +
505 $"supplied resource type {toPost.GetType()} does not appear to " +
506 $"implement the {typeof(IAPIResource_WithDataEndpoint)} interface.");
507 Console.WriteLine(
508 $"The following request succeeded (but should have failed):\n{toPost.Serialize(true)}\n" +
509 $"The resulting posted resource was:\n{posted.Serialize(true)}");
510 }, exceptionTest);
511
512 if (!EnvironmentSettings.EXECUTING_IN_PARALLEL)
513 {
514 // Check the number of resources on the server after the attempted post.
515 long numAfter = API.GetResourceList<T>(API.Parameters.Page(0, 0)).meta.total_count;
517 {
518 Assert.AreEqual(numBefore, numAfter, "The POST was unsuccessful, yet the " +
519 "resource collection count changed from " + numBefore + " to " + numAfter);
520 }
521 else
522 {
523 Assert.IsTrue((numBefore > 0 && numAfter == numBefore) || numAfter == numBefore + 1,
524 "After a successful POST, followed by a failed PUT of data, we expected the " +
525 "number of items on the server to either increase by 1, or stay the same " +
526 "(if the resource was already on the server), but the count went from " +
527 numBefore + " to " + numAfter);
528 }
529 }
530 }
531 #endregion Private Helper Methods
532 #endregion POST Generic Tests and Assertions
533
534 #region PUT Generic Tests and Assertions
541 public static void PUT_Fails<T, TException>(T toPut,
544 where TException : Exception
545 {
547 Assert.Inconclusive("RUN_OFFLINE = true");
548 AssertApi.ExceptionThrown(() => toPut.Put(), exceptionTest);
549 }
550 #endregion PUT Generic Tests and Assertions
551
552 #region Helper Methods
554 public static T Mock<T>(string id) where T : IAPIResource
555 {
556 T test = (T)Activator.CreateInstance(
558 test.id = id;
559 return test;
560 }
561
566 {
567 try
568 {
569 action();
570 }
571 catch (Exception ex)
572 {
573 // Provide some additional context information to logs before re-throwing
575 if (additionalContext != null)
576 Console.WriteLine(additionalContext());
577 throw;
578 }
579 }
580 #endregion Private Helper Methods
581 }
582}
Exposes sample resource objects, with built-in methods for injecting dependencies.
Definition Samples.cs:14
static Parameters UploadParams
The LargeDataUpload parameters to use when uploading data for test fixtures.
static void LogExceptionDetails(Exception ex)
Creates a useful log of the exception details, and in request/response resulting in the error if any ...
Definition AssertApi.cs:51
static void MethodIsAllowed(Action request, string methodName, bool methodAllowed=true)
Wrap a request in a tryGet with some formatting for testing purposes.
Definition AssertApi.cs:98
static Action< APIRequestException > ApiExceptionTest(HttpStatusCode expectedStatusCode)
Generate a function that will test a REST request exception in a standard way.
Definition AssertApi.cs:539
Retrieve settings from environment variables if they exist, or the project settings file otherwise.
static bool RUN_OFFLINE
Controls whether tests that normally require a connection to the server should be allowed to try to r...
static bool RANDOMIZED_TESTS_ENABLED
If false, tests that involve random generation of resources (which can be unstable) will be skipped.
Generic Unit test implementations that will test REST methods on arbitrary resources.
static void GET_Collection< T >(IResourceCollection< T > collection_source)
Ensures that a GET can be done on the collection.
static void POST_InvalidResource_Fails< T >(T invalidToPost)
Verify that posting the specified resource fails with a HttpStatusCode.BadRequest status code.
static T POST_ValidResource< T >(T toPost)
Verify that posting the specified resource succeeds.
static void ExecuteWithConsoleErrorLogging(Action action, Func< string > additionalContext=null)
Wraps the call to the action with a try/catch that doesn't handle the exception, just logs some usefu...
static T UploadData< T >(T resource, string data)
Performs a data upload against the specified resource. Returns the result of a fresh GET on the data ...
static void GET_ExistingResource_Succeeds< T >(T existingResource)
Verify that getting the specified resource succeeds (after posting it).
static void POST_InvalidResource_Fails< T, TException >(T invalidToPost, Action< TException > exceptionTest)
Verify that posting the specified resource fails.
static void GET_Collection_AllSubtypes< T >(IResourceCollection< T > collection_source, Reflection reflection, long offset=0, long limit=100, RequestParameters additionalParameters=null, Action< ICollectionResponse< T > > additionalActions=null, int? timeout=null)
A Mini test suite that discovers all types that derive from the specified base resource type,...
static void POST_InvalidResourceWithData_Fails< T >(T toPost, string data)
Verify that posting the specified resource with data fails.
static void GET_AllPropertiesRecognized< T >(T existingResource)
static T POST_ThenDoAction< T >(T toPost, Action< T > toExecute)
Post a valid resource under the assumption that it will succeed, then perform an action on the result...
static void POST_AllSubtypes_Succeeds< T >(Reflection reflection)
A Mini test suite that discovers all types that derive from the specified base resource type,...
static T POST_WithData_ThenDoAction< T >(T toPost, string data, Action< T > toExecute)
Post a valid resource under the assumption that it will succeed, then perform an action on the result...
static T Try_POST_Random_Resource< T >(Type subtype, Reflection reflection, int attempts=3)
Try to generate and POSt a random new resource on the server of the specified type.
static T POST_ValidResourceWithData< T >(T toPost, string data)
Verify that posting the specified resource succeeds.
static T Mock< T >(string id)
Create an instance of the specified resource.
static void PUT_Fails< T, TException >(T toPut, Action< TException > exceptionTest=null)
Asserts that executing a PUT on the specified resource fails.
static void POST_InvalidResourceWithData_Fails< T, TException >(T toPost, string data, Action< TException > exceptionTest)
Verify that posting the specified resource with data fails.
A collection of filthy hacks to populate some fields of APIResources objects of any type.
Definition Reflection.cs:41
static IEnumerable< Type > GetAllInstantiableSubtypes(Type type)
Creates a list of types that can be derived from a given type.
A custom exception class that includes the RestSharp.IRestResponse that generated the exception,...
Describes a collection of resources which can be listed.
Base class used by all persistent resources.
Parameters that can be added to your REST requests to access additional API features.
static RequestParameters Ids(IEnumerable< string > ids)
Can be added to your collection GET requests to return only the set of resources whose id matches one...
static RequestParameters Order(string ordering)
Can be added to collection GET requests to specify the way items should be ordered....
static RequestParameters Page(long offset, long limit)
When getting a collection of items, these parameters can be added to restrict the number of results y...
API methods / requests made available to the user.
static ICollectionResponse< IAPIResource > GetResourceList(Type resourceType, IEnumerable< Parameter > requestParameters=null, string collectionNameOverride=null, int? timeout=null)
Get a collection of resources from the server.
This attribute is used on IAPIType classes to indicate members (especially public properties) that ex...
Helper class which makes it easier to build a set of request parameters.
RequestParameters AddParameters(IEnumerable< Parameter > collection)
Adds the specified parameters to this list and returns the list.
Describes an APIResource class that adds a "/data" sub-resource, since this functionality is common t...
Interface for Base class used by all resources.
Base interface for all reference entities.
string ref_id
The id of the object being referred to.
Definition IReference.cs:17