Test Scripts¶
Check from the command line that the MetaMiner Server (MMS) API answers, that your API client token works, and that the Tableau Credentials API rejects bad requests; steps that change anything run only when you ask.
What you will build¶
A PowerShell script, Test-MetaMinerApi.ps1, that runs a set of API calls and prints PASS or FAIL for each, plus the same checks as curl commands. About 15 minutes.
By default every check is read-only, or a request that MMS rejects before it changes anything. Two steps change things and run only when you ask:
| Switch | What it really changes |
|---|---|
-ChangeCredentials |
The Tableau Server login MetaMiner uses for the environment, on every node of a high-availability pair |
-RotateToken |
The token of the API client you test with; the old token stops working at once |
Run the opt-in steps on a test server first
-ChangeCredentials validates the login with Tableau, then applies and saves it. -RotateToken prints the new token once; store it before you close the window.
1. Prepare¶
- Create an API client with full access in the MetaMiner Administrator (MMA) and copy its token (see Manage API Clients). The script needs an admin client without environment or site limits.
- Make sure API clients is the authorization mode (see Authorization Modes). The checks assume this mode.
- Note the base URL:
https://<mms-host>:<port>(default port 49714;http://only when SSL is not enabled on MMS). - Optional: note a Tableau Server environment id and its exact display name, and the id or name of an environment enabled for the Flow Scheduler. The first run lists the Tableau Server environments for you.
- Use a machine with
curl.exe(included in Windows 10 and later) and PowerShell 5.1 or 7.
2. Save the script¶
<#
Test-MetaMinerApi.ps1 - smoke test of the MetaMiner Server API.
Read-only unless you pass -ChangeCredentials or -RotateToken.
#>
param(
[Parameter(Mandatory)][string]$BaseUrl, # https://<mms-host>:<port>
[Parameter(Mandatory)][string]$Token, # admin API client without limits
[long] $EnvId = 0, # Tableau Server environment id
[string]$EnvName = "", # its exact display name
[string]$FssEnv = "", # environment enabled for the Flow Scheduler
[switch]$CheckBackup, # also read GET /fss/backup
[switch]$ChangeCredentials, # OPT-IN: really changes the Tableau login
[string]$TabUser = "", # with -ChangeCredentials: new user (optional)
[string]$TabPassword = "", # with -ChangeCredentials: new password
[switch]$RotateToken, # OPT-IN: gives the client a new token
[switch]$Insecure # lab only: accept an untrusted certificate
)
$BaseUrl = $BaseUrl.TrimEnd('/')
$script:pass = 0; $script:fail = 0
$bodyFile = [System.IO.Path]::GetTempFileName()
function Invoke-Api([string]$Method, [string]$Path, [string]$Body, [string]$Bearer) {
$a = @('-s', '-w', '\n%{http_code}', '-X', $Method, "$BaseUrl$Path", '--max-time', '180')
if ($Insecure) { $a += '-k' }
if ($Bearer) { $a += @('-H', "Authorization: Bearer $Bearer") }
if ($Body) {
[System.IO.File]::WriteAllText($bodyFile, $Body)
$a += @('-H', 'Content-Type: application/json', '--data-binary', "@$bodyFile")
}
$lines = @(& curl.exe @a 2>$null)
$status = 0
if ($lines.Count) { [void][int]::TryParse($lines[-1], [ref]$status) }
[pscustomobject]@{ Status = $status; Body = ($lines | Select-Object -SkipLast 1) -join "`n" }
}
function Check([string]$Name, [int]$Actual, [int[]]$Expected) {
if ($Expected -contains $Actual) { " PASS $Name (HTTP $Actual)"; $script:pass++ }
else { " FAIL $Name -> HTTP $Actual, expected $($Expected -join '/')"; $script:fail++ }
}
function CheckRejected([string]$Name, [int]$Actual) {
if ($Actual -ge 400) { " PASS $Name (HTTP $Actual)"; $script:pass++ }
else { " FAIL $Name -> HTTP $Actual, expected a rejection"; $script:fail++ }
}
function Json($o) { $o | ConvertTo-Json -Compress -Depth 4 }
"== Authorization =="
Check "token works: GET /fss/v1/info" (Invoke-Api GET '/fss/v1/info' '' $Token).Status @(200)
Check "no token -> 401" (Invoke-Api GET '/fss/v1/info' '' '').Status @(401)
Check "wrong token -> 401" (Invoke-Api GET '/fss/v1/info' '' 'mmsk_not-a-client').Status @(401)
Check "body over 64 KiB -> 413" (Invoke-Api POST '/tab/credentials' ('{"pad":"' + ('A' * 70000) + '"}') $Token).Status @(413)
"== Tableau credentials (read-only) =="
$st = Invoke-Api GET '/tab/credentials/status' '' $Token
Check "GET /tab/credentials/status" $st.Status @(200)
if ($st.Status -eq 200) {
($st.Body | ConvertFrom-Json).envs | ForEach-Object { " env $($_.envId) '$($_.envName)' user $($_.username)" }
}
if ($EnvId -and $EnvName) {
Check "missing password -> 400" (Invoke-Api POST '/tab/credentials' (Json @{ envId = $EnvId; envName = $EnvName }) $Token).Status @(400)
CheckRejected "body not JSON -> rejected" (Invoke-Api POST '/tab/credentials' 'not json' $Token).Status
Check "unknown envId -> 404" (Invoke-Api POST '/tab/credentials' (Json @{ envId = 999999; envName = 'nope'; password = 'x' }) $Token).Status @(404)
Check "envName mismatch -> 404" (Invoke-Api POST '/tab/credentials' (Json @{ envId = $EnvId; envName = 'WRONG NAME'; password = 'x' }) $Token).Status @(404)
} else { " SKIP credential request checks (pass -EnvId and -EnvName)" }
"== Flow Scheduler (read-only) =="
if ($FssEnv) {
Check "GET /fss/v1/runs" (Invoke-Api GET "/fss/v1/runs?env=$([uri]::EscapeDataString($FssEnv))&limit=5" '' $Token).Status @(200)
} else { " SKIP run list (pass -FssEnv)" }
if ($CheckBackup) {
Check "GET /fss/backup" (Invoke-Api GET '/fss/backup' '' $Token).Status @(200)
} else { " SKIP backup info (pass -CheckBackup)" }
"== Opt-in changes =="
if ($ChangeCredentials) {
if (-not ($EnvId -and $EnvName -and $TabPassword)) { throw "-ChangeCredentials needs -EnvId, -EnvName and -TabPassword" }
$req = @{ envId = $EnvId; envName = $EnvName; password = $TabPassword }
if ($TabUser) { $req.user = $TabUser }
$r = Invoke-Api POST '/tab/credentials' (Json $req) $Token
Check "change Tableau credentials -> 200" $r.Status @(200)
if ($r.Status -eq 200) { $j = $r.Body | ConvertFrom-Json; " applied $($j.applied -join ','), cluster $($j.cluster)" }
else { " $($r.Body)" }
} else { " SKIP credential change (pass -ChangeCredentials; it CHANGES the Tableau login)" }
if ($RotateToken) {
$r = Invoke-Api POST '/fss/v1/clients/me/rotate' '' $Token
Check "me/rotate -> 200" $r.Status @(200)
if ($r.Status -eq 200) {
$new = ($r.Body | ConvertFrom-Json).token
Check "old token -> 401" (Invoke-Api GET '/fss/v1/info' '' $Token).Status @(401)
Check "new token -> 200" (Invoke-Api GET '/fss/v1/info' '' $new).Status @(200)
" NOTE the client's new token is: $new (store it now; it cannot be shown again)"
}
} else { " SKIP token rotation (pass -RotateToken; it CHANGES the client's token)" }
Remove-Item $bodyFile -ErrorAction SilentlyContinue
""; "$($script:pass) passed, $($script:fail) failed"
if ($script:fail -gt 0) { exit 1 }
The script writes each request body to a temporary file and sends it with --data-binary, so JSON quoting survives in both PowerShell 5.1 and 7.
3. Run the read-only checks¶
.\Test-MetaMinerApi.ps1 -BaseUrl "https://<mms-host>:<port>" -Token "<token>"
Then add an environment, the Flow Scheduler checks, or both:
.\Test-MetaMinerApi.ps1 -BaseUrl "https://<mms-host>:<port>" -Token "<token>" `
-EnvId <envId> -EnvName "<display name>" -FssEnv "<envId>" -CheckBackup
| Check | Expected | Why |
|---|---|---|
GET /fss/v1/info with the token |
200 |
The token belongs to an active client; works with the Flow Scheduler switched off too |
| No token | 401 |
API clients mode refuses calls without a token |
| Wrong token | 401 |
Not the token of an active client |
| Body over 64 KiB | 413 |
The size limit applies to every API request, before the token is checked |
GET /tab/credentials/status |
200 |
Lists each Tableau Server environment's id, name and login |
| Missing password | 400 |
Refused before anything is checked against Tableau |
| Body not JSON | Rejected | Nothing changes |
Unknown envId |
404 |
No such environment on this node |
envName mismatch |
404 |
The name guard works |
GET /fss/v1/runs?env=... |
200 |
The Flow Scheduler API answers (needs the Flow Scheduler running) |
GET /fss/backup |
200 |
The backup API answers on this node |
The script exits with code 1 when a check fails, so a scheduler or pipeline can run it.
4. Run the opt-in steps¶
Change the Tableau Server login (on a test server first):
.\Test-MetaMinerApi.ps1 -BaseUrl "https://<mms-host>:<port>" -Token "<token>" `
-EnvId <envId> -EnvName "<display name>" -ChangeCredentials -TabPassword "<new password>"
The script prints the logins applied and the cluster result (see Tableau Credentials API).
Rotate the test client's token:
.\Test-MetaMinerApi.ps1 -BaseUrl "https://<mms-host>:<port>" -Token "<token>" -RotateToken
Expected: me/rotate 200, the old token 401, the new token 200.
5. The same checks with curl¶
Read-only (bash, or Git Bash on Windows):
BASE="https://<mms-host>:<port>"
TOKEN="<token>"
W='%{http_code}\n'
curl -s -o /dev/null -w "$W" "$BASE/fss/v1/info" -H "Authorization: Bearer $TOKEN" # 200
curl -s -o /dev/null -w "$W" "$BASE/fss/v1/info" # 401
curl -s -o /dev/null -w "$W" "$BASE/fss/v1/info" -H "Authorization: Bearer mmsk_not-a-client" # 401
printf '{"pad":"%s"}' "$(head -c 70000 /dev/zero | tr '\0' 'A')" > big.json
curl -s -o /dev/null -w "$W" -X POST "$BASE/tab/credentials" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" --data-binary @big.json # 413
curl -s "$BASE/tab/credentials/status" -H "Authorization: Bearer $TOKEN" # 200, lists envs
curl -s -o /dev/null -w "$W" -X POST "$BASE/tab/credentials" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"envId": <envId>, "envName": "<display name>"}' # 400
curl -s -o /dev/null -w "$W" -X POST "$BASE/tab/credentials" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"envId": 999999, "envName": "nope", "password": "x"}' # 404
curl -s -o /dev/null -w "$W" "$BASE/fss/v1/runs?env=<envId>&limit=5" -H "Authorization: Bearer $TOKEN" # 200
curl -s -o /dev/null -w "$W" "$BASE/fss/backup" -H "Authorization: Bearer $TOKEN" # 200
Opt-in, changes things:
# Changes the Tableau Server login, on every node of a pair
curl -s -X POST "$BASE/tab/credentials" --max-time 180 -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"envId": <envId>, "envName": "<display name>", "password": "<new password>"}'
# Gives the client a new token: store it, the old one stops at once
curl -s -X POST "$BASE/fss/v1/clients/me/rotate" -H "Authorization: Bearer $TOKEN"
Add -k only in a lab, for a server whose certificate your machine does not trust.
What you learned¶
- How the MMS API answers a good token, a missing one and a wrong one.
- That the size limit and the Tableau Credentials API guards refuse bad requests before anything changes.
- How to run the changing steps only when you mean to.