Follow HTTP redirects (#328)

- Change HttpClient to follow all redirects except HTTPS to HTTP.
- Run language snippet tests with --no-cache and real PackageServer
  instead of pre-seeded cache.
  This increases HTTP test coverage and enables testing of package redirects.
- Change PackageServer to return 301 for request paths starting with /HTTP301/
  and 307 for request paths starting with /HTTP307/.
- Update some outdated test package checksums that apparently weren't verified.
This commit is contained in:
translatenix
2024-03-21 10:26:07 -07:00
committed by GitHub
parent 60bcd56672
commit deaf6983c4
10 changed files with 105 additions and 16 deletions
@@ -588,7 +588,7 @@ class CliProjectPackagerTest {
"type": "remote", "type": "remote",
"uri": "projectpackage://localhost:0/birds@0.5.0", "uri": "projectpackage://localhost:0/birds@0.5.0",
"checksums": { "checksums": {
"sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" "sha256": "0a5ad2dc13f06f73f96ba94e8d01d48252bc934e2de71a837620ca0fef8a7453"
} }
}, },
"package://localhost:0/project2@5": { "package://localhost:0/project2@5": {
@@ -51,6 +51,10 @@ abstract class InputOutputTestEngine :
protected abstract fun generateOutputFor(inputFile: Path): Pair<Boolean, String> protected abstract fun generateOutputFor(inputFile: Path): Pair<Boolean, String>
protected open fun beforeAll() {}
protected open fun afterAll() {}
class ExecutionContext : EngineExecutionContext class ExecutionContext : EngineExecutionContext
override fun getId(): String = this::class.java.simpleName override fun getId(): String = this::class.java.simpleName
@@ -78,7 +82,17 @@ abstract class InputOutputTestEngine :
(classSelectors.isEmpty() || classSelectors.any { it.className == className }) (classSelectors.isEmpty() || classSelectors.any { it.className == className })
) { ) {
val rootNode = InputDirNode(uniqueId, inputDir, ClassSource.from(testClass.java)) val rootNode =
object : InputDirNode(uniqueId, inputDir, ClassSource.from(testClass.java)) {
override fun before(context: ExecutionContext): ExecutionContext {
beforeAll()
return context
}
override fun after(context: ExecutionContext) {
afterAll()
}
}
return doDiscover(rootNode, uniqueIdSelectors) return doDiscover(rootNode, uniqueIdSelectors)
} }
@@ -124,7 +138,11 @@ abstract class InputOutputTestEngine :
override fun createExecutionContext(request: ExecutionRequest) = ExecutionContext() override fun createExecutionContext(request: ExecutionRequest) = ExecutionContext()
private inner class InputDirNode(uniqueId: UniqueId, val inputDir: Path, source: TestSource) : private open inner class InputDirNode(
uniqueId: UniqueId,
val inputDir: Path,
source: TestSource
) :
AbstractTestDescriptor(uniqueId, inputDir.fileName.toString(), source), Node<ExecutionContext> { AbstractTestDescriptor(uniqueId, inputDir.fileName.toString(), source), Node<ExecutionContext> {
override fun getType() = Type.CONTAINER override fun getType() = Type.CONTAINER
} }
@@ -130,11 +130,23 @@ class PackageServer : AutoCloseable {
return@HttpHandler return@HttpHandler
} }
val path = exchange.requestURI.path val path = exchange.requestURI.path
if (path.startsWith("/HTTP301/")) {
exchange.responseHeaders.add("Location", path.removePrefix("/HTTP301"))
exchange.sendResponseHeaders(301, -1)
exchange.close()
return@HttpHandler
}
if (path.startsWith("/HTTP307/")) {
exchange.responseHeaders.add("Location", path.removePrefix("/HTTP307"))
exchange.sendResponseHeaders(307, -1)
exchange.close()
return@HttpHandler
}
val localPath = val localPath =
if (path.endsWith(".zip")) packagesDir.resolve(path.drop(1)) if (path.endsWith(".zip")) packagesDir.resolve(path.drop(1))
else packagesDir.resolve("${path.drop(1)}${path}.json") else packagesDir.resolve("${path.drop(1)}${path}.json")
if (!Files.exists(localPath)) { if (!Files.exists(localPath)) {
exchange.sendResponseHeaders(404, 0) exchange.sendResponseHeaders(404, -1)
exchange.close() exchange.close()
return@HttpHandler return@HttpHandler
} }
@@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType; import java.lang.invoke.MethodType;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandler;
@@ -81,6 +82,7 @@ final class JdkHttpClient implements HttpClient {
java.net.http.HttpClient.newBuilder() java.net.http.HttpClient.newBuilder()
.sslContext(createSslContext(certificateFiles, certificateUris)) .sslContext(createSslContext(certificateFiles, certificateUris))
.connectTimeout(connectTimeout) .connectTimeout(connectTimeout)
.followRedirects(Redirect.NORMAL)
.build(); .build();
} }
@@ -16,7 +16,7 @@ examples {
import("package://localhost:0/birds@0.5.0#/catalog/Ostritch.pkl") import("package://localhost:0/birds@0.5.0#/catalog/Ostritch.pkl")
} }
["importing while specifying checksum"] { ["importing while specifying checksum"] {
import("package://localhost:0/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118#/catalog/Swallow.pkl") import("package://localhost:0/birds@0.5.0::sha256:bfaf5281613d170a740505cc87561041f4e0cad1f0e6938bf94f7609f9a4673d#/catalog/Swallow.pkl")
} }
["reads"] { ["reads"] {
read("package://localhost:0/birds@0.5.0#/Bird.pkl") read("package://localhost:0/birds@0.5.0#/Bird.pkl")
@@ -20,6 +20,6 @@ examples {
import("package://localhost:0/birds@0.5.0#/allFruit.pkl").fruitFiles import("package://localhost:0/birds@0.5.0#/allFruit.pkl").fruitFiles
} }
["glob import while specifying checksum"] { ["glob import while specifying checksum"] {
import*("package://localhost:0/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118#/catalog/*.pkl") import*("package://localhost:0/birds@0.5.0::sha256:bfaf5281613d170a740505cc87561041f4e0cad1f0e6938bf94f7609f9a4673d#/catalog/*.pkl")
} }
} }
@@ -0,0 +1,15 @@
amends ".../snippetTest.pkl"
examples {
["permanent redirect is followed"] {
import("package://localhost:0/HTTP301/birds@0.5.0#/catalog/Swallow.pkl")
}
["temporary redirect is followed"] {
import("package://localhost:0/HTTP307/birds@0.5.0#/catalog/Swallow.pkl")
}
["double redirect is followed"] {
import("package://localhost:0/HTTP301/HTTP307/birds@0.5.0#/catalog/Swallow.pkl")
}
}
@@ -118,13 +118,13 @@ examples {
} }
["glob import while specifying checksum"] { ["glob import while specifying checksum"] {
new { new {
["package://localhost:0/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118#/catalog/Ostritch.pkl"] { ["package://localhost:0/birds@0.5.0::sha256:bfaf5281613d170a740505cc87561041f4e0cad1f0e6938bf94f7609f9a4673d#/catalog/Ostritch.pkl"] {
name = "Ostritch" name = "Ostritch"
favoriteFruit { favoriteFruit {
name = "Orange" name = "Orange"
} }
} }
["package://localhost:0/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118#/catalog/Swallow.pkl"] { ["package://localhost:0/birds@0.5.0::sha256:bfaf5281613d170a740505cc87561041f4e0cad1f0e6938bf94f7609f9a4673d#/catalog/Swallow.pkl"] {
name = "Swallow" name = "Swallow"
favoriteFruit { favoriteFruit {
name = "Apple" name = "Apple"
@@ -0,0 +1,26 @@
examples {
["permanent redirect is followed"] {
new {
name = "Swallow"
favoriteFruit {
name = "Apple"
}
}
}
["temporary redirect is followed"] {
new {
name = "Swallow"
favoriteFruit {
name = "Apple"
}
}
}
["double redirect is followed"] {
new {
name = "Swallow"
favoriteFruit {
name = "Apple"
}
}
}
}
@@ -4,9 +4,12 @@ import org.junit.platform.engine.EngineDiscoveryRequest
import org.junit.platform.engine.TestDescriptor import org.junit.platform.engine.TestDescriptor
import org.junit.platform.engine.UniqueId import org.junit.platform.engine.UniqueId
import org.junit.platform.engine.support.descriptor.EngineDescriptor import org.junit.platform.engine.support.descriptor.EngineDescriptor
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.InputOutputTestEngine import org.pkl.commons.test.InputOutputTestEngine
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.core.http.HttpClient
import org.pkl.core.project.Project import org.pkl.core.project.Project
import org.pkl.core.util.IoUtils
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.nio.file.Files import java.nio.file.Files
@@ -33,6 +36,8 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
//language=regexp //language=regexp
internal val selection: String = "" internal val selection: String = ""
protected val packageServer: PackageServer = PackageServer()
override val includedTests: List<Regex> = listOf(Regex(".*$selection\\.pkl")) override val includedTests: List<Regex> = listOf(Regex(".*$selection\\.pkl"))
override val excludedTests: List<Regex> = listOf(Regex(".*/native/.*")) override val excludedTests: List<Regex> = listOf(Regex(".*/native/.*"))
@@ -41,11 +46,6 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
override val isInputFile: (Path) -> Boolean = { it.isRegularFile() } override val isInputFile: (Path) -> Boolean = { it.isRegularFile() }
protected val cacheDir: Path by lazy {
rootProjectDir.resolve("pkl-core/build/packages-cache")
.also { PackageServer.populateCacheDir(it) }
}
protected tailrec fun Path.getProjectDir(): Path? = protected tailrec fun Path.getProjectDir(): Path? =
if (Files.exists(this.resolve("PklProject"))) this if (Files.exists(this.resolve("PklProject"))) this
else parent?.getProjectDir() else parent?.getProjectDir()
@@ -58,6 +58,15 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
return expectedOutputDir.resolve(stdoutPath) return expectedOutputDir.resolve(stdoutPath)
} }
override fun beforeAll() {
// disable SHA verification for packages
IoUtils.setTestMode()
}
override fun afterAll() {
packageServer.close()
}
protected fun String.stripFilePaths() = replace(snippetsDir.toString(), "/\$snippetsDir") protected fun String.stripFilePaths() = replace(snippetsDir.toString(), "/\$snippetsDir")
protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result -> protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result ->
@@ -92,7 +101,11 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
"name2" to "value2", "name2" to "value2",
"/foo/bar" to "foobar" "/foo/bar" to "foobar"
)) ))
.setModuleCacheDir(cacheDir) .setModuleCacheDir(null)
.setHttpClient(HttpClient.builder()
.setTestPort(packageServer.port)
.addCertificates(FileTestUtils.selfSignedCertificate)
.buildLazily())
} }
override val testClass: KClass<*> = LanguageSnippetTests::class override val testClass: KClass<*> = LanguageSnippetTests::class
@@ -158,8 +171,7 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
val args = buildList { val args = buildList {
add(pklExecutablePath.toString()) add(pklExecutablePath.toString())
add("eval") add("eval")
add("--cache-dir") add("--no-cache")
add(cacheDir.toString())
if (inputFile.startsWith(projectsDir)) { if (inputFile.startsWith(projectsDir)) {
val projectDir = inputFile.getProjectDir() val projectDir = inputFile.getProjectDir()
if (projectDir != null) { if (projectDir != null) {
@@ -187,7 +199,11 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
} }
add("--settings") add("--settings")
add("pkl:settings") add("pkl:settings")
add("--ca-certificates")
add(FileTestUtils.selfSignedCertificate.toString())
add("--test-mode") add("--test-mode")
add("--test-port")
add(packageServer.port.toString())
add(inputFile.toString()) add(inputFile.toString())
} }