Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make download*() methods work in ContainerGebSpec #74

Merged
merged 11 commits into from
Nov 15, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.DefaultDownloadSupport
import geb.download.DownloadSupport
import groovy.transform.CompileStatic
import groovy.transform.SelfType

import java.util.regex.Pattern

/**
* A custom implementation of {@link geb.download.DownloadSupport} for enabling the use of its {@code download*()} methods
* within {@code ContainerGebSpec} environments.
*
* <p>This implementation is based on {@code DefaultDownloadSupport} from Geb, with modifications to support
* containerized environments. Specifically, it enables file downloads by resolving URLs relative to the host
* rather than the internal hostname used by the browser within the container.</p>
*
* <p>These adaptations allow the download functionality to operate correctly when tests are executed in containerized
* setups, ensuring the host network context is used for download requests.</p>
*
* @author Mattias Reichel
* @since 5.0.0
*/
@CompileStatic
@SelfType(ContainerGebSpec)
trait ContainerAwareDownloadSupport implements DownloadSupport {
matrei marked this conversation as resolved.
Show resolved Hide resolved

@Delegate
private final DownloadSupport downloadSupport = new LocalhostDownloadSupport(browser, this)

abstract Browser getBrowser()
abstract String getHostNameFromHost()

private static class LocalhostDownloadSupport extends DefaultDownloadSupport {

private final static Pattern urlPattern = ~/(https?:\/\/)([^\/:]+)(:\d+\/.*)/

private final ContainerAwareDownloadSupport parent
private final Browser browser

LocalhostDownloadSupport(Browser browser, ContainerAwareDownloadSupport parent) {
super(browser)
this.browser = browser
this.parent = parent
}

@Override
HttpURLConnection download(Map options) {
return super.download([*: options, base: resolveBase(options)])
}

private String resolveBase(Map options) {
return options.base ?: browser.driver.currentUrl.replaceAll(urlPattern) { match, proto, host, rest ->
"${proto}${parent.hostNameFromHost}${rest}"
}
}
}
}
83 changes: 62 additions & 21 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,40 @@ import java.time.Duration
* @author Mattias Reichel
* @since 5.0.0
*/
class ContainerGebSpec extends GebSpec {
@CompileStatic
abstract class ContainerGebSpec extends GebSpec implements ContainerAwareDownloadSupport {

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

private String hostNameFromContainer = DEFAULT_HOSTNAME_FROM_CONTAINER

@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)
webDriverContainer.tap {
addExposedPort(serverPort)
withAccessToHost(true)
start()
}
if (hostName != DEFAULT_HOSTNAME) {
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))
webDriverContainer = new BrowserWebDriverContainer()
Testcontainers.exposeHostPorts(port)
webDriverContainer.tap {
addExposedPort(this.port)
withAccessToHost(true)
start()
}
if (hostNameChanged) {
webDriverContainer.execInContainer('/bin/sh', '-c', "echo '$hostIp\t$hostName' | sudo tee -a /etc/hosts")
}
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"
if (notInitialized) {
initialize()
}
browser.baseUrl = "$protocol://$hostName:$port"
}

def cleanupSpec() {
Expand Down Expand Up @@ -107,14 +112,50 @@ 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 hostNameFromContainer
}

void setHostName(String hostName) {
hostNameFromContainer = hostName
}

/**
* 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 hostNameChanged ? hostName : DEFAULT_HOSTNAME_FROM_HOST
}

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
}

private boolean isHostNameChanged() {
return hostNameFromContainer != DEFAULT_HOSTNAME_FROM_CONTAINER
}

private boolean isNotInitialized() {
webDriverContainer == null
}
}
Loading