Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
kotlin-tests:
runs-on: ubuntu-latest
env:
DEVELOCITY_API_URL: "${{ vars.DEVELOCITY_API_URL }}"
DEVELOCITY_URL: "${{ vars.DEVELOCITY_URL }}"
DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}"
DEVELOCITY_API_CACHE_ENABLED: "false"
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-library.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
env:
DEVELOCITY_API_URL: "${{ vars.DEVELOCITY_API_URL }}"
DEVELOCITY_URL: "${{ vars.DEVELOCITY_URL }}"
DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}"
steps:
- name: Checkout
Expand Down
12 changes: 6 additions & 6 deletions library/api/library.api
Original file line number Diff line number Diff line change
Expand Up @@ -78,25 +78,25 @@ public final class com/gabrielfeo/develocity/api/BuildsApi$DefaultImpls {

public final class com/gabrielfeo/develocity/api/Config {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component2 ()Ljava/net/URI;
public final fun component3 ()Lkotlin/jvm/functions/Function0;
public final fun component4 ()Lokhttp3/OkHttpClient$Builder;
public final fun component5 ()Ljava/lang/Integer;
public final fun component6 ()J
public final fun component7 ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)Lcom/gabrielfeo/develocity/api/Config;
public static synthetic fun copy$default (Lcom/gabrielfeo/develocity/api/Config;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILjava/lang/Object;)Lcom/gabrielfeo/develocity/api/Config;
public final fun copy (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)Lcom/gabrielfeo/develocity/api/Config;
public static synthetic fun copy$default (Lcom/gabrielfeo/develocity/api/Config;Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILjava/lang/Object;)Lcom/gabrielfeo/develocity/api/Config;
public fun equals (Ljava/lang/Object;)Z
public final fun getAccessKey ()Lkotlin/jvm/functions/Function0;
public final fun getApiUrl ()Ljava/lang/String;
public final fun getCacheConfig ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
public final fun getClientBuilder ()Lokhttp3/OkHttpClient$Builder;
public final fun getLogLevel ()Ljava/lang/String;
public final fun getMaxConcurrentRequests ()Ljava/lang/Integer;
public final fun getReadTimeoutMillis ()J
public final fun getServer ()Ljava/net/URI;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.gabrielfeo.develocity.api
import com.gabrielfeo.develocity.api.internal.*
import com.google.common.reflect.ClassPath
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import org.junit.jupiter.api.assertDoesNotThrow
import java.net.URI
import kotlin.reflect.KVisibility.PUBLIC
import kotlin.reflect.full.memberProperties
import kotlin.reflect.javaType
Expand Down Expand Up @@ -35,8 +35,8 @@ class DevelocityApiIntegrationTest {
env = FakeEnv()
assertDoesNotThrow {
val config = Config(
apiUrl = "https://google.com/api/",
accessKey = { "" },
server = URI("https://example.com/"),
accessKey = { "example.com=example-token" }
)
DevelocityApi.newInstance(config)
}
Expand Down
50 changes: 29 additions & 21 deletions library/src/main/kotlin/com/gabrielfeo/develocity/api/Config.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.gabrielfeo.develocity.api

import com.gabrielfeo.develocity.api.internal.auth.accessKeyResolver
import com.gabrielfeo.develocity.api.internal.basicOkHttpClient
import com.gabrielfeo.develocity.api.internal.env
import com.gabrielfeo.develocity.api.internal.systemProperties
import com.gabrielfeo.develocity.api.internal.*
import com.gabrielfeo.develocity.api.internal.auth.*
import okhttp3.Dispatcher
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import java.io.File
import java.net.URI
Expand Down Expand Up @@ -44,25 +43,28 @@ data class Config(
?: "off",

/**
* Provides the URL of a Develocity API instance REST API. By default, uses
* environment variable `DEVELOCITY_API_URL`. Must end with `/api/`.
* Provides the URL of a Develocity server to use in API requests. By default, uses environment
* variable `DEVELOCITY_URL`. Must be a valid URL with no path segments (trailing slash OK) or
* query parameters.
*
* Example value: `https://develocity.example.com/`
*/
val apiUrl: String =
env["DEVELOCITY_API_URL"]
?.also { requireValidUrl(it) }
?: error(ERROR_NULL_API_URL),
val server: URI =
requireNotNull(env["DEVELOCITY_URL"]?.let(::URI)) { ERROR_NULL_DEVELOCITY_URL },

/**
* Provides the access key for a Develocity API instance. By default, resolves to the first
* key from these sources that matches the host of [apiUrl]:
* Provides the access key for the Develocity server. By default, resolves to the first key from
* these sources that matches the host of [server]:
*
* - variable `DEVELOCITY_ACCESS_KEY`
* - variable `GRADLE_ENTERPRISE_ACCESS_KEY`
* - file `$GRADLE_USER_HOME/.gradle/develocity/keys.properties` or, if `GRADLE_USER_HOME` is
* not set, `~/.gradle/develocity/keys.properties`
* - file `~/.m2/.develocity/keys.properties`
*
* Refer to Develocity documentation for details on the format of such variables and files:
* Example value: `develocity.example.com=abcdefg1234567`
*
* Refer to Develocity documentation for more details on the format of such variables and files:
*
* - [Develocity Gradle Plugin User Manual][1]
* - [Develocity Maven Extension User Manual][2]
Expand All @@ -73,8 +75,7 @@ data class Config(
* @throws IllegalArgumentException if no matching key is found.
*/
val accessKey: () -> String = {
val host = URI(apiUrl).host
requireNotNull(accessKeyResolver.resolve(host)) { ERROR_NULL_ACCESS_KEY }
requireNotNull(accessKeyResolver.resolve(server.host)) { ERROR_NULL_ACCESS_KEY }
},

/**
Expand Down Expand Up @@ -118,6 +119,10 @@ data class Config(
CacheConfig(),
) {

init {
requireValidBaseUrl(server)
}

/**
* HTTP cache is off by default, but can speed up requests significantly. The Develocity
* API disallows HTTP caching, but this library forcefully enables it by overwriting
Expand Down Expand Up @@ -236,14 +241,17 @@ data class Config(
)
}

private fun requireValidUrl(string: String) {
requireNotNull(runCatching { URI(string) }.getOrNull()) {
ERROR_MALFORMED_API_URL.format(string)
}

private fun requireValidBaseUrl(url: URI) {
require(url.scheme == "http" || url.scheme == "https") { ERROR_MALFORMED_DEVELOCITY_URL.format(url) }
require(url.path.isNullOrEmpty() || url.path == "/") { ERROR_MALFORMED_DEVELOCITY_URL.format(url) }
require(url.query == null) { ERROR_MALFORMED_DEVELOCITY_URL.format(url) }
requireNotNull(url.toHttpUrlOrNull()) { ERROR_MALFORMED_DEVELOCITY_URL.format(url) }
}

private const val ERROR_NULL_API_URL = "DEVELOCITY_API_URL is required"
private const val ERROR_MALFORMED_API_URL = "DEVELOCITY_API_URL contains a malformed URL: %s"
private const val ERROR_NULL_DEVELOCITY_URL = "DEVELOCITY_URL is required"
private const val ERROR_MALFORMED_DEVELOCITY_URL = "DEVELOCITY_URL must be a valid HTTP or HTTPS " +
"URL to a Develocity server, with no path or query parameters: %s"
private const val ERROR_NULL_ACCESS_KEY = "Develocity access key not found. " +
"Please set DEVELOCITY_ACCESS_KEY='[host]=[accessKey]' or see Config.accessKey javadoc for " +
"other supported options."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.gabrielfeo.develocity.api.internal

import com.gabrielfeo.develocity.api.Config
import com.gabrielfeo.develocity.api.*
import com.squareup.moshi.Moshi
import okhttp3.OkHttpClient
import retrofit2.Retrofit
Expand All @@ -12,10 +12,7 @@ internal fun buildRetrofit(
client: OkHttpClient,
moshi: Moshi,
) = with(Retrofit.Builder()) {
val url = config.apiUrl
check("/api/" in url) { "A valid API URL must end in /api/" }
val instanceUrl = url.substringBefore("api/")
baseUrl(instanceUrl)
baseUrl(config.server.resolve("/").toString())
addConverterFactory(ScalarsConverterFactory.create())
addConverterFactory(MoshiConverterFactory.create(moshi))
client(client)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.gabrielfeo.develocity.api

import com.gabrielfeo.develocity.api.internal.*
import kotlin.test.*

class CacheConfigTest {

@BeforeTest
fun before() {
env = FakeEnv("DEVELOCITY_API_URL" to "https://example.com/api/")
}

@Test
fun `default longTermCacheUrlPattern matches attributes URLs`() {
Config.CacheConfig().longTermCacheUrlPattern.assertMatches(
Expand Down
66 changes: 44 additions & 22 deletions library/src/test/kotlin/com/gabrielfeo/develocity/api/ConfigTest.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
package com.gabrielfeo.develocity.api

import com.gabrielfeo.develocity.api.internal.FakeEnv
import com.gabrielfeo.develocity.api.internal.FakeSystemProperties
import com.gabrielfeo.develocity.api.internal.auth.AccessKeyResolver
import com.gabrielfeo.develocity.api.internal.auth.accessKeyResolver
import com.gabrielfeo.develocity.api.internal.env
import com.gabrielfeo.develocity.api.internal.systemProperties
import com.gabrielfeo.develocity.api.internal.*
import com.gabrielfeo.develocity.api.internal.auth.*
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.assertDoesNotThrow
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import java.net.URI
import kotlin.test.*

class ConfigTest {

@BeforeTest
fun before() {
env = FakeEnv("DEVELOCITY_API_URL" to "https://example.com/api/")
env = FakeEnv("DEVELOCITY_URL" to "https://example.com/")
systemProperties = FakeSystemProperties()
accessKeyResolver = AccessKeyResolver(
env,
Expand All @@ -30,38 +26,64 @@ class ConfigTest {
@Test
fun `Given no URL set in env, error`() {
env = FakeEnv()
assertFails {
assertFailsWith<IllegalArgumentException> {
Config()
}
}

@Test
fun `Given URL set in env, apiUrl is env URL`() {
(env as FakeEnv)["DEVELOCITY_API_URL"] = "https://example.com/api/"
assertEquals("https://example.com/api/", Config().apiUrl)
fun `Given server URL set in env, server is correct URL`() {
(env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/"
assertEquals(URI("https://example.com/"), Config().server)
}

@Test
fun `Given server URL set in code, server is correct URL`() {
val config = Config(server = URI("https://custom.example.com/"))
assertEquals(URI("https://custom.example.com/"), config.server)
}

@TestFactory
fun `Given malformed URL, error`() = listOf(
"mailto:[email protected]",
"file:///example/foo",
"http://example.com?foo",
"https://example.com?foo",
"https://example.com/foo",
"https://example.com/foo?bar=1",
"https://example.com/foo/bar/baz",
"https://example.com/foo/bar/baz?qux=1",
).map { url ->
dynamicTest(url) {
assertFailsWith<IllegalArgumentException> {
Config(server = URI(url))
}
}
}

@Test
fun `Given default access key function and resolvable key, accessKey is key`() {
(env as FakeEnv)["DEVELOCITY_API_URL"] = "https://example.com/api/"
(env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/"
(env as FakeEnv)["DEVELOCITY_ACCESS_KEY"] = "example.com=foo"
assertEquals("foo", Config().accessKey())
}

@Test
fun `Given default access key and no resolvable key, error`() {
(env as FakeEnv)["DEVELOCITY_API_URL"] = "https://example.com/api/"
fun `Given default access key function and no resolvable key, error`() {
(env as FakeEnv)["DEVELOCITY_URL"] = "https://example.com/"
(env as FakeEnv)["DEVELOCITY_ACCESS_KEY"] = "notexample.com=foo"
assertFails {
assertFailsWith<IllegalArgumentException> {
Config().accessKey()
}
}

@Test
fun `Given custom access key function fails, error`() {
assertFails {
Config(accessKey = { error("foo") }).accessKey()
fun `Given custom access key function fails, uncaught and unwrapped error`() {
val error = assertFails {
Config(accessKey = { throw RuntimeException("foo") }).accessKey()
}
assertIs<RuntimeException>(error)
assertEquals("foo", error.message)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package com.gabrielfeo.develocity.api
import com.gabrielfeo.develocity.api.internal.*
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.*

class DevelocityApiTest {

Expand All @@ -14,12 +13,12 @@ class DevelocityApiTest {
val error = assertThrows<Exception> {
DevelocityApi.newInstance(Config())
}
error.assertRootMessageContains("DEVELOCITY_API_URL")
error.assertRootMessageContains("DEVELOCITY_URL")
}

@Test
fun `Fails lazily if no access key`() {
env = FakeEnv("DEVELOCITY_API_URL" to "https://example.com/api/")
env = FakeEnv("DEVELOCITY_URL" to "https://example.com/")
val api = assertDoesNotThrow {
DevelocityApi.newInstance(Config())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ class OkHttpClientTest {
val fakeEnv = FakeEnv(*envVars)
if ("DEVELOCITY_ACCESS_KEY" !in fakeEnv)
fakeEnv["DEVELOCITY_ACCESS_KEY"] = "example.com=example-token"
if ("DEVELOCITY_API_URL" !in fakeEnv)
fakeEnv["DEVELOCITY_API_URL"] = "https://example.com/api/"
if ("DEVELOCITY_URL" !in fakeEnv)
fakeEnv["DEVELOCITY_URL"] = "https://example.com/"
env = fakeEnv
systemProperties = FakeSystemProperties()
accessKeyResolver = AccessKeyResolver(
Expand Down
Loading
Loading