diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ae64d5..5b4eb57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,15 +5,17 @@ name: CI Checks # Optionally, you can turn it on using a schedule for regular testing. on: pull_request: - paths-ignore: - - 'README.md' push: - paths-ignore: - - 'README.md' -# Testing only needs permissions to read the repository contents. +# Testing needs to read repository contents and write check runs (test-reporter) +# and step summaries (coverage report). actions:read lets PR runs download the +# base branch's coverage breakdown artifact; pull-requests:write lets the +# coverage-diff report be posted as a PR comment. permissions: contents: read + checks: write + actions: read + pull-requests: write jobs: # Ensure project builds before running testing matrix @@ -33,5 +35,65 @@ jobs: with: version: latest - run: make lint - - run: make testacc - - run: go build -v ./syntheticsclientv2 \ No newline at end of file + - run: make test-cover + # GitHub downgrades GITHUB_TOKEN to read-only for pull_request runs from forks, + # regardless of the checks:write/pull-requests:write requested above — creating a + # check run or posting a PR comment there fails with "Resource not accessible by + # integration". Skip both write-dependent reporting steps in that case; the job + # itself still fails (and blocks merge) on a test or coverage failure regardless. + - name: Report test results + uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3 + if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + with: + name: Go Tests + path: test-results.json + reporter: golang-json + only-summary: true + # Fetch v2's (the base branch's) last coverage breakdown so PR runs can + # report how this change moves coverage, not just its absolute value. + - name: Download base branch coverage breakdown + id: download-main-breakdown + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + branch: v2 + workflow_conclusion: success + name: v2.breakdown + if_no_artifact_found: warn + - name: Check test coverage + id: coverage + uses: vladopajic/go-test-coverage@f94bcf0d6b9fa5fb8b783830b22648f6c17475e2 # v2 + continue-on-error: true # fail after the coverage comment is posted below + with: + config: ./.testcoverage.yml + breakdown-file-name: ${{ github.ref_name == 'v2' && 'v2.breakdown' || '' }} + diff-base-breakdown-file-name: ${{ steps.download-main-breakdown.outputs.found_artifact == 'true' && 'v2.breakdown' || '' }} + - name: Upload coverage breakdown + if: ${{ github.ref_name == 'v2' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: v2.breakdown + path: v2.breakdown + if-no-files-found: error + # No status function here: leaving the implicit success() check in + # place means this (and the coverage step it reads outputs from) is + # skipped if make test-cover failed, instead of erroring on a missing + # report from a skipped step. Also skipped for fork PRs — see the + # test-reporter step above for why pull-requests:write isn't usable there. + - name: Post coverage report to PR + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 + with: + comment-tag: coverage-report + message: | + Code Coverage Report: + ``` + ${{ fromJSON(steps.coverage.outputs.report) }}``` + - name: Fail if coverage threshold not met + if: ${{ steps.coverage.outcome == 'failure' }} + run: echo "coverage check failed" && exit 1 + - name: Publish coverage summary + if: ${{ !cancelled() }} + run: | + echo "### Coverage" >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.txt | tail -1 >> "$GITHUB_STEP_SUMMARY" + - run: go build -v ./syntheticsclientv2/... diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..2f8b869 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,353 @@ +name: Integration Test + +# Trusted trigger: runs in the base repo's context (full permissions model below, +# environment secrets available) regardless of whether the triggering PR came from a +# fork. This is the privilege-separated counterpart to pr-metadata.yml, which does the +# untrusted checkout-free bookkeeping. +# See: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions +# +# workflow_run does not automatically attach as a PR status check, so this workflow +# explicitly creates/updates a Check Run on the PR's head SHA via the Checks API — that's +# what branch protection actually watches. +# +# run-name names the PR being tested: without it, every run of this workflow shows as +# "Integration Test" on branch v2 in the Actions list and in the environment-approval +# prompt, and a reviewer approving the live-token gate can't tell which PR they're +# approving from that alone. +run-name: >- + Integration Test: ${{ github.event.workflow_run.head_repository.full_name }}@${{ github.event.workflow_run.head_branch }} + (${{ github.event.workflow_run.head_sha }}) + +on: + workflow_run: + workflows: ["PR Metadata"] + types: [completed] + +permissions: {} + +jobs: + # Ungated on purpose: downloads and validates the pr-metadata artifact immediately, + # before any approval wait. An environment approval can take up to 30 days (GitHub's own + # timeout), which would otherwise force the artifact retention window to also cover the + # entire approval SLA. Resolving here means only this job's (short) runtime needs the + # artifact to still exist, not however long a reviewer takes to click approve. + resolve: + name: resolve + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read + checks: write + outputs: + found: ${{ steps.pr.outputs.found }} + number: ${{ steps.pr.outputs.number }} + sha: ${{ steps.pr.outputs.sha }} + check-run-id: ${{ steps.create-check.outputs.id }} + + steps: + - name: Download PR metadata + id: download + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: pr-metadata + path: pr-metadata + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Read and validate PR metadata + id: pr + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require('fs'); + + // pr-metadata.yml is a pull_request-triggered workflow, so its *file* can be + // rewritten by a fork PR to upload arbitrary pr-number/pr-sha content — never + // trust the artifact as-is. Enforce strict formats and cross-check against + // fields GitHub itself attaches to this trusted workflow_run event, which a + // rewritten pr-metadata.yml cannot forge. + // + // The job-level guard above (conclusion == 'success') already means a draft + // PR (whose record-pr job is skipped, reporting conclusion 'skipped') never + // reaches this job at all. So getting here with a missing or malformed + // artifact is always an anomaly, not a legitimate "nothing to test" case: fail + // loudly rather than silently exiting success with no check or comment. + const rawNumber = fs.readFileSync('pr-metadata/pr-number', 'utf8').trim(); + const rawSha = fs.readFileSync('pr-metadata/pr-sha', 'utf8').trim(); + + if (!/^[0-9]+$/.test(rawNumber)) { + core.setFailed(`pr-metadata artifact has malformed pr-number: ${JSON.stringify(rawNumber)}`); + return; + } + if (!/^[0-9a-f]{40}$/.test(rawSha)) { + core.setFailed(`pr-metadata artifact has malformed pr-sha: ${JSON.stringify(rawSha)}`); + return; + } + + // github.event.workflow_run.head_sha is the commit that triggered the + // pr-metadata.yml run itself — a value GitHub computes and attaches to the + // workflow_run event independently of anything pr-metadata.yml's script did. + // Trust this, not the artifact's own pr-sha file. + const trustedSha = context.payload.workflow_run.head_sha; + if (rawSha !== trustedSha) { + core.setFailed(`pr-metadata artifact sha (${rawSha}) does not match the workflow_run's own head_sha (${trustedSha}) — refusing to trust a forged or stale artifact.`); + return; + } + + // Fetch the PR the artifact claims by number, then verify every field that + // matters against values GitHub itself attached to this trusted workflow_run + // event — never trust the artifact's number on its own. + // + // Deliberately not using listPullRequestsAssociatedWithCommit or the + // workflow_run payload's own `pull_requests` field: (a) the commit-associated + // lookup can return multiple open/merged PRs for the same commit (e.g. a + // commit shared via cherry-pick or a stacked branch), letting a forged + // artifact redirect handling to an unrelated but-also-matching PR; and (b) + // workflow_run.pull_requests is documented to come back empty for PRs from + // forked repositories, which is precisely the case this workflow exists to + // support. Instead: fetch the claimed PR by number directly, then require + // every trust-relevant field to match values GitHub attached to this event. + let pr; + try { + const resp = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: Number(rawNumber), + }); + pr = resp.data; + } catch (e) { + core.setFailed(`Could not fetch PR #${rawNumber} via the API — refusing to trust the pr-metadata artifact: ${e.message}`); + return; + } + + const headRepo = context.payload.workflow_run.head_repository; + const baseRepo = context.payload.workflow_run.repository; + const headBranch = context.payload.workflow_run.head_branch; + + if (pr.state !== 'open') { + core.setFailed(`PR #${rawNumber} is not open (state: ${pr.state}) — refusing to trust the pr-metadata artifact.`); + return; + } + if (pr.draft) { + core.setFailed(`PR #${rawNumber} is a draft — refusing to run live integration tests against it.`); + return; + } + if (pr.head.sha !== trustedSha) { + core.setFailed(`PR #${rawNumber}'s current head sha (${pr.head.sha}) does not match the trusted workflow_run head_sha (${trustedSha}) — PR head moved or artifact is stale/forged.`); + return; + } + if (!pr.head.repo || pr.head.repo.full_name !== headRepo.full_name) { + core.setFailed(`PR #${rawNumber}'s head repo (${pr.head.repo && pr.head.repo.full_name}) does not match the trusted workflow_run head_repository (${headRepo.full_name}) — refusing to trust the pr-metadata artifact.`); + return; + } + if (pr.head.ref !== headBranch) { + core.setFailed(`PR #${rawNumber}'s head branch (${pr.head.ref}) does not match the trusted workflow_run head_branch (${headBranch}) — refusing to trust the pr-metadata artifact.`); + return; + } + if (pr.base.repo.full_name !== baseRepo.full_name) { + core.setFailed(`PR #${rawNumber}'s base repo (${pr.base.repo.full_name}) is not this repository — refusing to trust the pr-metadata artifact.`); + return; + } + + core.setOutput('found', 'true'); + core.setOutput('number', String(pr.number)); + core.setOutput('sha', trustedSha); + + - name: Create pending check run + id: create-check + if: steps.pr.outputs.found == 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + PR_SHA: ${{ steps.pr.outputs.sha }} + with: + script: | + // Capture the created check run's own ID as a job output rather than having + // `report` re-look it up by ref+name later: a rerun of this workflow (or of + // PR Metadata) for the same head SHA would create a second `integration-test` + // check run, and listForRef().check_runs[0] has no guarantee of returning + // *this* run's check — it could complete a different run's check and leave + // this one stuck at queued. + const { data: check } = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'integration-test', + head_sha: process.env.PR_SHA, + status: 'queued', + details_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }); + core.setOutput('id', String(check.id)); + + integration-test: + name: integration-test + needs: resolve + if: needs.resolve.outputs.found == 'true' + runs-on: ubuntu-latest + timeout-minutes: 25 + environment: integration-test # gate: waits for reviewer approval here + concurrency: + # One fixed group for every PR/branch, not keyed by head repo/branch: every run — + # regardless of which fork or branch it came from — hits the same live org (sg0) + # and provisions the same fixed-name resources (see integration_test.go TestMain, + # and e.g. TestLiveCreateVariableV2ReturnsErrorOnDuplicateName, which asserts that + # duplicate names are rejected). A per-branch key would let two approved PRs run + # this job at the same time and collide on those names. + # + # Scoped to this job rather than the whole workflow so a second PR's `resolve` job + # (validation + opening the queued check) still runs immediately and isn't stuck + # behind however long the first PR's environment approval takes to arrive. + group: integration-test-live-suite + cancel-in-progress: false + # Deliberately just contents: read — this job runs a fork PR's code with a live API + # token in its environment. Reporting the outcome (checks:write, pull-requests:write) + # is split into the ungated `report` job below so fork code never runs on a runner that + # also holds write access to checks or PR comments. + permissions: + contents: read + + steps: + - name: Checkout PR head + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Checkout by the exact commit SHA recorded by `resolve`, not refs/pull//head: + # that ref is mutable and the environment approval above can sit for hours or + # days, during which the PR could gain new, unreviewed commits. + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false + + - name: Verify checked-out SHA matches recorded PR head + env: + PR_SHA: ${{ needs.resolve.outputs.sha }} + run: | + actual="$(git rev-parse HEAD)" + if [ "$actual" != "$PR_SHA" ]; then + echo "Checked-out SHA ($actual) does not match PR metadata ($PR_SHA) — refusing to run privileged tests against an unexpected checkout." >&2 + exit 1 + fi + + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + # No build cache here: workflow_run executes on the base repo's default-branch + # ref, so a cache written in this job would be saved into the shared v2 cache + # scope and later restored by trusted CI runs — a poisoning path for fork code + # to persist beyond its own PR. Every other job that needs a fast Go setup runs + # on ci.yml, which is unaffected by this. + cache: false + + - name: Run live integration tests + env: + API_ACCESS_TOKEN: ${{ secrets.API_ACCESS_TOKEN }} + REALM: ${{ secrets.REALM }} + run: make test-integration + + - name: Upload integration test log + # Always upload, even on failure, so the report job can render a result table + # instead of just "the job failed". make test-integration already strips the + # X-Sf-Token header line from this log before it's written. + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: integration-test-log + path: integration.jsonl + if-no-files-found: warn + retention-days: 3 + + # Ungated, and runs regardless of how integration-test ended — deliberately the only job + # with checks:write/pull-requests:write, so those never coexist on a runner with fork + # code. Also the counterpart to the sibling repo's separate finalize-on-incomplete job: + # environment-approval rejection or timeout fails integration-test before any of its own + # steps run (including an artifact upload), which this handles via needs.*.result rather + # than requiring a second job to notice a check stuck at queued. + report: + name: report + needs: [resolve, integration-test] + if: always() && needs.resolve.outputs.found == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + pull-requests: write + checks: write + steps: + - name: Download integration test log + id: download-log + continue-on-error: true # absent when the job never started (rejected/timed-out approval) + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: integration-test-log + path: integration-test-log + + - name: Report check run result + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + JOB_RESULT: ${{ needs.integration-test.result }} + CHECK_RUN_ID: ${{ needs.resolve.outputs.check-run-id }} + PR_NUMBER: ${{ needs.resolve.outputs.number }} + with: + script: | + const fs = require('fs'); + const conclusion = process.env.JOB_RESULT === 'success' ? 'success' : 'failure'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + let table = '_No test output was captured — the job likely did not start (environment approval was rejected or timed out before the run began)._'; + const logPath = 'integration-test-log/integration.jsonl'; + if (fs.existsSync(logPath)) { + const lines = fs.readFileSync(logPath, 'utf8').split('\n'); + const results = []; + let buildFailed = false; + for (const line of lines) { + if (!line) continue; + let event; + try { + event = JSON.parse(line); + } catch (e) { + continue; + } + if (event.Action === 'build-fail') buildFailed = true; + // Only live integration tests (TestLive* — the convention every test in + // integration_test.go follows) show up here; the table reports on those, + // never on raw stdout/response bodies. + if (event.Test && event.Test.startsWith('TestLive') && (event.Action === 'pass' || event.Action === 'fail' || event.Action === 'skip')) { + results.push(event); + } + } + results.sort((a, b) => a.Test.localeCompare(b.Test)); + + if (buildFailed) { + table = '⚠️ Build failed before any tests ran — see the job log for compiler errors.'; + } else if (results.length === 0) { + table = '⚠️ No live integration test (TestLive*) results were produced — see the job log for details.'; + } else { + const passed = results.filter(r => r.Action === 'pass').length; + const failed = results.filter(r => r.Action === 'fail').length; + const skipped = results.filter(r => r.Action === 'skip').length; + const icon = r => r.Action === 'pass' ? '✅ Pass' : r.Action === 'skip' ? '⚠️ Skip' : '❌ Fail'; + const rows = results.map(r => `| ${r.Test} | ${icon(r)} | ${r.Elapsed ?? 0} |`).join('\n'); + table = `**Live integration tests — Total:** ${results.length} | **Passed:** ${passed} | **Failed:** ${failed} | **Skipped:** ${skipped}\n\n` + + `| Test | Result | Duration (s) |\n|---|---|---|\n${rows}`; + } + } + + // Update the exact check run `resolve` created (its ID was threaded through as + // a job output), not a re-lookup by ref+name: a rerun of this workflow for the + // same head SHA would create a second `integration-test` check run, and + // listForRef().check_runs[0] has no guarantee of returning *this* run's check — + // it could complete a different run's check and leave this one stuck at queued. + if (process.env.CHECK_RUN_ID) { + await github.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: Number(process.env.CHECK_RUN_ID), + status: 'completed', + conclusion, + details_url: runUrl, + }); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(process.env.PR_NUMBER), + body: `### Integration Test: ${conclusion === 'success' ? '✅ Passed' : '❌ Failed'}\n\n[View run](${runUrl})\n\n${table}`, + }); diff --git a/.github/workflows/pr-metadata.yml b/.github/workflows/pr-metadata.yml new file mode 100644 index 0000000..5a97ac4 --- /dev/null +++ b/.github/workflows/pr-metadata.yml @@ -0,0 +1,36 @@ +name: PR Metadata + +# Untrusted trigger: runs for PRs from forks with no secrets and read-only +# permissions. Its only job is to record which PR/SHA to test, so the +# privileged integration-test workflow (triggered via workflow_run) never has +# to check out or execute anything from this workflow's own run context. +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + record-pr: + name: record-pr + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Write PR metadata + run: | + mkdir -p pr-metadata + echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number + echo "${{ github.event.pull_request.head.sha }}" > pr-metadata/pr-sha + + - name: Upload PR metadata + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-metadata + path: pr-metadata/ + # integration-test.yml's ungated `resolve` job downloads and validates this + # artifact immediately on workflow_run, before any approval wait — so this only + # needs to survive normal workflow_run delivery/retry latency, not the (up to + # 30-day) environment approval SLA. Keeping several days as a rerun buffer. + retention-days: 3 diff --git a/.gitignore b/.gitignore index ae03155..0a124ce 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,13 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +coverage.txt +test-results.json +*.breakdown +integration.jsonl + +# Local credentials for running the live integration suite (make test-integration) +.env.testacc # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.testcoverage.yml b/.testcoverage.yml new file mode 100644 index 0000000..4e7b47b --- /dev/null +++ b/.testcoverage.yml @@ -0,0 +1,9 @@ +profile: coverage.txt + +threshold: + # SYN-6889 added error-path unit tests, raising syntheticsclientv2 coverage + # from 73% to 94.4%. Floor set at 90 (the ticket's target) rather than the + # measured value, to leave headroom for minor fluctuations without + # relaxing the original goal. syntheticsclient (v1) is deprecated and + # excluded from tests/coverage entirely (see Makefile). + total: 90 diff --git a/Makefile b/Makefile index 9f141cc..9c4ab24 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,24 @@ -.PHONY: default all build clean test fmtcheck testacc sonarqube +.PHONY: default all build clean test fmtcheck test-cover test-integration -PKG_NAME=syntheticsclientv2 +# syntheticsclient (v1) is deprecated and excluded from builds and coverage; +# only syntheticsclientv2 is built/covered. v1's tests still run via +# TEST_FILES below so `go test` reports them as an explicit SKIP (see +# skipDeprecated in syntheticsclient/synthetics_test.go) rather than the +# package silently vanishing from CI output. FILES=./syntheticsclientv2/... +TEST_FILES=./syntheticsclient/... ./syntheticsclientv2/... -default: test +default: test -all: clean build test +all: clean build test build: fmtcheck - go build -tags=unit_tests + go build $(FILES) clean: @echo "==> Cleaning out old builds " go clean - rm -rf coverage.txt .sonar .scannerwork + rm -rf coverage.txt test-results.json v2.breakdown integration.jsonl fmt: @@ -28,11 +33,21 @@ fmtcheck: fmt lint test: fmtcheck @echo "==> Running all tests" - go test $(FILES) -v -tags=unit_tests -timeout=30s -parallel=4 -cover - -testacc: clean fmtcheck - @echo "==> Running all tests" - go test $(FILES) -v -tags=unit_tests -timeout=30s -parallel=8 -cover -coverprofile coverage.txt - -sonarqube: testacc - docker run -it -v "${PWD}:/usr/src" sonarsource/sonar-scanner-cli + go test $(TEST_FILES) -v -timeout=30s -parallel=4 -cover -coverpkg=$(FILES) + +test-cover: clean fmtcheck + @echo "==> Running all tests with coverage and JSON output" + go test $(TEST_FILES) -timeout=30s -parallel=8 -json -cover -covermode=atomic -coverpkg=$(FILES) -coverprofile coverage.txt > test-results.json + +# Runs the live integration suite (syntheticsclientv2/integration_test.go) against a real +# Synthetics org. Requires API_ACCESS_TOKEN and REALM in the environment. Not part of the +# default test/test-cover targets: it mutates live state and needs real credentials, so it +# only runs where those are deliberately provided (a developer's shell, or the gated +# integration-test CI job). +test-integration: SHELL:=/bin/bash +test-integration: + @echo "==> Running live integration tests" + set -o pipefail; go test -tags=integration -json ./syntheticsclientv2/... -timeout 30m \ + | sed '/X-Sf-Token/d' \ + | tee integration.jsonl \ + | jq -j -r 'if .Action == "output" then .Output else empty end' diff --git a/README.md b/README.md index bbc5de5..318882f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # syntheticsclient + +[![Release](https://img.shields.io/github/v/release/splunk/syntheticsclient)](https://github.com/splunk/syntheticsclient/releases) +[![CI Checks](https://img.shields.io/github/actions/workflow/status/splunk/syntheticsclient/ci.yml?branch=v2&label=CI)](https://github.com/splunk/syntheticsclient/actions/workflows/ci.yml?query=branch%3Av2) +[![Build](https://img.shields.io/github/actions/workflow/status/splunk/syntheticsclient/ci.yml?branch=v2&label=build)](https://github.com/splunk/syntheticsclient/actions/workflows/ci.yml?query=branch%3Av2) +[![License](https://img.shields.io/github/license/splunk/syntheticsclient)](https://github.com/splunk/syntheticsclient/blob/v2/LICENSE) + A Splunk Synthetics for Splunk Observability (Formerly Rigor) client for golang. ## Installation diff --git a/coverage.txt b/coverage.txt deleted file mode 100644 index 4dd54a3..0000000 --- a/coverage.txt +++ /dev/null @@ -1 +0,0 @@ -mode: set \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 85a9be8..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,4 +0,0 @@ -sonar.projectKey=syntheticsclient -sonar.exclusions=coverage*,**/*_test.go -sonar.sources=./syntheticsclient/ -sonar.go.coverage.reportPaths=coverage.txt diff --git a/syntheticsclient/create_browsercheck_test.go b/syntheticsclient/create_browsercheck_test.go index 1a56703..c653b61 100644 --- a/syntheticsclient/create_browsercheck_test.go +++ b/syntheticsclient/create_browsercheck_test.go @@ -26,6 +26,7 @@ var ( ) func TestCreateBrowseCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/create_httpcheck_test.go b/syntheticsclient/create_httpcheck_test.go index 66c0927..f5bba57 100644 --- a/syntheticsclient/create_httpcheck_test.go +++ b/syntheticsclient/create_httpcheck_test.go @@ -26,6 +26,7 @@ var ( ) func TestCreateHttpCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/delete_browsercheck_test.go b/syntheticsclient/delete_browsercheck_test.go index 3dcefb0..48105a6 100644 --- a/syntheticsclient/delete_browsercheck_test.go +++ b/syntheticsclient/delete_browsercheck_test.go @@ -24,6 +24,7 @@ var ( ) func TestDeleteBrowseCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/delete_httpcheck_test.go b/syntheticsclient/delete_httpcheck_test.go index 2a5489c..022d2d5 100644 --- a/syntheticsclient/delete_httpcheck_test.go +++ b/syntheticsclient/delete_httpcheck_test.go @@ -24,6 +24,7 @@ var ( ) func TestDeleteHttpCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/get_check_test.go b/syntheticsclient/get_check_test.go index ddd118d..466bc2d 100644 --- a/syntheticsclient/get_check_test.go +++ b/syntheticsclient/get_check_test.go @@ -27,6 +27,7 @@ var ( ) func TestGetBrowserCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/synthetics_test.go b/syntheticsclient/synthetics_test.go index e11627b..dc18df9 100644 --- a/syntheticsclient/synthetics_test.go +++ b/syntheticsclient/synthetics_test.go @@ -53,7 +53,17 @@ func testMethod(t *testing.T, r *http.Request, want string) { } } +// skipDeprecated marks a test as skipped rather than removing it outright, +// so `go test ./...` still reports every syntheticsclient (v1) test by name +// with a SKIP status and this note, instead of the package silently +// vanishing from CI output. See SYN-6889: v1 is slated for deprecation and +// is excluded from the coverage gate and build (Makefile FILES var). +func skipDeprecated(t *testing.T) { + t.Skip("syntheticsclient (v1) is deprecated; skipping in favor of syntheticsclientv2") +} + func TestConfigurableClient(t *testing.T) { + skipDeprecated(t) testMux = http.NewServeMux() testServer = httptest.NewServer(testMux) args := ClientArgs{ @@ -72,6 +82,7 @@ func TestConfigurableClient(t *testing.T) { } func TestConfigurableClientTimeout(t *testing.T) { + skipDeprecated(t) testMux = http.NewServeMux() testServer = httptest.NewServer(testMux) diff --git a/syntheticsclient/update_browsercheck_test.go b/syntheticsclient/update_browsercheck_test.go index df57624..5d67895 100644 --- a/syntheticsclient/update_browsercheck_test.go +++ b/syntheticsclient/update_browsercheck_test.go @@ -51,6 +51,7 @@ var ( ) func TestUpdateBrowserCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclient/update_httpcheck_test.go b/syntheticsclient/update_httpcheck_test.go index 1219d24..b56ecca 100644 --- a/syntheticsclient/update_httpcheck_test.go +++ b/syntheticsclient/update_httpcheck_test.go @@ -26,6 +26,7 @@ var ( ) func TestUpdateHttpCheck(t *testing.T) { + skipDeprecated(t) setup() defer teardown() diff --git a/syntheticsclientv2/clientcertificate_models_test.go b/syntheticsclientv2/clientcertificate_models_test.go index 838df4a..6c904bb 100644 --- a/syntheticsclientv2/clientcertificate_models_test.go +++ b/syntheticsclientv2/clientcertificate_models_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/syntheticsclientv2/common_models_test.go b/syntheticsclientv2/common_models_test.go new file mode 100644 index 0000000..c2c1611 --- /dev/null +++ b/syntheticsclientv2/common_models_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 Splunk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syntheticsclientv2 + +import ( + "encoding/json" + "testing" +) + +func TestNewNullableString(t *testing.T) { + n := NewNullableString("hello") + if n.Value == nil || *n.Value != "hello" { + t.Errorf("returned \n\n%#v want value \n\n%#v", n.Value, "hello") + } + + body, err := json.Marshal(n) + if err != nil { + t.Fatal(err) + } + if string(body) != `"hello"` { + t.Errorf("returned \n\n%#v want \n\n%#v", string(body), `"hello"`) + } +} + +func TestNullableStringUnmarshalJSONError(t *testing.T) { + var n NullableString + err := n.UnmarshalJSON([]byte(`123`)) + if err == nil { + t.Fatal("expected an error unmarshalling a non-string value into NullableString") + } +} + +func TestNullableIntUnmarshalJSONError(t *testing.T) { + var n NullableInt + err := n.UnmarshalJSON([]byte(`"not-an-int"`)) + if err == nil { + t.Fatal("expected an error unmarshalling a non-numeric value into NullableInt") + } +} diff --git a/syntheticsclientv2/create_apicheckv2_test.go b/syntheticsclientv2/create_apicheckv2_test.go index a1b026c..52913c2 100644 --- a/syntheticsclientv2/create_apicheckv2_test.go +++ b/syntheticsclientv2/create_apicheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -105,3 +102,54 @@ func TestCreateApiCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputData.Test.Customproperties) } } + +func TestCreateApiCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/api", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createApiV2Body), &inputData) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.CreateApiCheckV2(&inputData) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestCreateApiCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(createApiV2Body), &inputData) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.CreateApiCheckV2(&inputData) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/create_browsercheckv2_test.go b/syntheticsclientv2/create_browsercheckv2_test.go index 20fdc0b..e913f85 100644 --- a/syntheticsclientv2/create_browsercheckv2_test.go +++ b/syntheticsclientv2/create_browsercheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -100,3 +97,54 @@ func TestCreateBrowserCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputBrowserCheckV2Data.Test.Customproperties) } } + +func TestCreateBrowserCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/browser", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createBrowserCheckV2Body), &inputBrowserCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.CreateBrowserCheckV2(&inputBrowserCheckV2Data) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestCreateBrowserCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(createBrowserCheckV2Body), &inputBrowserCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.CreateBrowserCheckV2(&inputBrowserCheckV2Data) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/create_cacertificatev2_test.go b/syntheticsclientv2/create_cacertificatev2_test.go index 8c8c512..5537d44 100644 --- a/syntheticsclientv2/create_cacertificatev2_test.go +++ b/syntheticsclientv2/create_cacertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +19,7 @@ import ( "io" "net/http" "reflect" + "strings" "testing" ) @@ -122,3 +120,49 @@ func TestCreateCaCertificateV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.CaCert, outputCaCertificateV2Data.CaCert) } } + +func TestCreateCaCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createCaCertificateV2Body), &inputCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreateCaCertificateV2(&inputCaCertificateV2Data) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestCreateCaCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(createCaCertificateV2Body), &inputCaCertificateV2Data) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.CreateCaCertificateV2(&inputCaCertificateV2Data) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/create_clientcertificatev2_test.go b/syntheticsclientv2/create_clientcertificatev2_test.go index cb12f16..23a72d0 100644 --- a/syntheticsclientv2/create_clientcertificatev2_test.go +++ b/syntheticsclientv2/create_clientcertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +19,7 @@ import ( "io" "net/http" "reflect" + "strings" "testing" ) @@ -114,3 +112,47 @@ func readClientCertificateRequestFields(t *testing.T, r *http.Request) ([]byte, return requestBody, requestCertificateFields } + +func TestCreateClientCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + if err := json.Unmarshal([]byte(createClientCertificateV2Body), &inputClientCertificateV2Data); err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreateClientCertificateV2(&inputClientCertificateV2Data) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestCreateClientCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + if err := json.Unmarshal([]byte(createClientCertificateV2Body), &inputClientCertificateV2Data); err != nil { + t.Fatal(err) + } + + _, _, err := unreachableClient.CreateClientCertificateV2(&inputClientCertificateV2Data) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/create_downtimeconfigurationv2_test.go b/syntheticsclientv2/create_downtimeconfigurationv2_test.go index f368633..1691ea4 100644 --- a/syntheticsclientv2/create_downtimeconfigurationv2_test.go +++ b/syntheticsclientv2/create_downtimeconfigurationv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2024 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -88,3 +85,35 @@ func TestCreateDowntimeConfigurationV2(t *testing.T) { } } + +func TestCreateDowntimeConfigurationV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/downtime_configurations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.CreateDowntimeConfigurationV2(&DowntimeConfigurationV2Input{}) + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestCreateDowntimeConfigurationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.CreateDowntimeConfigurationV2(&DowntimeConfigurationV2Input{}) + if err == nil { + t.Fatal("expected a connection error") + } +} diff --git a/syntheticsclientv2/create_httpcheckv2_test.go b/syntheticsclientv2/create_httpcheckv2_test.go index a47f736..e0b7274 100644 --- a/syntheticsclientv2/create_httpcheckv2_test.go +++ b/syntheticsclientv2/create_httpcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -101,3 +98,99 @@ func TestCreateHttpCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputHttpCheckV2Data.Test.Port) } } + +func TestCreateHttpCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createHttpCheckV2Body), &inputHttpCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.CreateHttpCheckV2(&inputHttpCheckV2Data) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestCreateHttpCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(createHttpCheckV2Body), &inputHttpCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.CreateHttpCheckV2(&inputHttpCheckV2Data) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} + +func TestCreateHttpCheckV2WithNullablePortReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + inputWithNullablePort := HttpCheckV2InputWithNullablePort{} + inputWithNullablePort.Test.Name = "test-http" + resp, details, err := testClient.CreateHttpCheckV2WithNullablePort(&inputWithNullablePort) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestCreateHttpCheckV2WithNullablePortReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputWithNullablePort := HttpCheckV2InputWithNullablePort{} + inputWithNullablePort.Test.Name = "test-http" + resp, details, err := unreachableClient.CreateHttpCheckV2WithNullablePort(&inputWithNullablePort) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/create_locationv2_test.go b/syntheticsclientv2/create_locationv2_test.go index e297611..b1c109d 100644 --- a/syntheticsclientv2/create_locationv2_test.go +++ b/syntheticsclientv2/create_locationv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -64,3 +61,35 @@ func TestCreateLocationV2(t *testing.T) { } } + +func TestCreateLocationV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.CreateLocationV2(&LocationV2Input{}) + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestCreateLocationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.CreateLocationV2(&LocationV2Input{}) + if err == nil { + t.Fatal("expected a connection error") + } +} diff --git a/syntheticsclientv2/create_portcheckv2_test.go b/syntheticsclientv2/create_portcheckv2_test.go index 22c63b6..b79354b 100644 --- a/syntheticsclientv2/create_portcheckv2_test.go +++ b/syntheticsclientv2/create_portcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +19,7 @@ import ( "fmt" "net/http" "reflect" + "strings" "testing" ) @@ -87,3 +85,49 @@ func TestCreatePortCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputPortCheckV2Data.Test.Customproperties) } } + +func TestCreatePortCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/port", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(createPortCheckV2Body), &inputPortCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreatePortCheckV2(&inputPortCheckV2Data) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestCreatePortCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(createPortCheckV2Body), &inputPortCheckV2Data) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.CreatePortCheckV2(&inputPortCheckV2Data) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/create_sslcheckv2_test.go b/syntheticsclientv2/create_sslcheckv2_test.go index 679d0ce..241151a 100644 --- a/syntheticsclientv2/create_sslcheckv2_test.go +++ b/syntheticsclientv2/create_sslcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -170,6 +167,54 @@ func assertStringPtr(t *testing.T, got *string, want string) { } } +func TestCreateSslCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(createSslCheckV2Body), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.CreateSslCheckV2(&inputSslCheckV2Data) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestCreateSslCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputSslCheckV2Data := SslCheckV2Input{} + err := json.Unmarshal([]byte(createSslCheckV2Body), &inputSslCheckV2Data) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.CreateSslCheckV2(&inputSslCheckV2Data) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + func TestCreateSslCheckV2DefaultsNilValidations(t *testing.T) { setup() defer teardown() diff --git a/syntheticsclientv2/create_variablev2_test.go b/syntheticsclientv2/create_variablev2_test.go index 5964019..b62c45c 100644 --- a/syntheticsclientv2/create_variablev2_test.go +++ b/syntheticsclientv2/create_variablev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -76,3 +73,47 @@ func TestCreateVariableV2(t *testing.T) { } } + +func TestCreateVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + err := json.Unmarshal([]byte(createVariableV2Body), &inputVariableV2Data) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.CreateVariableV2(&inputVariableV2Data) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestCreateVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputData := VariableV2Input{ + Variable: Variable{ + Name: "test-var", + Value: "test-value", + Secret: false, + Description: "test description", + }, + } + + _, _, err := unreachableClient.CreateVariableV2(&inputData) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/delete_apicheckv2_test.go b/syntheticsclientv2/delete_apicheckv2_test.go index 7cae14d..0613d7e 100644 --- a/syntheticsclientv2/delete_apicheckv2_test.go +++ b/syntheticsclientv2/delete_apicheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,38 @@ func TestDeleteApiCheckV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteApiCheckV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/api/20", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusInternalServerError) + }) + + status, err := testClient.DeleteApiCheckV2(20) + + if err == nil { + t.Fatal("expected an error on non-2xx status, but got none") + } + if !strings.Contains(err.Error(), "unknown error, status code") { + t.Fatalf("expected error message to contain 'unknown error, status code', but got: %s", err.Error()) + } + if status != 1 { + t.Errorf("expected status 1 on API error, but got %d", status) + } +} + +func TestDeleteApiCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + status, err := unreachableClient.DeleteApiCheckV2(21) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if status != 1 { + t.Errorf("expected status 1 on network error, but got %d", status) + } +} diff --git a/syntheticsclientv2/delete_browsercheckv2_test.go b/syntheticsclientv2/delete_browsercheckv2_test.go index 3b724b9..e706533 100644 --- a/syntheticsclientv2/delete_browsercheckv2_test.go +++ b/syntheticsclientv2/delete_browsercheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,38 @@ func TestDeleteBrowserCheckV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteBrowserCheckV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/browser/20", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNotFound) + }) + + status, err := testClient.DeleteBrowserCheckV2(20) + + if err == nil { + t.Fatal("expected an error on non-2xx status, but got none") + } + if !strings.Contains(err.Error(), "unknown error, status code") { + t.Fatalf("expected error message to contain 'unknown error, status code', but got: %s", err.Error()) + } + if status != 1 { + t.Errorf("expected status 1 on API error, but got %d", status) + } +} + +func TestDeleteBrowserCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + status, err := unreachableClient.DeleteBrowserCheckV2(21) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if status != 1 { + t.Errorf("expected status 1 on network error, but got %d", status) + } +} diff --git a/syntheticsclientv2/delete_cacertificatev2_test.go b/syntheticsclientv2/delete_cacertificatev2_test.go index 9c2a625..3e45943 100644 --- a/syntheticsclientv2/delete_cacertificatev2_test.go +++ b/syntheticsclientv2/delete_cacertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +16,7 @@ package syntheticsclientv2 import ( "net/http" + "strings" "testing" ) @@ -39,3 +37,35 @@ func TestDeleteCaCertificateV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp, http.StatusNoContent) } } + +func TestDeleteCaCertificateV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteCaCertificateV2(1) + if err == nil { + t.Fatal("expected error on non-2xx status, got nil") + } + if resp != http.StatusMultipleChoices { + t.Errorf("returned status \n\n%#v want \n\n%#v", resp, http.StatusMultipleChoices) + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error message containing 'Response code', got: %v", err) + } +} + +func TestDeleteCaCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeleteCaCertificateV2(1) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/delete_clientcertificatev2_test.go b/syntheticsclientv2/delete_clientcertificatev2_test.go index 96ff4be..661d4d9 100644 --- a/syntheticsclientv2/delete_clientcertificatev2_test.go +++ b/syntheticsclientv2/delete_clientcertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +16,7 @@ package syntheticsclientv2 import ( "net/http" + "strings" "testing" ) @@ -39,3 +37,35 @@ func TestDeleteClientCertificateV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp, http.StatusNoContent) } } + +func TestDeleteClientCertificateV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates/123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteClientCertificateV2(123) + if err == nil { + t.Fatal("expected error on non-2xx status, got nil") + } + if resp != http.StatusMultipleChoices { + t.Errorf("returned status \n\n%#v want \n\n%#v", resp, http.StatusMultipleChoices) + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error message containing 'Response code', got: %v", err) + } +} + +func TestDeleteClientCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeleteClientCertificateV2(123) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/delete_downtimeconfigurationv2_test.go b/syntheticsclientv2/delete_downtimeconfigurationv2_test.go index b96ad92..89e4adb 100644 --- a/syntheticsclientv2/delete_downtimeconfigurationv2_test.go +++ b/syntheticsclientv2/delete_downtimeconfigurationv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2024 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,32 @@ func TestDeleteDowntimeConfigurationV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteDowntimeConfigurationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, err := unreachableClient.DeleteDowntimeConfigurationV2(19) + if err == nil { + t.Fatal("expected a connection error") + } +} + +func TestDeleteDowntimeConfigurationV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/downtime_configurations/19", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteDowntimeConfigurationV2(19) + if err == nil { + t.Fatalf("expected an error for non-2xx status, but got none") + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error message to contain 'Response code', got: %v", err) + } + if resp != http.StatusMultipleChoices { + t.Errorf("expected status code %d, got %d", http.StatusMultipleChoices, resp) + } +} diff --git a/syntheticsclientv2/delete_httpcheckv2_test.go b/syntheticsclientv2/delete_httpcheckv2_test.go index 8e6221b..4f1b454 100644 --- a/syntheticsclientv2/delete_httpcheckv2_test.go +++ b/syntheticsclientv2/delete_httpcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,38 @@ func TestDeleteHttpCheckV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteHttpCheckV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/20", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusBadRequest) + }) + + status, err := testClient.DeleteHttpCheckV2(20) + + if err == nil { + t.Fatal("expected an error on non-2xx status, but got none") + } + if !strings.Contains(err.Error(), "unknown error, status code") { + t.Fatalf("expected error message to contain 'unknown error, status code', but got: %s", err.Error()) + } + if status != 1 { + t.Errorf("expected status 1 on API error, but got %d", status) + } +} + +func TestDeleteHttpCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + status, err := unreachableClient.DeleteHttpCheckV2(21) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if status != 1 { + t.Errorf("expected status 1 on network error, but got %d", status) + } +} diff --git a/syntheticsclientv2/delete_locationv2_test.go b/syntheticsclientv2/delete_locationv2_test.go index e6fbf93..47bb5ef 100644 --- a/syntheticsclientv2/delete_locationv2_test.go +++ b/syntheticsclientv2/delete_locationv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,32 @@ func TestDeleteLocationV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteLocationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, err := unreachableClient.DeleteLocationV2("beep") + if err == nil { + t.Fatal("expected a connection error") + } +} + +func TestDeleteLocationV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/locations/beep", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteLocationV2("beep") + if err == nil { + t.Fatalf("expected an error for non-2xx status, but got none") + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error message to contain 'Response code', got: %v", err) + } + if resp != http.StatusMultipleChoices { + t.Errorf("expected status code %d, got %d", http.StatusMultipleChoices, resp) + } +} diff --git a/syntheticsclientv2/delete_portcheckv2_test.go b/syntheticsclientv2/delete_portcheckv2_test.go index 00d1d24..ba2eba9 100644 --- a/syntheticsclientv2/delete_portcheckv2_test.go +++ b/syntheticsclientv2/delete_portcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,36 @@ func TestDeletePortCheckV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeletePortCheckV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/port/19", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeletePortCheckV2(19) + + if err == nil { + t.Fatal("expected error on non-2xx response, got nil") + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected 'Response code' in error message, got: %v", err) + } + if resp != http.StatusMultipleChoices { + t.Errorf("expected status code %d, got %d", http.StatusMultipleChoices, resp) + } +} + +func TestDeletePortCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeletePortCheckV2(19) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/delete_sslcheckv2_test.go b/syntheticsclientv2/delete_sslcheckv2_test.go index b7d6911..fc08730 100644 --- a/syntheticsclientv2/delete_sslcheckv2_test.go +++ b/syntheticsclientv2/delete_sslcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +16,7 @@ package syntheticsclientv2 import ( "net/http" + "strings" "testing" ) @@ -39,3 +37,36 @@ func TestDeleteSslCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp, http.StatusNoContent) } } + +func TestDeleteSslCheckV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/19", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteSslCheckV2(19) + + if err == nil { + t.Fatal("expected error on non-2xx response, got nil") + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected 'Response code' in error message, got: %v", err) + } + if resp != http.StatusMultipleChoices { + t.Errorf("expected status code %d, got %d", http.StatusMultipleChoices, resp) + } +} + +func TestDeleteSslCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeleteSslCheckV2(19) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/delete_variablesv2_test.go b/syntheticsclientv2/delete_variablesv2_test.go index 29ac73d..09adad0 100644 --- a/syntheticsclientv2/delete_variablesv2_test.go +++ b/syntheticsclientv2/delete_variablesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "fmt" "net/http" + "strings" "testing" ) @@ -46,3 +44,33 @@ func TestDeleteVariableV2(t *testing.T) { } fmt.Println(resp) } + +func TestDeleteVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeleteVariableV2(19) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestDeleteVariableV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/variables/20", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteVariableV2(20) + if err == nil { + t.Fatal("expected error on non-2xx status code, got nil") + } + if resp != http.StatusMultipleChoices { + t.Errorf("returned status \n\n%#v want \n\n%#v", resp, http.StatusMultipleChoices) + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error to contain 'Response code', got %s", err.Error()) + } +} diff --git a/syntheticsclientv2/get_apicheckv2_test.go b/syntheticsclientv2/get_apicheckv2_test.go index 499fa84..e38d9a9 100644 --- a/syntheticsclientv2/get_apicheckv2_test.go +++ b/syntheticsclientv2/get_apicheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -95,3 +92,44 @@ func verifyApiCheckV2Input(stringInput string) *ApiCheckV2Response { } return check } + +func TestGetApiCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/api/490", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetApiCheckV2(490) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestGetApiCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + resp, details, err := unreachableClient.GetApiCheckV2(491) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/get_browsercheckv2_test.go b/syntheticsclientv2/get_browsercheckv2_test.go index 779430e..88915f5 100644 --- a/syntheticsclientv2/get_browsercheckv2_test.go +++ b/syntheticsclientv2/get_browsercheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -171,3 +168,44 @@ func verifyBrowserCheckV2Input(stringInput string) *BrowserCheckV2Response { } return check } + +func TestGetBrowserCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/browser/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetBrowserCheckV2(2) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestGetBrowserCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + resp, details, err := unreachableClient.GetBrowserCheckV2(3) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/get_cacertificatesv2_test.go b/syntheticsclientv2/get_cacertificatesv2_test.go index 5b30388..4fdfd05 100644 --- a/syntheticsclientv2/get_cacertificatesv2_test.go +++ b/syntheticsclientv2/get_cacertificatesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -59,3 +57,39 @@ func verifyCaCertificatesV2Input(stringInput string) *CaCertificatesV2Response { } return check } + +func TestGetCaCertificatesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetCaCertificatesV2() + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetCaCertificatesV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetCaCertificatesV2() + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_cacertificatev2_test.go b/syntheticsclientv2/get_cacertificatev2_test.go index c3fdc5a..a59ac1c 100644 --- a/syntheticsclientv2/get_cacertificatev2_test.go +++ b/syntheticsclientv2/get_cacertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -59,3 +57,39 @@ func verifyCaCertificateV2Input(stringInput string) *CaCertificateV2Response { } return check } + +func TestGetCaCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetCaCertificateV2(1) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetCaCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetCaCertificateV2(1) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_checksv2_test.go b/syntheticsclientv2/get_checksv2_test.go index de819d9..bc17bde 100644 --- a/syntheticsclientv2/get_checksv2_test.go +++ b/syntheticsclientv2/get_checksv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -67,3 +65,100 @@ func verifyChecksV2Input(stringInput string) *GetChecksV2Options { } return check } + +func TestGetChecksV2WithAllQueryParamsPopulated(t *testing.T) { + setup() + defer teardown() + + var gotQuery string + testMux.HandleFunc("/tests", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + gotQuery = r.URL.RawQuery + _, err := w.Write([]byte(`{"tests":[]}`)) + if err != nil { + t.Fatal(err) + } + }) + + active := true + params := &GetChecksV2Options{ + TestType: "api", + Page: 2, + PerPage: 25, + OrderBy: "id", + Search: "beep", + Active: &active, + SchedulingStrategy: "round_robin", + CustomProperties: []CustomProperties{{Key: "env", Value: "prod"}}, + LastRunStatus: []string{"success", "pending"}, + LocationIds: []string{"aws-us-east-1", "aws-ap-northeast-1"}, + TestTypes: []string{"api", "browser"}, + Frequencies: []int{5, 10}, + } + + _, _, err := testClient.GetChecksV2(params) + if err != nil { + t.Fatal(err) + } + + for _, want := range []string{ + "active=true", + "customProperties%5B%5D=env%3Aprod", + "lastRunStatus%5B%5D=success", + "lastRunStatus%5B%5D=pending", + "locationIds%5B%5D=aws-us-east-1", + "locationIds%5B%5D=aws-ap-northeast-1", + "testTypes%5B%5D=api", + "testTypes%5B%5D=browser", + "frequencies%5B%5D=5", + "frequencies%5B%5D=10", + } { + if !strings.Contains(gotQuery, want) { + t.Errorf("query %q missing expected substring %q", gotQuery, want) + } + } +} + +func TestActiveQueryParam(t *testing.T) { + active := true + if got, want := activeQueryParam(&active), "&active=true"; got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } + if got, want := activeQueryParam(nil), ""; got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + +func TestCustomPropsQueryParam(t *testing.T) { + params := []CustomProperties{{Key: "env", Value: "prod"}, {Key: "team", Value: "synthetics"}} + got := customPropsQueryParam(params) + want := "&customProperties[]=env:prod&customProperties[]=team:synthetics" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } + if got := customPropsQueryParam(nil); got != "" { + t.Errorf("returned \n\n%#v want empty string", got) + } +} + +func TestIntegersQueryParam(t *testing.T) { + got := integersQueryParam([]int{5, 10}, "&frequencies[]=") + want := "&frequencies[]=5&frequencies[]=10" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } + if got := integersQueryParam(nil, "&frequencies[]="); got != "" { + t.Errorf("returned \n\n%#v want empty string", got) + } +} + +func TestStringsQueryParam(t *testing.T) { + got := stringsQueryParam([]string{"success", "pending"}, "&lastRunStatus[]=") + want := "&lastRunStatus[]=success&lastRunStatus[]=pending" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } + if got := stringsQueryParam(nil, "&lastRunStatus[]="); got != "" { + t.Errorf("returned \n\n%#v want empty string", got) + } +} diff --git a/syntheticsclientv2/get_clientcertificatesv2_test.go b/syntheticsclientv2/get_clientcertificatesv2_test.go index 1a7d27c..fb4783b 100644 --- a/syntheticsclientv2/get_clientcertificatesv2_test.go +++ b/syntheticsclientv2/get_clientcertificatesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -58,3 +56,39 @@ func verifyClientCertificatesV2Input(stringInput string) *ClientCertificatesV2Re } return check } + +func TestGetClientCertificatesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetClientCertificatesV2() + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetClientCertificatesV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetClientCertificatesV2() + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_clientcertificatev2_test.go b/syntheticsclientv2/get_clientcertificatev2_test.go index 8e3c370..0a2b705 100644 --- a/syntheticsclientv2/get_clientcertificatev2_test.go +++ b/syntheticsclientv2/get_clientcertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -58,3 +56,39 @@ func verifyClientCertificateV2Input(stringInput string) *ClientCertificateV2Resp } return check } + +func TestGetClientCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates/123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetClientCertificateV2(123) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetClientCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetClientCertificateV2(123) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_devicesv2_test.go b/syntheticsclientv2/get_devicesv2_test.go index 7e665dc..f8807f7 100644 --- a/syntheticsclientv2/get_devicesv2_test.go +++ b/syntheticsclientv2/get_devicesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -81,3 +78,35 @@ func verifyDevicesV2Input(stringInput string) *DevicesV2Response { } return check } + +func TestGetDevicesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/devices", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetDevicesV2() + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestGetDevicesV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.GetDevicesV2() + if err == nil { + t.Fatal("expected a connection error") + } +} diff --git a/syntheticsclientv2/get_downtimeconfigurationsv2_test.go b/syntheticsclientv2/get_downtimeconfigurationsv2_test.go index d9fe4c2..abb6b47 100644 --- a/syntheticsclientv2/get_downtimeconfigurationsv2_test.go +++ b/syntheticsclientv2/get_downtimeconfigurationsv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2024 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -145,3 +142,35 @@ func verifyDowntimeConfigurationsV2Input(stringInput string) *GetDowntimeConfigu } return check } + +func TestGetDowntimeConfigurationsV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/downtime_configurations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetDowntimeConfigurationsV2(&GetDowntimeConfigurationsV2Options{}) + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestGetDowntimeConfigurationsV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.GetDowntimeConfigurationsV2(&GetDowntimeConfigurationsV2Options{}) + if err == nil { + t.Fatal("expected a connection error") + } +} diff --git a/syntheticsclientv2/get_excludedfiletypesv2_test.go b/syntheticsclientv2/get_excludedfiletypesv2_test.go index 5c31ebc..ec0fdcc 100644 --- a/syntheticsclientv2/get_excludedfiletypesv2_test.go +++ b/syntheticsclientv2/get_excludedfiletypesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,3 +62,45 @@ func TestParseExcludedFileTypesV2Response(t *testing.T) { t.Fatalf("ExcludedFileTypes = %#v, want %#v", resp.ExcludedFileTypes, expected) } } + +func TestGetExcludedFileTypesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/excluded_file_types", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetExcludedFileTypesV2() + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestGetExcludedFileTypesV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.GetExcludedFileTypesV2() + if err == nil { + t.Fatal("expected a connection error") + } +} + +func TestParseExcludedFileTypesV2ResponseReturnsErrorOnMalformedJSON(t *testing.T) { + resp, err := parseExcludedFileTypesV2Response("{not valid json") + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} diff --git a/syntheticsclientv2/get_httpcheckv2_test.go b/syntheticsclientv2/get_httpcheckv2_test.go index fd1abfa..94cf24c 100644 --- a/syntheticsclientv2/get_httpcheckv2_test.go +++ b/syntheticsclientv2/get_httpcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -115,3 +112,85 @@ func verifyHttpCheckV2Input(stringInput string) *HttpCheckV2Response { } return check } + +func TestGetHttpCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetHttpCheckV2(2) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestGetHttpCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + resp, details, err := unreachableClient.GetHttpCheckV2(3) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} + +func TestGetHttpCheckV2WithNullablePortReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/4", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetHttpCheckV2WithNullablePort(4) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestGetHttpCheckV2WithNullablePortReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + resp, details, err := unreachableClient.GetHttpCheckV2WithNullablePort(5) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/get_locationsv2_test.go b/syntheticsclientv2/get_locationsv2_test.go index 436030e..ac4c16b 100644 --- a/syntheticsclientv2/get_locationsv2_test.go +++ b/syntheticsclientv2/get_locationsv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -140,3 +137,67 @@ func verifyLocationV2Input(stringInput string) *LocationV2Response { } return check } + +func TestGetLocationsV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/locations/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetLocationsV2() + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestGetLocationsV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.GetLocationsV2() + if err == nil { + t.Fatal("expected a connection error") + } +} + +func TestGetLocationV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/locations/aws-us-east-1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.GetLocationV2("aws-us-east-1") + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestGetLocationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.GetLocationV2("aws-us-east-1") + if err == nil { + t.Fatal("expected a connection error") + } +} diff --git a/syntheticsclientv2/get_portcheckv2_test.go b/syntheticsclientv2/get_portcheckv2_test.go index 1811ffd..a925856 100644 --- a/syntheticsclientv2/get_portcheckv2_test.go +++ b/syntheticsclientv2/get_portcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "net/http" "reflect" + "strings" "testing" ) @@ -99,3 +97,39 @@ func verifyPortCheckV2Input(stringInput string) *PortCheckV2Response { } return check } + +func TestGetPortCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/port/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetPortCheckV2(1) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetPortCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetPortCheckV2(1) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_sslcheckv2_test.go b/syntheticsclientv2/get_sslcheckv2_test.go index 0c9cee5..bd6d4ea 100644 --- a/syntheticsclientv2/get_sslcheckv2_test.go +++ b/syntheticsclientv2/get_sslcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -116,3 +113,39 @@ func verifySslCheckV2Input(stringInput string) *SslCheckV2Response { } return check } + +func TestGetSslCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetSslCheckV2(1) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestGetSslCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetSslCheckV2(1) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/get_variablev2_test.go b/syntheticsclientv2/get_variablev2_test.go index 5fc6b1b..94fc536 100644 --- a/syntheticsclientv2/get_variablev2_test.go +++ b/syntheticsclientv2/get_variablev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -142,3 +139,63 @@ func verifyVariablesV2Input(stringInput string) *VariablesV2Response { } return check } + +func TestGetVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/variables/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{invalid json}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetVariableV2(1) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestGetVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetVariableV2(1) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestGetVariablesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{invalid json array}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetVariablesV2() + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestGetVariablesV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetVariablesV2() + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/httpcheckv2_nullable_port_test.go b/syntheticsclientv2/httpcheckv2_nullable_port_test.go index 2e98604..43fe1bf 100644 --- a/syntheticsclientv2/httpcheckv2_nullable_port_test.go +++ b/syntheticsclientv2/httpcheckv2_nullable_port_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/syntheticsclientv2/integration_test.go b/syntheticsclientv2/integration_test.go index 4d48d0d..d088255 100644 --- a/syntheticsclientv2/integration_test.go +++ b/syntheticsclientv2/integration_test.go @@ -23,56 +23,122 @@ import ( "log" "os" "testing" + "time" ) var ( - token = os.Getenv("API_ACCESS_TOKEN") - realm = os.Getenv("REALM") - getChecksV2Body = `{"testType":"","page":1,"perPage":50,"search":"","orderBy":"id"}` - inputGetChecksV2 = GetChecksV2Options{} - createVariableV2Body = `{"variable":{"description":"beep-var","name":"a-variable-named-foodz","secret":false,"value":"bar"}}` - inputVariableV2Data = VariableV2Input{} - updateVariableV2Body = `{"variable":{"description":"My super awesome test variable22","name":"a-variable-named-foodz","secret":false,"value":"bar"}}` - updateVariableV2Data = VariableV2Input{} - createLocationV2Body = `{"location":{"id":"private-data-center-go-test","label":"Data Center place", "default":false}}` - inputLocationV2Data = LocationV2Input{} - createHttpCheckV2Body = `{"test":{"name":"a-minimal-http-integration-test","type":"http", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"https://www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true,"requestMethod":"GET","body":null,"userAgent":null,"authentication":null,"verifyCertificates":false}}` - inputHttpCheckV2Data = HttpCheckV2Input{} - updateHttpCheckV2Body = `{"test":{"name":"a-maximal-http-integration-test-update","type":"http", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"https://www.splunk.com/updated","locationIds":["aws-us-east-1","aws-ap-southeast-2","aws-ap-southeast-4"],"frequency":30,"schedulingStrategy":"round_robin","active":true,"requestMethod":"GET","body":null,"headers":[{"name":"header-1","value":"value-1"},{"name":"header_2","value":"value_2"}],"validations":[{"type":"assert_string","actual":"{{response.first_byte_time}}","expected":"100","comparator":"equals"},{"type":"assert_string","actual":"{{headers.Content-Length}}","expected":"100","comparator":"does_not_equal"}],"userAgent":"user-agent_standards met","authentication":{"username":"beepusers","password":"{{env.terraform-test-foo-301}}"},"verifyCertificates":true}}` - updateHttpCheckV2Data = HttpCheckV2Input{} - createMaximalBrowserCheckV2Body = `{"test":{"name":"a-maximal-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url","options":{"url":"https://splunk.com"}},{"name":"click","type":"click_element","selectors":[{"type":"id","value":"clicky"}],"waitForNav":true,"waitForNavTimeout":2000},{"name":"fill in fieldz","type":"enter_value","selectors":[{"type":"id","value":"beep"}],"value":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"accept---Alert","type":"accept_alert"},{"name":"Select-Val-Index","type":"select_option","selectors":[{"type":"id","value":"selectionz"}],"optionSelectorType":"index","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-val-text","type":"select_option","selectors":[{"type":"id","value":"textzz"}],"optionSelectorType":"text","optionSelector":"sdad","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-Val-Val","type":"select_option","selectors":[{"type":"id","value":"valz"}],"optionSelectorType":"value","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Run JS","type":"run_javascript","value":"beeeeeeep","waitForNav":true,"waitForNavTimeout":2000},{"name":"Save as text","type":"store_variable_from_element","selectors":[{"type":"link","value":"beepval"}],"variableName":"{{env.terraform-test-foo-301}}"},{"name":"Wait","type":"wait","duration":1312},{"name":"Save JS return Val","type":"store_variable_from_javascript","value":"sdasds","variableName":"{{env.terraform-test-foo-301}}","waitForNav":true,"waitForNavTimeout":2000}]}],"urlProtocol":"https://","startUrl":"www.splunk.com","locationIds":["aws-us-east-1"],"deviceId":1,"frequency":5,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true,"authentication":{"username":"boopuser","password":"{{env.beep-var}}"},"headers":[{"name":"batman","value":"Agentoz","domain":"www.batmansagent.com"}],"cookies":[{"key":"super","value":"duper","domain":"www.batmansagent.com","path":"/boom/goes/beep"}]}}}` - createMinimalBrowserCheckV2Body = `{"test":{"name":"a-minimal-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url"}]}],"locationIds":["aws-us-east-1"],"deviceId":1,"frequency":5,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true}}}` - inputBrowserCheckV2Data = BrowserCheckV2Input{} - updateBrowserCheckV2Body = `{"test":{"name":"a-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url","options":{"url":"https://splunk.com"}},{"name":"click","type":"click_element","selectors":[{"type":"id","value":"clicky"}],"waitForNav":true,"waitForNavTimeout":2000},{"name":"fill in fieldz","type":"enter_value","selectors":[{"type":"id","value":"beep"}],"value":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"accept---Alert","type":"accept_alert"},{"name":"Select-Val-Index","type":"select_option","selectors":[{"type":"id","value":"selectionz"}],"optionSelectorType":"index","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-val-text","type":"select_option","selectors":[{"type":"id","value":"textzz"}],"optionSelectorType":"text","optionSelector":"sdad","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-Val-Val","type":"select_option","selectors":[{"type":"id","value":"valz"}],"optionSelectorType":"value","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Run JS","type":"run_javascript","value":"beeeeeeep","waitForNav":true,"waitForNavTimeout":2000},{"name":"Save as text","type":"store_variable_from_element","selectors":[{"type":"link","value":"beepval"}],"variableName":"{{env.terraform-test-foo-301}}"},{"name":"Save JS return Val","type":"store_variable_from_javascript","value":"sdasds","variableName":"{{env.terraform-test-foo-301}}","waitForNav":true,"waitForNavTimeout":2000}]}],"urlProtocol":"https://","startUrl":"www.splunk.com","locationIds":["aws-us-east-1"],"deviceId":1,"frequency":15,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true,"authentication":{"username":"boopuser","password":"{{env.beep-var}}"},"headers":[{"name":"batman","value":"Agentoz","domain":"www.batmansagent.com"}],"cookies":[{"key":"super","value":"dooper","domain":"www.batmansagent.com","path":"/boom/goes/beep"}]}}}` - updateBrowserCheckV2Data = BrowserCheckV2Input{} - createPortCheckV2Body = `{"test":{"name":"a - port 443 check","type":"port", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"","port":443,"protocol":"tcp","host":"www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true}}` - inputPortCheckV2Data = PortCheckV2Input{} - updatePortCheckV2Body = `{"test":{"name":"a2 - port 443 check","type":"port", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"","port":448,"protocol":"tcp","host":"www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true}}` - updatePortCheckV2Data = PortCheckV2Input{} - createApiV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-maximual-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"Get-Test","requestMethod":"GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/489","headers":{"X-SF-TOKEN":"jinglebellsbatmanshells","beep":"boop"},"body":null},"setup":[{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"sd","variable":"extractsetupvar"},{"name":"JavaScript run","type":"javascript","code":"asdasd","variable":"jsvarsetup"},{"name":"Save response body","type":"save","value":"{{response.body}}","variable":"savesetupvar"}],"validations":[{"name":"JavaScript run","type":"javascript","code":"codetorun","variable":"jscodevar"},{"name":"Save response body","type":"save","value":"{{response.body}}","variable":"saverespvar"},{"name":"Assert response code equals 200","type":"assert_numeric","actual":"{{response.code}}","expected":"200","comparator":"equals"},{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"js.extractor","variable":"extractjvar"}]}]}}` - createMinimalApiV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-minimal-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"apishortGet-Test","requestMethod":"GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/489"}}]}}` - inputApiCheckV2Data = ApiCheckV2Input{} - updateApiCheckV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"Get-Test","requestMethod": "GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/4892","headers":{"X-SF-TOKEN":"jinglebellsbatmanshells", "beep":"boop"},"body":null},"setup":[{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"$.requests","variable":"custom-varz"}],"validations":[{"name":"Assert response code equals 200","type":"assert_numeric","actual":"{{response.code}}","expected":"200","comparator":"equals"}]}]}}` - updateApiCheckV2Data = ApiCheckV2Input{} - inputDowntimeConfigurationV2Data = DowntimeConfigurationV2Input{} - updateDowntimeConfigurationV2Data = DowntimeConfigurationV2Input{} + token = os.Getenv("API_ACCESS_TOKEN") + realm = os.Getenv("REALM") + liveGetChecksV2Body = `{"testType":"","page":1,"perPage":50,"search":"","orderBy":"id"}` + liveInputGetChecksV2 = GetChecksV2Options{} + liveCreateVariableV2Body = `{"variable":{"description":"beep-var","name":"a-variable-named-foodz","secret":false,"value":"bar"}}` + liveInputVariableV2Data = VariableV2Input{} + liveUpdateVariableV2Body = `{"variable":{"description":"My super awesome test variable22","name":"a-variable-named-foodz","secret":false,"value":"bar"}}` + updateVariableV2Data = VariableV2Input{} + liveCreateLocationV2Body = `{"location":{"id":"private-data-center-go-test","label":"Data Center place", "default":false}}` + liveInputLocationV2Data = LocationV2Input{} + liveCreateHttpCheckV2Body = `{"test":{"name":"a-minimal-http-integration-test","type":"http", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"https://www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true,"requestMethod":"GET","body":null,"userAgent":null,"authentication":null,"verifyCertificates":false}}` + liveInputHttpCheckV2Data = HttpCheckV2Input{} + liveUpdateHttpCheckV2Body = `{"test":{"name":"a-maximal-http-integration-test-update","type":"http", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"https://www.splunk.com/updated","locationIds":["aws-us-east-1","aws-ap-southeast-2","aws-ap-southeast-4"],"frequency":30,"schedulingStrategy":"round_robin","active":true,"requestMethod":"GET","body":null,"headers":[{"name":"header-1","value":"value-1"},{"name":"header_2","value":"value_2"}],"validations":[{"type":"assert_string","actual":"{{response.first_byte_time}}","expected":"100","comparator":"equals"},{"type":"assert_string","actual":"{{headers.Content-Length}}","expected":"100","comparator":"does_not_equal"}],"userAgent":"user-agent_standards met","authentication":{"username":"beepusers","password":"{{env.terraform-test-foo-301}}"},"verifyCertificates":true}}` + updateHttpCheckV2Data = HttpCheckV2Input{} + createMaximalBrowserCheckV2Body = `{"test":{"name":"a-maximal-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url","options":{"url":"https://splunk.com"}},{"name":"click","type":"click_element","selectors":[{"type":"id","value":"clicky"}],"waitForNav":true,"waitForNavTimeout":2000},{"name":"fill in fieldz","type":"enter_value","selectors":[{"type":"id","value":"beep"}],"value":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"accept---Alert","type":"accept_alert"},{"name":"Select-Val-Index","type":"select_option","selectors":[{"type":"id","value":"selectionz"}],"optionSelectorType":"index","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-val-text","type":"select_option","selectors":[{"type":"id","value":"textzz"}],"optionSelectorType":"text","optionSelector":"sdad","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-Val-Val","type":"select_option","selectors":[{"type":"id","value":"valz"}],"optionSelectorType":"value","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Run JS","type":"run_javascript","value":"beeeeeeep","waitForNav":true,"waitForNavTimeout":2000},{"name":"Save as text","type":"store_variable_from_element","selectors":[{"type":"link","value":"beepval"}],"variableName":"{{env.terraform-test-foo-301}}"},{"name":"Wait","type":"wait","duration":1312},{"name":"Save JS return Val","type":"store_variable_from_javascript","value":"sdasds","variableName":"{{env.terraform-test-foo-301}}","waitForNav":true,"waitForNavTimeout":2000}]}],"urlProtocol":"https://","startUrl":"www.splunk.com","locationIds":["aws-us-east-1"],"deviceId":1,"frequency":5,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true,"authentication":{"username":"boopuser","password":"{{env.beep-var}}"},"headers":[{"name":"batman","value":"Agentoz","domain":"www.batmansagent.com"}],"cookies":[{"key":"super","value":"duper","domain":"www.batmansagent.com","path":"/boom/goes/beep"}]}}}` + createMinimalBrowserCheckV2Body = `{"test":{"name":"a-minimal-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url"}]}],"locationIds":["aws-us-east-1"],"deviceId":1,"frequency":5,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true}}}` + liveInputBrowserCheckV2Data = BrowserCheckV2Input{} + liveUpdateBrowserCheckV2Body = `{"test":{"name":"a-browser-beep-test", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"transactions":[{"name":"Synthetic transaction 1","steps":[{"name":"Go to URL","type":"go_to_url","url":"https://splunk.com","action":"go_to_url","options":{"url":"https://splunk.com"}},{"name":"click","type":"click_element","selectors":[{"type":"id","value":"clicky"}],"waitForNav":true,"waitForNavTimeout":2000},{"name":"fill in fieldz","type":"enter_value","selectors":[{"type":"id","value":"beep"}],"value":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"accept---Alert","type":"accept_alert"},{"name":"Select-Val-Index","type":"select_option","selectors":[{"type":"id","value":"selectionz"}],"optionSelectorType":"index","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-val-text","type":"select_option","selectors":[{"type":"id","value":"textzz"}],"optionSelectorType":"text","optionSelector":"sdad","waitForNav":false,"waitForNavTimeout":50},{"name":"Select-Val-Val","type":"select_option","selectors":[{"type":"id","value":"valz"}],"optionSelectorType":"value","optionSelector":"{{env.beep-var}}","waitForNav":false,"waitForNavTimeout":50},{"name":"Run JS","type":"run_javascript","value":"beeeeeeep","waitForNav":true,"waitForNavTimeout":2000},{"name":"Save as text","type":"store_variable_from_element","selectors":[{"type":"link","value":"beepval"}],"variableName":"{{env.terraform-test-foo-301}}"},{"name":"Save JS return Val","type":"store_variable_from_javascript","value":"sdasds","variableName":"{{env.terraform-test-foo-301}}","waitForNav":true,"waitForNavTimeout":2000}]}],"urlProtocol":"https://","startUrl":"www.splunk.com","locationIds":["aws-us-east-1"],"deviceId":1,"frequency":15,"schedulingStrategy":"round_robin","active":true,"advancedSettings":{"verifyCertificates":true,"authentication":{"username":"boopuser","password":"{{env.beep-var}}"},"headers":[{"name":"batman","value":"Agentoz","domain":"www.batmansagent.com"}],"cookies":[{"key":"super","value":"dooper","domain":"www.batmansagent.com","path":"/boom/goes/beep"}]}}}` + updateBrowserCheckV2Data = BrowserCheckV2Input{} + liveCreatePortCheckV2Body = `{"test":{"name":"a - port 443 check","type":"port", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"","port":443,"protocol":"tcp","host":"www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true}}` + liveInputPortCheckV2Data = PortCheckV2Input{} + liveUpdatePortCheckV2Body = `{"test":{"name":"a2 - port 443 check","type":"port", "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"url":"","port":448,"protocol":"tcp","host":"www.splunk.com","locationIds":["aws-us-east-1"],"frequency":10,"schedulingStrategy":"round_robin","active":true}}` + updatePortCheckV2Data = PortCheckV2Input{} + liveCreateApiV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-maximual-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"Get-Test","requestMethod":"GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/489","headers":{"X-SF-TOKEN":"jinglebellsbatmanshells","beep":"boop"},"body":null},"setup":[{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"sd","variable":"extractsetupvar"},{"name":"JavaScript run","type":"javascript","code":"asdasd","variable":"jsvarsetup"},{"name":"Save response body","type":"save","value":"{{response.body}}","variable":"savesetupvar"}],"validations":[{"name":"JavaScript run","type":"javascript","code":"codetorun","variable":"jscodevar"},{"name":"Save response body","type":"save","value":"{{response.body}}","variable":"saverespvar"},{"name":"Assert response code equals 200","type":"assert_numeric","actual":"{{response.code}}","expected":"200","comparator":"equals"},{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"js.extractor","variable":"extractjvar"}]}]}}` + createMinimalApiV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-minimal-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"apishortGet-Test","requestMethod":"GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/489"}}]}}` + inputApiCheckV2Data = ApiCheckV2Input{} + liveUpdateApiCheckV2Body = `{"test":{"active":true,"deviceId":1,"frequency":5, "automaticRetries": 1, "customProperties": [{"key": "Test_Key", "value": "Test Custom Properties"}],"locationIds":["aws-us-east-1"],"name":"a-API-boop-test","schedulingStrategy":"round_robin","requests":[{"configuration":{"name":"Get-Test","requestMethod": "GET","url":"https://api.us1.signalfx.com/v2/synthetics/v2/tests/api/4892","headers":{"X-SF-TOKEN":"jinglebellsbatmanshells", "beep":"boop"},"body":null},"setup":[{"name":"Extract from response body","type":"extract_json","source":"{{response.body}}","extractor":"$.requests","variable":"custom-varz"}],"validations":[{"name":"Assert response code equals 200","type":"assert_numeric","actual":"{{response.code}}","expected":"200","comparator":"equals"}]}]}}` + updateApiCheckV2Data = ApiCheckV2Input{} + liveInputDowntimeConfigurationV2Data = DowntimeConfigurationV2Input{} + updateDowntimeConfigurationV2Data = DowntimeConfigurationV2Input{} ) -// You will need to fill in values for the get and delete tests -// as the check ids will vary from organization to organization +// Every test below creates whatever fixture it needs and deletes it before +// returning, so the suite can be re-run repeatedly against the same org +// without accumulating orphaned resources or colliding on fixed names/ids. + +const ( + livePrereqBeepVarName = "beep-var" + livePrereqTerraformVarName = "terraform-test-foo-301" +) + +// TestMain provisions the two variables referenced by name +// (via {{env.beep-var}} and {{env.terraform-test-foo-301}}) in the http/browser +// check bodies used below, since the Synthetics API requires an +// authentication.password variable reference to resolve to an existing +// variable. Both are torn down after the suite runs. +func TestMain(m *testing.M) { + c := NewClient(token, realm) + + // A prior run killed before m.Run() returned (e.g. a cancelled CI job) skips + // both t.Cleanup and the deletes below, leaving these fixed-name variables + // behind. Reclaim any leftovers before creating fresh ones so that run isn't + // stuck failing every subsequent run with a duplicate-name error. + reclaimPrerequisiteVariable(c, livePrereqBeepVarName) + reclaimPrerequisiteVariable(c, livePrereqTerraformVarName) + + beepVar, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: livePrereqBeepVarName, Value: "bar", Secret: false}}) + if err != nil { + log.Fatalf("failed to create prerequisite variable %q: %v", livePrereqBeepVarName, err) + } + + terraformVar, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: livePrereqTerraformVarName, Value: "bar", Secret: false}}) + if err != nil { + if _, delErr := c.DeleteVariableV2(beepVar.Variable.ID); delErr != nil { + log.Printf("failed to clean up prerequisite variable %q: %v", livePrereqBeepVarName, delErr) + } + log.Fatalf("failed to create prerequisite variable %q: %v", livePrereqTerraformVarName, err) + } + + code := m.Run() + + if _, err := c.DeleteVariableV2(beepVar.Variable.ID); err != nil { + log.Printf("failed to clean up prerequisite variable %q: %v", livePrereqBeepVarName, err) + } + if _, err := c.DeleteVariableV2(terraformVar.Variable.ID); err != nil { + log.Printf("failed to clean up prerequisite variable %q: %v", livePrereqTerraformVarName, err) + } + + os.Exit(code) +} + +// reclaimPrerequisiteVariable deletes any existing variable with the given name so a +// stale leftover from an interrupted prior run doesn't cause CreateVariableV2 below to +// fail with a duplicate-name error. +func reclaimPrerequisiteVariable(c *Client, name string) { + existing, _, err := c.GetVariablesV2() + if err != nil { + log.Printf("failed to list variables while checking for leftover %q: %v", name, err) + return + } + for _, v := range existing.Variable { + if v.Name != name { + continue + } + if _, err := c.DeleteVariableV2(v.ID); err != nil { + log.Printf("failed to reclaim leftover prerequisite variable %q: %v", name, err) + } + } +} func TestLiveGetChecksV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := json.Unmarshal([]byte(getChecksV2Body), &inputGetChecksV2) + err := json.Unmarshal([]byte(liveGetChecksV2Body), &liveInputGetChecksV2) if err != nil { t.Fatal(err) } // Make the request with your check settings and print result - res, _, err := c.GetChecksV2(&inputGetChecksV2) + res, _, err := c.GetChecksV2(&liveInputGetChecksV2) if err != nil { fmt.Println(err) } else { @@ -87,7 +153,7 @@ func TestLiveGetChecksV2(t *testing.T) { func TestLiveCreateVariableV2(t *testing.T) { - err := json.Unmarshal([]byte(createVariableV2Body), &inputVariableV2Data) + err := json.Unmarshal([]byte(liveCreateVariableV2Body), &liveInputVariableV2Data) if err != nil { t.Fatal(err) } @@ -95,47 +161,53 @@ func TestLiveCreateVariableV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - fmt.Println(inputVariableV2Data) + fmt.Println(liveInputVariableV2Data) // Make the request with your check settings and print result - res, reqDetail, err := c.CreateVariableV2(&inputVariableV2Data) - if err != nil { - fmt.Println(err) - } else { - fmt.Println(reqDetail) - JsonPrint(res) - } - + res, reqDetail, err := c.CreateVariableV2(&liveInputVariableV2Data) if err != nil { t.Fatal(err) } + fmt.Println(reqDetail) + JsonPrint(res) + + t.Cleanup(func() { + if _, err := c.DeleteVariableV2(res.Variable.ID); err != nil { + t.Errorf("failed to clean up variable %d: %v", res.Variable.ID, err) + } + }) } func TestLiveUpdateVariableV2(t *testing.T) { - err := json.Unmarshal([]byte(updateVariableV2Body), &updateVariableV2Data) + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-variable-named-foodz", Value: "bar", Secret: false}}) if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if _, err := c.DeleteVariableV2(created.Variable.ID); err != nil { + t.Errorf("failed to clean up variable %d: %v", created.Variable.ID, err) + } + }) - //Update your client with the token - c := NewClient(token, realm) + err = json.Unmarshal([]byte(liveUpdateVariableV2Body), &updateVariableV2Data) + if err != nil { + t.Fatal(err) + } fmt.Println(updateVariableV2Data) // Make the request with your check settings and print result - res, reqDetail, err := c.UpdateVariableV2(859, &updateVariableV2Data) - if err != nil { - fmt.Println(err) - } else { - fmt.Println(reqDetail) - JsonPrint(res) - } - + res, reqDetail, err := c.UpdateVariableV2(created.Variable.ID, &updateVariableV2Data) if err != nil { t.Fatal(err) } + fmt.Println(reqDetail) + JsonPrint(res) } @@ -144,17 +216,22 @@ func TestLiveGetVariableV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - // Make the request with your check settings and print result - res, _, err := c.GetVariableV2(859) + created, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-variable-named-foodz", Value: "bar", Secret: false}}) if err != nil { - fmt.Println(err) - } else { - JsonPrint(res) + t.Fatal(err) } + t.Cleanup(func() { + if _, err := c.DeleteVariableV2(created.Variable.ID); err != nil { + t.Errorf("failed to clean up variable %d: %v", created.Variable.ID, err) + } + }) + // Make the request with your check settings and print result + res, _, err := c.GetVariableV2(created.Variable.ID) if err != nil { t.Fatal(err) } + JsonPrint(res) } @@ -163,17 +240,17 @@ func TestLiveDeleteVariableV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - // Make the request with your check settings and print result - res, err := c.DeleteVariableV2(398) + created, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-variable-named-foodz", Value: "bar", Secret: false}}) if err != nil { - fmt.Println(err) - } else { - JsonPrint(res) + t.Fatal(err) } + // Make the request with your check settings and print result + res, err := c.DeleteVariableV2(created.Variable.ID) if err != nil { t.Fatal(err) } + JsonPrint(res) } @@ -201,12 +278,17 @@ func TestLiveHttpCheckCreateUpdateDeleteV2(t *testing.T) { c := NewClient(token, realm) var err error - checkId, err := CreateHttpCheckV2(createHttpCheckV2Body, c) + checkId, err := CreateHttpCheckV2(liveCreateHttpCheckV2Body, c) if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeleteHttpCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up http check %d: %v", checkId, err) + } + }) - err = UpdateHttpCheckV2(checkId, updateHttpCheckV2Body, c) + err = UpdateHttpCheckV2(checkId, liveUpdateHttpCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -216,7 +298,7 @@ func TestLiveHttpCheckCreateUpdateDeleteV2(t *testing.T) { t.Fatal(err) } - err2 := UpdateHttpCheckV2(checkId, createHttpCheckV2Body, c) + err2 := UpdateHttpCheckV2(checkId, liveCreateHttpCheckV2Body, c) if err2 != nil { t.Fatal(err2) } @@ -226,47 +308,42 @@ func TestLiveHttpCheckCreateUpdateDeleteV2(t *testing.T) { t.Fatal(err) } - err = DeleteHttpCheckV2(checkId, c) - if err != nil { - t.Fatal(err) - } - } func CreateHttpCheckV2(test string, c *Client) (int, error) { - err := json.Unmarshal([]byte(test), &inputHttpCheckV2Data) + err := json.Unmarshal([]byte(test), &liveInputHttpCheckV2Data) if err != nil { return 0, err } // Make the request with your check settings and print result - res, reqDetail, err := c.CreateHttpCheckV2(&inputHttpCheckV2Data) + res, reqDetail, err := c.CreateHttpCheckV2(&liveInputHttpCheckV2Data) if err != nil { return 0, err } fmt.Printf("\nReq was: \n%v\n", reqDetail) JsonPrint(res) - inputHttpCheckV2Data = HttpCheckV2Input{} + liveInputHttpCheckV2Data = HttpCheckV2Input{} return res.Test.ID, nil } func UpdateHttpCheckV2(checkId int, test string, c *Client) error { - err := json.Unmarshal([]byte(test), &inputHttpCheckV2Data) + err := json.Unmarshal([]byte(test), &liveInputHttpCheckV2Data) if err != nil { return err } // Make the request with your check settings and print result - res, reqDetail, err := c.UpdateHttpCheckV2(checkId, &inputHttpCheckV2Data) + res, reqDetail, err := c.UpdateHttpCheckV2(checkId, &liveInputHttpCheckV2Data) if err != nil { return err } fmt.Printf("\nReq was: \n%v\n", reqDetail) JsonPrint(res) - inputHttpCheckV2Data = HttpCheckV2Input{} + liveInputHttpCheckV2Data = HttpCheckV2Input{} return nil } @@ -301,12 +378,18 @@ func TestLiveCreateHttpCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - checkId, err := CreateHttpCheckV2(createHttpCheckV2Body, c) + checkId, err := CreateHttpCheckV2(liveCreateHttpCheckV2Body, c) if err != nil { t.Fatal(err) } log.Printf("Http Check ID: %d", checkId) + t.Cleanup(func() { + if err := DeleteHttpCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up http check %d: %v", checkId, err) + } + }) + } func TestLiveUpdateHttpCheckV2(t *testing.T) { @@ -314,7 +397,17 @@ func TestLiveUpdateHttpCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := UpdateHttpCheckV2(1111, updateHttpCheckV2Body, c) + checkId, err := CreateHttpCheckV2(liveCreateHttpCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteHttpCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up http check %d: %v", checkId, err) + } + }) + + err = UpdateHttpCheckV2(checkId, liveUpdateHttpCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -326,7 +419,17 @@ func TestLiveGetHttpCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := GetHttpCheckV2(1111, c) + checkId, err := CreateHttpCheckV2(liveCreateHttpCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteHttpCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up http check %d: %v", checkId, err) + } + }) + + err = GetHttpCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -338,7 +441,12 @@ func TestLiveDeleteHttpCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := DeleteHttpCheckV2(1111, c) + checkId, err := CreateHttpCheckV2(liveCreateHttpCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + + err = DeleteHttpCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -355,6 +463,11 @@ func TestLiveBrowserCheckCreateUpdateAndDeleteV2(t *testing.T) { if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeleteBrowserCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up browser check %d: %v", checkId, err) + } + }) err = UpdateBrowserCheckV2(checkId, createMinimalBrowserCheckV2Body, c) if err != nil { @@ -376,22 +489,17 @@ func TestLiveBrowserCheckCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err = DeleteBrowserCheckV2(checkId, c) - if err != nil { - t.Fatal(err) - } - } func CreateBrowserCheckV2(test string, c *Client) (int, error) { - err := json.Unmarshal([]byte(test), &inputBrowserCheckV2Data) + err := json.Unmarshal([]byte(test), &liveInputBrowserCheckV2Data) if err != nil { return 0, err } // Make the request with your check settings and print result - res, reqDetail, err := c.CreateBrowserCheckV2(&inputBrowserCheckV2Data) + res, reqDetail, err := c.CreateBrowserCheckV2(&liveInputBrowserCheckV2Data) if err != nil { return 0, err } @@ -451,6 +559,12 @@ func TestLiveCreateBrowserCheckV2(t *testing.T) { } log.Printf("Browser Check ID: %d", checkId) + + t.Cleanup(func() { + if err := DeleteBrowserCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up browser check %d: %v", checkId, err) + } + }) } func TestLiveGetBrowserCheckV2(t *testing.T) { @@ -458,7 +572,17 @@ func TestLiveGetBrowserCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := GetBrowserCheckV2(1111, c) + checkId, err := CreateBrowserCheckV2(createMaximalBrowserCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteBrowserCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up browser check %d: %v", checkId, err) + } + }) + + err = GetBrowserCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -470,7 +594,17 @@ func TestLiveUpdateBrowserCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := UpdateBrowserCheckV2(1111, createMinimalBrowserCheckV2Body, c) + checkId, err := CreateBrowserCheckV2(createMaximalBrowserCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteBrowserCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up browser check %d: %v", checkId, err) + } + }) + + err = UpdateBrowserCheckV2(checkId, createMinimalBrowserCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -482,7 +616,12 @@ func TestLiveDeleteBrowserCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := DeleteBrowserCheckV2(1111, c) + checkId, err := CreateBrowserCheckV2(createMaximalBrowserCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + + err = DeleteBrowserCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -553,10 +692,15 @@ func TestLiveApiCheckCreateUpdateAndDeleteV2(t *testing.T) { c := NewClient(token, realm) var err error - checkId, err := CreateApiCheckV2(createApiV2Body, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) err = UpdateApiCheckV2(checkId, createMinimalApiV2Body, c) if err != nil { @@ -568,7 +712,7 @@ func TestLiveApiCheckCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err2 := UpdateApiCheckV2(checkId, createApiV2Body, c) + err2 := UpdateApiCheckV2(checkId, liveCreateApiV2Body, c) if err2 != nil { t.Fatal(err2) } @@ -578,23 +722,24 @@ func TestLiveApiCheckCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err = DeleteApiCheckV2(checkId, c) - if err != nil { - t.Fatal(err) - } - } func TestLiveCreateApiCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - checkId, err := CreateApiCheckV2(createApiV2Body, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) if err != nil { t.Fatal(err) } log.Printf("Api Check ID: %d", checkId) + + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) } func TestLiveGetApiCheckV2(t *testing.T) { @@ -602,7 +747,17 @@ func TestLiveGetApiCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := GetApiCheckV2(1111, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) + + err = GetApiCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -614,7 +769,17 @@ func TestLiveUpdateApiCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := UpdateApiCheckV2(1111, updateApiCheckV2Body, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) + + err = UpdateApiCheckV2(checkId, liveUpdateApiCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -626,7 +791,12 @@ func TestLiveDeleteApiCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := DeleteApiCheckV2(1111, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) + if err != nil { + t.Fatal(err) + } + + err = DeleteApiCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -635,13 +805,13 @@ func TestLiveDeleteApiCheckV2(t *testing.T) { func CreatePortCheckV2(test string, c *Client) (int, error) { - err := json.Unmarshal([]byte(test), &inputPortCheckV2Data) + err := json.Unmarshal([]byte(test), &liveInputPortCheckV2Data) if err != nil { return 0, err } // Make the request with your check settings and print result - res, reqDetail, err := c.CreatePortCheckV2(&inputPortCheckV2Data) + res, reqDetail, err := c.CreatePortCheckV2(&liveInputPortCheckV2Data) if err != nil { return 0, err } @@ -697,12 +867,17 @@ func TestLivePortCheckCreateUpdateAndDeleteV2(t *testing.T) { c := NewClient(token, realm) var err error - checkId, err := CreatePortCheckV2(createPortCheckV2Body, c) + checkId, err := CreatePortCheckV2(liveCreatePortCheckV2Body, c) if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeletePortCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up port check %d: %v", checkId, err) + } + }) - err = UpdatePortCheckV2(checkId, updatePortCheckV2Body, c) + err = UpdatePortCheckV2(checkId, liveUpdatePortCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -712,7 +887,7 @@ func TestLivePortCheckCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err2 := UpdatePortCheckV2(checkId, createPortCheckV2Body, c) + err2 := UpdatePortCheckV2(checkId, liveCreatePortCheckV2Body, c) if err2 != nil { t.Fatal(err2) } @@ -722,23 +897,24 @@ func TestLivePortCheckCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err = DeletePortCheckV2(checkId, c) - if err != nil { - t.Fatal(err) - } - } func TestLiveCreatePortCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - checkId, err := CreatePortCheckV2(createPortCheckV2Body, c) + checkId, err := CreatePortCheckV2(liveCreatePortCheckV2Body, c) if err != nil { t.Fatal(err) } log.Printf("Port Check ID: %d", checkId) + + t.Cleanup(func() { + if err := DeletePortCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up port check %d: %v", checkId, err) + } + }) } func TestLiveGetPortCheckV2(t *testing.T) { @@ -746,7 +922,17 @@ func TestLiveGetPortCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := GetPortCheckV2(1111, c) + checkId, err := CreatePortCheckV2(liveCreatePortCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeletePortCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up port check %d: %v", checkId, err) + } + }) + + err = GetPortCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -758,7 +944,17 @@ func TestLiveUpdatePortCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := UpdatePortCheckV2(1111, updatePortCheckV2Body, c) + checkId, err := CreatePortCheckV2(liveCreatePortCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeletePortCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up port check %d: %v", checkId, err) + } + }) + + err = UpdatePortCheckV2(checkId, liveUpdatePortCheckV2Body, c) if err != nil { t.Fatal(err) } @@ -770,7 +966,12 @@ func TestLiveDeletePortCheckV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - err := DeletePortCheckV2(1111, c) + checkId, err := CreatePortCheckV2(liveCreatePortCheckV2Body, c) + if err != nil { + t.Fatal(err) + } + + err = DeletePortCheckV2(checkId, c) if err != nil { t.Fatal(err) } @@ -779,7 +980,7 @@ func TestLiveDeletePortCheckV2(t *testing.T) { func TestLiveCreateLocationV2(t *testing.T) { - err := json.Unmarshal([]byte(createLocationV2Body), &inputLocationV2Data) + err := json.Unmarshal([]byte(liveCreateLocationV2Body), &liveInputLocationV2Data) if err != nil { t.Fatal(err) } @@ -787,20 +988,21 @@ func TestLiveCreateLocationV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - fmt.Println(inputLocationV2Data) + fmt.Println(liveInputLocationV2Data) // Make the request with your location settings and print result - res, reqDetail, err := c.CreateLocationV2(&inputLocationV2Data) - if err != nil { - fmt.Println(err) - } else { - fmt.Println(reqDetail) - JsonPrint(res) - } - + res, reqDetail, err := c.CreateLocationV2(&liveInputLocationV2Data) if err != nil { t.Fatal(err) } + fmt.Println(reqDetail) + JsonPrint(res) + + t.Cleanup(func() { + if _, err := c.DeleteLocationV2(res.Location.ID); err != nil { + t.Errorf("failed to clean up location %q: %v", res.Location.ID, err) + } + }) } @@ -828,17 +1030,17 @@ func TestLiveDeleteLocationV2(t *testing.T) { //Create your client with the token c := NewClient(token, realm) - // Make the request with your location settings and print result - res, err := c.DeleteLocationV2("private-data-center-go-test") + created, _, err := c.CreateLocationV2(&LocationV2Input{Location: Location{ID: "private-data-center-go-test-delete", Label: "Data Center place", Default: false}}) if err != nil { - fmt.Println(err) - } else { - JsonPrint(res) + t.Fatal(err) } + // Make the request with your location settings and print result + res, err := c.DeleteLocationV2(created.Location.ID) if err != nil { t.Fatal(err) } + JsonPrint(res) } @@ -848,17 +1050,22 @@ func TestLiveDowntimeConfigurationCreateUpdateAndDeleteV2(t *testing.T) { c := NewClient(token, realm) var err error - checkId, err := CreateApiCheckV2(createApiV2Body, c) + checkId, err := CreateApiCheckV2(liveCreateApiV2Body, c) if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) //There are restrictions on startTime and endTime for a downtime_configuration so we set the startTime to 10 days //in the future and the endTime to be 1 hour after the startTime tenDaysFromNow := time.Now().AddDate(0, 0, 10) year, month, day := tenDaysFromNow.Date() - startTime := fmt.Sprintf("%s-%s-%sT20:00:00.000Z", year, int(month), day) - endTime := fmt.Sprintf("%s-%s-%sT21:00:00.000Z", year, int(month), day) + startTime := fmt.Sprintf("%d-%02d-%02dT20:00:00.000Z", year, int(month), day) + endTime := fmt.Sprintf("%d-%02d-%02dT21:00:00.000Z", year, int(month), day) createDowntimeConfigurationV2Body := fmt.Sprintf("{\"downtimeConfiguration\":{\"name\":\"dc test\",\"description\":\"My super awesome test downtimeConfiguration\",\"rule\":\"augment_data\",\"testIds\":[%d],\"startTime\":\"%s\",\"endTime\":\"%s\"}}", checkId, startTime, endTime) @@ -866,6 +1073,11 @@ func TestLiveDowntimeConfigurationCreateUpdateAndDeleteV2(t *testing.T) { if err != nil { t.Fatal(err) } + t.Cleanup(func() { + if err := DeleteDowntimeConfigurationV2(downtimeConfigId, c); err != nil { + t.Errorf("failed to clean up downtime configuration %d: %v", downtimeConfigId, err) + } + }) err = GetDowntimeConfigurationV2(downtimeConfigId, c) if err != nil { @@ -884,27 +1096,17 @@ func TestLiveDowntimeConfigurationCreateUpdateAndDeleteV2(t *testing.T) { t.Fatal(err) } - err = DeleteDowntimeConfigurationV2(downtimeConfigId, c) - if err != nil { - t.Fatal(err) - } - - err = DeleteApiCheckV2(checkId, c) - if err != nil { - t.Fatal(err) - } - } func CreateDowntimeConfigurationV2(downtimeConfiguration string, c *Client) (int, error) { - err := json.Unmarshal([]byte(downtimeConfiguration), &inputDowntimeConfigurationV2Data) + err := json.Unmarshal([]byte(downtimeConfiguration), &liveInputDowntimeConfigurationV2Data) if err != nil { return 0, err } // Make the request with your check settings and print result - res, reqDetail, err := c.CreateDowntimeConfigurationV2(&inputDowntimeConfigurationV2Data) + res, reqDetail, err := c.CreateDowntimeConfigurationV2(&liveInputDowntimeConfigurationV2Data) if err != nil { return 0, err } @@ -953,3 +1155,771 @@ func DeleteDowntimeConfigurationV2(downtimeConfigId int, c *Client) error { return nil } + +func liveCreateSslCheckV2Input(name string) *SslCheckV2Input { + input := &SslCheckV2Input{} + input.Test.Name = name + input.Test.LocationIds = []string{"aws-us-east-1"} + input.Test.Frequency = 10 + input.Test.SchedulingStrategy = "round_robin" + input.Test.Active = true + input.Test.Automaticretries = 1 + input.Test.Host = "www.splunk.com" + input.Test.Port = 443 + input.Test.AllowSelfSigned = false + input.Test.AllowUntrustedRoot = false + return input +} + +func TestLiveSslCheckCreateUpdateAndDeleteV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateSslCheckV2(liveCreateSslCheckV2Input("a-maximal-ssl-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteSslCheckV2(created.Test.ID); err != nil { + t.Errorf("failed to clean up ssl check %d: %v", created.Test.ID, err) + } + }) + JsonPrint(created) + + newFrequency := 30 + newActive := false + update := &SslCheckV2UpdateInput{} + update.Test.Frequency = &newFrequency + update.Test.Active = &newActive + + updated, _, err := c.UpdateSslCheckV2(created.Test.ID, update) + if err != nil { + t.Fatal(err) + } + JsonPrint(updated) + + res, _, err := c.GetSslCheckV2(created.Test.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func TestLiveCreateSslCheckV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.CreateSslCheckV2(liveCreateSslCheckV2Input("a-minimal-ssl-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteSslCheckV2(res.Test.ID); err != nil { + t.Errorf("failed to clean up ssl check %d: %v", res.Test.ID, err) + } + }) + JsonPrint(res) +} + +func TestLiveGetSslCheckV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateSslCheckV2(liveCreateSslCheckV2Input("a-gettable-ssl-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteSslCheckV2(created.Test.ID); err != nil { + t.Errorf("failed to clean up ssl check %d: %v", created.Test.ID, err) + } + }) + + res, _, err := c.GetSslCheckV2(created.Test.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func TestLiveUpdateSslCheckV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateSslCheckV2(liveCreateSslCheckV2Input("an-updatable-ssl-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteSslCheckV2(created.Test.ID); err != nil { + t.Errorf("failed to clean up ssl check %d: %v", created.Test.ID, err) + } + }) + + newFrequency := 30 + newActive := false + update := &SslCheckV2UpdateInput{} + update.Test.Frequency = &newFrequency + update.Test.Active = &newActive + + res, _, err := c.UpdateSslCheckV2(created.Test.ID, update) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + confirmed, _, err := c.GetSslCheckV2(created.Test.ID) + if err != nil { + t.Fatal(err) + } + if confirmed.Test.Frequency != newFrequency { + t.Errorf("frequency = %d, want %d", confirmed.Test.Frequency, newFrequency) + } +} + +func TestLiveDeleteSslCheckV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateSslCheckV2(liveCreateSslCheckV2Input("a-deletable-ssl-integration-test")) + if err != nil { + t.Fatal(err) + } + + if _, err := c.DeleteSslCheckV2(created.Test.ID); err != nil { + t.Fatal(err) + } +} + +func liveCreateTotpVariableV2Input(name string) *TotpVariableV2Input { + return &TotpVariableV2Input{Totp: TotpVariableInput{ + Name: name, + Secret: "JBSWY3DPEHPK3PXP", + Digits: 6, + Interval: 30, + HmacDigest: "sha1", + }} +} + +func TestLiveTotpVariableCreateUpdateAndDeleteV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateTotpVariableV2(liveCreateTotpVariableV2Input("a-maximal-totp-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteTotpVariableV2(created.Totp.ID); err != nil { + t.Errorf("failed to clean up totp variable %d: %v", created.Totp.ID, err) + } + }) + JsonPrint(created) + + newDigits := 8 + newInterval := 60 + updated, _, err := c.UpdateTotpVariableV2(created.Totp.ID, &TotpVariableV2UpdateInput{Totp: TotpVariableUpdateInput{ + Digits: &newDigits, + Interval: &newInterval, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(updated) + + res, _, err := c.GetTotpVariableV2(created.Totp.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + list, _, err := c.GetTotpVariablesV2() + if err != nil { + t.Fatal(err) + } + found := false + for _, totp := range list.Totps { + if totp.ID == created.Totp.ID { + found = true + break + } + } + if !found { + t.Errorf("created totp variable %d not present in GetTotpVariablesV2 response", created.Totp.ID) + } +} + +func TestLiveCreateTotpVariableV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.CreateTotpVariableV2(liveCreateTotpVariableV2Input("a-minimal-totp-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteTotpVariableV2(res.Totp.ID); err != nil { + t.Errorf("failed to clean up totp variable %d: %v", res.Totp.ID, err) + } + }) + JsonPrint(res) +} + +func TestLiveGetTotpVariableV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateTotpVariableV2(liveCreateTotpVariableV2Input("a-gettable-totp-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteTotpVariableV2(created.Totp.ID); err != nil { + t.Errorf("failed to clean up totp variable %d: %v", created.Totp.ID, err) + } + }) + + res, _, err := c.GetTotpVariableV2(created.Totp.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func TestLiveUpdateTotpVariableV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateTotpVariableV2(liveCreateTotpVariableV2Input("an-updatable-totp-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteTotpVariableV2(created.Totp.ID); err != nil { + t.Errorf("failed to clean up totp variable %d: %v", created.Totp.ID, err) + } + }) + + newDigits := 8 + newInterval := 60 + res, _, err := c.UpdateTotpVariableV2(created.Totp.ID, &TotpVariableV2UpdateInput{Totp: TotpVariableUpdateInput{ + Digits: &newDigits, + Interval: &newInterval, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + confirmed, _, err := c.GetTotpVariableV2(created.Totp.ID) + if err != nil { + t.Fatal(err) + } + if confirmed.Totp.Digits != newDigits { + t.Errorf("digits = %d, want %d", confirmed.Totp.Digits, newDigits) + } + if confirmed.Totp.Interval != newInterval { + t.Errorf("interval = %d, want %d", confirmed.Totp.Interval, newInterval) + } +} + +func TestLiveDeleteTotpVariableV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateTotpVariableV2(liveCreateTotpVariableV2Input("a-deletable-totp-integration-test")) + if err != nil { + t.Fatal(err) + } + + if _, err := c.DeleteTotpVariableV2(created.Totp.ID); err != nil { + t.Fatal(err) + } +} + +const liveTestCaCertContent = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJekNDQWd1Z0F3SUJBZ0lVZWtuOHdoNzhCRG9CaHkxZ21xWXBPVjhnWGJjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0lURWZNQjBHQTFVRUF3d1djM2x1ZEdndGFXNTBaV2R5WVhScGIyNHRkR1Z6ZERBZUZ3MHlOakE0TVRBeApOVFU0TXpsYUZ3MHpOakE0TURjeE5UVTRNemxhTUNFeEh6QWRCZ05WQkFNTUZuTjViblJvTFdsdWRHVm5jbUYwCmFXOXVMWFJsYzNRd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURKODdKRWVNKzUKV0x5UGF1ZnBmSzVJbWZJMFJhSndybnozZnEzd3c3d2VnSlR4bkt2WkJZNUJaOHhyVjVWcDl6WS8vUE5TM29GZwpLbUJUQnVQZEVMVHU0VkZKZDlTc0lYajhtUnNoNS81aWJUekxaNk1GNVI3NjIzZkJpQ1lNMWZOVXhRMVQvZTlIClV1cC9xb2ZBZ21nMDJEZzJUWE1RMU1IVi9HUGJvaTBjWVR0TnlZR1hVbWNDYkhCZllkNGN1UmI4a0FWbE1UZW8KTjlmU2FEakxqbzNnazhIK1VITlpaRU1WS0dVczkzUE1MV2JzV2NmTE01TWptU21FakxrT3BMbXkwRXRYWC9MOApGYVordU9YTmtLRU5XcCtlRVJJb256aVlPcXN3bitCdVAxbmpMUktTV3AvK1ZtL0s2V1ZGTDFPOHFlOEN6Y3J2CmpWWXNraFVHR21pbkFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCVDMrMDhpTkdkajFLTEdrdlY5TVE5bXkvd0gKWURBZkJnTlZIU01FR0RBV2dCVDMrMDhpTkdkajFLTEdrdlY5TVE5bXkvd0hZREFQQmdOVkhSTUJBZjhFQlRBRApBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCOGxLK0hVays0Wk5ZUG5ZWE94QzhBK1VTL2tvU0U5cmlECjlYb29Oc3diQ3pMR2RWTkFKb1pybHAzRnZBZVZXTXN1bkNrdE9YaTlUSEJnN0c0cmJ3b09PaE1nTU11Q3Z4QlQKOHByZmNOYm5xU3lSemNGWTIyNHBuYU85ci9YajNFTkhpZmg0QzBKZ2xVTk5wWjgwdTFUS0ZKaGl3OUlicUFCdQpXM0pEeEY2MGk3R2hmdnFmUmZBWkt3cFNOYTFBaTVvTFBYSVVlaytFeEFaeC8wd3htT0xOdGxhK2RyT0UvWUpYCjNsZGJ5cjBydFZkZEVTcy9tR1k3Y0tMQmhLVkRWTUpwTHkzSi9qTEIxUm5uOHZtRmF6b1JyTjYvUXhNajVGTSsKdDhINDRnaVRwbC90VTdDelpETTdNbHByN01qMU80WFdqSWlRcW5HVXhPNElxL1d0dWZIQwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" + +func liveCreateCaCertificateV2Input(name string) *CaCertificateV2Input { + return &CaCertificateV2Input{CaCert: CaCertificateInput{ + Name: name, + Content: liveTestCaCertContent, + FileExtension: "pem", + Filename: "ca_cert_file.pem", + }} +} + +func TestLiveCaCertificateCreateUpdateAndDeleteV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateCaCertificateV2(liveCreateCaCertificateV2Input("a-maximal-cacert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteCaCertificateV2(created.CaCert.ID); err != nil { + t.Errorf("failed to clean up ca certificate %d: %v", created.CaCert.ID, err) + } + }) + JsonPrint(created) + + newDescription := "updated by integration test" + updated, _, err := c.UpdateCaCertificateV2(created.CaCert.ID, &CaCertificateV2UpdateInput{CaCert: CaCertificateUpdateInput{ + Description: &newDescription, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(updated) + + res, _, err := c.GetCaCertificateV2(created.CaCert.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + list, _, err := c.GetCaCertificatesV2() + if err != nil { + t.Fatal(err) + } + found := false + for _, cert := range list.CaCerts { + if cert.ID == created.CaCert.ID { + found = true + break + } + } + if !found { + t.Errorf("created ca certificate %d not present in GetCaCertificatesV2 response", created.CaCert.ID) + } +} + +func TestLiveCreateCaCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.CreateCaCertificateV2(liveCreateCaCertificateV2Input("a-minimal-cacert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteCaCertificateV2(res.CaCert.ID); err != nil { + t.Errorf("failed to clean up ca certificate %d: %v", res.CaCert.ID, err) + } + }) + JsonPrint(res) +} + +func TestLiveGetCaCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateCaCertificateV2(liveCreateCaCertificateV2Input("a-gettable-cacert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteCaCertificateV2(created.CaCert.ID); err != nil { + t.Errorf("failed to clean up ca certificate %d: %v", created.CaCert.ID, err) + } + }) + + res, _, err := c.GetCaCertificateV2(created.CaCert.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func TestLiveUpdateCaCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateCaCertificateV2(liveCreateCaCertificateV2Input("an-updatable-cacert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteCaCertificateV2(created.CaCert.ID); err != nil { + t.Errorf("failed to clean up ca certificate %d: %v", created.CaCert.ID, err) + } + }) + + newDescription := "updated by integration test" + res, _, err := c.UpdateCaCertificateV2(created.CaCert.ID, &CaCertificateV2UpdateInput{CaCert: CaCertificateUpdateInput{ + Description: &newDescription, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + confirmed, _, err := c.GetCaCertificateV2(created.CaCert.ID) + if err != nil { + t.Fatal(err) + } + if confirmed.CaCert.Description != newDescription { + t.Errorf("description = %q, want %q", confirmed.CaCert.Description, newDescription) + } +} + +func TestLiveDeleteCaCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateCaCertificateV2(liveCreateCaCertificateV2Input("a-deletable-cacert-integration-test")) + if err != nil { + t.Fatal(err) + } + + if _, err := c.DeleteCaCertificateV2(created.CaCert.ID); err != nil { + t.Fatal(err) + } +} + +const liveTestClientCertPrivateKeyContent = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRREo4N0pFZU0rNVdMeVAKYXVmcGZLNUltZkkwUmFKd3JuejNmcTN3dzd3ZWdKVHhuS3ZaQlk1Qlo4eHJWNVZwOXpZLy9QTlMzb0ZnS21CVApCdVBkRUxUdTRWRkpkOVNzSVhqOG1Sc2g1LzVpYlR6TFo2TUY1Ujc2MjNmQmlDWU0xZk5VeFExVC9lOUhVdXAvCnFvZkFnbWcwMkRnMlRYTVExTUhWL0dQYm9pMGNZVHROeVlHWFVtY0NiSEJmWWQ0Y3VSYjhrQVZsTVRlb045ZlMKYURqTGpvM2drOEgrVUhOWlpFTVZLR1VzOTNQTUxXYnNXY2ZMTTVNam1TbUVqTGtPcExteTBFdFhYL0w4RmFaKwp1T1hOa0tFTldwK2VFUklvbnppWU9xc3duK0J1UDFuakxSS1NXcC8rVm0vSzZXVkZMMU84cWU4Q3pjcnZqVllzCmtoVUdHbWluQWdNQkFBRUNnZ0VBSXI0SVpJd3VIREkrV2lQbm1yem0xTG1iTjgvazlxS21BQVBzazVkd3hRU1UKMnczN2FGM3l6NkMyUTU4eEpxWXZVSW5KS0cvNzdObk5jV3NsaHpIcEZwRnZwUVoyOFZmZTB3SFo3NWJVSmdXcAo2RW8vZXZPa1JUNjlWdTkvc0VTY1ZIQ0Q3dmVvRXVxYVNmVkIzbVh3M0dwNEhTdHN5Sy81V3NGTlFvc2ZYSnExClZtdGFqMWxkRXR2UmhLanY5ZkdYcnRZSFpVR2c2VzVqV0h0a0F5VmZhSkdSbU9HcTA2UVd4aWg0YW9lVHVhQ3QKSUxwci9lUkdQbHpiSVIxWXlhblhGMzVkdGVRTE9mQWR2YXBCWFJlbzA0VWZSWW1hbnVVYSt3Um5ENnBUTEhIKwpNaFNoQkZhQzR4OERxOGJJZnFWOGR5VElyU3pnUUtQY0t3akM3dGZqQVFLQmdRRG1KQklhYnh0L0pacXEvc2gzCm9MUmg5aERFcEV1ejVtYnAyaVNhcXZEYkZQRm9Ic2JVQ2daTHZUL01seS9QYmY0QlBYdjluRWkyRGphNTFFVHAKeWNMZDNnU2JkVS9pQzgwV1BOQzErZ0hwbDBYS0RTaEtGRlRHT2orZ1VNc2ExMkl6aUFRK0R1Ymh5Y1c2Y0QrbwpOeFc2YUloT0JjZHBjK0t0ZUtWWmxVQXRBUUtCZ1FEZ3BNZDN6cktINXNvemlWaXBtRlRwNEI1em1Pd0JPc3grCjN5c2tBSFNXZUkyVkl5T1djcGoxWlhzMUtwaUlUMU9FVGU3UWVpTUFXcmNoVjRMekxiVXdwRzYvRG1icUU5aEUKTW12bmYrS01wL3F1ZVU4akdNSS9PdXpRdVBNVHFIcml6Y0xkdTZCYStnVVNFV1EzdENaeXM5MnlQdlpYTDN3dgpGMkVMZ3ZRTnB3S0JnSHdPOWJOS01ZaFl2UWR3VUtBc0FSRE5sRHhzVkdLbDBOUSt3M3ljcVRsd0VMSVA1UjVvClNQeUxCOWxCcG9RcXhzSGtZdkpUVE43V3lxbGh3OFJDL3NpYTVlRG5YQ2grTkEvSXVMbGdDNmZmNDc4SFdMQ1cKUlJ5V1NiWWgxMXFnd0U4SEEwSnd4Z1R3djZYQTNJL1JJZVZhZEIrYS9lUGFsRmJ1c2pPWVFRQUJBb0dBZThoUQpZU1AwSEE1L3ZJWWg1TkdiZUlPV1Evd3ZqejNuRU1ISDg3Nk1mNTFONXEvR0hGQnBHRThpNU5qajA3aGlQTFQwCnNzdWFIY2Zld1BDSHA1ZTREMldMNEpyKytseVUvbjhLRmpYUmo4Ky93Z1AySjFDdE9Fb3YwNU1WM2U4b1IzRTUKdnhSejk2MXN2ZGYzY1BwRGRWREhDRURKWEtFOXZIVVZkRkprU0dFQ2dZQmM5ZUNrR2Z0clk3SGk3WktadnFEVwpVd210S2t5aW9zTnFraW4zZUZNRE50UGVMaGh5S3laa1N3WExpcVYyVkZqNG1uYXd4KzFYd2hoanlTTFJualhECmlKdlRQQTFtKzBNRmQrWWhiRjZkVmxQaThTNkMzeDNremlFOGdSZVduVDBNaGx2UG8rVm10Q1E1empRMTFzZUMKWFVoQytiaEdxZjFaRUpua1RsbHUrUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" + +func liveCreateClientCertificateV2Input(name string) *ClientCertificateV2Input { + return &ClientCertificateV2Input{Certificate: ClientCertificateInput{ + Name: name, + Domain: "api.integration-test.example.com", + PublicKey: ClientCertificateKeyInput{ + Content: liveTestCaCertContent, + Filename: "client.crt", + FileExtension: "pem", + }, + PrivateKey: ClientCertificatePrivateKeyInput{ + Content: liveTestClientCertPrivateKeyContent, + Filename: "client.key", + FileExtension: "pem", + }, + }} +} + +func TestLiveClientCertificateCreateUpdateAndDeleteV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateClientCertificateV2(liveCreateClientCertificateV2Input("a-maximal-clientcert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteClientCertificateV2(created.Certificate.ID); err != nil { + t.Errorf("failed to clean up client certificate %d: %v", created.Certificate.ID, err) + } + }) + JsonPrint(created) + + newDescription := "updated by integration test" + updated, _, err := c.UpdateClientCertificateV2(created.Certificate.ID, &ClientCertificateV2UpdateInput{Certificate: ClientCertificateUpdateInput{ + Description: &newDescription, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(updated) + + res, _, err := c.GetClientCertificateV2(created.Certificate.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + list, _, err := c.GetClientCertificatesV2() + if err != nil { + t.Fatal(err) + } + found := false + for _, cert := range list.Certificates { + if cert.ID == created.Certificate.ID { + found = true + break + } + } + if !found { + t.Errorf("created client certificate %d not present in GetClientCertificatesV2 response", created.Certificate.ID) + } +} + +func TestLiveCreateClientCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.CreateClientCertificateV2(liveCreateClientCertificateV2Input("a-minimal-clientcert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteClientCertificateV2(res.Certificate.ID); err != nil { + t.Errorf("failed to clean up client certificate %d: %v", res.Certificate.ID, err) + } + }) + JsonPrint(res) +} + +func TestLiveGetClientCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateClientCertificateV2(liveCreateClientCertificateV2Input("a-gettable-clientcert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteClientCertificateV2(created.Certificate.ID); err != nil { + t.Errorf("failed to clean up client certificate %d: %v", created.Certificate.ID, err) + } + }) + + res, _, err := c.GetClientCertificateV2(created.Certificate.ID) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func TestLiveUpdateClientCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateClientCertificateV2(liveCreateClientCertificateV2Input("an-updatable-clientcert-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteClientCertificateV2(created.Certificate.ID); err != nil { + t.Errorf("failed to clean up client certificate %d: %v", created.Certificate.ID, err) + } + }) + + newDescription := "updated by integration test" + res, _, err := c.UpdateClientCertificateV2(created.Certificate.ID, &ClientCertificateV2UpdateInput{Certificate: ClientCertificateUpdateInput{ + Description: &newDescription, + }}) + if err != nil { + t.Fatal(err) + } + JsonPrint(res) + + confirmed, _, err := c.GetClientCertificateV2(created.Certificate.ID) + if err != nil { + t.Fatal(err) + } + if confirmed.Certificate.Description != newDescription { + t.Errorf("description = %q, want %q", confirmed.Certificate.Description, newDescription) + } +} + +func TestLiveDeleteClientCertificateV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateClientCertificateV2(liveCreateClientCertificateV2Input("a-deletable-clientcert-integration-test")) + if err != nil { + t.Fatal(err) + } + + if _, err := c.DeleteClientCertificateV2(created.Certificate.ID); err != nil { + t.Fatal(err) + } +} + +func TestLiveGetVariablesV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-listable-variable-integration-test", Value: "bar", Secret: false}}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteVariableV2(created.Variable.ID); err != nil { + t.Errorf("failed to clean up variable %d: %v", created.Variable.ID, err) + } + }) + + res, _, err := c.GetVariablesV2() + if err != nil { + t.Fatal(err) + } + + found := false + for _, v := range res.Variable { + if v.ID == created.Variable.ID { + found = true + break + } + } + if !found { + t.Errorf("created variable %d not present in GetVariablesV2 response", created.Variable.ID) + } +} + +func TestLiveGetLocationsV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.GetLocationsV2() + if err != nil { + t.Fatal(err) + } + if len(res.Location) == 0 { + t.Error("expected at least one location in GetLocationsV2 response, got none") + } + JsonPrint(res) +} + +func TestLiveGetDowntimeConfigurationsV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + checkId, err := CreateApiCheckV2(createMinimalApiV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteApiCheckV2(checkId, c); err != nil { + t.Errorf("failed to clean up api check %d: %v", checkId, err) + } + }) + + tenDaysFromNow := time.Now().AddDate(0, 0, 10) + year, month, day := tenDaysFromNow.Date() + startTime := fmt.Sprintf("%d-%02d-%02dT20:00:00.000Z", year, int(month), day) + endTime := fmt.Sprintf("%d-%02d-%02dT21:00:00.000Z", year, int(month), day) + + createDowntimeConfigurationV2Body := fmt.Sprintf("{\"downtimeConfiguration\":{\"name\":\"dc list test\",\"description\":\"created by integration test\",\"rule\":\"augment_data\",\"testIds\":[%d],\"startTime\":\"%s\",\"endTime\":\"%s\"}}", checkId, startTime, endTime) + + downtimeConfigId, err := CreateDowntimeConfigurationV2(createDowntimeConfigurationV2Body, c) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := DeleteDowntimeConfigurationV2(downtimeConfigId, c); err != nil { + t.Errorf("failed to clean up downtime configuration %d: %v", downtimeConfigId, err) + } + }) + + res, _, err := c.GetDowntimeConfigurationsV2(&GetDowntimeConfigurationsV2Options{}) + if err != nil { + t.Fatal(err) + } + + found := false + for _, dc := range res.Downtimeconfigurations { + if dc.ID == downtimeConfigId { + found = true + break + } + } + if !found { + t.Errorf("created downtime configuration %d not present in GetDowntimeConfigurationsV2 response", downtimeConfigId) + } +} + +func TestLiveGetExcludedFileTypesV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + res, _, err := c.GetExcludedFileTypesV2() + if err != nil { + t.Fatal(err) + } + JsonPrint(res) +} + +func liveCreateHttpCheckV2WithNullablePortInput(name string) *HttpCheckV2InputWithNullablePort { + input := &HttpCheckV2InputWithNullablePort{} + input.Test.Name = name + input.Test.Type = "http" + input.Test.URL = "https://www.splunk.com" + input.Test.LocationIds = []string{"aws-us-east-1"} + input.Test.Frequency = 10 + input.Test.SchedulingStrategy = "round_robin" + input.Test.Active = true + input.Test.RequestMethod = "GET" + input.Test.Automaticretries = 1 + input.Test.Port = *NewNullableInt(443) + return input +} + +func TestLiveHttpCheckWithNullablePortCreateUpdateAndDeleteV2(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateHttpCheckV2WithNullablePort(liveCreateHttpCheckV2WithNullablePortInput("a-maximal-nullable-port-http-integration-test")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteHttpCheckV2(created.Test.ID); err != nil { + t.Errorf("failed to clean up http check %d: %v", created.Test.ID, err) + } + }) + JsonPrint(created) + + update := liveCreateHttpCheckV2WithNullablePortInput("a-maximal-nullable-port-http-integration-test") + update.Test.Port = *NewNullInt() + + updated, _, err := c.UpdateHttpCheckV2WithNullablePort(created.Test.ID, update) + if err != nil { + t.Fatal(err) + } + JsonPrint(updated) + + res, _, err := c.GetHttpCheckV2WithNullablePort(created.Test.ID) + if err != nil { + t.Fatal(err) + } + if res.Test.Port.Value != nil { + t.Errorf("port = %v, want nil", *res.Test.Port.Value) + } + JsonPrint(res) +} + +func TestLiveGetHttpCheckV2ReturnsErrorForNonexistentID(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + if _, _, err := c.GetHttpCheckV2(0); err == nil { + t.Fatal("expected error getting nonexistent http check, got nil") + } +} + +func TestLiveGetVariableV2ReturnsErrorForNonexistentID(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + if _, _, err := c.GetVariableV2(0); err == nil { + t.Fatal("expected error getting nonexistent variable, got nil") + } +} + +func TestLiveDeleteApiCheckV2ReturnsErrorForNonexistentID(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + if _, err := c.DeleteApiCheckV2(0); err == nil { + t.Fatal("expected error deleting nonexistent api check, got nil") + } +} + +func TestLiveCreateVariableV2ReturnsErrorOnDuplicateName(t *testing.T) { + //Create your client with the token + c := NewClient(token, realm) + + created, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-duplicate-name-integration-test", Value: "bar", Secret: false}}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.DeleteVariableV2(created.Variable.ID); err != nil { + t.Errorf("failed to clean up variable %d: %v", created.Variable.ID, err) + } + }) + + if _, _, err := c.CreateVariableV2(&VariableV2Input{Variable: Variable{Name: "a-duplicate-name-integration-test", Value: "bar", Secret: false}}); err == nil { + t.Fatal("expected error creating variable with duplicate name, got nil") + } +} diff --git a/syntheticsclientv2/synthetics_test.go b/syntheticsclientv2/synthetics_test.go index 7fe4863..26c0501 100644 --- a/syntheticsclientv2/synthetics_test.go +++ b/syntheticsclientv2/synthetics_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +16,7 @@ package syntheticsclientv2 import ( "bytes" + "errors" "log" "net/http" "net/http/httptest" @@ -106,7 +104,10 @@ func TestConfigurableClientErrorStatusCode(t *testing.T) { testMux.HandleFunc("/tests", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(`{"status":"404"}`)) + _, err := w.Write([]byte(`{"status":"404"}`)) + if err != nil { + t.Fatal(err) + } }) testConfigurableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{ @@ -130,7 +131,10 @@ func TestMakePublicAPICallRedactsAPIKeyFromRequestDetails(t *testing.T) { testMux.HandleFunc("/tests", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{}`)) + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatal(err) + } }) apiKey := "secret-api-key" @@ -159,7 +163,10 @@ func TestMakePublicAPICallRedactsCaCertificateContentFromRequestDetails(t *testi testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{}`)) + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatal(err) + } }) apiKey := "secret-api-key" @@ -234,7 +241,10 @@ func TestCreateCaCertificateV2RedactsRequestDetails(t *testing.T) { testMux.HandleFunc("/cacerts", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"cacert":{"id":1,"name":"test-ca","description":"private test CA","content":"","fileExtension":"pem","filename":"ca.pem"}}`)) + _, err := w.Write([]byte(`{"cacert":{"id":1,"name":"test-ca","description":"private test CA","content":"","fileExtension":"pem","filename":"ca.pem"}}`)) + if err != nil { + t.Fatal(err) + } }) apiKey := "secret-api-key" @@ -378,6 +388,180 @@ func TestSanitizeRequestDumpRedactsURLQueryValues(t *testing.T) { } } +func TestClientString(t *testing.T) { + setup() + defer teardown() + + got := testClient.String() + want := "Splunk Synthetics Client: URL: " + testClient.publicBaseURL + " " + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + +func TestNewClientArgs(t *testing.T) { + args := NewClientArgs(45, "https://example.com") + if args.timeoutSeconds != 45 { + t.Errorf("returned \n\n%#v want \n\n%#v", args.timeoutSeconds, 45) + } + if args.publicBaseUrl != "https://example.com" { + t.Errorf("returned \n\n%#v want \n\n%#v", args.publicBaseUrl, "https://example.com") + } +} + +func TestNewClient(t *testing.T) { + client := NewClient("snakedonut", "us0") + if client.apiKey != "snakedonut" { + t.Errorf("returned \n\n%#v want \n\n%#v", client.apiKey, "snakedonut") + } + if client.realm != "us0" { + t.Errorf("returned \n\n%#v want \n\n%#v", client.realm, "us0") + } + if client.publicBaseURL != "https://api.us0.signalfx.com/v2/synthetics" { + t.Errorf("returned \n\n%#v want \n\n%#v", client.publicBaseURL, "https://api.us0.signalfx.com/v2/synthetics") + } + if client.GetHTTPClient().Timeout != 30*time.Second { + t.Errorf("returned \n\n%#v want \n\n%#v", client.GetHTTPClient().Timeout, 30*time.Second) + } +} + +func TestJsonPrint(t *testing.T) { + // JsonPrint only writes to stdout; this exercises both the success and + // marshal-error branches without asserting on stdout content. + JsonPrint(map[string]string{"key": "value"}) + JsonPrint(make(chan int)) +} + +func TestMakePublicAPICallReturnsUnknownErrorForNonJSONErrorBody(t *testing.T) { + testMux = http.NewServeMux() + testServer = httptest.NewServer(testMux) + defer testServer.Close() + + testMux.HandleFunc("/tests", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + testConfigurableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{ + publicBaseUrl: testServer.URL, + }) + details, err := testConfigurableClient.makePublicAPICall("GET", "/tests", nil, nil) + + if err == nil { + t.Fatal("expected an error for a non-2xx response with an empty body") + } + if !strings.Contains(err.Error(), "unknown error, status code: 500") { + t.Errorf("returned \n\n%#v want error containing \n\n%#v", err.Error(), "unknown error, status code: 500") + } + if details.StatusCode != http.StatusInternalServerError { + t.Errorf("returned \n\n%#v want \n\n%#v", details.StatusCode, http.StatusInternalServerError) + } +} + +func TestMakePublicAPICallSetsQueryParams(t *testing.T) { + testMux = http.NewServeMux() + testServer = httptest.NewServer(testMux) + defer testServer.Close() + + testMux.HandleFunc("/tests", func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Query().Get("page"); got != "2" { + t.Errorf("returned query param \n\n%#v want \n\n%#v", got, "2") + } + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatal(err) + } + }) + + testConfigurableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{ + publicBaseUrl: testServer.URL, + }) + _, err := testConfigurableClient.makePublicAPICall("GET", "/tests", nil, map[string]string{"page": "2"}) + if err != nil { + t.Fatalf("expected no error, but saw: %s", err.Error()) + } +} + +func TestMakePublicAPICallReturnsErrorForInvalidMethod(t *testing.T) { + setup() + defer teardown() + + _, err := testClient.makePublicAPICall("IN VALID", "/tests", nil, nil) + if err == nil { + t.Fatal("expected an error for an invalid HTTP method") + } +} + +type erroringReader struct{} + +func (erroringReader) Read([]byte) (int, error) { + return 0, errors.New("simulated read error") +} + +func TestMakePublicAPICallReturnsErrorWhenRequestBodyFailsToRead(t *testing.T) { + setup() + defer teardown() + + _, err := testClient.makePublicAPICall("POST", "/tests", erroringReader{}, nil) + if err == nil { + t.Fatal("expected an error when the request body fails to read") + } +} + +func TestRedactRequestDumpURLQueryWithNoNewline(t *testing.T) { + requestLine := "GET /tests?token=secret HTTP/1.1" + got := redactRequestDumpURLQuery(requestLine) + want := "GET /tests?token=[REDACTED] HTTP/1.1" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + +func TestRedactRequestLineURLQueryWithMalformedRequestLine(t *testing.T) { + requestLine := "malformed-request-line" + got := redactRequestLineURLQuery(requestLine) + if got != requestLine { + t.Errorf("returned \n\n%#v want \n\n%#v", got, requestLine) + } +} + +func TestSplitRequestDumpReturnsFalseWhenNoSeparatorFound(t *testing.T) { + headers, body, separator, ok := splitRequestDump("no separator here") + if ok { + t.Fatalf("expected ok=false, but saw headers=%q body=%q separator=%q", headers, body, separator) + } +} + +func TestIsSensitiveHeaderNameMatchesKnownHeaderExactly(t *testing.T) { + if !isSensitiveHeaderName("Authorization") { + t.Error("expected Authorization to be treated as a sensitive header name") + } +} + +func TestRedactURLQueryValuesWithFragmentAndEmptyParts(t *testing.T) { + got := redactURLQueryValues("https://example.com/path?a=1&&=orphan#fragment-only") + want := "https://example.com/path?a=[REDACTED]&&[REDACTED]#fragment-only" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + +func TestRedactURLQueryValuesWithFragmentOnlyQuery(t *testing.T) { + got := redactURLQueryValues("https://example.com/path?#fragment-only") + want := "https://example.com/path?#fragment-only" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + +func TestRedactURLUserinfoWithProtocolRelativeURL(t *testing.T) { + got := redactURLUserinfo("//user:pass@example.com/path") + want := "//[REDACTED]@example.com/path" + if got != want { + t.Errorf("returned \n\n%#v want \n\n%#v", got, want) + } +} + func TestMakePublicAPICallDoesNotExposeRawRequest(t *testing.T) { testMux = http.NewServeMux() testServer = httptest.NewServer(testMux) @@ -385,7 +569,10 @@ func TestMakePublicAPICallDoesNotExposeRawRequest(t *testing.T) { testMux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{}`)) + _, err := w.Write([]byte(`{}`)) + if err != nil { + t.Fatal(err) + } }) apiKey := "secret-api-key" diff --git a/syntheticsclientv2/totpvariablev2_test.go b/syntheticsclientv2/totpvariablev2_test.go index 1fd788b..05771b7 100644 --- a/syntheticsclientv2/totpvariablev2_test.go +++ b/syntheticsclientv2/totpvariablev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -241,6 +238,236 @@ func TestDeleteTotpVariableV2(t *testing.T) { } } +func TestCreateTotpVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + err := json.Unmarshal([]byte(createTotpVariableV2Body), &inputTotpVariableV2Data) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/totps", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.CreateTotpVariableV2(&inputTotpVariableV2Data) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestCreateTotpVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputData := TotpVariableV2Input{ + Totp: TotpVariableInput{ + Name: "test-totp", + Secret: "test-secret", + Digits: 6, + Interval: 30, + HmacDigest: "SHA1", + }, + } + + _, _, err := unreachableClient.CreateTotpVariableV2(&inputData) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestGetTotpVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/totps/102", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{invalid json}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetTotpVariableV2(102) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestGetTotpVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetTotpVariableV2(102) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestGetTotpVariablesV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/totps", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, err := w.Write([]byte("{invalid json array}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.GetTotpVariablesV2() + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestGetTotpVariablesV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, _, err := unreachableClient.GetTotpVariablesV2() + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestUpdateTotpVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + description := "Updated TOTP" + digits := 8 + hmacDigest := "SHA512" + interval := 45 + secret := "update-totp-secret" + updateInput := TotpVariableV2UpdateInput{ + Totp: TotpVariableUpdateInput{ + Description: &description, + Digits: &digits, + HmacDigest: &hmacDigest, + Interval: &interval, + Secret: &secret, + }, + } + + testMux.HandleFunc("/totps/103", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{malformed response}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.UpdateTotpVariableV2(103, &updateInput) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestUpdateTotpVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + description := "Updated TOTP" + digits := 8 + hmacDigest := "SHA512" + interval := 45 + secret := "update-totp-secret" + updateInput := TotpVariableV2UpdateInput{ + Totp: TotpVariableUpdateInput{ + Description: &description, + Digits: &digits, + HmacDigest: &hmacDigest, + Interval: &interval, + Secret: &secret, + }, + } + + _, _, err := unreachableClient.UpdateTotpVariableV2(103, &updateInput) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestUpdateTotpVariableV2ReturnsNonNilResponseOnEmptyBody(t *testing.T) { + setup() + defer teardown() + + description := "Updated TOTP" + digits := 8 + hmacDigest := "SHA512" + interval := 45 + updateInput := TotpVariableV2UpdateInput{ + Totp: TotpVariableUpdateInput{ + Description: &description, + Digits: &digits, + HmacDigest: &hmacDigest, + Interval: &interval, + }, + } + + testMux.HandleFunc("/totps/106", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.UpdateTotpVariableV2(106, &updateInput) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for empty body with 2xx status") + } +} + +func TestDeleteTotpVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + _, err := unreachableClient.DeleteTotpVariableV2(105) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestDeleteTotpVariableV2ReturnsErrorOnNon2xxStatus(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/totps/107", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusMultipleChoices) + }) + + resp, err := testClient.DeleteTotpVariableV2(107) + if err == nil { + t.Fatal("expected error on non-2xx status code, got nil") + } + if resp != http.StatusMultipleChoices { + t.Errorf("returned status \n\n%#v want \n\n%#v", resp, http.StatusMultipleChoices) + } + if !strings.Contains(err.Error(), "Response code") { + t.Errorf("expected error to contain 'Response code', got %s", err.Error()) + } +} + func verifyTotpVariableV2Input(stringInput string) *TotpVariableV2Response { check := &TotpVariableV2Response{} err := json.Unmarshal([]byte(stringInput), check) diff --git a/syntheticsclientv2/update_apicheckv2_test.go b/syntheticsclientv2/update_apicheckv2_test.go index 2045fba..c6a9d43 100644 --- a/syntheticsclientv2/update_apicheckv2_test.go +++ b/syntheticsclientv2/update_apicheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -82,3 +79,81 @@ func TestUpdateApiCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputApiCheckV2Update.Test.Customproperties) } } + +func TestUpdateApiCheckV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/api/10", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusNoContent) + }) + + err := json.Unmarshal([]byte(updateApiCheckV2Body), &inputApiCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateApiCheckV2(10, &inputApiCheckV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response on empty body") + } + if details == nil { + t.Fatal("expected request details") + } +} + +func TestUpdateApiCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/api/10", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateApiCheckV2Body), &inputApiCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateApiCheckV2(10, &inputApiCheckV2Update) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestUpdateApiCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updateApiCheckV2Body), &inputApiCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.UpdateApiCheckV2(10, &inputApiCheckV2Update) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/update_browsercheckv2_test.go b/syntheticsclientv2/update_browsercheckv2_test.go index 0173363..c8ed0c4 100644 --- a/syntheticsclientv2/update_browsercheckv2_test.go +++ b/syntheticsclientv2/update_browsercheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -302,3 +299,81 @@ func TestUpdateBrowserCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Advancedsettings, inputBrowserCheckV2Update.Test.Advancedsettings) } } + +func TestUpdateBrowserCheckV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/browser/15", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusNoContent) + }) + + err := json.Unmarshal([]byte(updateBrowserCheckV2Body), &inputBrowserCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateBrowserCheckV2(15, &inputBrowserCheckV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response on empty body") + } + if details == nil { + t.Fatal("expected request details") + } +} + +func TestUpdateBrowserCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/v2/tests/browser/16", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateBrowserCheckV2Body), &inputBrowserCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateBrowserCheckV2(16, &inputBrowserCheckV2Update) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestUpdateBrowserCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updateBrowserCheckV2Body), &inputBrowserCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.UpdateBrowserCheckV2(17, &inputBrowserCheckV2Update) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} diff --git a/syntheticsclientv2/update_cacertificatev2_test.go b/syntheticsclientv2/update_cacertificatev2_test.go index c1b3522..e4563c1 100644 --- a/syntheticsclientv2/update_cacertificatev2_test.go +++ b/syntheticsclientv2/update_cacertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +18,7 @@ import ( "encoding/json" "io" "net/http" + "strings" "testing" ) @@ -176,3 +174,74 @@ func readCaCertificateUpdateRequestFields(t *testing.T, r *http.Request) ([]byte return requestBody, requestCaCertFields } + +func TestUpdateCaCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateCaCertificateV2Body), &inputCaCertificateV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateCaCertificateV2(1, &inputCaCertificateV2Update) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestUpdateCaCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updateCaCertificateV2Body), &inputCaCertificateV2Update) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.UpdateCaCertificateV2(1, &inputCaCertificateV2Update) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestUpdateCaCertificateV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/cacerts/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + // Write empty response body + }) + + err := json.Unmarshal([]byte(updateCaCertificateV2Body), &inputCaCertificateV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateCaCertificateV2(1, &inputCaCertificateV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for empty body") + } +} diff --git a/syntheticsclientv2/update_clientcertificatev2_test.go b/syntheticsclientv2/update_clientcertificatev2_test.go index 92fcc20..7980542 100644 --- a/syntheticsclientv2/update_clientcertificatev2_test.go +++ b/syntheticsclientv2/update_clientcertificatev2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +17,7 @@ package syntheticsclientv2 import ( "encoding/json" "net/http" + "strings" "testing" ) @@ -107,3 +105,71 @@ func assertClientCertificateUpdateRequestBody(t *testing.T, r *http.Request) { } } } + +func TestUpdateClientCertificateV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates/123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + if err := json.Unmarshal([]byte(updateClientCertificateV2Body), &inputClientCertificateV2Update); err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateClientCertificateV2(123, &inputClientCertificateV2Update) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestUpdateClientCertificateV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + if err := json.Unmarshal([]byte(updateClientCertificateV2Body), &inputClientCertificateV2Update); err != nil { + t.Fatal(err) + } + + _, _, err := unreachableClient.UpdateClientCertificateV2(123, &inputClientCertificateV2Update) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestUpdateClientCertificateV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/certificates/123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + // Write empty response body + }) + + if err := json.Unmarshal([]byte(updateClientCertificateV2Body), &inputClientCertificateV2Update); err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateClientCertificateV2(123, &inputClientCertificateV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for empty body") + } +} diff --git a/syntheticsclientv2/update_downtimeconfigurationv2_test.go b/syntheticsclientv2/update_downtimeconfigurationv2_test.go index 5e85801..e306567 100644 --- a/syntheticsclientv2/update_downtimeconfigurationv2_test.go +++ b/syntheticsclientv2/update_downtimeconfigurationv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2024 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -78,3 +75,56 @@ func TestUpdateDowntimeConfigurationV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.DowntimeConfiguration.Endtime, inputDowntimeConfigurationV2Update.DowntimeConfiguration.Endtime) } } + +func TestUpdateDowntimeConfigurationV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/downtime_configurations/10", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + resp, details, err := testClient.UpdateDowntimeConfigurationV2(10, &DowntimeConfigurationV2Input{}) + if err == nil { + t.Fatal("expected a parse error, but got none") + } + if details == nil { + t.Fatal("expected request details") + } + if resp != nil { + t.Errorf("expected nil response, got %#v", resp) + } +} + +func TestUpdateDowntimeConfigurationV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + _, _, err := unreachableClient.UpdateDowntimeConfigurationV2(10, &DowntimeConfigurationV2Input{}) + if err == nil { + t.Fatal("expected a connection error") + } +} + +func TestUpdateDowntimeConfigurationV2ReturnsResponseOnEmptyBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/downtime_configurations/10", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + }) + + resp, details, err := testClient.UpdateDowntimeConfigurationV2(10, &DowntimeConfigurationV2Input{}) + if err != nil { + t.Fatalf("expected no error for empty response, but got: %v", err) + } + if details == nil { + t.Fatal("expected request details") + } + if resp == nil { + t.Fatal("expected non-nil response for empty body") + } +} diff --git a/syntheticsclientv2/update_httpcheckv2_test.go b/syntheticsclientv2/update_httpcheckv2_test.go index 2cefbdc..ecbd5ff 100644 --- a/syntheticsclientv2/update_httpcheckv2_test.go +++ b/syntheticsclientv2/update_httpcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -66,3 +63,150 @@ func TestUpdateHttpCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Port, inputHttpCheckV2Update.Test.Port) } } + +func TestUpdateHttpCheckV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/11", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusNoContent) + }) + + err := json.Unmarshal([]byte(updateHttpCheckV2Body), &inputHttpCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateHttpCheckV2(11, &inputHttpCheckV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response on empty body") + } + if details == nil { + t.Fatal("expected request details") + } +} + +func TestUpdateHttpCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/12", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateHttpCheckV2Body), &inputHttpCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := testClient.UpdateHttpCheckV2(12, &inputHttpCheckV2Update) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestUpdateHttpCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updateHttpCheckV2Body), &inputHttpCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, details, err := unreachableClient.UpdateHttpCheckV2(13, &inputHttpCheckV2Update) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} + +func TestUpdateHttpCheckV2WithNullablePortReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/15", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + inputWithNullablePort := HttpCheckV2InputWithNullablePort{} + inputWithNullablePort.Test.Name = "test-http" + resp, details, err := testClient.UpdateHttpCheckV2WithNullablePort(15, &inputWithNullablePort) + + if err == nil { + t.Fatal("expected an error on malformed JSON response, but got none") + } + if resp != nil && resp.Test.Name != "" { + t.Error("expected empty response struct on parse error") + } + if details == nil { + t.Fatal("expected request details even on parse error") + } +} + +func TestUpdateHttpCheckV2WithNullablePortReturnsErrorWhenRequestFails(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputWithNullablePort := HttpCheckV2InputWithNullablePort{} + inputWithNullablePort.Test.Name = "test-http" + resp, details, err := unreachableClient.UpdateHttpCheckV2WithNullablePort(16, &inputWithNullablePort) + + if err == nil { + t.Fatal("expected a connection error, but got none") + } + if resp != nil { + t.Errorf("expected nil response on network error, but got %#v", resp) + } + if details == nil { + t.Fatal("expected request details to be populated") + } +} + +func TestUpdateHttpCheckV2WithNullablePortHandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/http/14", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusNoContent) + }) + + inputWithNullablePort := HttpCheckV2InputWithNullablePort{} + inputWithNullablePort.Test.Name = "test-http" + resp, details, err := testClient.UpdateHttpCheckV2WithNullablePort(14, &inputWithNullablePort) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response on empty body") + } + if details == nil { + t.Fatal("expected request details") + } +} diff --git a/syntheticsclientv2/update_portcheckv2_test.go b/syntheticsclientv2/update_portcheckv2_test.go index 3ac2af2..83791bb 100644 --- a/syntheticsclientv2/update_portcheckv2_test.go +++ b/syntheticsclientv2/update_portcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +19,7 @@ import ( "fmt" "net/http" "reflect" + "strings" "testing" ) @@ -62,3 +60,78 @@ func TestUpdatePortCheckV2(t *testing.T) { t.Errorf("returned \n\n%#v want \n\n%#v", resp.Test.Customproperties, inputPortCheckV2Update.Test.Customproperties) } } + +func TestUpdatePortCheckV2HandlesEmptyResponseBody(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/port/1650", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + // Write empty body + _, err := w.Write([]byte("")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updatePortCheckV2Body), &inputPortCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdatePortCheckV2(1650, &inputPortCheckV2Update) + + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Errorf("expected non-nil response on empty body, got nil") + } +} + +func TestUpdatePortCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/port/1650", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updatePortCheckV2Body), &inputPortCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdatePortCheckV2(1650, &inputPortCheckV2Update) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestUpdatePortCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updatePortCheckV2Body), &inputPortCheckV2Update) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.UpdatePortCheckV2(1650, &inputPortCheckV2Update) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/update_sslcheckv2_test.go b/syntheticsclientv2/update_sslcheckv2_test.go index 8e4459d..dafc2b8 100644 --- a/syntheticsclientv2/update_sslcheckv2_test.go +++ b/syntheticsclientv2/update_sslcheckv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2026 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +19,7 @@ import ( "io" "net/http" "reflect" + "strings" "testing" ) @@ -332,3 +330,49 @@ func TestUpdateSslCheckV2BlankResponse(t *testing.T) { t.Fatal("expected non-nil response for blank successful update body") } } + +func TestUpdateSslCheckV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/tests/ssl/1650", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{not valid json")) + if err != nil { + t.Fatal(err) + } + }) + + err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + + resp, _, err := testClient.UpdateSslCheckV2(1650, &inputSslCheckV2Update) + + if err == nil { + t.Fatal("expected error on malformed JSON response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on error, got %#v", resp) + } + if !strings.Contains(err.Error(), "invalid character") { + t.Errorf("expected JSON unmarshal error, got: %v", err) + } +} + +func TestUpdateSslCheckV2ReturnsErrorWhenRequestFails(t *testing.T) { + // Use an unreachable address to trigger a connection error + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + err := json.Unmarshal([]byte(updateSslCheckV2Body), &inputSslCheckV2Update) + if err != nil { + t.Fatal(err) + } + + _, _, err = unreachableClient.UpdateSslCheckV2(1650, &inputSslCheckV2Update) + + if err == nil { + t.Fatal("expected connection error, got nil") + } +} diff --git a/syntheticsclientv2/update_variablesv2_test.go b/syntheticsclientv2/update_variablesv2_test.go index ee69d14..ce73e41 100644 --- a/syntheticsclientv2/update_variablesv2_test.go +++ b/syntheticsclientv2/update_variablesv2_test.go @@ -1,6 +1,3 @@ -//go:build unit_tests -// +build unit_tests - // Copyright 2021 Splunk, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -71,3 +68,74 @@ func TestUpdateVariableV2(t *testing.T) { } } + +func TestUpdateVariableV2ReturnsErrorOnMalformedResponse(t *testing.T) { + setup() + defer teardown() + + err := json.Unmarshal([]byte(updateVariableV2Body), &inputVariableV2Update) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/variables/10", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, err := w.Write([]byte("{malformed response}")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.UpdateVariableV2(10, &inputVariableV2Update) + if err == nil { + t.Fatal("expected error on malformed response, got nil") + } + if resp != nil { + t.Errorf("expected nil response on parse error, got %#v", resp) + } +} + +func TestUpdateVariableV2ReturnsErrorOnNetworkFailure(t *testing.T) { + unreachableClient := NewConfigurableClient("apiKey", "realm", ClientArgs{publicBaseUrl: "http://127.0.0.1:1"}) + + inputData := VariableV2Input{ + Variable: Variable{ + Name: "test-var", + Value: "test-value", + Secret: false, + Description: "test description", + }, + } + + _, _, err := unreachableClient.UpdateVariableV2(10, &inputData) + if err == nil { + t.Fatal("expected connection error, got nil") + } +} + +func TestUpdateVariableV2ReturnsNonNilResponseOnEmptyBody(t *testing.T) { + setup() + defer teardown() + + err := json.Unmarshal([]byte(updateVariableV2Body), &inputVariableV2Update) + if err != nil { + t.Fatal(err) + } + + testMux.HandleFunc("/variables/20", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("")) + if err != nil { + t.Fatal(err) + } + }) + + resp, _, err := testClient.UpdateVariableV2(20, &inputVariableV2Update) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected non-nil response for empty body with 2xx status") + } +}