add: wiremock tests and boilerplate, update test readme, extend openapi spec

pull/80/head
Luciano Righetti 2022-01-17 16:02:15 +01:00
parent 25ded7e3bf
commit 299cb126dc
42 changed files with 340 additions and 204 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ webroot/theme/node_modules
docker/run/
.phpunit.result.cache
config.json
phpunit.xml

View File

@ -23,7 +23,8 @@
"josegonzalez/dotenv": "^3.2",
"league/openapi-psr7-validator": "^0.16.4",
"phpunit/phpunit": "^8.5",
"psy/psysh": "@stable"
"psy/psysh": "@stable",
"wiremock-php/wiremock-php": "^2.32"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
@ -53,7 +54,11 @@
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"stan": "phpstan analyse src/",
"test": "phpunit --colors=always"
"test": [
"sh ./tests/Helper/wiremock/start.sh",
"phpunit",
"sh ./tests/Helper/wiremock/stop.sh"
]
},
"prefer-stable": true,
"config": {

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
<phpunit colors="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/bootstrap.php">
<php>
<ini name="memory_limit" value="-1" />
<ini name="apc.enable_cli" value="1" />
<env name="WIREMOCK_HOST" value="localhost" />
<env name="WIREMOCK_PORT" value="8080" />
<env name="OPENAPI_SPEC" value="webroot/docs/openapi.yaml" />
<env name="SKIP_DB_MIGRATIONS" value="0" />
</php>
<!-- Add any additional test suites you want to run here -->

View File

@ -16,6 +16,9 @@ class BroodsFixture extends TestFixture
public const BROOD_B_ID = 2;
public const BROOD_B_API_KEY = 'ae4f281df5a5d0ff3cad6371f76d5c29b6d953ec';
public const BROOD_WIREMOCK_ID = 3;
public const BROOD_WIREMOCK_API_KEY = 'bfc63c07f74fa18b52d3cced97072cad00e51346';
public function init(): void
{
$faker = \Faker\Factory::create();
@ -48,6 +51,20 @@ class BroodsFixture extends TestFixture
'authkey' => self::BROOD_B_API_KEY,
'created' => $faker->dateTime()->getTimestamp(),
'modified' => $faker->dateTime()->getTimestamp()
],
[
'id' => self::BROOD_WIREMOCK_ID,
'uuid' => $faker->uuid(),
'name' => 'wiremock',
'url' => 'http://localhost:8080',
'description' => $faker->text,
'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID,
'trusted' => true,
'pull' => true,
'skip_proxy' => true,
'authkey' => self::BROOD_WIREMOCK_API_KEY,
'created' => $faker->dateTime()->getTimestamp(),
'modified' => $faker->dateTime()->getTimestamp()
]
];
parent::init();

View File

