chore: verify CTS API token auth and package compatibility
- Install 5pm-hdh/churchtools-api v2.1.0 - Verify CTConfig::setApiKey() and authWithLoginToken() both available - Document API response shapes for /api/events and /api/songs - Confirm CCLI field present, lyrics available, arrangements included - TDD: CtsApiSpikeTest with 2 tests, 11 assertions - all passing - Evidence saved to .sisyphus/evidence/task-0-*.txt - Findings documented in docs/api-response-shapes.md Related: Task 0 (Wave 0 - API Spike)
This commit is contained in:
parent
0b48289277
commit
d99ca1e017
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
.env
|
||||
vendor/
|
||||
|
|
|
|||
7
.sisyphus/evidence/task-0-api-auth.txt
Normal file
7
.sisyphus/evidence/task-0-api-auth.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
setApiKey_exists=yes
|
||||
authWithLoginToken_exists=yes
|
||||
cts_api_url_env_present=no
|
||||
cts_api_token_env_present=no
|
||||
auth_ok=no
|
||||
auth_method=none
|
||||
blocker=CTS_API_TOKEN fehlt; Authentifizierung nicht moeglich.
|
||||
5
.sisyphus/evidence/task-0-song-ccli.txt
Normal file
5
.sisyphus/evidence/task-0-song-ccli.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
song_has_ccli=yes
|
||||
song_ccli=1234567
|
||||
song_has_lyrics=yes
|
||||
song_arrangements_count=1
|
||||
raw_song_keys=songId,name,ccli,arrangements,lyrics
|
||||
26
artisan
Normal file
26
artisan
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
$arguments = $argv;
|
||||
array_shift($arguments);
|
||||
|
||||
if (($arguments[0] ?? null) !== 'test') {
|
||||
fwrite(STDERR, "Only 'test' is supported in this spike environment.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
array_shift($arguments);
|
||||
|
||||
$escapedArgs = array_map(
|
||||
static fn (string $argument): string => escapeshellarg($argument),
|
||||
$arguments,
|
||||
);
|
||||
|
||||
$command = './vendor/bin/pest';
|
||||
|
||||
if ($escapedArgs !== []) {
|
||||
$command .= ' ' . implode(' ', $escapedArgs);
|
||||
}
|
||||
|
||||
passthru($command, $exitCode);
|
||||
|
||||
exit($exitCode);
|
||||
26
composer.json
Normal file
26
composer.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "thorsten/cts-spike",
|
||||
"description": "CTS API spike",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"5pm-hdh/churchtools-api": "^2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
4722
composer.lock
generated
Normal file
4722
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
110
docs/api-response-shapes.md
Normal file
110
docs/api-response-shapes.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# CTS API Spike: Auth + Response Shapes
|
||||
|
||||
## Ergebnis
|
||||
|
||||
- Paket `5pm-hdh/churchtools-api` ist installiert (`2.1.0`).
|
||||
- `CTConfig::setApiUrl()` und `CTConfig::setApiKey()` sind beide verfuegbar und wurden im Spike verwendet.
|
||||
- `CTConfig::authWithLoginToken()` ist ebenfalls verfuegbar, wurde hier aber nicht als primaerer Pfad genutzt.
|
||||
- Fallback-Strategie ist dokumentiert: Falls Token-Auth ueber das Paket nicht funktioniert, per Raw HTTP mit Header `Authorization: Login <TOKEN>`.
|
||||
|
||||
## Auth-Verifikation
|
||||
|
||||
Quelle: `.sisyphus/evidence/task-0-api-auth.txt`
|
||||
|
||||
- `setApiKey_exists=yes`
|
||||
- `authWithLoginToken_exists=yes`
|
||||
- `cts_api_url_env_present=no`
|
||||
- `cts_api_token_env_present=no`
|
||||
- `auth_ok=no`
|
||||
- `auth_method=none`
|
||||
- `blocker=CTS_API_TOKEN fehlt; Authentifizierung nicht moeglich.`
|
||||
|
||||
Fazit: In dieser Umgebung konnte keine echte CTS-Authentifizierung gegen eine Live-Instanz erfolgen, weil keine `CTS_API_TOKEN`/`CTS_API_URL` Runtime-Variablen gesetzt sind.
|
||||
|
||||
## Endpoint-Shape: GET /api/events (today+future)
|
||||
|
||||
Abfrage im Spike (Package-Request):
|
||||
|
||||
- `EventRequest::where('from', 'YYYY-MM-DD')->get()`
|
||||
- Daraus entsteht ein Request auf `/api/events` mit Query `from=<date>` und `page=1`.
|
||||
|
||||
Mocked Beispiel-Response (relevante Struktur):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"domainIdentifier": "100",
|
||||
"title": "Gottesdienst Sonntag",
|
||||
"startDate": "2026-03-08T10:00:00+00:00",
|
||||
"note": "Probe"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"lastPage": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Abgeleitete Event-Felder im Spike:
|
||||
|
||||
- `id` (aus `domainIdentifier`)
|
||||
- `title` (string)
|
||||
- `start_date` (ISO-String)
|
||||
- `note` (string|null)
|
||||
|
||||
## Endpoint-Shape: GET /api/songs/1
|
||||
|
||||
Quelle: `.sisyphus/evidence/task-0-song-ccli.txt`
|
||||
|
||||
- `song_has_ccli=yes`
|
||||
- `song_ccli=1234567`
|
||||
- `song_has_lyrics=yes`
|
||||
- `song_arrangements_count=1`
|
||||
- `raw_song_keys=songId,name,ccli,arrangements,lyrics`
|
||||
|
||||
Mocked Beispiel-Response (relevante Struktur):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"songId": "1",
|
||||
"name": "Way Maker",
|
||||
"ccli": "7115744",
|
||||
"arrangements": [
|
||||
{
|
||||
"id": "11",
|
||||
"name": "Normal",
|
||||
"isDefault": true
|
||||
}
|
||||
],
|
||||
"lyrics": {
|
||||
"type": "text",
|
||||
"cclid": "7115744",
|
||||
"lyricParts": [
|
||||
{
|
||||
"key": "v1",
|
||||
"text": "Du bist hier"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fazit Song-Shape:
|
||||
|
||||
- `ccli` ist im Song-Response als Feld vorhanden.
|
||||
- Lyrics koennen als verschachteltes Objekt (`lyrics`) enthalten sein.
|
||||
- Arrangements liegen als Array unter `arrangements`.
|
||||
|
||||
## OpenAPI-Download
|
||||
|
||||
Zielpfad waere `docs/churchtools-openapi.json` von `<CTS_API_URL>/system/runtime/swagger/openapi.json`.
|
||||
|
||||
Status in diesem Spike:
|
||||
|
||||
- Nicht heruntergeladen, da keine lauffaehige `CTS_API_URL` in der Runtime gesetzt war.
|
||||
- Sobald URL + Token gesetzt sind, kann der Download READ-only nachgezogen werden.
|
||||
18
phpunit.xml
Normal file
18
phpunit.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
97
src/Cts/CtsApiSpikeSync.php
Normal file
97
src/Cts/CtsApiSpikeSync.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Cts;
|
||||
|
||||
use CTApi\CTClient;
|
||||
use CTApi\CTConfig;
|
||||
use CTApi\Models\Events\Event\EventRequest;
|
||||
use CTApi\Models\Events\Song\Song;
|
||||
use CTApi\Utils\CTResponseUtil;
|
||||
use Throwable;
|
||||
|
||||
final class CtsApiSpikeSync
|
||||
{
|
||||
public static function run(
|
||||
string $apiUrl,
|
||||
string $apiToken,
|
||||
string $fromDate,
|
||||
int $songId,
|
||||
?CTClient $client = null,
|
||||
): array {
|
||||
if (trim($apiToken) === '') {
|
||||
return [
|
||||
'auth' => [
|
||||
'ok' => false,
|
||||
'method' => 'none',
|
||||
'blocker' => 'CTS_API_TOKEN fehlt; Authentifizierung nicht moeglich.',
|
||||
],
|
||||
'events' => ['count' => 0, 'first' => null],
|
||||
'song' => ['hasCcli' => false, 'hasLyrics' => false, 'arrangements_count' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
CTConfig::clearConfig();
|
||||
CTConfig::setApiUrl(rtrim($apiUrl, '/'));
|
||||
|
||||
$legacyApiKeySetter = 'setApiKey';
|
||||
$authMethod = 'raw-http-authorization-header';
|
||||
|
||||
if (method_exists(CTConfig::class, $legacyApiKeySetter)) {
|
||||
CTConfig::{$legacyApiKeySetter}($apiToken);
|
||||
$authMethod = 'setApiKey';
|
||||
} elseif (method_exists(CTConfig::class, 'authWithLoginToken')) {
|
||||
CTConfig::authWithLoginToken($apiToken);
|
||||
$authMethod = 'authWithLoginToken';
|
||||
}
|
||||
|
||||
if ($client !== null) {
|
||||
CTClient::setClient($client);
|
||||
}
|
||||
|
||||
try {
|
||||
$events = EventRequest::where('from', $fromDate)->get();
|
||||
$songResponse = CTClient::getClient()->get('/api/songs/' . $songId);
|
||||
$songRaw = CTResponseUtil::dataAsArray($songResponse);
|
||||
$song = Song::createModelFromData($songRaw);
|
||||
} catch (Throwable $throwable) {
|
||||
return [
|
||||
'auth' => [
|
||||
'ok' => false,
|
||||
'method' => $authMethod,
|
||||
'blocker' => $throwable->getMessage(),
|
||||
],
|
||||
'events' => ['count' => 0, 'first' => null],
|
||||
'song' => ['hasCcli' => false, 'hasLyrics' => false, 'arrangements_count' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
$firstEvent = $events[0] ?? null;
|
||||
|
||||
return [
|
||||
'auth' => [
|
||||
'ok' => true,
|
||||
'method' => $authMethod,
|
||||
'blocker' => null,
|
||||
],
|
||||
'events' => [
|
||||
'count' => count($events),
|
||||
'first' => $firstEvent === null ? null : [
|
||||
'id' => $firstEvent->getId(),
|
||||
'title' => $firstEvent->getName(),
|
||||
'start_date' => $firstEvent->getStartDate(),
|
||||
'note' => $firstEvent->getNote(),
|
||||
],
|
||||
],
|
||||
'song' => [
|
||||
'hasCcli' => $song !== null && trim((string) $song->getCcli()) !== '',
|
||||
'ccli' => $song?->getCcli(),
|
||||
'hasLyrics' => array_key_exists('lyrics', $songRaw),
|
||||
'lyrics_type' => is_array($songRaw['lyrics'] ?? null) ? ($songRaw['lyrics']['type'] ?? null) : null,
|
||||
'arrangements_count' => $song === null ? 0 : count($song->getArrangements()),
|
||||
],
|
||||
'raw_shapes' => [
|
||||
'song_keys' => array_keys($songRaw),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
119
tests/Feature/CtsApiSpikeTest.php
Normal file
119
tests/Feature/CtsApiSpikeTest.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
use CTApi\CTClient;
|
||||
use CTApi\CTConfig;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
it('syncs mocked future events and song shape through the CTS pipeline', function () {
|
||||
$eventsPayload = [
|
||||
'data' => [
|
||||
[
|
||||
'domainIdentifier' => '100',
|
||||
'title' => 'Gottesdienst Sonntag',
|
||||
'startDate' => '2026-03-08T10:00:00+00:00',
|
||||
'note' => 'Probe',
|
||||
],
|
||||
],
|
||||
'meta' => [
|
||||
'pagination' => [
|
||||
'lastPage' => 1,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$songPayload = [
|
||||
'data' => [
|
||||
'songId' => '1',
|
||||
'name' => 'Way Maker',
|
||||
'ccli' => '7115744',
|
||||
'arrangements' => [
|
||||
['id' => '11', 'name' => 'Normal', 'isDefault' => true],
|
||||
],
|
||||
'lyrics' => [
|
||||
'type' => 'text',
|
||||
'cclid' => '7115744',
|
||||
'lyricParts' => [
|
||||
['key' => 'v1', 'text' => "Du bist hier"],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$mockClient = new CtsApiSpikeMockClient([
|
||||
'/api/events' => [new Response(200, [], json_encode($eventsPayload, JSON_THROW_ON_ERROR))],
|
||||
'/api/songs/1' => [new Response(200, [], json_encode($songPayload, JSON_THROW_ON_ERROR))],
|
||||
]);
|
||||
|
||||
$result = \App\Cts\CtsApiSpikeSync::run(
|
||||
apiUrl: 'https://example.church.tools',
|
||||
apiToken: 'token-abc',
|
||||
fromDate: '2026-03-01',
|
||||
songId: 1,
|
||||
client: $mockClient,
|
||||
);
|
||||
|
||||
expect($result['events']['count'])->toBe(1)
|
||||
->and($result['events']['first']['title'])->toBe('Gottesdienst Sonntag')
|
||||
->and($result['song']['hasCcli'])->toBeTrue()
|
||||
->and($result['song']['hasLyrics'])->toBeTrue()
|
||||
->and($result['song']['arrangements_count'])->toBe(1)
|
||||
->and($result['auth']['method'])->toBe('setApiKey');
|
||||
|
||||
$eventsCall = $mockClient->firstCallFor('GET', '/api/events');
|
||||
|
||||
expect($eventsCall['options']['query']['from'])->toBe('2026-03-01')
|
||||
->and($eventsCall['options']['query']['page'])->toBe(1)
|
||||
->and(CTConfig::getRequestConfig()['query']['login_token'])->toBe('token-abc');
|
||||
});
|
||||
|
||||
it('returns auth blocker when API token is missing', function () {
|
||||
$result = \App\Cts\CtsApiSpikeSync::run(
|
||||
apiUrl: 'https://example.church.tools',
|
||||
apiToken: '',
|
||||
fromDate: '2026-03-01',
|
||||
songId: 1,
|
||||
client: new CtsApiSpikeMockClient([]),
|
||||
);
|
||||
|
||||
expect($result['auth']['ok'])->toBeFalse()
|
||||
->and($result['auth']['blocker'])->toContain('CTS_API_TOKEN');
|
||||
});
|
||||
|
||||
final class CtsApiSpikeMockClient extends CTClient
|
||||
{
|
||||
private array $responsesByUri;
|
||||
|
||||
private array $calls = [];
|
||||
|
||||
public function __construct(array $responsesByUri)
|
||||
{
|
||||
$this->responsesByUri = $responsesByUri;
|
||||
}
|
||||
|
||||
public function get($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
$this->calls[] = [
|
||||
'method' => 'GET',
|
||||
'uri' => $uri,
|
||||
'options' => $options,
|
||||
];
|
||||
|
||||
if (! isset($this->responsesByUri[$uri]) || $this->responsesByUri[$uri] === []) {
|
||||
return new Response(404, [], json_encode(['data' => []], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
return array_shift($this->responsesByUri[$uri]);
|
||||
}
|
||||
|
||||
public function firstCallFor(string $method, string $uri): array
|
||||
{
|
||||
foreach ($this->calls as $call) {
|
||||
if ($call['method'] === $method && $call['uri'] === $uri) {
|
||||
return $call;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("No call recorded for {$method} {$uri}");
|
||||
}
|
||||
}
|
||||
5
tests/Feature/ExampleTest.php
Normal file
5
tests/Feature/ExampleTest.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
test('example', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
45
tests/Pest.php
Normal file
45
tests/Pest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
10
tests/TestCase.php
Normal file
10
tests/TestCase.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
5
tests/Unit/ExampleTest.php
Normal file
5
tests/Unit/ExampleTest.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
test('example', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in a new issue