fix(jfrog-oauth): fail when access_token is empty (#574)

## Summary

Fixes #72 - The `jfrog-oauth` module now fails with a clear error
message when the JFrog access token is empty, instead of silently
creating configurations with empty tokens.

## Changes

### 1. Added Precondition Validation (`main.tf`)

```hcl
lifecycle {
  precondition {
    condition     = data.coder_external_auth.jfrog.access_token != ""
    error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
  }
}
```

This ensures the module fails at **plan time** with a clear error when
users haven't authenticated via external auth.

### 2. Replaced `main.test.ts` with `jfrog-oauth.tftest.hcl`

**Why we removed the TypeScript tests:**

The TypeScript tests used `runTerraformApply()` which runs `terraform
apply` directly. This approach **cannot mock data sources** like
`coder_external_auth`. The Coder provider returns empty strings for
tokens by default when running outside a real Coder workspace.

With our new precondition, the TypeScript tests would always fail
because:
1. `terraform apply` runs → empty `access_token` from mock provider
2. Precondition check fails → "JFrog access token is empty"
3. Test fails before any assertions run

**The solution:** Terraform's native `.tftest.hcl` format supports
`override_data` blocks that can properly mock data sources:

```hcl
override_data {
  target = data.coder_external_auth.jfrog
  values = {
    access_token = "valid-token-value"  # or "" to test failure
  }
}
```

### 3. Comprehensive Test Coverage

The new `jfrog-oauth.tftest.hcl` includes **12 tests** (up from 7):

| Test | What it validates |
|------|------------------|
| `test_required_vars` | Basic module works with required variables |
| `test_empty_access_token_fails` | **NEW:** Precondition rejects empty
tokens |
| `test_valid_access_token_succeeds` | Module works with valid token |
| `test_jfrog_url_validation` | **NEW:** URL must start with http(s)://
|
| `test_username_field_validation` | **NEW:** Must be "email" or
"username" |
| `test_with_npm_package_manager` | NPM config with scoped repos (script
content) |
| `test_configure_code_server` | **NEW:** IDE env vars created when
enabled |
| `test_go_proxy_env` | GOPROXY env value with multiple repos |
| `test_pypi_package_manager` | pip.conf with extra-index-url |
| `test_docker_package_manager` | register_docker commands for all repos
|
| `test_conda_package_manager` | .condarc channels configuration |
| `test_maven_package_manager` | settings.xml with servers and repos |

All package manager tests use `strcontains()` to verify the actual
script content matches expected configuration formats.

## Test Limitations (Acknowledged)

The tests verify **template rendering** but not **runtime execution**:

|  What we test |  What we don't test |
|----------------|----------------------|
| Configuration file formats | Script syntax errors at runtime |
| Variable interpolation | JFrog CLI compatibility |
| Precondition validation | Actual JFrog authentication |
| Script contains expected content | Commands execute successfully |

**Rationale:** The original TypeScript tests also only checked script
content (`toContain()`), not execution. Full execution testing would
require a mock JFrog server, which adds significant complexity for
limited benefit. The script is straightforward bash that configures
files and runs CLI commands.

## Testing

```bash
cd registry/coder/modules/jfrog-oauth
terraform test
# Success! 12 passed, 0 failed.
```

_Generated with [mux](https://github.com/coder/mux)_
This commit is contained in:
Atif Ali
2025-12-03 00:17:39 +05:00
committed by GitHub
parent 392f6b120a
commit d21db0d220
4 changed files with 409 additions and 191 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
module "jfrog" { module "jfrog" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder" source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.2.3" version = "1.2.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
module "jfrog" { module "jfrog" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder" source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.2.3" version = "1.2.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "email" username_field = "email"
@@ -0,0 +1,400 @@
# Test for jfrog-oauth module
run "test_required_vars" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with valid access token for basic test
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
}
run "test_empty_access_token_fails" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with empty access token
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = ""
}
}
expect_failures = [
resource.coder_script.jfrog
]
}
run "test_valid_access_token_succeeds" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {}
}
# Mock external auth with valid access token
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify the script resource is created
assert {
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
error_message = "coder_script agent_id should match the input variable"
}
assert {
condition = resource.coder_script.jfrog.display_name == "jfrog"
error_message = "coder_script display_name should be 'jfrog'"
}
}
run "test_jfrog_url_validation" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "invalid-url"
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
expect_failures = [
var.jfrog_url
]
}
run "test_username_field_validation" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
username_field = "invalid"
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
expect_failures = [
var.username_field
]
}
run "test_with_npm_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
npm = ["global", "@foo:foo", "@bar:bar"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
assert {
condition = resource.coder_script.jfrog.run_on_start == true
error_message = "coder_script should run on start"
}
# Verify npm configuration is in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
error_message = "script should contain jf npmc command for npm"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
error_message = "script should contain scoped npm registry for @foo"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
error_message = "script should contain scoped npm registry for @bar"
}
}
run "test_configure_code_server" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
configure_code_server = true
package_managers = {}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# When configure_code_server is true, env vars should be created
assert {
condition = length(resource.coder_env.jfrog_ide_url) == 1
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
}
assert {
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
}
}
run "test_go_proxy_env" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
go = ["foo", "bar", "baz"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# When go package manager is configured, GOPROXY env should be set
assert {
condition = length(resource.coder_env.goproxy) == 1
error_message = "coder_env.goproxy should be created when go package manager is configured"
}
# Verify GOPROXY contains all repos
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
error_message = "GOPROXY should contain foo repo"
}
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
error_message = "GOPROXY should contain bar repo"
}
assert {
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
error_message = "GOPROXY should contain baz repo"
}
# Verify script contains go configuration
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
error_message = "script should contain jf goc command"
}
}
run "test_pypi_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
pypi = ["global", "foo", "bar"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify pip configuration in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
error_message = "script should contain jf pipc command"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
error_message = "script should contain pip index-url configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
error_message = "script should contain extra-index-url for additional repos"
}
}
run "test_docker_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify docker registration commands in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
error_message = "script should contain register_docker for foo.jfrog.io"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
error_message = "script should contain register_docker for bar.jfrog.io"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
error_message = "script should contain register_docker for baz.jfrog.io"
}
}
run "test_conda_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
conda = ["conda-main", "conda-secondary", "conda-local"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify conda configuration in script
assert {
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
error_message = "script should contain conda channels configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
error_message = "script should contain conda-main channel"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
error_message = "script should contain conda-secondary channel"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
error_message = "script should contain conda-local channel"
}
}
run "test_maven_package_manager" {
command = plan
variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
maven = ["central", "snapshots", "local"]
}
}
override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}
# Verify maven jf mvnc command
assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
error_message = "script should contain jf mvnc command"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
error_message = "script should contain repo-resolve-releases for central"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
error_message = "script should contain repo-resolve-snapshots for central"
}
# Verify settings.xml content
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
error_message = "script should contain maven servers configuration"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
error_message = "script should contain central server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
error_message = "script should contain snapshots server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
error_message = "script should contain local server id"
}
assert {
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
error_message = "script should contain central repository URL"
}
}
@@ -1,189 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformInit,
runTerraformApply,
testRequiredVariables,
} from "~test";
describe("jfrog-oauth", async () => {
type TestVariables = {
agent_id: string;
jfrog_url: string;
package_managers: string;
username_field?: string;
jfrog_server_id?: string;
external_auth_id?: string;
configure_code_server?: boolean;
};
await runTerraformInit(import.meta.dir);
const fakeFrogApi = "localhost:8081/artifactory/api";
const fakeFrogUrl = "http://localhost:8081";
const user = "default";
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: "{}",
});
it("generates an npmrc with scoped repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
npm: ["global", "@foo:foo", "@bar:bar"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const npmrcStanza = `cat << EOF > ~/.npmrc
email=${user}@example.com
registry=http://${fakeFrogApi}/npm/global
//${fakeFrogApi}/npm/global/:_authToken=
@foo:registry=http://${fakeFrogApi}/npm/foo
//${fakeFrogApi}/npm/foo/:_authToken=
@bar:registry=http://${fakeFrogApi}/npm/bar
//${fakeFrogApi}/npm/bar/:_authToken=
EOF`;
expect(coderScript.script).toContain(npmrcStanza);
expect(coderScript.script).toContain(
'jf npmc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured npm',
);
});
it("generates a pip config with extra-indexes", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
pypi: ["global", "foo", "bar"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const pipStanza = `cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
extra-index-url =
https://${user}:@${fakeFrogApi}/pypi/foo/simple
https://${user}:@${fakeFrogApi}/pypi/bar/simple
EOF`;
expect(coderScript.script).toContain(pipStanza);
expect(coderScript.script).toContain(
'jf pipc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured pypi',
);
});
it("registers multiple docker repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const dockerStanza = ["foo", "bar", "baz"]
.map((r) => `register_docker "${r}.jfrog.io"`)
.join("\n");
expect(coderScript.script).toContain(dockerStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured docker',
);
});
it("sets goproxy with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
go: ["foo", "bar", "baz"],
}),
});
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
const proxies = ["foo", "bar", "baz"]
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
.join(",");
expect(proxyEnv.value).toEqual(proxies);
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain(
'jf goc --global --repo-resolve "foo"',
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:@${fakeFrogApi}/conda/conda-main
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
- https://${user}:@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
it("generates a maven settings.xml with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
maven: ["central", "snapshots", "local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain("jf mvnc --global");
expect(coderScript.script).toContain('--server-id-resolve="0"');
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
expect(coderScript.script).toContain('--server-id-deploy="0"');
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
expect(coderScript.script).toContain("<servers>");
expect(coderScript.script).toContain("<id>central</id>");
expect(coderScript.script).toContain("<id>snapshots</id>");
expect(coderScript.script).toContain("<id>local</id>");
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/central</url>",
);
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/snapshots</url>",
);
expect(coderScript.script).toContain(
"<url>http://localhost:8081/artifactory/local</url>",
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured maven',
);
});
});
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
} }
)) ))
run_on_start = true run_on_start = true
lifecycle {
precondition {
condition = data.coder_external_auth.jfrog.access_token != ""
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
}
}
} }
resource "coder_env" "jfrog_ide_url" { resource "coder_env" "jfrog_ide_url" {