@ -24,6 +24,12 @@ trait ApiTestTrait
/** @var ResponseValidator */
private $responseValidator;
public function setUp(): void
{
parent::setUp();
$this->initializeOpenApiValidator($_ENV['OPENAPI_SPEC'] ?? APP . '../webroot/docs/openapi.yaml');
}
public function setAuthToken(string $authToken): void
{
$this->_authToken = $authToken;
@ -51,7 +57,7 @@ trait ApiTestTrait
* @param string $specFile
* @return void
*/
public function initializeValidator(string $specFile): void
public function initializeOpenApiValidator(string $specFile): void
{
$this->validator = (new ValidatorBuilder)->fromYamlFile($specFile);
$this->requestValidator = $this->validator->getRequestValidator();

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Test\Helper;
use \WireMock\Client\WireMock;
use Exception;
trait WireMockTestTrait
{
/** @var WireMock */
private $wiremock;
/** @var array<mixed> */
private $config = [
'hostname' => 'localhost',
'port' => 8080
];
public function initializeWireMock(): void
{
$this->wiremock = WireMock::create(
$_ENV['WIREMOCK_HOST'] ?? $this->config['hostname'],
$_ENV['WIREMOCK_PORT'] ?? $this->config['port']
);
if (!$this->wiremock->isAlive()) {
throw new Exception('Failed to connect to WireMock server.');
}
$this->clearWireMockStubs();
}
public function clearWireMockStubs(): void
{
$this->wiremock->resetToDefault();
}
public function getWireMock(): WireMock
{
return $this->wiremock;
}
public function getWireMockBaseUrl(): string
{
return sprintf('http://%s:%s', $this->config['hostname'], $this->config['port']);
}
}

View File

@ -0,0 +1,38 @@
#!/bin/bash
# Adapted from @rowanhill wiremock start.sh script
# https://github.com/rowanhill/wiremock-php/blob/master/wiremock/start.sh
cd ./tmp/
instance=1
port=8080
if [ $# -gt 0 ]; then
instance=$1
port=$2
fi
pidFile=wiremock.$instance.pid
logFile=wiremock.$instance.log
# Ensure WireMock isn't already running
if [ -e $pidFile ]; then
echo WireMock is already started: see process `cat $pidFile` 1>&2
exit 0
fi
# Download the wiremock jar if we need it
if ! [ -e wiremock-standalone.jar ]; then
echo WireMock standalone JAR missing. Downloading.
curl https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.32.0/wiremock-jre8-standalone-2.32.0.jar -o wiremock-standalone.jar
status=$?
if [ ${status} -ne 0 ]; then
echo curl could not download WireMock JAR 1>&2
exit ${status}
fi
fi
# Start WireMock in standalone mode (in a background process) and save its output to a log
java -jar wiremock-standalone.jar --port $port --root-dir $instance --disable-banner &> $logFile 2>&1 &
pgrep -f wiremock-standalone.jar > $pidFile
echo WireMock $instance started on port $port

View File

@ -0,0 +1,23 @@
#!/bin/bash
# Adapted from @rowanhill wiremock stop.sh script
# https://github.com/rowanhill/wiremock-php/blob/master/wiremock/stop.sh
cd ./tmp/
instance=1
if [ $# -gt 0 ]; then
instance=$1
fi
pidFile=wiremock.$instance.pid
if [ -e $pidFile ]; then
kill -9 `cat $pidFile`
rm $pidFile
else
echo WireMock is not started 2>&1
exit 1
fi
echo WireMock $instance stopped

View File

@ -1,6 +1,5 @@
# Testing
## Configuration
1. Add a `cerebrate_test` database to the db:
1. Add a `cerebrate_test` database to the database:
```mysql
CREATE DATABASE cerebrate_test;
GRANT ALL PRIVILEGES ON cerebrate_test.* to cerebrate@localhost;
@ -28,12 +27,24 @@ QUIT;
```
## Runing the tests
```
$ composer install
$ vendor/bin/phpunit
$ composer test
> sh ./tests/Helper/wiremock/start.sh
WireMock 1 started on port 8080
> phpunit
[ * ] Running DB migrations, it may take some time ...
The WireMock server is started .....
port: 8080
enable-browser-proxying: false
disable-banner: true
no-request-journal: false
verbose: false
PHPUnit 8.5.22 by Sebastian Bergmann and contributors.
..... 5 / 5 (100%)
Time: 11.61 seconds, Memory: 26.00 MB
@ -51,12 +62,35 @@ Available suites:
* `controller`: runs only controller tests
* _to be continued ..._
By default the database is re-generated before running the test suite, to skip this step and speed up the test run use the `-d skip-migrations` option:
By default the database is re-generated before running the test suite, to skip this step and speed up the test run set the following env variable in `phpunit.xml`:
```xml
<php>
...
<env name="SKIP_DB_MIGRATIONS" value="1" />
</php>
```
$ vendor/bin/phpunit -d skip-migrations
## Extras
### WireMock
Some integration tests perform calls to external APIs, we use WireMock to mock the response of these API calls.
To download and run WireMock run the following script in a separate terminal:
```
sh ./tests/Helper/wiremock/start.sh
```
## Coverage
You can also run WireMock with docker, check the official docs: http://wiremock.org/docs/docker/
> NOTE: When running the tests with `composer test` WireMock is automatically started and stoped after the tests finish.
The default `hostname` and `port` for WireMock are set in `phpunit.xml` as environment variables:
```xml
<php>
...
<env name="WIREMOCK_HOST" value="localhost" />
<env name="WIREMOCK_PORT" value="8080" />
</php>
```
### Coverage
HTML:
```
$ vendor/bin/phpunit --coverage-html tmp/coverage
@ -66,3 +100,23 @@ XML:
```
$ vendor/bin/phpunit --verbose --coverage-clover=coverage.xml
```
### OpenAPI validation
API tests can assert the API response matches the OpenAPI specification, after the request add this line:
```php
$this->assertResponseMatchesOpenApiSpec(self::ENDPOINT);
```
The default OpenAPI spec path is set in `phpunit.xml` as a environment variablea:
```xml
<php>
...
<env name="OPENAPI_SPEC" value="webroot/docs/openapi.yaml" />
</php>
```
## TODO
- [ ] Validate API requests against the OpenAPI spec
- [ ] Validate response content matches / implement _seeResponseContainsJson()_ or equivalent
- [ ] Parse OpenAPI spec only once per test run, currently re-loading in every _TestCase::setUp()_

View File

@ -26,12 +26,6 @@ class AddBroodApiTest extends TestCase
'app.Broods'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddBrood(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class DeleteBroodsApiTest extends TestCase
'app.Broods'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testDeleteBrood(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -27,12 +27,6 @@ class EditBroodApiTest extends TestCase
'app.Broods'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testEditBrood(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class IndexBroodsApiTest extends TestCase
'app.Broods'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexBroods(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Api\Users;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
use App\Test\Fixture\AuthKeysFixture;
use App\Test\Fixture\BroodsFixture;
use App\Test\Helper\ApiTestTrait;
use App\Test\Helper\WireMockTestTrait;
use \WireMock\Client\WireMock;
class TestBroodConnectionApiTest extends TestCase
{
use IntegrationTestTrait;
use ApiTestTrait;
use WireMockTestTrait;
protected const ENDPOINT = '/api/v1/broods/testConnection';
protected $fixtures = [
'app.Organisations',
'app.Individuals',
'app.Roles',
'app.Users',
'app.AuthKeys',
'app.Broods'
];
public function testTestBroodConnection(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
$this->initializeWireMock();
$this->mockCerebrateStatusResponse();
$url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_WIREMOCK_ID);
$this->get($url);
$this->getWireMock()->verify(
WireMock::getRequestedFor(WireMock::urlEqualTo('/instance/status.json'))
->withHeader('Content-Type', WireMock::equalTo('application/json'))
->withHeader('Authorization', WireMock::equalTo(BroodsFixture::BROOD_WIREMOCK_API_KEY))
);
$this->assertResponseOk();
$this->assertResponseContains('"user": "wiremock"');
// TODO: $this->assertRequestMatchesOpenApiSpec();
$this->assertResponseMatchesOpenApiSpec($url);
}
private function mockCerebrateStatusResponse(): \WireMock\Stubbing\StubMapping
{
return $this->getWireMock()->stubFor(
WireMock::get(WireMock::urlEqualTo('/instance/status.json'))
->willReturn(WireMock::aResponse()
->withHeader('Content-Type', 'application/json')
->withBody((string)json_encode([
"version" => "0.1",
"application" => "Cerebrate",
"user" => [
"id" => 1,
"username" => "wiremock",
"role" => [
"id" => 1
]
]
])))
);
}
}

View File

@ -26,12 +26,6 @@ class ViewBroodApiTest extends TestCase
'app.Broods'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testViewBroodGroupById(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class CreateInboxEntryApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddUserRegistrationInbox(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class IndexInboxApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexInbox(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -24,12 +24,6 @@ class AddIndividualApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddIndividual(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class DeleteIndividualApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testDeleteIndividual(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class EditIndividualApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testEditIndividualAsAdmin(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class IndexIndividualsApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexIndividuals(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class ViewIndividualApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testViewIndividualById(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -24,12 +24,6 @@ class AddOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddOrganisation(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class DeleteOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testDeleteOrganisation(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class EditOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testEditOrganisation(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class IndexOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexOrganisations(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -28,12 +28,6 @@ class TagOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testTagOrganisation(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -28,12 +28,6 @@ class UntagOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testUntagOrganisation(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -28,12 +28,6 @@ class ViewOrganisationApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testViewOrganisationById(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -27,12 +27,6 @@ class AddSharingGroupApiTest extends TestCase
'app.SharingGroups'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddSharingGroup(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class DeleteSharingGroupApiTest extends TestCase
'app.SharingGroups'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testDeleteSharingGroup(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -27,12 +27,6 @@ class EditSharingGroupApiTest extends TestCase
'app.SharingGroups'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testEditSharingGroup(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class IndexSharingGroupsApiTest extends TestCase
'app.SharingGroups'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexSharingGroups(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class ViewSharingGroupApiTest extends TestCase
'app.SharingGroups'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testViewSharingGroupById(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class IndexTagsApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexTags(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -27,12 +27,6 @@ class AddUserApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testAddUser(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -27,12 +27,6 @@ class DeleteUserApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testDeleteUser(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -26,12 +26,6 @@ class EditUserApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testEditUser(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class IndexUsersApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testIndexUsers(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -25,12 +25,6 @@ class ViewUserApiTest extends TestCase
'app.AuthKeys'
];
public function setUp(): void
{
parent::setUp();
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
}
public function testViewMyUser(): void
{
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);

View File

@ -18,7 +18,6 @@ declare(strict_types=1);
use Cake\Core\Configure;
use Cake\Datasource\ConnectionManager;
use Cake\TestSuite\Fixture\SchemaLoader;
use Migrations\TestSuite\Migrator;
/**
@ -54,8 +53,7 @@ ConnectionManager::alias('test_debug_kit', 'debug_kit');
// has been written to.
session_id('cli');
// hacky way to skip migrations
if (!in_array('skip-migrations', $_SERVER['argv'])) {
if (!$_ENV['SKIP_DB_MIGRATIONS']) {
echo "[ * ] Running DB migrations, it may take some time ...\n";
$migrator = new Migrator();
$migrator->runMany([

View File

@ -613,6 +613,24 @@ paths:
default:
$ref: "#/components/responses/ApiErrorResponse"
/api/v1/broods/testConnection/{broodId}:
get:
summary: "Test brood connection by ID"
operationId: testBroodConnectionById
tags:
- Broods
parameters:
- $ref: "#/components/parameters/broodId"
responses:
"200":
$ref: "#/components/responses/TestBroodConnectionResponse"
"403":
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
"405":
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
default:
$ref: "#/components/responses/ApiErrorResponse"
components:
schemas:
# General
@ -714,6 +732,15 @@ components:
$ref: "#/components/schemas/DateTime"
organisation_id:
$ref: "#/components/schemas/ID"
organisation:
$ref: "#/components/schemas/Organisation"
individual:
$ref: "#/components/schemas/Individual"
role:
$ref: "#/components/schemas/Role"
# user_settings: TODO
# user_settings_by_name: TODO
# user_settings_by_name_with_fallback: TODO
UserList:
type: array
@ -1605,6 +1632,33 @@ components:
schema:
$ref: "#/components/schemas/BroodList"
TestBroodConnectionResponse:
description: "Brood list response"
content:
application/json:
schema:
type: object
properties:
code:
type: integer
description: "HTTP status code"
example: 200
response:
type: object
properties:
version:
type: string
example: "0.1"
application:
type: string
example: "Cerebrate"
user:
type: string
example: "sync"
ping:
type: number
format: float
# Errors
ApiErrorResponse:
description: "Unexpected API error"