mirror of
https://github.com/apple/pkl.git
synced 2026-06-10 15:52:50 +02:00
d012285f7d
This implements HTTP redirect following ourselves. The goal is: 1. All I/O is checked against `--allowed-resources` and `--allowed-modules`, including HTTP redirects 2. HTTP rewrite rules can affect redirect following 3. HTTP headers can affect redirect following --------- Co-authored-by: Islon Scherer <islonscherer@gmail.com>
500 lines
13 KiB
Kotlin
500 lines
13 KiB
Kotlin
/*
|
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
|
*
|
|
* 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 org.pkl.cli
|
|
|
|
import java.io.File
|
|
import java.io.StringWriter
|
|
import java.nio.file.Path
|
|
import kotlin.io.path.createDirectories
|
|
import org.assertj.core.api.Assertions.assertThat
|
|
import org.assertj.core.api.Assertions.assertThatCode
|
|
import org.junit.jupiter.api.AfterAll
|
|
import org.junit.jupiter.api.Test
|
|
import org.junit.jupiter.api.assertThrows
|
|
import org.junit.jupiter.api.io.TempDir
|
|
import org.pkl.commons.cli.CliBaseOptions
|
|
import org.pkl.commons.cli.CliException
|
|
import org.pkl.commons.test.FileTestUtils
|
|
import org.pkl.commons.test.PackageServer
|
|
import org.pkl.commons.writeString
|
|
import org.pkl.core.util.IoUtils
|
|
|
|
class CliProjectResolverTest {
|
|
companion object {
|
|
private val packageServer = PackageServer()
|
|
|
|
@AfterAll
|
|
@JvmStatic
|
|
fun afterAll() {
|
|
packageServer.close()
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `missing PklProject when inferring a project dir`(@TempDir tempDir: Path) {
|
|
val packager =
|
|
CliProjectResolver(
|
|
CliBaseOptions(workingDir = tempDir, noCache = true),
|
|
emptyList(),
|
|
consoleWriter = StringWriter(),
|
|
errWriter = StringWriter(),
|
|
)
|
|
val err = assertThrows<CliException> { packager.run() }
|
|
assertThat(err).hasMessageStartingWith("No project visible to the working directory.")
|
|
}
|
|
|
|
@Test
|
|
fun `missing PklProject when explicit dir is provided`(@TempDir tempDir: Path) {
|
|
val packager =
|
|
CliProjectResolver(
|
|
CliBaseOptions(noCache = true),
|
|
listOf(tempDir),
|
|
consoleWriter = StringWriter(),
|
|
errWriter = StringWriter(),
|
|
)
|
|
val err = assertThrows<CliException> { packager.run() }
|
|
assertThat(err).hasMessageStartingWith("Directory $tempDir does not contain a PklProject file.")
|
|
}
|
|
|
|
@Test
|
|
fun `basic project`(@TempDir tempDir: Path) {
|
|
tempDir.writeFile(
|
|
"PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
workingDir = tempDir,
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
listOf(tempDir),
|
|
consoleWriter = StringWriter(),
|
|
errWriter = StringWriter(),
|
|
)
|
|
.run()
|
|
val expectedOutput = tempDir.resolve("PklProject.deps.json")
|
|
assertThat(expectedOutput)
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/birds@0": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/birds@0.5.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.BIRDS_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.0.5",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_SHA}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `basic project, inferred from working dir`(@TempDir tempDir: Path) {
|
|
tempDir.writeFile(
|
|
"PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
workingDir = tempDir,
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
emptyList(),
|
|
consoleWriter = StringWriter(),
|
|
errWriter = StringWriter(),
|
|
)
|
|
.run()
|
|
val expectedOutput = tempDir.resolve("PklProject.deps.json")
|
|
assertThat(expectedOutput)
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/birds@0": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/birds@0.5.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.BIRDS_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.0.5",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_SHA}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `local dependencies`(@TempDir tempDir: Path) {
|
|
val projectDir = tempDir.resolve("theproject")
|
|
projectDir.writeFile(
|
|
"PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
["project2"] = import("../project2/PklProject")
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
projectDir.writeFile(
|
|
"../project2/PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
package {
|
|
name = "project2"
|
|
baseUri = "package://localhost:0/package2"
|
|
version = "5.0.0"
|
|
packageZipUrl = "https://foo.com/package2.zip"
|
|
}
|
|
|
|
dependencies {
|
|
["fruit"] {
|
|
uri = "package://localhost:0/fruit@1.0.5"
|
|
}
|
|
["project3"] = import("../project3/PklProject")
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
projectDir.writeFile(
|
|
"../project3/PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
package {
|
|
name = "project3"
|
|
baseUri = "package://localhost:0/package3"
|
|
version = "5.0.0"
|
|
packageZipUrl = "https://foo.com/package3.zip"
|
|
}
|
|
|
|
dependencies {
|
|
["fruit"] {
|
|
uri = "package://localhost:0/fruit@1.1.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
listOf(projectDir),
|
|
consoleWriter = StringWriter(),
|
|
errWriter = StringWriter(),
|
|
)
|
|
.run()
|
|
val expectedOutput = projectDir.resolve("PklProject.deps.json")
|
|
assertThat(expectedOutput)
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/birds@0": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/birds@0.5.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.BIRDS_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.1.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_1_1_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/package2@5": {
|
|
"type": "local",
|
|
"uri": "projectpackage://localhost:0/package2@5.0.0",
|
|
"path": "../project2"
|
|
},
|
|
"package://localhost:0/package3@5": {
|
|
"type": "local",
|
|
"uri": "projectpackage://localhost:0/package3@5.0.0",
|
|
"path": "../project3"
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `local dependency overridden by remote dependency`(@TempDir tempDir: Path) {
|
|
val projectDir = tempDir.resolve("theproject")
|
|
projectDir.writeFile(
|
|
"PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
["fruit"] = import("../fruit/PklProject")
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
projectDir.writeFile(
|
|
"../fruit/PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
package {
|
|
name = "fruit"
|
|
baseUri = "package://localhost:0/fruit"
|
|
version = "1.0.0"
|
|
packageZipUrl = "https://foo.com/fruit.zip"
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
val consoleOut = StringWriter()
|
|
val errOut = StringWriter()
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
listOf(projectDir),
|
|
consoleWriter = consoleOut,
|
|
errWriter = errOut,
|
|
)
|
|
.run()
|
|
val expectedOutput = projectDir.resolve("PklProject.deps.json")
|
|
assertThat(expectedOutput)
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/birds@0": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/birds@0.5.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.BIRDS_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.0.5",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_SHA}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
assertThat(errOut.toString())
|
|
.isEqualTo(
|
|
"WARN: local dependency `package://localhost:0/fruit@1.0.0` was overridden to remote dependency `package://localhost:0/fruit@1.0.5`.${IoUtils.getLineSeparator()}"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `resolving multiple projects`(@TempDir tempDir: Path) {
|
|
tempDir.writeFile(
|
|
"project1/PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
tempDir.writeFile(
|
|
"project2/PklProject",
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["fruit"] {
|
|
uri = "package://localhost:0/fruit@1.1.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent(),
|
|
)
|
|
|
|
val consoleOut = StringWriter()
|
|
val errOut = StringWriter()
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
listOf(tempDir.resolve("project1"), tempDir.resolve("project2")),
|
|
consoleWriter = consoleOut,
|
|
errWriter = errOut,
|
|
)
|
|
.run()
|
|
val sep = File.separatorChar
|
|
assertThat(consoleOut.toString())
|
|
.isEqualToNormalizingNewlines(
|
|
"""
|
|
$tempDir${sep}project1${sep}PklProject.deps.json
|
|
$tempDir${sep}project2${sep}PklProject.deps.json
|
|
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
assertThat(tempDir.resolve("project1/PklProject.deps.json"))
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/birds@0": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/birds@0.5.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.BIRDS_SHA}"
|
|
}
|
|
},
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.0.5",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_SHA}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
assertThat(tempDir.resolve("project2/PklProject.deps.json"))
|
|
.hasContent(
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"resolvedDependencies": {
|
|
"package://localhost:0/fruit@1": {
|
|
"type": "remote",
|
|
"uri": "projectpackage://localhost:0/fruit@1.1.0",
|
|
"checksums": {
|
|
"sha256": "${PackageServer.FRUIT_1_1_SHA}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `IOException when writing to PklProject-deps-json`(@TempDir tempDir: Path) {
|
|
val consoleOut = StringWriter()
|
|
val errOut = StringWriter()
|
|
tempDir
|
|
.resolve("PklProject")
|
|
.writeString(
|
|
"""
|
|
amends "pkl:Project"
|
|
|
|
dependencies {
|
|
["birds"] {
|
|
uri = "package://localhost:0/birds@0.5.0"
|
|
}
|
|
}
|
|
"""
|
|
.trimIndent()
|
|
)
|
|
// coerce an IOException by making this a directory
|
|
val depsJsonFile = tempDir.resolve("PklProject.deps.json").also { it.createDirectories() }
|
|
assertThatCode {
|
|
CliProjectResolver(
|
|
CliBaseOptions(
|
|
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
|
testPort = packageServer.port,
|
|
noCache = true,
|
|
),
|
|
listOf(tempDir),
|
|
consoleWriter = consoleOut,
|
|
errWriter = errOut,
|
|
)
|
|
.run()
|
|
}
|
|
.hasMessageContaining("Failed to write to $depsJsonFile")
|
|
}
|
|
}
|