Skip to content

Commit 85c3d8a

Browse files
Modernize client to v2: resource-oriented, typed, full API coverage
Rewrite the TestingBot PHP client (PHP 8.1+) around a resource-grouped `TestingBot\Client` with a backward-compatible `TestingBot\TestingBotAPI` facade. The 1.x flat method names/signatures are preserved; the one behaviour change is that failures now throw typed exceptions instead of returning an `['error' => ...]` array. Highlights: - Full API coverage across 13 resources (tests, builds, storage, tunnels, user, team-management, codeless lab tests + suites, screenshots, devices, browsers, configuration, jobs). - Single cURL chokepoint behind HttpClientInterface (timeouts, TLS verify, versioned UA, curl_errno -> NetworkException, HTTP-status checking) with a mockable seam for tests. - Typed exception hierarchy (Authentication/NotFound/RateLimit/Api/Network). - Replace PHP8-fatal mime_content_type() with ext-fileinfo. - PSR-4 autoload; drop bogus runtime deps (phpunit-selenium, paratest, appium); type=library; add phpstan (L8), php-cs-fixer (PSR-12), .editorconfig. - 86 unit tests via FakeHttpClient (no network) + gated live integration suite. - GitHub Actions: CI matrix PHP 8.1-8.4 + token-free tag-driven release workflow. - Rewritten README, CHANGELOG, CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8ecf6ce commit 85c3d8a

66 files changed

Lines changed: 3740 additions & 3657 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.{yml,yaml,json,neon}]
12+
indent_size = 2
13+
14+
[*.md]
15+
trim_trailing_whitespace = false

.github/workflows/dependency-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: 'Checkout Repository'
18-
uses: actions/checkout@v3
18+
uses: actions/checkout@v4
1919
- name: 'Dependency Review'
20-
uses: actions/dependency-review-action@v1
20+
uses: actions/dependency-review-action@v4

.github/workflows/release.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Release
2+
3+
# Cutting a release is just pushing a version tag:
4+
#
5+
# git tag 2.0.0 && git push origin 2.0.0
6+
#
7+
# Packagist picks up the new version automatically through its GitHub
8+
# integration (no token needed). This workflow only validates the tag and
9+
# publishes a GitHub Release, using the built-in GITHUB_TOKEN — there are no
10+
# secrets to configure.
11+
12+
on:
13+
push:
14+
tags:
15+
- '[0-9]+.[0-9]+.[0-9]+'
16+
- '[0-9]+.[0-9]+.[0-9]+-*'
17+
- 'v[0-9]+.[0-9]+.[0-9]+'
18+
- 'v[0-9]+.[0-9]+.[0-9]+-*'
19+
20+
permissions:
21+
contents: write
22+
23+
jobs:
24+
release:
25+
name: Publish release
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Set up PHP
31+
uses: shivammathur/setup-php@v2
32+
with:
33+
php-version: '8.3'
34+
extensions: curl, json, fileinfo
35+
coverage: none
36+
tools: composer:v2
37+
38+
- name: Install dependencies
39+
run: composer install --prefer-dist --no-interaction --no-progress
40+
41+
- name: Validate composer.json
42+
run: composer validate --strict
43+
44+
- name: Verify the tag matches Client::VERSION
45+
run: |
46+
tag="${GITHUB_REF_NAME#v}"
47+
version="$(php -r 'require "src/Client.php"; echo TestingBot\Client::VERSION;')"
48+
if [ "$tag" != "$version" ]; then
49+
echo "::error::Tag '$tag' does not match Client::VERSION '$version'. Bump the VERSION constant before tagging."
50+
exit 1
51+
fi
52+
echo "Tag and Client::VERSION agree: $version"
53+
54+
- name: Coding standards
55+
run: composer cs-check
56+
57+
- name: Static analysis
58+
run: composer phpstan
59+
60+
- name: Unit tests
61+
run: composer test
62+
63+
- name: Extract release notes from CHANGELOG
64+
run: |
65+
version="${GITHUB_REF_NAME#v}"
66+
awk -v ver="$version" '
67+
$0 ~ "^## \\[" ver "\\]" {flag=1; next}
68+
/^## / && flag {flag=0}
69+
flag {print}
70+
' CHANGELOG.md > release-notes.md
71+
if [ ! -s release-notes.md ]; then
72+
echo "No CHANGELOG section found for $version; GitHub will auto-generate notes."
73+
fi
74+
75+
- name: Create GitHub Release
76+
env:
77+
GH_TOKEN: ${{ github.token }}
78+
run: |
79+
if [ -s release-notes.md ]; then
80+
gh release create "$GITHUB_REF_NAME" \
81+
--title "$GITHUB_REF_NAME" \
82+
--notes-file release-notes.md
83+
else
84+
gh release create "$GITHUB_REF_NAME" \
85+
--title "$GITHUB_REF_NAME" \
86+
--generate-notes
87+
fi

.github/workflows/test.yml

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,61 @@
1-
name: Test Changes
1+
name: CI
22

33
on: [push, pull_request]
44

