package e2e; import com.danielflower.apprunner.Config; import com.danielflower.apprunner.io.LineConsumer; import com.danielflower.apprunner.mgmt.AppManager; import com.danielflower.apprunner.mgmt.FileBasedGitRepoLoader; import com.danielflower.apprunner.mgmt.GitRepoLoader; import com.danielflower.apprunner.runners.*; import org.apache.commons.exec.CommandLine; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.maven.shared.invoker.InvocationRequest; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.InitCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.PersonIdent; import org.hamcrest.Matchers; import org.json.JSONArray; import org.json.JSONObject; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; import scaffolding.AppRepo; import scaffolding.Photocopier; import scaffolding.RestClient; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static com.danielflower.apprunner.FileSandbox.fullPath; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static scaffolding.ContentResponseMatcher.equalTo; public class SystemTest { private static final int httpsPort = AppManager.getAFreePort(); private static final String appRunnerUrl = "https://localhost:" + httpsPort; private static final RestClient restClient = RestClient.create(appRunnerUrl); private static final AppRepo leinApp = AppRepo.create("lein"); private static final AppRepo mavenApp = AppRepo.create("maven"); private static final AppRepo nodeApp = AppRepo.create("nodejs"); private static final File dataDir = new File("target/datadirs/" + System.currentTimeMillis()); private static MavenRunner mavenRunner; private static final HttpClient client = RestClient.httpClient; @BeforeClass public static void setup() throws Exception { // ensure the zips exist new ZipSamplesTask().zipTheSamplesAndPutThemInTheResourcesDir(); buildAndStartUberJar(asList("-DskipTests=true", "package")); createAndDeploy(mavenApp); } private static void createAndDeploy(AppRepo app) throws Exception { assertThat(restClient.createApp(app.gitUrl()).getStatus(), is(201)); assertThat(restClient.deploy(app.name).getStatus(), is(200)); } private static void buildAndStartUberJar(List<String> goals) throws Exception { mavenRunner = new MavenRunner(new File("."), new HomeProvider() { public InvocationRequest mungeMavenInvocationRequest(InvocationRequest request) { return HomeProvider.default_java_home.mungeMavenInvocationRequest(request); } public CommandLine commandLine(Map<String, String> envVarsForApp) { return HomeProvider.default_java_home.commandLine(envVarsForApp).addArgument("-Dlogback.configurationFile=src/test/resources/logback-test.xml"); } }, goals); Map<String, String> env = new HashMap<String, String>(System.getenv()) {{ put(Config.SERVER_HTTPS_PORT, String.valueOf(httpsPort)); put("apprunner.keystore.path", fullPath(new File("local/test.keystore"))); put("apprunner.keystore.password", "password"); put("apprunner.keymanager.password", "password"); put(Config.DATA_DIR, fullPath(dataDir)); }}; LineConsumer logHandler = line -> System.out.print("Uber jar output > " + line); try (Waiter startupWaiter = new Waiter("AppRunner uber jar", httpClient -> { try { JSONObject sysInfo = new JSONObject(client.GET(appRunnerUrl + "/api/v1/system").getContentAsString()); return sysInfo.getBoolean("appRunnerStarted"); } catch (Exception e) { return false; } }, 2, TimeUnit.MINUTES)) { mavenRunner.start(logHandler, logHandler, env, startupWaiter); } } private static void shutDownAppRunner() throws Exception { JSONObject apps = new JSONObject(restClient.get("/api/v1/apps").getContentAsString()); for (Object o : apps.getJSONArray("apps")) { JSONObject app = (JSONObject) o; restClient.stop(app.getString("name")); } mavenRunner.shutdown(); } @AfterClass public static void cleanup() throws Exception { shutDownAppRunner(); } @Test public void leinAppsWork() throws Exception { LeinRunnerTest.ignoreTestIfNotSupported(); createAndDeploy(leinApp); assertThat(restClient.homepage(leinApp.name), is(equalTo(200, containsString("Hello from lein")))); assertThat(restClient.deleteApp(leinApp.name), equalTo(200, containsString("{"))); assertThat(getAllApps().getJSONArray("apps").length(), is(1)); } @Test public void nodeAppsWork() throws Exception { NodeRunnerTest.ignoreTestIfNotSupported(); createAndDeploy(nodeApp); assertThat(restClient.homepage(nodeApp.name), is(equalTo(200, containsString("Hello from nodejs!")))); assertThat(restClient.deleteApp(nodeApp.name), equalTo(200, containsString("{"))); assertThat(getAllApps().getJSONArray("apps").length(), is(1)); } @Test public void theReverseProxyBehavesItself() throws Exception { ContentResponse resp = restClient.homepage(mavenApp.name); assertThat(resp, is(equalTo(200, containsString("My Maven App")))); HttpFields headers = resp.getHeaders(); assertThat(headers.getValuesList("Date"), hasSize(1)); assertThat(headers.getValuesList("Via"), Matchers.equalTo(singletonList("HTTP/1.1 apprunner"))); assertThat(restClient.deleteApp(mavenApp.name), equalTo(200, containsString("{"))); } @Test public void canUpdateAppsByDeployingThem() throws Exception { AppRepo changesApp = AppRepo.create("maven"); restClient.createApp(changesApp.gitUrl(), "changes-app"); restClient.deploy("changes-app"); assertThat(restClient.homepage("changes-app"), is(equalTo(200, containsString("My Maven App")))); updateHeaderAndCommit(changesApp, "My new and improved maven app!"); assertThat(restClient.deploy("changes-app"), is(equalTo(200, containsString("buildLogUrl")))); JSONObject appInfo = new JSONObject(client.GET(appRunnerUrl + "/api/v1/apps/changes-app").getContentAsString()); assertThat( client.GET(appInfo.getString("url")), is(equalTo(200, containsString("My new and improved maven app!")))); assertThat( client.GET(appInfo.getString("buildLogUrl")), is(equalTo(200, containsString("[INFO] Building my-maven-app 1.0-SNAPSHOT")))); assertThat( client.GET(appInfo.getString("consoleLogUrl")), is(equalTo(200, containsString("Starting changes-app on port")))); restClient.deleteApp("changes-app"); } @Test public void appRunnerCanStartEvenWithInvalidApps() throws Exception { shutDownAppRunner(); GitRepoLoader repoLoader = FileBasedGitRepoLoader.getGitRepoLoader(dataDir); repoLoader.save("invalid-app", "invalid-url"); buildAndStartUberJar(Collections.emptyList()); assertThat(restClient.homepage(mavenApp.name), is(equalTo(200, containsString("My Maven App")))); JSONObject allApps = getAllApps(); assertThat("Actual apps: " + allApps.toString(4), allApps.getJSONArray("apps").length(), is(1)); } @Test public void pushingAnEmptyRepoIsRejected() throws Exception { File dir = Photocopier.folderForSampleProject("empty-project"); InitCommand initCommand = Git.init(); initCommand.setDirectory(dir); Git origin = initCommand.call(); ContentResponse resp = restClient.createApp(dir.toURI().toString(), "empty-project"); assertThat(resp, equalTo(501, containsString("No suitable runner found for this app"))); Photocopier.copySampleAppToDir("maven", dir); origin.add().addFilepattern(".").call(); origin.commit().setMessage("Initial commit") .setAuthor(new PersonIdent("Author Test", "[email protected]")) .call(); resp = restClient.createApp(dir.toURI().toString(), "empty-project"); assertThat(resp, equalTo(201, containsString("empty-project"))); assertThat(new JSONObject(resp.getContentAsString()).get("name"), Matchers.equalTo("empty-project")); assertThat(restClient.deleteApp("empty-project"), equalTo(200, containsString("{"))); } @Test public void stoppedAppsSayTheyAreStopped() throws Exception { try { restClient.createApp(mavenApp.gitUrl(), "maven-status-test"); assertMavenAppAvailable("maven-status-test", false, "Not started"); restClient.deploy("maven-status-test"); assertMavenAppAvailable("maven-status-test", true, "Running"); restClient.stop("maven-status-test"); assertMavenAppAvailable("maven-status-test", false, "Stopped"); restClient.deploy("maven-status-test"); assertMavenAppAvailable("maven-status-test", true, "Running"); // Detecting crashed apps not supported yet // crash app // assertMavenAppAvailable("maven-status-test", false, "Crashed"); } finally { restClient.deleteApp("maven-status-test"); } } private static void assertMavenAppAvailable(String appName, boolean available, String message) throws InterruptedException, ExecutionException, TimeoutException { JSONObject appInfo = new JSONObject(client.GET(appRunnerUrl + "/api/v1/apps/" + appName).getContentAsString()); assertThat(appInfo.getBoolean("available"), is(available)); assertThat(appInfo.getString("availableStatus"), is(message)); } private static void updateHeaderAndCommit(AppRepo mavenApp, String replacement) throws IOException, GitAPIException { File indexHtml = new File(mavenApp.originDir, FilenameUtils.separatorsToSystem("src/main/resources/web/index.html")); String newVersion = FileUtils.readFileToString(indexHtml, "UTF-8").replaceAll("<h1>.*</h1>", "<h1>" + replacement + "</h1>"); FileUtils.write(indexHtml, newVersion, "UTF-8", false); mavenApp.origin.add().addFilepattern(".").call(); mavenApp.origin.commit().setMessage("Updated index.html").setAuthor("Dan F", "[email protected]").call(); } @Test public void theRestAPILives() throws Exception { JSONObject all = getAllApps(); System.out.println("all = " + all.toString(4)); assertThat(all.getInt("appCount"), is(1)); JSONAssert.assertEquals("{apps:[" + "{ name: \"maven\", url: \"" + appRunnerUrl + "/maven/\" }" + "]}", all, JSONCompareMode.LENIENT); assertThat(restClient.deploy("invalid-app-name"), is(equalTo(404, is("No app found with name 'invalid-app-name'. Valid names: maven")))); ContentResponse resp = client.GET(appRunnerUrl + "/api/v1/apps/maven"); assertThat(resp.getStatus(), is(200)); JSONObject single = new JSONObject(resp.getContentAsString()); JSONAssert.assertEquals(all.getJSONArray("apps").getJSONObject(0), single, JSONCompareMode.STRICT_ORDER); assertThat(single.has("lastBuild"), is(true)); assertThat(single.has("lastSuccessfulBuild"), is(true)); } private static JSONObject getAllApps() throws InterruptedException, ExecutionException, TimeoutException { ContentResponse resp = client.GET(appRunnerUrl + "/api/v1/apps"); assertThat(resp.getStatus(), is(200)); return new JSONObject(resp.getContentAsString()); } @Test public void appsCanBeDeleted() throws Exception { AppRepo newMavenApp = AppRepo.create("maven"); updateHeaderAndCommit(newMavenApp, "Different repo"); assertThat(restClient.createApp(newMavenApp.gitUrl(), "another-app").getStatus(), is(201)); restClient.deploy(newMavenApp.name); assertThat(getAllApps().getJSONArray("apps").length(), is(2)); assertThat( restClient.deleteApp("another-app"), is(equalTo(200, containsString("another-app")))); assertThat(getAllApps().getJSONArray("apps").length(), is(1)); } @Test public void appNamesCannotContainSpaces() throws Exception { AppRepo originalApp = AppRepo.create("maven"); assertThat(restClient.createApp(originalApp.gitUrl(), "some app"), equalTo(400, containsString("app name can only contain letters, numbers, hyphens and underscores"))); } @Test public void appsCanHaveTheirGitUrlsUpdated() throws Exception { AppRepo originalApp = AppRepo.create("maven"); updateHeaderAndCommit(originalApp, "From original repo"); assertThat(restClient.createApp(originalApp.gitUrl(), "some-app"), equalTo(201, containsString(originalApp.gitUrl()))); restClient.deploy("some-app"); assertThat( restClient.homepage("some-app"), is(equalTo(200, containsString("From original repo")))); AppRepo changedApp = AppRepo.create("maven"); updateHeaderAndCommit(changedApp, "From changed repo"); assertThat(restClient.updateApp(changedApp.gitUrl(), "some-app"), equalTo(200, containsString(changedApp.gitUrl()))); restClient.deploy("some-app"); assertThat( restClient.homepage("some-app"), is(equalTo(200, containsString("From changed repo")))); assertThat(restClient.updateApp(changedApp.gitUrl(), "this-does-not-exist"), equalTo(404, containsString("No application called this-does-not-exist exists"))); assertThat(restClient.updateApp("", "some-app"), equalTo(400, containsString("No git URL was specified"))); assertThat(restClient.createApp(originalApp.gitUrl(), "some-app"), equalTo(409, containsString("There is already an app with that ID"))); restClient.deleteApp("some-app"); assertThat(getAllApps().getJSONArray("apps").length(), is(1)); } @Test public void appsCanGetAuthors() throws Exception { ContentResponse resp = client.GET(appRunnerUrl + "/api/v1/apps/maven"); JSONObject respJson = new JSONObject(resp.getContentAsString()); assertThat( respJson.getString("contributors"), is("Author Test")); } @Test public void theSystemApiReturnsOsInfoAndZipsOfSampleProjects() throws IOException, InterruptedException, ExecutionException, TimeoutException { JSONObject sysInfo = new JSONObject(client.GET(appRunnerUrl + "/api/v1/system").getContentAsString()); assertThat(sysInfo.getString("appRunnerVersion"), startsWith("2.")); assertThat(sysInfo.get("host"), is(notNullValue())); assertThat(sysInfo.get("user"), is(notNullValue())); assertThat(sysInfo.get("os"), is(notNullValue())); JSONArray samples = sysInfo.getJSONArray("samples"); for (Object app : samples) { JSONObject json = (JSONObject) app; if (json.getString("name").equalsIgnoreCase("maven")) { JSONAssert.assertEquals( "{ name: 'maven', runCommands: [ 'mvn clean package', 'java -jar target/{artifactid}-{version}.jar' ] }", json, JSONCompareMode.LENIENT); } String url = json.getString("url"); ContentResponse zip = client.GET(url); assertThat(url, zip.getStatus(), is(200)); assertThat(url, zip.getHeaders().get("Content-Type"), is("application/zip")); } assertThat(client.GET(appRunnerUrl + "/api/v1/system/samples/badname.zip").getStatus(), is((404))); } @Test public void theSwaggerJSONDescribesTheAPI() throws Exception { ContentResponse swagger = restClient.get("/api/v1/swagger.json"); assertThat(swagger.getStatus(), is(200)); System.out.println("swagger.getContentAsString() = " + swagger.getContentAsString()); JSONAssert.assertEquals("{ " + "paths: {" + "'/apps': {}," + "'/system': {}" + "}" + "}", swagger.getContentAsString(), JSONCompareMode.LENIENT); } }