// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.config.server.http.v2; import ai.vespa.http.HttpURL.Path; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.application.api.ApplicationFile; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.vespa.config.server.ApplicationRepository; import com.yahoo.vespa.config.server.application.CompressedApplicationInputStreamTest; import com.yahoo.vespa.config.server.application.OrchestratorMock; import com.yahoo.vespa.config.server.http.HttpErrorResponse; import com.yahoo.vespa.config.server.http.SessionHandlerTest; import com.yahoo.vespa.config.server.session.Session; import com.yahoo.vespa.config.server.tenant.TenantRepository; import com.yahoo.vespa.config.server.tenant.TestTenantRepository; import org.apache.hc.core5.http.ContentType; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED; import static com.yahoo.jdisc.Response.Status.OK; import static com.yahoo.jdisc.http.HttpRequest.Method.GET; import static com.yahoo.jdisc.http.HttpRequest.Method.POST; import static com.yahoo.vespa.config.server.http.HandlerTest.assertHttpStatusCodeErrorCodeAndMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * @author hmusum */ public class SessionCreateHandlerTest extends SessionHandlerTest { private static final TenantName tenant = TenantName.from("test"); private static final HashMap postHeaders = new HashMap<>(); ApplicationRepository applicationRepository; private String pathPrefix = "/application/v2/session/"; private String createdMessage = " created.\""; private String tenantMessage = ""; static { postHeaders.put(ApplicationApiHandler.contentTypeHeader, ApplicationApiHandler.APPLICATION_X_GZIP); } @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); private ConfigserverConfig configserverConfig; @Before public void setupRepo() throws IOException { configserverConfig = new ConfigserverConfig.Builder() .configServerDBDir(temporaryFolder.newFolder().getAbsolutePath()) .configDefinitionsDir(temporaryFolder.newFolder().getAbsolutePath()) .fileReferencesDir(temporaryFolder.newFolder().getAbsolutePath()) .build(); TenantRepository tenantRepository = new TestTenantRepository.Builder() .withConfigserverConfig(configserverConfig) .build(); applicationRepository = new ApplicationRepository.Builder() .withTenantRepository(tenantRepository) .withOrchestrator(new OrchestratorMock()) .build(); tenantRepository.addTenant(tenant); pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; createdMessage = " for tenant '" + tenant + "' created.\""; tenantMessage = ",\"tenant\":\"test\""; } @Ignore @Test public void require_that_from_parameter_cannot_be_set_if_data_in_request() throws IOException { HttpRequest request = post(Collections.singletonMap("from", "active")); HttpResponse response = createHandler().handle(request); assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.ErrorCode.BAD_REQUEST, "Parameter 'from' is illegal for POST"); } @Test public void require_that_post_request_must_contain_data() throws IOException { HttpResponse response = createHandler().handle(post()); assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.ErrorCode.BAD_REQUEST, "Request contains no data"); } @Test public void require_that_post_request_must_have_correct_content_type() throws IOException { HashMap headers = new HashMap<>(); // no Content-Type header File outFile = CompressedApplicationInputStreamTest.createTarFile(temporaryFolder.getRoot().toPath()); HttpResponse response = createHandler().handle(post(outFile, headers, null)); assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.ErrorCode.BAD_REQUEST, "Request contains no Content-Type header"); } private void assertIllegalFromParameter(String fromValue) throws IOException { File outFile = CompressedApplicationInputStreamTest.createTarFile(temporaryFolder.getRoot().toPath()); HttpRequest request = post(outFile, postHeaders, Collections.singletonMap("from", fromValue)); assertHttpStatusCodeErrorCodeAndMessage(createHandler().handle(request), BAD_REQUEST, HttpErrorResponse.ErrorCode.BAD_REQUEST, "Parameter 'from' has illegal value '" + fromValue + "'"); } @Test public void require_that_prepare_url_is_returned_on_success() throws IOException { File outFile = CompressedApplicationInputStreamTest.createTarFile(temporaryFolder.getRoot().toPath()); Map parameters = Collections.singletonMap("name", "foo"); HttpResponse response = createHandler().handle(post(outFile, postHeaders, parameters)); assertNotNull(response); assertEquals(OK, response.getStatus()); assertEquals(SessionHandlerTest.getRenderedString(response), "{\"log\":[]" + tenantMessage + ",\"session-id\":\"2\",\"prepared\":\"http://" + hostname + ":" + port + pathPrefix + "2/prepared\",\"content\":\"http://" + hostname + ":" + port + pathPrefix + "2/content/\",\"message\":\"Session 2" + createdMessage + "}"); } @Test public void require_that_handler_does_not_support_get() throws IOException { HttpResponse response = createHandler().handle(HttpRequest.createTestRequest(pathPrefix, GET)); assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED, HttpErrorResponse.ErrorCode.METHOD_NOT_ALLOWED, "Method 'GET' is not supported"); } @Test public void require_internal_error_when_exception() throws IOException { File outFile = CompressedApplicationInputStreamTest.createTarFile(temporaryFolder.getRoot().toPath()); new FileWriter(outFile).write("rubbish"); HttpResponse response = createHandler().handle(post(outFile)); assertHttpStatusCodeErrorCodeAndMessage(response, INTERNAL_SERVER_ERROR, HttpErrorResponse.ErrorCode.INTERNAL_SERVER_ERROR, "Unable to create compressed application stream"); } @Test public void require_that_handler_unpacks_application() throws IOException { File outFile = CompressedApplicationInputStreamTest.createTarFile(temporaryFolder.getRoot().toPath()); createHandler().handle(post(outFile)); ApplicationFile applicationFile = applicationRepository.getApplicationFileFromSession(tenant, 2, Path.parse("services.xml"), Session.Mode.READ); assertTrue(applicationFile.exists()); } @Test public void require_that_application_urls_can_be_given_as_from_parameter() throws Exception { ApplicationId applicationId = ApplicationId.from(tenant.value(), "foo", "quux"); HttpRequest request = post(Collections.singletonMap( "from", "http://myhost:40555/application/v2/tenant/" + tenant + "/application/foo/environment/test/region/baz/instance/quux")); assertEquals(applicationId, SessionCreateHandler.getFromApplicationId(request)); } @Test public void require_that_from_parameter_must_be_valid() throws IOException { assertIllegalFromParameter("active"); assertIllegalFromParameter(""); assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/lol"); assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod"); assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz"); assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz/instance"); } @Test public void require_that_content_type_is_parsed_correctly() throws FileNotFoundException { HttpRequest request = post(new ByteArrayInputStream("foo".getBytes(StandardCharsets.UTF_8)), Map.of("Content-Type", "multipart/form-data; charset=ISO-8859-1; boundary=g5gJAzUWl_t6"), Collections.emptyMap()); // Valid header should validate ok SessionCreateHandler.validateDataAndHeader(request, List.of(ContentType.MULTIPART_FORM_DATA.getMimeType())); // Accepting only application/json should fail: try { SessionCreateHandler.validateDataAndHeader(request, List.of(ContentType.APPLICATION_JSON.getMimeType())); fail("Request contained invalid content type, but validated ok"); } catch (Exception expected) {} } private SessionCreateHandler createHandler() { return new SessionCreateHandler(SessionCreateHandler.testContext(), applicationRepository, configserverConfig); } private HttpRequest post() throws FileNotFoundException { return post((InputStream) null, postHeaders, new HashMap<>()); } private HttpRequest post(File file) throws FileNotFoundException { return post(file, postHeaders, new HashMap<>()); } private HttpRequest post(File file, Map headers, Map parameters) throws FileNotFoundException { return post(file == null ? null : new FileInputStream(file), headers, parameters); } private HttpRequest post(InputStream data, Map headers, Map < String, String > parameters) throws FileNotFoundException { HttpRequest request = HttpRequest.createTestRequest("http://" + hostname + ":" + port + "/application/v2/tenant/" + tenant + "/session", POST, data, parameters); for (Map.Entry entry : headers.entrySet()) { request.getJDiscRequest().headers().put(entry.getKey(), entry.getValue()); } return request; } private HttpRequest post(Map parameters) throws FileNotFoundException { return post((InputStream) null, new HashMap<>(), parameters); } }