55
jobs:
6-
test:
6+
unit:
7+
name: Unit (PHP ${{ matrix.php }})
78
runs-on: ubuntu-latest
8-
name: PHP test
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
php: ['8.1', '8.2', '8.3', '8.4']
913
steps:
10-
- uses: actions/checkout@v2
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up PHP
17+
uses: shivammathur/setup-php@v2
18+
with:
19+
php-version: ${{ matrix.php }}
20+
extensions: curl, json, fileinfo
21+
coverage: none
22+
tools: composer:v2
23+
1124
- name: Install dependencies
12-
uses: php-actions/composer@v5
13-
- name: PHPUnit tests
14-
uses: php-actions/phpunit@v2
25+
run: composer install --prefer-dist --no-interaction --no-progress
26+
27+
- name: Check coding standards
28+
run: composer cs-check
29+
30+
- name: Static analysis
31+
run: composer phpstan
32+
33+
- name: Unit tests
34+
run: composer test
35+
36+
integration:
37+
name: Integration (live API)
38+
runs-on: ubuntu-latest
39+
# Only run where the repository secrets are available: pushes to the main
40+
# branch and tags. Pull requests (including from forks) skip the live suite.
41+
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
42+
needs: unit
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- name: Set up PHP
47+
uses: shivammathur/setup-php@v2
48+
with:
49+
php-version: '8.3'
50+
extensions: curl, json, fileinfo
51+
coverage: none
52+
tools: composer:v2
53+
54+
- name: Install dependencies
55+
run: composer install --prefer-dist --no-interaction --no-progress
56+
57+
- name: Integration tests
1558
env:
1659
TB_KEY: ${{ secrets.TB_KEY }}
1760
TB_SECRET: ${{ secrets.TB_SECRET }}
18-
with:
19-
configuration: tests/phpunit.xml
61+
run: composer test:integration

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
composer.phar
2+
composer.lock
23
vendor/
4+
.phpunit.result.cache
5+
.phpunit.cache
6+
.php-cs-fixer.cache
7+
.phpstan.cache

.php-cs-fixer.dist.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
$finder = PhpCsFixer\Finder::create()
6+
->in([__DIR__ . '/src', __DIR__ . '/tests']);
7+
8+
return (new PhpCsFixer\Config())
9+
->setRiskyAllowed(true)
10+
->setRules([
11+
'@PSR12' => true,
12+
'declare_strict_types' => true,
13+
'array_syntax' => ['syntax' => 'short'],
14+
'no_unused_imports' => true,
15+
'ordered_imports' => ['sort_algorithm' => 'alpha'],
16+
'single_quote' => true,
17+
'trailing_comma_in_multiline' => true,
18+
])
19+
->setFinder($finder);

.travis.yml

Lines changed: 0 additions & 10 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [2.0.0]
9+
10+
A full modernization of the client. The 1.x flat method surface is preserved
11+
(see *Backward compatibility* below), but the internals, error handling and
12+
minimum PHP version have changed.
13+
14+
### Added
15+
- New resource-oriented entry point `TestingBot\Client` with grouped accessors:
16+
`tests()`, `builds()`, `storage()`, `tunnels()`, `user()`, `teamManagement()`,
17+
`lab()`, `labSuites()`, `screenshots()`, `devices()`, `browsers()`,
18+
`configuration()`, `jobs()`.
19+
- Full API coverage: full test CRUD, user info/keys/update, team management
20+
(sub-accounts), codeless tests **and** suites, screenshots, tunnel
21+
create/get/list, devices, browsers, configuration IP ranges, and a job poller
22+
(`jobs()->waitForCompletion()`).
23+
- Typed exception hierarchy: `AuthenticationException` (401/403),
24+
`NotFoundException` (404), `RateLimitException` (429, with `getRetryAfter()`),
25+
`ApiException` (other non-2xx, with `getStatusCode()`/`getResponseBody()`),
26+
and `NetworkException` (transport failures). All implement
27+
`TestingBot\Exception\TestingBotExceptionInterface`.
28+
- A mockable transport seam (`TestingBot\Http\HttpClientInterface`) so the
29+
client can be unit-tested without hitting the live API.
30+
- HTTP timeouts, explicit TLS verification, a versioned User-Agent, and
31+
transport-level error detection.
32+
- `Client::request()` low-level escape hatch for endpoints not yet wrapped.
33+
- `phpstan` (level 8), `php-cs-fixer` (PSR-12), `.editorconfig`, and a GitHub
34+
Actions matrix across PHP 8.1–8.4.
35+
36+
### Changed
37+
- **Errors now throw exceptions** instead of returning an array containing an
38+
`error` key. Wrap calls in `try/catch` (see README "Migrating from 1.x").
39+
- Minimum PHP version is now **8.1**.
40+
- Autoloading moved from PSR-0 to **PSR-4** (the `TestingBot\TestingBotAPI`
41+
class name is unchanged).
42+
- `composer.json` `type` corrected to `library`.
43+
- `TestingBotAPI::createLabTest()` now wraps `$extras` keys as plain field
44+
names automatically (no need to pre-wrap them as `test[...]`).
45+
- `TestingBotAPI::modifyLabTestSteps()` keeps sending the exact legacy body for
46+
compatibility; prefer `client()->lab()->setSteps()`, which URL-encodes values.
47+
48+
### Fixed
49+
- File uploads no longer fatal on PHP 8: `mime_content_type()` replaced with the
50+
`fileinfo` extension.
51+
- Network failures (DNS, timeout, TLS) are surfaced as `NetworkException`
52+
instead of being silently swallowed as `null`.
53+
54+
### Removed
55+
- The bogus runtime dependencies `phpunit/phpunit-selenium`,
56+
`brianium/paratest` and `appium/php-client` (they were never used by the
57+
client and forced unnecessary installs).
58+
- The obsolete `.travis.yml` (CI runs on GitHub Actions).
59+
60+
### Backward compatibility
61+
All 1.x `TestingBotAPI` methods keep their names and signatures and continue to
62+
work. The behavioural change to be aware of is that failures now throw instead
63+
of returning `['error' => ...]`. Argument-validation now throws
64+
`TestingBot\Exception\InvalidArgumentException`, a subclass of the SPL
65+
`\InvalidArgumentException`, so existing `catch (\Exception $e)` blocks still
66+
catch it.
67+
68+
## [1.0.3] and earlier
69+
70+
See the git history for changes prior to the 2.0 rewrite.

