Skip to content

Commit

Permalink
fix: download*() methods work
Browse files Browse the repository at this point in the history
There must be a more elegant way to solve this, but this seems to work.

Resolves gh-72
  • Loading branch information
matrei committed Nov 11, 2024
1 parent 23a3044 commit e421833
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* Copyright 2024 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
*
* https://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 grails.plugin.geb

import geb.Browser
import geb.download.DownloadException
import geb.download.DownloadSupport
import groovy.transform.CompileStatic
import org.openqa.selenium.Cookie

/**
* A {@link geb.download.DownloadSupport} implementation that allows for using the {@code download*()} methods.
*
* @author Mattias Reichel
* @since 5.0.0
*/
@CompileStatic
trait ContainerDownloadSupport implements DownloadSupport {

// HTTP 1.1 states that this charset is the default if none was specified
static final private DEFAULT_CHARSET = 'ISO-8859-1'

abstract Browser getBrowser()
abstract String getHostNameFromHost()
abstract String getProtocol()
abstract int getPort()

/**
* Creates a http url connection to a url, that has the same cookies as the browser.
* <p>
* Valid options are:
*
* <ul>
* <li>{@code uri} - <em>optional</em> - the uri to resolve relative to the base option (current browser page used if {@code null})
* <li>{@code base} - <em>optional</em> - what to resolve the uri against (current browser page used if {@code null})
* </ul>
*/
HttpURLConnection download(Map options = [:]) {
URL url = resolveUrl(options)
HttpURLConnection connection = (HttpURLConnection) url.openConnection()
applyCookies(connection, browser)
browser.config.downloadConfig?.call(connection)
connection
}

HttpURLConnection download(String uri) {
download(uri: uri)
}

/**
* Opens a url connection via {@link #download(Map)} and returns the response input stream.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
InputStream downloadStream(Map options = [:], Closure connectionConfig = null) {
(InputStream) wrapInDownloadException(downloadWithConfig(options, connectionConfig)) {
((HttpURLConnection) it).inputStream
}
}

/**
* Opens a url connection via {@link #download(String)} and returns the response input stream.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
InputStream downloadStream(String uri, Closure connectionConfig = null) {
downloadStream(uri: uri, connectionConfig)
}

InputStream downloadStream(Closure connectionConfig) {
downloadStream([:], connectionConfig)
}

/**
* Opens a url connection via {@link #download(Map)} and returns the response text, if the content type was textual.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
String downloadText(Map options = [:], Closure connectionConfig = null) {
HttpURLConnection connection = downloadWithConfig(options, connectionConfig)
connection.connect()
String contentType = connection.contentType

if (isTextContentType(contentType)) {
String charset = determineCharset(contentType)
wrapInDownloadException(connection) { ((HttpURLConnection) it).inputStream.getText(charset) }
} else {
throw new DownloadException(connection, "cannot extract text from connection as content type is non text (is: $contentType)")
}
}

/**
* Opens a url connection via {@link #download(String)} and returns the response text, if the content type was textual.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
String downloadText(String uri, Closure connectionConfig = null) {
downloadText(uri: uri, connectionConfig)
}

String downloadText(Closure connectionConfig) {
downloadText([:], connectionConfig)
}

/**
* Opens a url connection via {@link #download(Map)} and returns the raw bytes.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
byte[] downloadBytes(Map options = [:], Closure connectionConfig = null) {
downloadStream(options, connectionConfig).bytes
}

byte[] downloadBytes(Closure connectionConfig) {
downloadStream(connectionConfig).bytes
}

/**
* Opens a url connection via {@link #download(String)} and returns the raw bytes.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*/
byte[] downloadBytes(String uri, Closure connectionConfig = null) {
downloadBytes(uri: uri, connectionConfig)
}

/**
* Opens a url connection via {@link #download(Map)} and returns the content object.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*
* @see URLConnection#getContent()
*/
Object downloadContent(Map options = [:], Closure connectionConfig = null) {
wrapInDownloadException(downloadWithConfig(options, connectionConfig)) { ((HttpURLConnection) it).content }
}

/**
* Opens a url connection via {@link #download(String)} and returns the content object.
* <p>
* If connectionConfig is given, it is called with the {@link HttpURLConnection} before the request is made.
*
* @see URLConnection#getContent()
*/
Object downloadContent(String uri, Closure connectionConfig = null) {
downloadContent(uri: uri, connectionConfig)
}

Object downloadContent(Closure connectionConfig) {
downloadContent([:], connectionConfig)
}

private HttpURLConnection downloadWithConfig(Map options, Closure config) {
def connection = download(options)
config?.call(connection)
connection
}

/**
* Returns a URL for what is to be downloaded.
* <p>
* If uri is non {@code null}, it is resolved against the browser's current page url. If it is {@code null},
* the browser's current page url will be returned.
*/
private URL resolveUrl(Map options) {
String uri = options.uri
String base = options.base ?: browser.driver.currentUrl.replaceAll(/(https?:\/\/)([^\/:]+)(:\d+\/.*)/) { match, protocol, host, rest ->
"${protocol}${hostNameFromHost}${rest}"
}
uri ? new URI(base).resolve(uri).toURL() : new URL(base)
}

/**
* Copies the browser's current cookies to the given connection via the "Cookie" header.
*/
private applyCookies(HttpURLConnection connection, Browser browser) {
applyCookies(connection, browser.driver.manage().cookies)
}

/**
* Copies the given cookies to the given connection via the "Cookie" header.
*/
private applyCookies(HttpURLConnection connection, Collection<Cookie> cookies) {
def cookieHeader = cookies.collect { "${it.name}=${it.value}" }.join('; ')
connection.setRequestProperty('Cookie', cookieHeader)
}

private boolean isTextContentType(String contentType) {
contentType?.startsWith('text/')
}

private determineCharset(String contentType) {
if (contentType) {
def parts = contentType.split(';')*.trim()
def charsetPart = parts.find { it.startsWith('charset=') }
if (charsetPart) {
charsetPart.split('=', 2)[1]
} else {
DEFAULT_CHARSET
}
} else {
DEFAULT_CHARSET
}
}

private wrapInDownloadException(HttpURLConnection connection, Closure operation) {
try {
operation(connection)
} catch (Throwable e) {
throw new DownloadException(connection, 'An error occurred during the download operation', e)
}
}
}
52 changes: 40 additions & 12 deletions src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
package grails.plugin.geb

