Skip to content

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

  1. 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.
  2. Make sure API clients is the authorization mode (see Authorization Modes). The checks assume this mode.
  3. Note the base URL: https://<mms-host>:<port> (default port 49714; http:// only when SSL is not enabled on MMS).
  4. 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.
  5. 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
<#
  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.

Next steps