/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.core.db.jpa; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Stage; import com.google.inject.name.Names; import org.eclipse.che.account.api.AccountManager; import org.eclipse.che.account.api.AccountModule; import org.eclipse.che.account.event.BeforeAccountRemovedEvent; import org.eclipse.che.account.spi.AccountDao; import org.eclipse.che.account.spi.AccountImpl; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.machine.server.jpa.MachineJpaModule; import org.eclipse.che.api.machine.server.model.impl.CommandImpl; import org.eclipse.che.api.machine.server.model.impl.SnapshotImpl; import org.eclipse.che.api.machine.server.recipe.RecipeImpl; import org.eclipse.che.api.machine.server.spi.SnapshotDao; import org.eclipse.che.api.ssh.server.jpa.JpaSshDao.RemoveSshKeysBeforeUserRemovedEventSubscriber; import org.eclipse.che.api.ssh.server.jpa.SshJpaModule; import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl; import org.eclipse.che.api.ssh.server.spi.SshDao; import org.eclipse.che.api.user.server.UserManager; import org.eclipse.che.api.user.server.event.BeforeUserRemovedEvent; import org.eclipse.che.api.user.server.jpa.PreferenceEntity; import org.eclipse.che.api.user.server.jpa.UserJpaModule; import org.eclipse.che.api.user.server.model.impl.ProfileImpl; import org.eclipse.che.api.user.server.model.impl.UserImpl; import org.eclipse.che.api.user.server.spi.PreferenceDao; import org.eclipse.che.api.user.server.spi.ProfileDao; import org.eclipse.che.api.user.server.spi.UserDao; import org.eclipse.che.api.workspace.server.WorkspaceManager; import org.eclipse.che.api.workspace.server.WorkspaceRuntimes; import org.eclipse.che.api.workspace.server.WorkspaceSharedPool; import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent; import org.eclipse.che.api.workspace.server.jpa.JpaWorkspaceDao.RemoveSnapshotsBeforeWorkspaceRemovedEventSubscriber; import org.eclipse.che.api.workspace.server.jpa.JpaWorkspaceDao.RemoveWorkspaceBeforeAccountRemovedEventSubscriber; import org.eclipse.che.api.workspace.server.jpa.WorkspaceJpaModule; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentRecipeImpl; import org.eclipse.che.api.workspace.server.model.impl.ExtendedMachineImpl; import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.ServerConf2Impl; import org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl; import org.eclipse.che.api.workspace.server.spi.WorkspaceDao; import org.eclipse.che.commons.test.db.H2DBTestServer; import org.eclipse.che.commons.test.db.PersistTestModuleBuilder; import org.eclipse.che.core.db.DBInitializer; import org.eclipse.che.core.db.cascade.CascadeEventSubscriber; import org.eclipse.che.core.db.cascade.event.CascadeEvent; import org.eclipse.che.core.db.h2.jpa.eclipselink.H2ExceptionHandler; import org.eclipse.che.core.db.schema.SchemaInitializer; import org.eclipse.che.core.db.schema.impl.flyway.FlywaySchemaInitializer; import org.eclipse.che.inject.lifecycle.InitModule; import org.h2.Driver; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.annotation.PostConstruct; import javax.inject.Singleton; import javax.persistence.EntityManagerFactory; import java.util.Map; import java.util.concurrent.Callable; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createAccount; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createPreferences; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createProfile; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createSnapshot; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createSshPair; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createUser; import static org.eclipse.che.core.db.jpa.TestObjectsFactory.createWorkspace; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /** * Tests top-level entities cascade removals. * * @author Yevhenii Voevodin */ public class CascadeRemovalTest { private Injector injector; private EventService eventService; private AccountDao accountDao; private PreferenceDao preferenceDao; private UserDao userDao; private ProfileDao profileDao; private WorkspaceDao workspaceDao; private SnapshotDao snapshotDao; private SshDao sshDao; /** Account and User are a root of dependency tree. */ private AccountImpl account; private UserImpl user; private UserManager userManager; private AccountManager accountManager; /** Profile depends on user. */ private ProfileImpl profile; /** Preferences depend on user. */ private Map<String, String> preferences; /** Workspaces depend on user. */ private WorkspaceImpl workspace1; private WorkspaceImpl workspace2; /** SshPairs depend on user. */ private SshPairImpl sshPair1; private SshPairImpl sshPair2; /** Snapshots depend on workspace. */ private SnapshotImpl snapshot1; private SnapshotImpl snapshot2; private SnapshotImpl snapshot3; private SnapshotImpl snapshot4; private H2DBTestServer server; @BeforeMethod public void setUp() throws Exception { server = H2DBTestServer.startDefault(); injector = Guice.createInjector(Stage.PRODUCTION, new AbstractModule() { @Override protected void configure() { install(new PersistTestModuleBuilder().setDriver(Driver.class) .runningOn(server) .addEntityClasses(AccountImpl.class, UserImpl.class, ProfileImpl.class, PreferenceEntity.class, WorkspaceImpl.class, WorkspaceConfigImpl.class, ProjectConfigImpl.class, EnvironmentImpl.class, EnvironmentRecipeImpl.class, ExtendedMachineImpl.class, SourceStorageImpl.class, ServerConf2Impl.class, StackImpl.class, CommandImpl.class, SnapshotImpl.class, RecipeImpl.class, SshPairImpl.class) .addEntityClass("org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl$Attribute") .setExceptionHandler(H2ExceptionHandler.class) .build()); bind(EventService.class).in(Singleton.class); install(new InitModule(PostConstruct.class)); bind(SchemaInitializer.class).toInstance(new FlywaySchemaInitializer(server.getDataSource(), "che-schema")); bind(DBInitializer.class).asEagerSingleton(); bind(String[].class).annotatedWith(Names.named("che.auth.reserved_user_names")).toInstance(new String[0]); bind(UserManager.class); bind(AccountManager.class); install(new UserJpaModule()); install(new AccountModule()); install(new SshJpaModule()); install(new WorkspaceJpaModule()); install(new MachineJpaModule()); bind(WorkspaceManager.class); final WorkspaceRuntimes wR = mock(WorkspaceRuntimes.class); when(wR.hasRuntime(anyString())).thenReturn(false); bind(WorkspaceRuntimes.class).toInstance(wR); bind(AccountManager.class); bind(Boolean.class).annotatedWith(Names.named("che.workspace.auto_snapshot")).toInstance(false); bind(Boolean.class).annotatedWith(Names.named("che.workspace.auto_restore")).toInstance(false); bind(WorkspaceSharedPool.class).toInstance(new WorkspaceSharedPool("cached", null, null)); } }); eventService = injector.getInstance(EventService.class); accountDao = injector.getInstance(AccountDao.class); userDao = injector.getInstance(UserDao.class); userManager = injector.getInstance(UserManager.class); accountManager = injector.getInstance(AccountManager.class); preferenceDao = injector.getInstance(PreferenceDao.class); profileDao = injector.getInstance(ProfileDao.class); sshDao = injector.getInstance(SshDao.class); snapshotDao = injector.getInstance(SnapshotDao.class); workspaceDao = injector.getInstance(WorkspaceDao.class); } @AfterMethod public void cleanup() { injector.getInstance(EntityManagerFactory.class).close(); server.shutdown(); } @Test public void shouldDeleteAllTheEntitiesWhenUserAndAccountIsDeleted() throws Exception { createTestData(); // Remove the user, all entries must be removed along with the user accountManager.remove(account.getId()); userManager.remove(user.getId()); // Check all the entities are removed assertNull(notFoundToNull(() -> userDao.getById(user.getId()))); assertNull(notFoundToNull(() -> profileDao.getById(user.getId()))); assertTrue(preferenceDao.getPreferences(user.getId()).isEmpty()); assertTrue(sshDao.get(user.getId()).isEmpty()); assertTrue(workspaceDao.getByNamespace(user.getName()).isEmpty()); assertTrue(snapshotDao.findSnapshots(workspace1.getId()).isEmpty()); assertTrue(snapshotDao.findSnapshots(workspace2.getId()).isEmpty()); } @Test(dataProvider = "beforeUserRemoveRollbackActions") public void shouldRollbackTransactionWhenFailedToRemoveAnyOfEntriesDuringUserRemoving( Class<CascadeEventSubscriber<CascadeEvent>> subscriberClass, Class<CascadeEvent> eventClass) throws Exception { createTestData(); eventService.unsubscribe(injector.getInstance(subscriberClass), eventClass); // Remove the user, all entries must be rolled back after fail try { userManager.remove(user.getId()); fail("UserManager#remove has to throw exception"); } catch (Exception ignored) { } // Check all the data rolled back assertNotNull(userDao.getById(user.getId())); assertNotNull(profileDao.getById(user.getId())); assertFalse(preferenceDao.getPreferences(user.getId()).isEmpty()); assertFalse(sshDao.get(user.getId()).isEmpty()); wipeTestData(); } @DataProvider(name = "beforeUserRemoveRollbackActions") public Object[][] beforeUserRemoveActions() { return new Class[][] { {RemoveSshKeysBeforeUserRemovedEventSubscriber.class, BeforeUserRemovedEvent.class} }; } @Test(dataProvider = "beforeAccountRemoveRollbackActions") public void shouldRollbackTransactionWhenFailedToRemoveAnyOfEntriesDuringAccountRemoving( Class<CascadeEventSubscriber<CascadeEvent>> subscriberClass, Class<CascadeEvent> eventClass) throws Exception { createTestData(); eventService.unsubscribe(injector.getInstance(subscriberClass), eventClass); // Remove the user, all entries must be rolled back after fail try { accountDao.remove(account.getId()); fail("AccountDao#remove had to throw exception"); } catch (Exception ignored) { } // Check all the data rolled back assertFalse(workspaceDao.getByNamespace(user.getName()).isEmpty()); assertFalse(snapshotDao.findSnapshots(workspace1.getId()).isEmpty()); assertFalse(snapshotDao.findSnapshots(workspace2.getId()).isEmpty()); wipeTestData(); } @DataProvider(name = "beforeAccountRemoveRollbackActions") public Object[][] beforeAccountRemoveActions() { return new Class[][] { {RemoveWorkspaceBeforeAccountRemovedEventSubscriber.class, BeforeAccountRemovedEvent.class}, {RemoveSnapshotsBeforeWorkspaceRemovedEventSubscriber.class, BeforeWorkspaceRemovedEvent.class}, }; } private void createTestData() throws ConflictException, ServerException { accountDao.create(account = createAccount("bobby")); userDao.create(user = createUser("bobby")); profileDao.create(profile = createProfile(user.getId())); preferenceDao.setPreferences(user.getId(), preferences = createPreferences()); workspaceDao.create(workspace1 = createWorkspace("workspace1", account)); workspaceDao.create(workspace2 = createWorkspace("workspace2", account)); sshDao.create(sshPair1 = createSshPair(user.getId(), "service", "name1")); sshDao.create(sshPair2 = createSshPair(user.getId(), "service", "name2")); snapshotDao.saveSnapshot(snapshot1 = createSnapshot("snapshot1", workspace1.getId())); snapshotDao.saveSnapshot(snapshot2 = createSnapshot("snapshot2", workspace1.getId())); snapshotDao.saveSnapshot(snapshot3 = createSnapshot("snapshot3", workspace2.getId())); snapshotDao.saveSnapshot(snapshot4 = createSnapshot("snapshot4", workspace2.getId())); } private void wipeTestData() throws ConflictException, ServerException, NotFoundException { snapshotDao.removeSnapshot(snapshot1.getId()); snapshotDao.removeSnapshot(snapshot2.getId()); snapshotDao.removeSnapshot(snapshot3.getId()); snapshotDao.removeSnapshot(snapshot4.getId()); sshDao.remove(sshPair1.getOwner(), sshPair1.getService(), sshPair1.getName()); sshDao.remove(sshPair2.getOwner(), sshPair2.getService(), sshPair2.getName()); workspaceDao.remove(workspace1.getId()); workspaceDao.remove(workspace2.getId()); preferenceDao.remove(user.getId()); profileDao.remove(user.getId()); userDao.remove(user.getId()); accountDao.remove(account.getId()); } private static <T> T notFoundToNull(Callable<T> action) throws Exception { try { return action.call(); } catch (NotFoundException x) { return null; } } }