- name
- databricks-isv-testing
- description
- How to run and structure auth tests for Databricks ISV integrations: clean env per test, runner script patterns, U2M browser flows, and language-specific considerations.
<!-- skill-version: 1.0.0 -->
# Testing Auth for ISV Integrations
Use this skill when **running or designing tests** for Databricks partner authentication (PAT, OAuth M2M, U2M).
## Core Principle: Clean Environment Per Test
**Never run multiple auth types in one process** with mixed environment variables. SDKs and drivers error with "more than one authorization method configured" if conflicting credentials are set.
### Option A: `env -i` (Automated Test Runners)
```bash
env -i PATH="$PATH" HOME="$HOME" \
DATABRICKS_HOST="https://myworkspace.cloud.databricks.com" \
DATABRICKS_TOKEN="dapi..." \
python your_pat_example.py
```
**Caveat:** In long interactive sessions, `env -i` may strip needed vars like `PYTHONPATH`. Use Option B for manual testing.
### Option B: Fresh Bash Subshell (Interactive Testing)
```bash
/bin/bash -c '
export DATABRICKS_HOST="https://myworkspace.cloud.databricks.com"
export DATABRICKS_TOKEN="dapi..."
unset DATABRICKS_CLIENT_ID DATABRICKS_CLIENT_SECRET
export PYTHONUNBUFFERED=1
python your_pat_example.py
'
```
Starts a completely clean child process. More reliable for interactive manual testing.
---
## Environment Variables by Auth Type
| Auth Type | Selector Value | Required Variables | Must NOT Be Set |
|-----------|----------------|--------------------|-----------------|
| PAT | `pat` | `DATABRICKS_HOST`, `DATABRICKS_TOKEN` | `CLIENT_ID`, `CLIENT_SECRET` |
| OAuth M2M | `oauth_m2m` | `DATABRICKS_HOST`, `DATABRICKS_CLIENT_ID`, `DATABRICKS_CLIENT_SECRET` | `DATABRICKS_TOKEN` |
| U2M External Browser | `u2m_external_browser` | `DATABRICKS_HOST` | `CLIENT_ID`, `CLIENT_SECRET`, `TOKEN` |
| U2M Custom OAuth App | `u2m_custom_oauth_app` | `DATABRICKS_HOST`, `DATABRICKS_U2M_CLIENT_ID` | `DATABRICKS_CLIENT_ID` |
| U2M Token-Env | `u2m_token_env` | `DATABRICKS_HOST`, `DATABRICKS_ACCESS_TOKEN` | `CLIENT_ID`, `CLIENT_SECRET` |
**Optional for all:** `DATABRICKS_HTTP_PATH` (SQL drivers), `DATABRICKS_WAREHOUSE_ID` (Statement Execution API).
**Optional for U2M Custom OAuth App:** `DATABRICKS_U2M_CLIENT_SECRET` (if confidential app), `DATABRICKS_REDIRECT_URI` (default varies by connector).
### CLIENT_ID Distinction (Critical)
| Env Var | Auth Type | What It Is |
|---------|-----------|------------|
| `DATABRICKS_CLIENT_ID` | `oauth_m2m` | M2M Service Principal UUID |
| `DATABRICKS_U2M_CLIENT_ID` | `u2m_custom_oauth_app` | Custom OAuth App from App Connections |
Using the wrong one causes `"OAuth application with client_id not available"`.
---
## Test Runner Script Pattern
A well-structured test runner:
1. **Sources credentials once** from an env file
2. **Runs each test with isolated env** using `env -i` plus only required vars
3. **Records pass/fail** per test
4. **Prints summary** at the end
### Example Runner Structure
```bash
#!/usr/bin/env bash
set -euo pipefail
# Load credentials
source "${1:-credentials.env}"
PASSED=0
FAILED=0
run_test() {
local name="$1"
local cmd="$2"
echo "Running: $name"
if eval "$cmd" > /dev/null 2>&1; then
echo " PASS"
((PASSED++))
else
echo " FAIL"
((FAILED++))
fi
}
# PAT Test
run_test "PAT" "env -i PATH=\"$PATH\" HOME=\"$HOME\" \
DATABRICKS_HOST=\"$DATABRICKS_HOST\" \
DATABRICKS_TOKEN=\"$DATABRICKS_TOKEN\" \
python your_pat_example.py"
# OAuth M2M Test
run_test "OAuth M2M" "env -i PATH=\"$PATH\" HOME=\"$HOME\" \
DATABRICKS_HOST=\"$DATABRICKS_HOST\" \
DATABRICKS_CLIENT_ID=\"$DATABRICKS_CLIENT_ID\" \
DATABRICKS_CLIENT_SECRET=\"$DATABRICKS_CLIENT_SECRET\" \
python your_m2m_example.py"
# Summary
echo ""
echo "Results: $PASSED passed, $FAILED failed"
```
---
## U2M Browser Test Considerations
### Built-in OAuth App (External Browser)
- Do NOT pass M2M credentials (`DATABRICKS_CLIENT_ID`/`DATABRICKS_CLIENT_SECRET`)
- The SDK uses the built-in `databricks-cli` OAuth app
- Redirect URI is typically `http://localhost:8020`
```bash
env -i PATH="$PATH" HOME="$HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
USER="$USER" DISPLAY="${DISPLAY:-}" \
python your_u2m_browser_example.py
```
### Custom OAuth App (PKCE)
- Use `DATABRICKS_U2M_CLIENT_ID` (NOT `DATABRICKS_CLIENT_ID`)
- Optionally set `DATABRICKS_U2M_CLIENT_SECRET` and `DATABRICKS_REDIRECT_URI`
- Default redirect URI is typically `http://localhost:8040/callback`
```bash
env -i PATH="$PATH" HOME="$HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_U2M_CLIENT_ID="$DATABRICKS_U2M_CLIENT_ID" \
DATABRICKS_U2M_CLIENT_SECRET="$DATABRICKS_U2M_CLIENT_SECRET" \
DATABRICKS_REDIRECT_URI="http://localhost:8040/callback" \
USER="$USER" DISPLAY="${DISPLAY:-}" \
python your_u2m_custom_app_example.py
```
### Port Cleanup Before U2M Tests
Before running custom OAuth app tests, kill any process holding the redirect port:
```bash
lsof -ti:8040 | xargs kill -9 2>/dev/null || true
```
A previous hung test can leave the port bound.
---
## HTTPServer Pitfalls (PKCE Flow Implementation)
When implementing the local callback server for PKCE:
### Python Implementation Pattern
```python
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
class CallbackHandler(BaseHTTPRequestHandler):
auth_code = None
callback_received = threading.Event()
def do_GET(self):
if "/callback" in self.path:
# Extract code from query params
from urllib.parse import urlparse, parse_qs
query = parse_qs(urlparse(self.path).query)
CallbackHandler.auth_code = query.get("code", [None])[0]
self.send_response(200)
self.end_headers()
self.wfile.write(b"Authentication complete. You can close this tab.")
CallbackHandler.callback_received.set()
else:
# Ignore other requests (favicon, etc.)
self.send_response(204)
self.end_headers()
def log_message(self, format, *args):
pass # Suppress logging
def run_callback_server(port=8040, timeout=120):
server = HTTPServer(("localhost", port), CallbackHandler)
server.timeout = timeout # CRITICAL: Set timeout
# Handle requests until callback received or timeout
while not CallbackHandler.callback_received.is_set():
server.handle_request()
server.server_close() # Use server_close(), NOT shutdown()
return CallbackHandler.auth_code
```
**Key Points:**
- **Always set `server.timeout`** — Without it, `handle_request()` blocks forever
- **Use `server.server_close()`** — NOT `server.shutdown()` (which hangs with `handle_request()`)
- **Handle multiple requests** — Browser may send favicon requests before the callback
- **Use `threading.Event`** — To signal when the actual callback is received
---
## Language-Specific Notes
### Python
```bash
# PAT
env -i PATH="$PATH" HOME="$HOME" PYTHONUNBUFFERED=1 \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_TOKEN="$DATABRICKS_TOKEN" \
python your_example.py
# OAuth M2M
env -i PATH="$PATH" HOME="$HOME" PYTHONUNBUFFERED=1 \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_CLIENT_ID="$DATABRICKS_CLIENT_ID" \
DATABRICKS_CLIENT_SECRET="$DATABRICKS_CLIENT_SECRET" \
python your_example.py
# U2M (browser)
env -i PATH="$PATH" HOME="$HOME" USER="$USER" DISPLAY="${DISPLAY:-}" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_U2M_CLIENT_ID="$DATABRICKS_U2M_CLIENT_ID" \
python your_example.py
```
### Java
- **Environment isolation:** Run with `env -i` to prevent Java SDK auto-reading conflicting env vars
- **Java 17+ JDBC:** Add `--add-opens=java.base/java.nio=ALL-UNNAMED` to JVM args for Arrow-based OSS JDBC driver
- **SDK Workarounds (U2M):** Must call `config.setScopes(Arrays.asList("all-apis"))` and `config.resolve()` before `config.authenticate()`
```bash
export MAVEN_OPTS="--add-opens=java.base/java.nio=ALL-UNNAMED"
# PAT
env -i PATH="$PATH" HOME="$HOME" JAVA_HOME="$JAVA_HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_TOKEN="$DATABRICKS_TOKEN" \
mvn -q exec:java -Dexec.mainClass="com.example.PatExample"
# OAuth M2M
env -i PATH="$PATH" HOME="$HOME" JAVA_HOME="$JAVA_HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_CLIENT_ID="$DATABRICKS_CLIENT_ID" \
DATABRICKS_CLIENT_SECRET="$DATABRICKS_CLIENT_SECRET" \
mvn -q exec:java -Dexec.mainClass="com.example.M2MExample"
```
### Go
- **Go SDK uses `APP_AUTH_TYPE`:** The SDK reads `DATABRICKS_AUTH_TYPE` internally, so use `APP_AUTH_TYPE` for your auth selector
- **Go SQL Driver uses `DATABRICKS_AUTH_TYPE`:** Safe to use as the selector
- **Build once, run with `env -i`:** Build the binary first, then run with isolated env
```bash
# Build once (example for Go SDK with build tags)
go build -tags all_auth_example -o ./my_example ./
# PAT (Go SDK uses APP_AUTH_TYPE)
env -i PATH="$PATH" HOME="$HOME" \
APP_AUTH_TYPE=pat \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_TOKEN="$DATABRICKS_TOKEN" \
./my_example
# OAuth M2M
env -i PATH="$PATH" HOME="$HOME" \
APP_AUTH_TYPE=oauth_m2m \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_CLIENT_ID="$DATABRICKS_CLIENT_ID" \
DATABRICKS_CLIENT_SECRET="$DATABRICKS_CLIENT_SECRET" \
./my_example
# U2M (browser) - needs USER and DISPLAY
env -i PATH="$PATH" HOME="$HOME" USER="$USER" DISPLAY="${DISPLAY:-}" \
APP_AUTH_TYPE=u2m_custom_oauth_app \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_U2M_CLIENT_ID="$DATABRICKS_U2M_CLIENT_ID" \
./my_example
```
### Node.js
```bash
# PAT
env -i PATH="$PATH" HOME="$HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_HTTP_PATH="$DATABRICKS_HTTP_PATH" \
DATABRICKS_TOKEN="$DATABRICKS_TOKEN" \
node your_example.js
# OAuth M2M
env -i PATH="$PATH" HOME="$HOME" \
DATABRICKS_HOST="$DATABRICKS_HOST" \
DATABRICKS_HTTP_PATH="$DATABRICKS_HTTP_PATH" \
DATABRICKS_CLIENT_ID="$DATABRICKS_CLIENT_ID" \
DATABRICKS_CLIENT_SECRET="$DATABRICKS_CLIENT_SECRET" \
node your_example.js
```
---
## Multi-Auth Example Pattern
Support multiple auth types via an environment variable selector:
```python
import os
auth_type = os.environ.get("APP_AUTH_TYPE", "oauth_m2m")
View on GitHub