import geb.spock.GebSpec
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.remote.RemoteWebDriver
import org.testcontainers.Testcontainers
Expand Down Expand Up @@ -44,38 +47,38 @@ import java.time.Duration
* @author Mattias Reichel
* @since 5.0.0
*/
class ContainerGebSpec extends GebSpec {
@CompileStatic
class ContainerGebSpec extends GebSpec implements ContainerDownloadSupport {

private static final String DEFAULT_PROTOCOL = 'http'
private static final String DEFAULT_HOSTNAME = 'host.testcontainers.internal'
private static final String DEFAULT_HOSTNAME_FROM_CONTAINER = 'host.testcontainers.internal'
private static final String DEFAULT_HOSTNAME_FROM_HOST = 'localhost'

@Shared
BrowserWebDriverContainer webDriverContainer

@PackageScope
void initialize() {
if (!webDriverContainer) {
if (!hasProperty('serverPort')) {
throw new IllegalStateException('Test class must be annotated with @Integration for serverPort to be injected')
}
webDriverContainer = new BrowserWebDriverContainer()
Testcontainers.exposeHostPorts(serverPort)
Testcontainers.exposeHostPorts(port)
webDriverContainer.tap {
addExposedPort(serverPort)
addExposedPort(this.port)
withAccessToHost(true)
start()
}
if (hostName != DEFAULT_HOSTNAME) {
if (hostName != DEFAULT_HOSTNAME_FROM_CONTAINER) {
webDriverContainer.execInContainer('/bin/sh', '-c', "echo '$hostIp\t$hostName' | sudo tee -a /etc/hosts")
}
browser.driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions())
browser.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30))
WebDriver driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions())
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30))
browser.driver = driver
}
}

void setup() {
initialize()
baseUrl = "$protocol://$hostName:$serverPort"
browser.baseUrl = "$protocol://$hostName:$port"
}

def cleanupSpec() {
Expand Down Expand Up @@ -107,13 +110,38 @@ class ContainerGebSpec extends GebSpec {
/**
* Returns the hostname that the browser will use to access the server under test.
* <p>Defaults to {@code host.testcontainers.internal}.
* <p>This is useful when the server under test needs to be accessed with a certain hostname.
*
* @return the hostname for accessing the server under test
*/
String getHostName() {
return DEFAULT_HOSTNAME
return DEFAULT_HOSTNAME_FROM_CONTAINER
}

/**
* Returns the hostname that the server under test is available on from the host.
* <p>This is useful when using any of the {@code download*()} methods as they will connect from the host,
* and not from within the container.
* <p>Defaults to {@code localhost}. If the value returned by {@code getHostName()}
* is different from the default, this method will return the same value same as {@code getHostName()}.
*
* @return the hostname for accessing the server under test from the host
*/
@Override
String getHostNameFromHost() {
return hostName != DEFAULT_HOSTNAME_FROM_CONTAINER ? DEFAULT_HOSTNAME_FROM_CONTAINER : DEFAULT_HOSTNAME_FROM_HOST
}

@Override
int getPort() {
try {
return (int) getProperty('serverPort')
} catch (Exception ignore) {
throw new IllegalStateException('Test class must be annotated with @Integration for serverPort to be injected')
}
}

@CompileDynamic
private static String getHostIp() {
PortForwardingContainer.INSTANCE.network.get().ipAddress
}
Expand Down

0 comments on commit e421833

Please sign in to comment.