public async Task PullAsync_Cancels_WhenCancellationTokenIsCancelled() { var store = new MobileServiceLocalStoreMock(); var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = op => Task.Delay(TimeSpan.MaxValue).ContinueWith<JObject>(t => null); // long slow operation var hijack = new TestHttpHandler(); hijack.OnSendingRequest = async req => { await Task.Delay(1000); return req; }; IMobileServiceClient service = new MobileServiceClient(MobileAppUriValidator.DummyMobileApp, hijack); await service.SyncContext.InitializeAsync(store, handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); using (var cts = new CancellationTokenSource()) { Task pullTask = table.PullAsync(null, null, null, cancellationToken: cts.Token); cts.Cancel(); var ex = await ThrowsAsync<Exception>(() => pullTask); Assert.IsTrue((ex is OperationCanceledException || ex is TaskCanceledException)); Assert.AreEqual(pullTask.Status, TaskStatus.Canceled); } }
public async Task PullAsync_Cancels_WhenCancellationTokenIsCancelled() { var store = new MobileServiceLocalStoreMock(); var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = op => Task.Delay(TimeSpan.MaxValue).ContinueWith<JObject>(t => null); // long slow operation IMobileServiceClient service = new MobileServiceClient("http://www.test.com", "secret..."); await service.SyncContext.InitializeAsync(store, handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); using (var cts = new CancellationTokenSource()) { // now pull Task pullTask = table.PullAsync(null, null, null, cancellationToken: cts.Token); cts.Cancel(); var ex = await ThrowsAsync<Exception>(() => pullTask); Assert.IsTrue((ex is OperationCanceledException || ex is TaskCanceledException)); Assert.AreEqual(pullTask.Status, TaskStatus.Canceled); } }
public async Task PushAsync_Succeeds_WithPendingOperations_AndOpQueueIsConsistent() { // Essentially async ManualResetEvents SemaphoreSlim untilPendingOpsCreated = new SemaphoreSlim(0, 1); SemaphoreSlim untilAboutToExecuteOp = new SemaphoreSlim(0, 1); int pushState = 0; var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = async op => { try { untilAboutToExecuteOp.Release(); await untilPendingOpsCreated.WaitAsync(); JObject result = await op.ExecuteAsync(); if (0 == pushState) { Assert.AreEqual(MobileServiceTableOperationKind.Insert, op.Kind); Assert.AreEqual(0, op.Item.Value<int>("value")); } else { Assert.AreEqual(MobileServiceTableOperationKind.Update, op.Kind); Assert.AreEqual(2, op.Item.Value<int>("value")); // We shouldn't see the value == 1, since it should have been collapsed } // We don't care what the server actually returned, as long as there was no exception raised in our Push logic return result; } catch (Exception ex) { Assert.Fail("Things are bad: " + ex.Message); } return null; }; var hijack = new TestHttpHandler(); IMobileServiceClient service = new MobileServiceClient("http://www.test.com", hijack); LocalStoreWithDelay mockLocalStore = new LocalStoreWithDelay(); await service.SyncContext.InitializeAsync(mockLocalStore, handler); JObject item = null; // Add the initial operation and perform a push IMobileServiceSyncTable table = service.GetSyncTable("someTable"); string responseContent = @"{ ""id"": ""abc"", ""value"": ""0"", ""version"": ""v0"" }"; // Whatever is fine, since we won't use it or look at it // Do this Insert/Push/Update+Update/Push cycle several times fast to try to hit any race conditions that would cause an error for (int id = 0; id < 10; id++) { hijack.SetResponseContent(responseContent); string idStr = "id" + id; // Generate a new Id each time in case the mock objects ever care that we insert an item that already exists // The Operations and PushAction don't necessarily clone the JObject, so we need a fresh one for each operation or else we'll change // the in-memory representation of the JObject stored in all operations, as well as in the "batch" the PushAction owns. This is problematic. item = new JObject() { { "id", idStr }, { "value", 0 } }; await table.InsertAsync(item); Task pushComplete = service.SyncContext.PushAsync(); // Make sure the PushAction has actually called into our SyncHandler, otherwise the two UpdateOperations could collapse onto it, and // there won't necessarily even be a second PushAction await untilAboutToExecuteOp.WaitAsync(); // Add some more operations while that push is in flight. Since these operations affect the same item in someTable, the operations // will be stuck awaiting the PushAction since it locks on the row. item = new JObject() { { "id", idStr }, { "value", 1 } }; Task updateOnce = table.UpdateAsync(item); item = new JObject() { { "id", idStr }, { "value", 2 } }; Task updateTwice = table.UpdateAsync(item); // Before we let the push finish, let's inject a delay that will cause it to take a long time deleting the operation from the queue. // This will give the other operations, if there's an unaddressed race condition, a chance to wreak havoc on the op queue. mockLocalStore.SetLookupDelay(500); // Let the first push finish untilPendingOpsCreated.Release(); await pushComplete; mockLocalStore.SetLookupDelay(0); await updateOnce; await updateTwice; // Push again, but now the operation condensed from the two updates should be executed remotely pushState = (pushState + 1) % 2; hijack.SetResponseContent(responseContent); pushComplete = service.SyncContext.PushAsync(); await untilAboutToExecuteOp.WaitAsync(); // not strictly necessary other than to keep the semaphore count at 0 untilPendingOpsCreated.Release(); await pushComplete; pushState = (pushState + 1) % 2; } }
public async Task PushAsync_Succeeds_WithClientWinsPolicy() { var hijack = new TestHttpHandler(); hijack.Responses.Add(new HttpResponseMessage(HttpStatusCode.PreconditionFailed) { Content = new StringContent("{\"id\":\"abc\",\"version\":\"Hey\"}") }); hijack.AddResponseContent(@"{""id"": ""abc""}"); var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = async op => { for (int i = 0; i < 2; i++) { try { return await op.ExecuteAsync(); } catch (MobileServicePreconditionFailedException ex) { op.Item[MobileServiceSystemColumns.Version] = ex.Value[MobileServiceSystemColumns.Version]; } } return null; }; IMobileServiceClient service = new MobileServiceClient(MobileAppUriValidator.DummyMobileApp, hijack); await service.SyncContext.InitializeAsync(new MobileServiceLocalStoreMock(), handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); await table.UpdateAsync(new JObject() { { "id", "abc" }, { "version", "Wow" } }); await service.SyncContext.PushAsync(); }
public async Task UpdateOperationAsync_UpsertsTheItemInLocalStore_AndDeletesTheError_FromSyncHandler() { // Arrange string itemId = "abc"; var hijack = new TestHttpHandler(); hijack.Responses.Add(new HttpResponseMessage(HttpStatusCode.InternalServerError)); var handler = new MobileServiceSyncHandlerMock(); handler.PushCompleteAction = async pushCompletionResult => { foreach (var error in pushCompletionResult.Errors) { await error.UpdateOperationAsync(JObject.Parse("{\"id\":\"abc\",\"__version\":\"Hey\"}")); } }; var store = new MobileServiceLocalStoreMock(); IMobileServiceClient service = new MobileServiceClient("http://www.test.com", hijack); await service.SyncContext.InitializeAsync(store, handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); await table.InsertAsync(new JObject() { { "id", "abc" }, { "__version", "Wow" } }); // Act await (service.SyncContext as MobileServiceSyncContext).PushAsync(CancellationToken.None, MobileServiceTableKind.Table); // Assert var syncError = store.TableMap[MobileServiceLocalSystemTables.SyncErrors].Values; var operation = store.TableMap[MobileServiceLocalSystemTables.OperationQueue].Values.FirstOrDefault(); var item = JObject.Parse("{\"id\":\"abc\",\"__version\":\"Hey\"}"); JObject upserted = await store.LookupAsync("someTable", itemId); // item is upserted Assert.IsNotNull(upserted); // verify if the record was updated Assert.AreEqual(item.ToString(), upserted.ToString()); // verify if the errors were cleared Assert.AreEqual(0, syncError.Count); // Verify operation is still present Assert.AreEqual(operation.Value<string>("itemId"),itemId); }
public async Task PushAsync_InvokesHandler_WhenTableTypeIsTable() { var hijack = new TestHttpHandler(); hijack.Responses.Add(new HttpResponseMessage(HttpStatusCode.InternalServerError)); bool invoked = false; var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = op => { invoked = true; return Task.FromResult(JObject.Parse("{\"id\":\"abc\",\"version\":\"Hey\"}")); }; IMobileServiceClient service = new MobileServiceClient(MobileAppUriValidator.DummyMobileApp, hijack); await service.SyncContext.InitializeAsync(new MobileServiceLocalStoreMock(), handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); await table.InsertAsync(new JObject() { { "id", "abc" }, { "version", "Wow" } }); await (service.SyncContext as MobileServiceSyncContext).PushAsync(CancellationToken.None, MobileServiceTableKind.Table); Assert.IsTrue(invoked); }
public async Task PushAsync_DoesNotRunHandler_WhenTableTypeIsNotTable() { var hijack = new TestHttpHandler(); hijack.AddResponseContent("{\"id\":\"abc\",\"version\":\"Hey\"}"); bool invoked = false; var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = op => { invoked = true; throw new InvalidOperationException(); }; IMobileServiceClient service = new MobileServiceClient(MobileAppUriValidator.DummyMobileApp, hijack); await service.SyncContext.InitializeAsync(new MobileServiceLocalStoreMock(), handler); IMobileServiceSyncTable table = service.GetSyncTable("someTable"); await table.InsertAsync(new JObject() { { "id", "abc" }, { "version", "Wow" } }); await (service.SyncContext as MobileServiceSyncContext).PushAsync(CancellationToken.None, (MobileServiceTableKind)1); Assert.IsFalse(invoked); }
/// <summary> /// Tests that throwing an exception of type toThrow from the http handler causes the push sync to be aborted /// </summary> /// <param name="toThrow">The exception to simulate coming from http layer</param> /// <param name="expectedStatus">The expected status of push operation as reported in PushCompletionResult and PushFailedException</param> /// <returns></returns> private async Task TestPushAbort(Exception toThrow, MobileServicePushStatus expectedStatus) { bool thrown = false; var hijack = new TestHttpHandler(); hijack.OnSendingRequest = req => { if (!thrown) { thrown = true; throw toThrow; } return Task.FromResult(req); }; var operationHandler = new MobileServiceSyncHandlerMock(); hijack.SetResponseContent("{\"id\":\"abc\",\"String\":\"Hey\"}"); IMobileServiceClient service = new MobileServiceClient("http://www.test.com", "secret...", hijack); await service.SyncContext.InitializeAsync(new MobileServiceLocalStoreMock(), operationHandler); IMobileServiceSyncTable<StringIdType> table = service.GetSyncTable<StringIdType>(); var item = new StringIdType() { Id = "an id", String = "what?" }; await table.InsertAsync(item); Assert.AreEqual(service.SyncContext.PendingOperations, 1L); MobileServicePushFailedException ex = await ThrowsAsync<MobileServicePushFailedException>(service.SyncContext.PushAsync); Assert.AreEqual(ex.PushResult.Status, expectedStatus); Assert.AreEqual(ex.PushResult.Errors.Count(), 0); Assert.AreEqual(operationHandler.PushCompletionResult.Status, expectedStatus); // the insert operation is still in queue Assert.AreEqual(service.SyncContext.PendingOperations, 1L); await service.SyncContext.PushAsync(); Assert.AreEqual(service.SyncContext.PendingOperations, 0L); Assert.AreEqual(operationHandler.PushCompletionResult.Status, MobileServicePushStatus.Complete); }
public async Task PushAsync_Succeeds_WithPendingOperations_AndOpQueueIsConsistent() { // Essentially async ManualResetEvents SemaphoreSlim untilPendingOpsCreated = new SemaphoreSlim(0, 1); SemaphoreSlim untilAboutToExecuteOp = new SemaphoreSlim(0, 1); int pushState = 0; var handler = new MobileServiceSyncHandlerMock(); handler.TableOperationAction = async op => { try { untilAboutToExecuteOp.Release(); await untilPendingOpsCreated.WaitAsync(); JObject result = await op.ExecuteAsync(); if (0 == pushState) { Assert.AreEqual(MobileServiceTableOperationKind.Insert, op.Kind); Assert.AreEqual(0, op.Item.Value <int>("value")); } else { Assert.AreEqual(MobileServiceTableOperationKind.Update, op.Kind); Assert.AreEqual(2, op.Item.Value <int>("value")); // We shouldn't see the value == 1, since it should have been collapsed } // We don't care what the server actually returned, as long as there was no exception raised in our Push logic return(result); } catch (Exception ex) { Assert.Fail("Things are bad: " + ex.Message); } return(null); }; var hijack = new TestHttpHandler(); IMobileServiceClient service = new MobileServiceClient("http://www.test.com", "secret...", hijack); LocalStoreWithDelay mockLocalStore = new LocalStoreWithDelay(); await service.SyncContext.InitializeAsync(mockLocalStore, handler); JObject item = null; // Add the initial operation and perform a push IMobileServiceSyncTable table = service.GetSyncTable("someTable"); string responseContent = @"{ ""id"": ""abc"", ""value"": ""0"", ""__version"": ""v0"" }"; // Whatever is fine, since we won't use it or look at it // Do this Insert/Push/Update+Update/Push cycle several times fast to try to hit any race conditions that would cause an error for (int id = 0; id < 10; id++) { hijack.SetResponseContent(responseContent); string idStr = "id" + id; // Generate a new Id each time in case the mock objects ever care that we insert an item that already exists // The Operations and PushAction don't necessarily clone the JObject, so we need a fresh one for each operation or else we'll change // the in-memory representation of the JObject stored in all operations, as well as in the "batch" the PushAction owns. This is problematic. item = new JObject() { { "id", idStr }, { "value", 0 } }; await table.InsertAsync(item); Task pushComplete = service.SyncContext.PushAsync(); // Make sure the PushAction has actually called into our SyncHandler, otherwise the two UpdateOperations could collapse onto it, and // there won't necessarily even be a second PushAction await untilAboutToExecuteOp.WaitAsync(); // Add some more operations while that push is in flight. Since these operations affect the same item in someTable, the operations // will be stuck awaiting the PushAction since it locks on the row. item = new JObject() { { "id", idStr }, { "value", 1 } }; Task updateOnce = table.UpdateAsync(item); item = new JObject() { { "id", idStr }, { "value", 2 } }; Task updateTwice = table.UpdateAsync(item); // Before we let the push finish, let's inject a delay that will cause it to take a long time deleting the operation from the queue. // This will give the other operations, if there's an unaddressed race condition, a chance to wreak havoc on the op queue. mockLocalStore.SetLookupDelay(500); // Let the first push finish untilPendingOpsCreated.Release(); await pushComplete; mockLocalStore.SetLookupDelay(0); await updateOnce; await updateTwice; // Push again, but now the operation condensed from the two updates should be executed remotely pushState = (pushState + 1) % 2; hijack.SetResponseContent(responseContent); pushComplete = service.SyncContext.PushAsync(); await untilAboutToExecuteOp.WaitAsync(); // not strictly necessary other than to keep the semaphore count at 0 untilPendingOpsCreated.Release(); await pushComplete; pushState = (pushState + 1) % 2; } }