/* * Copyright 2014 the original author or 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. */ package org.springframework.sync.diffsync.web; import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.List; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.sync.Todo; import org.springframework.sync.TodoRepository; import org.springframework.sync.diffsync.EmbeddedDataSourceConfig; import org.springframework.sync.diffsync.PersistenceCallbackRegistry; import org.springframework.sync.diffsync.ShadowStore; import org.springframework.sync.diffsync.shadowstore.MapBasedShadowStore; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=EmbeddedDataSourceConfig.class) @Transactional public class DiffSyncControllerTest { private static final String RESOURCE_PATH = "/todos"; @Autowired private TodoRepository repository; private static final MediaType JSON_PATCH = new MediaType("application", "json-patch+json"); // // entity patching // @Test public void patchSendsEntityStatusChange() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH + "/2") .content(resource("patch-change-entity-status")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(new Todo(1L, "A", false), all.get(0)); assertEquals(new Todo(2L, "B", true), all.get(1)); assertEquals(new Todo(3L, "C", false), all.get(2)); } @Test public void patchSendsEntityDescriptionChange() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH + "/2") .content(resource("patch-change-entity-description")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(new Todo(1L, "A", false), all.get(0)); assertEquals(new Todo(2L, "BBB", false), all.get(1)); assertEquals(new Todo(3L, "C", false), all.get(2)); } @Test public void patchSendsEntityIdChange() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH + "/2") .content(resource("patch-change-entity-id")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(4, all.size()); assertEquals(new Todo(1L, "A", false), all.get(0)); assertEquals(new Todo(2L, "B", false), all.get(1)); assertEquals(new Todo(3L, "C", false), all.get(2)); assertEquals(new Todo(4L, "B", false), all.get(3)); // This is odd behavior, but correct in the context of the backing database. } // // list patching // @Test public void noChangesFromEitherSide() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content("[]") .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)) .andExpect(status().isOk()); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(2L, "B", false)); assertEquals(all.get(2), new Todo(3L, "C", false)); } @Test public void patchSendsSingleStatusChange() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-change-single-status")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(2L, "B", true)); assertEquals(all.get(2), new Todo(3L, "C", false)); } @Test public void patchSendsAStatusChangeAndADescriptionChangeForSameItem() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-change-single-status-and-desc")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(2L, "BBB", true)); assertEquals(all.get(2), new Todo(3L, "C", false)); } @Test public void patchSendsAStatusChangeAndADescriptionChangeForDifferentItems() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-change-two-status-and-desc")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(3, all.size()); assertEquals(all.get(0), new Todo(1L, "AAA", false)); assertEquals(all.get(1), new Todo(2L, "B", true)); assertEquals(all.get(2), new Todo(3L, "C", false)); } @Test @Ignore public void patchAddsAnItem() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-add-new-item")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[{\"op\":\"test\",\"path\":\"/3/id\"},{\"op\":\"add\",\"path\":\"/3/id\",\"value\":4}]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(4, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(2L, "B", false)); assertEquals(all.get(2), new Todo(3L, "C", false)); assertEquals(all.get(2), new Todo(4L, "D", false)); } @Test public void patchRemovesAnItem() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-remove-item")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(2, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(3L, "C", false)); } @Test public void patchRemovesTwoItems() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-remove-two-items")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(1, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); } @Test public void patchUpdatesStatusOnOneItemAndRemovesTwoOtherItems() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-change-status-and-delete-two-items")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(1, all.size()); assertEquals(all.get(0), new Todo(1L, "A", true)); } @Test public void patchRemovesTwoOtherItemsAndUpdatesStatusOnAnother() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-delete-twoitems-and-change-status-on-another")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(1, all.size()); assertEquals(all.get(0), new Todo(3L, "C", true)); } @Test public void patchChangesItemStatusAndThenRemovesThatSameItem() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-modify-then-remove-item")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(status().isOk()) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(2, all.size()); assertEquals(all.get(0), new Todo(1L, "A", false)); assertEquals(all.get(1), new Todo(3L, "C", false)); } // // server-side changes // @Test @Ignore public void noChangesFromClientSide_itemDeletedFromServer() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); performNoOpRequestToSetupShadow(mvc); repository.delete(2L); mvc.perform( patch(RESOURCE_PATH) .content("[]") .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(content().string(resource("patch-remove-item"))) .andExpect(content().contentType(JSON_PATCH)) .andExpect(status().isOk()); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(2, all.size()); assertEquals(new Todo(1L, "A", false), all.get(0)); assertEquals(new Todo(3L, "C", false), all.get(1)); } @Test @Ignore public void statusChangedOnClient_itemDeletedFromServer() throws Exception { TodoRepository todoRepository = todoRepository(); MockMvc mvc = mockMvc(todoRepository); performNoOpRequestToSetupShadow(mvc); repository.delete(2L); mvc.perform( patch(RESOURCE_PATH) .content(resource("patch-change-single-status")) .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(content().string(resource("patch-remove-completed-item"))) .andExpect(content().contentType(JSON_PATCH)) .andExpect(status().isOk()); List<Todo> all = (List<Todo>) repository.findAll(); assertEquals(2, all.size()); assertEquals(new Todo(1L, "A", false), all.get(0)); assertEquals(new Todo(3L, "C", false), all.get(1)); } // // private helpers // private void performNoOpRequestToSetupShadow(MockMvc mvc) throws Exception { mvc.perform( patch(RESOURCE_PATH) .content("[]") .accept(JSON_PATCH) .contentType(JSON_PATCH)) .andExpect(content().string("[]")) .andExpect(content().contentType(JSON_PATCH)) .andExpect(status().isOk()); } private String resource(String name) throws IOException { ClassPathResource resource = new ClassPathResource("/org/springframework/sync/" + name + ".json"); BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream())); StringBuilder builder = new StringBuilder(); while(reader.ready()) { builder.append(reader.readLine()); } return builder.toString(); } private TodoRepository todoRepository() { return repository; } private MockMvc mockMvc(TodoRepository todoRepository) { ShadowStore shadowStore = new MapBasedShadowStore("x"); PersistenceCallbackRegistry callbackRegistry = new PersistenceCallbackRegistry(); callbackRegistry.addPersistenceCallback(new JpaPersistenceCallback<Todo>(todoRepository, Todo.class)); DiffSyncController controller = new DiffSyncController(callbackRegistry, shadowStore); MockMvc mvc = standaloneSetup(controller) .setMessageConverters(new JsonPatchHttpMessageConverter()) .build(); return mvc; } }