CLAUDE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
PHP client library for the [TestingBot REST API](https://testingbot.com/support/api), distributed via Composer as `testingbot/testingbot-php`. Targets **PHP 8.1+**, PSR-4 autoloaded under `TestingBot\``src/`.
8+
9+
There are two entry points:
10+
- `TestingBot\Client` (`src/Client.php`) — the modern, resource-grouped client. New work goes here.
11+
- `TestingBot\TestingBotAPI` (`src/TestingBotAPI.php`) — a thin backward-compatible facade that preserves the 1.x flat method names and delegates to `Client`.
12+
13+
## Commands
14+
15+
```bash
16+
composer install
17+
composer test # unit suite — no credentials, no network (uses FakeHttpClient)
18+
composer phpstan # static analysis, level 8
19+
composer cs-check # PSR-12 dry-run; composer cs-fix to apply
20+
composer test:integration # live API; needs TB_KEY / TB_SECRET, otherwise skipped
21+
vendor/bin/phpunit --filter testUpdateWrapsFields # run a single test
22+
```
23+
24+
PHP/Composer are provided via Homebrew (`/opt/homebrew/bin`); add it to `PATH` if `php` isn't found.
25+
26+
## Architecture
27+
28+
Request flow: a resource method builds a `Http\Request` value object and hands it to `AbstractResource::request()` (or `Client::request()` for the raw escape hatch), which sends it through an `Http\HttpClientInterface` and maps the `Http\Response` to a decoded array — or throws.
29+
30+
- **`Http\HttpClientInterface`** is the seam. `Http\CurlHttpClient` is the only class that touches cURL (auth, timeouts, TLS verify, User-Agent, `curl_errno``NetworkException`, status via `CURLINFO_RESPONSE_CODE`). Tests inject `tests/Support/FakeHttpClient.php`, which records the `Request` and replays canned `Response`s — this is why the unit suite needs no network. Inject a fake via the optional 4th constructor arg on both `Client` and `TestingBotAPI`.
31+
- **Resources** (`src/Resource/*.php`) extend `AbstractResource` and each map to one API resource group. Helpers on the base class: `wrap('test', $fields)` for the `test[...]`/`user[...]`/`suite[...]` param convention, `stripAppScheme()` for `tb://` URLs, `paginationQuery()`, and `requireNonEmpty()` for id guards.
32+
- **Errors throw.** `AbstractResource::request()` throws `ApiException::fromResponse()` on non-2xx, which selects the subclass by status (401/403 → `AuthenticationException`, 404 → `NotFoundException`, 429 → `RateLimitException`, else `ApiException`). All library throwables implement `Exception\TestingBotExceptionInterface`.
33+
- **Multipart uploads** go through `Http\Multipart::fromFile()` (uses `ext-fileinfo`, not the removed `mime_content_type()`) and are sent through the same `CurlHttpClient` path as everything else — no separate curl handle.
34+
- **The facade** keeps loose/untyped params on purpose so 1.x callers don't hit `TypeError`s; phpstan's `missingType` rule is ignored for `src/TestingBotAPI.php` only. `modifyLabTestSteps()` deliberately builds the legacy `steps[][...]` raw-string body via `Client::request()` for byte-for-byte compatibility — prefer `lab()->setSteps()` elsewhere.
35+
36+
When adding an endpoint: add a typed method on the relevant `Resource` class that builds a `Request` and returns `$this->request(...)`, add a `FakeHttpClient`-based unit test asserting the request shaping, and (if it was a 1.x method) a delegating method on the facade.
37+
38+
## Source of truth
39+
40+
The canonical API surface is the Grape definition at `/Users/jochen/projects/web/app/api/testingbot/api.rb`. All endpoints accept HTTP Basic auth (`key:secret`) and a generic `?omit=a,b,c` field filter; list endpoints return `{data, meta:{offset,count,total}}`.

0 commit comments

Comments
 (0)