Setzt den in #84 skizzierten Workflow um: record.html nimmt im Handy-Browser eine Sprachnotiz per MediaRecorder auf und laedt sie unauthentifiziert zu POST /api/v1/agents/{id}/recordings hoch. Ein Klick auf die Personen-Figur jedes registrierten Agenten in der Buero-Visualisierung verlinkt dorthin, ein Zaehler am Schreibtisch zeigt offene Aufnahmen. Der Agent selbst holt seine Warteschlange ueber die bestehenden Api-Key-authentifizierten Endpunkte ab (GET .../recordings, GET /recordings/{id}/audio) und markiert eine Aufnahme nach der Transkription per PATCH als erledigt - die eigentliche Transkription (whisper.cpp/faster-whisper) bleibt agent-seitig, siehe fds2610/Agent#17. Schliesst #85
223 lines
7.6 KiB
PHP
223 lines
7.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Tests\Controller;
|
|
|
|
use Monitor\Api\Controller\VoiceRecordingController;
|
|
use Monitor\Api\Db\Database;
|
|
use Monitor\Api\Http\Request;
|
|
use Monitor\Api\Repository\AgentRepository;
|
|
use Monitor\Api\Repository\VoiceRecordingRepository;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class VoiceRecordingControllerTest extends TestCase
|
|
{
|
|
private VoiceRecordingController $controller;
|
|
private AgentRepository $agents;
|
|
private VoiceRecordingRepository $recordings;
|
|
private string $storageDir;
|
|
private string $tmpUploadFile;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$db = Database::inMemory();
|
|
$this->agents = new AgentRepository($db);
|
|
$this->storageDir = sys_get_temp_dir() . '/monitor-recordings-controller-' . uniqid();
|
|
$this->recordings = new VoiceRecordingRepository($db, $this->storageDir);
|
|
$this->controller = new VoiceRecordingController($this->recordings, $this->agents);
|
|
|
|
$this->tmpUploadFile = sys_get_temp_dir() . '/monitor-upload-' . uniqid();
|
|
file_put_contents($this->tmpUploadFile, 'echte audiodaten');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (file_exists($this->tmpUploadFile)) {
|
|
unlink($this->tmpUploadFile);
|
|
}
|
|
if (is_dir($this->storageDir)) {
|
|
foreach (glob($this->storageDir . '/*') ?: [] as $file) {
|
|
unlink($file);
|
|
}
|
|
rmdir($this->storageDir);
|
|
}
|
|
}
|
|
|
|
/** @return array<string, mixed> ein $_FILES-artiges Feld fuer Request::create(). */
|
|
private function uploadedAudioField(int $error = UPLOAD_ERR_OK): array
|
|
{
|
|
return [
|
|
'name' => 'aufnahme.webm',
|
|
'type' => 'audio/webm',
|
|
'tmp_name' => $this->tmpUploadFile,
|
|
'error' => $error,
|
|
'size' => filesize($this->tmpUploadFile),
|
|
];
|
|
}
|
|
|
|
public function testUploadRejectsUnknownAgent(): void
|
|
{
|
|
$response = $this->controller->upload(
|
|
Request::create('POST', '/x', files: ['audio' => $this->uploadedAudioField()]),
|
|
['id' => '999']
|
|
);
|
|
|
|
self::assertSame(404, $response->status);
|
|
}
|
|
|
|
public function testUploadRejectsMissingFile(): void
|
|
{
|
|
$agentId = (int) $this->agents->register('Alice', 'entwickler')['agent']['id'];
|
|
|
|
$response = $this->controller->upload(Request::create('POST', '/x'), ['id' => (string) $agentId]);
|
|
|
|
self::assertSame(422, $response->status);
|
|
}
|
|
|
|
public function testUploadRejectsTooLargeUploadError(): void
|
|
{
|
|
$agentId = (int) $this->agents->register('Alice', 'entwickler')['agent']['id'];
|
|
|
|
$response = $this->controller->upload(
|
|
Request::create('POST', '/x', files: ['audio' => $this->uploadedAudioField(UPLOAD_ERR_INI_SIZE)]),
|
|
['id' => (string) $agentId]
|
|
);
|
|
|
|
self::assertSame(413, $response->status);
|
|
}
|
|
|
|
public function testUploadStoresRecordingForKnownAgent(): void
|
|
{
|
|
$agentId = (int) $this->agents->register('Alice', 'entwickler')['agent']['id'];
|
|
|
|
$response = $this->controller->upload(
|
|
Request::create('POST', '/x', files: ['audio' => $this->uploadedAudioField()]),
|
|
['id' => (string) $agentId]
|
|
);
|
|
|
|
self::assertSame(201, $response->status);
|
|
self::assertSame($agentId, $response->body()['recording']['agent_id']);
|
|
self::assertSame('new', $response->body()['recording']['status']);
|
|
}
|
|
|
|
public function testIndexRejectsMismatchedAgentId(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$other = $this->agents->register('Bob', 'qa');
|
|
|
|
$response = $this->controller->index(
|
|
Request::create('GET', '/x'),
|
|
['id' => (string) $other['agent']['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(403, $response->status);
|
|
}
|
|
|
|
public function testIndexListsOwnRecordings(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$agentId = (int) $registration['agent']['id'];
|
|
$this->recordings->store($agentId, 'a', 'audio/webm');
|
|
|
|
$response = $this->controller->index(
|
|
Request::create('GET', '/x'),
|
|
['id' => (string) $agentId],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(200, $response->status);
|
|
self::assertCount(1, $response->body()['recordings']);
|
|
}
|
|
|
|
public function testDownloadRejectsForeignAgent(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$other = $this->agents->register('Bob', 'qa');
|
|
$recording = $this->recordings->store((int) $other['agent']['id'], 'a', 'audio/webm');
|
|
|
|
$response = $this->controller->download(
|
|
Request::create('GET', '/x'),
|
|
['id' => (string) $recording['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(403, $response->status);
|
|
}
|
|
|
|
public function testDownloadReturnsAudioBytesForOwnRecording(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$agentId = (int) $registration['agent']['id'];
|
|
$recording = $this->recordings->store($agentId, 'echte audiodaten', 'audio/webm');
|
|
|
|
$response = $this->controller->download(
|
|
Request::create('GET', '/x'),
|
|
['id' => (string) $recording['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(200, $response->status);
|
|
self::assertSame('echte audiodaten', $response->body());
|
|
}
|
|
|
|
public function testDownloadReturns404ForUnknownRecording(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
|
|
$response = $this->controller->download(
|
|
Request::create('GET', '/x'),
|
|
['id' => '999'],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(404, $response->status);
|
|
}
|
|
|
|
public function testMarkProcessedRejectsForeignAgent(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$other = $this->agents->register('Bob', 'qa');
|
|
$recording = $this->recordings->store((int) $other['agent']['id'], 'a', 'audio/webm');
|
|
|
|
$response = $this->controller->markProcessed(
|
|
Request::create('PATCH', '/x', body: ['status' => 'done']),
|
|
['id' => (string) $recording['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(403, $response->status);
|
|
}
|
|
|
|
public function testMarkProcessedRejectsUnsupportedStatus(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$agentId = (int) $registration['agent']['id'];
|
|
$recording = $this->recordings->store($agentId, 'a', 'audio/webm');
|
|
|
|
$response = $this->controller->markProcessed(
|
|
Request::create('PATCH', '/x', body: ['status' => 'archived']),
|
|
['id' => (string) $recording['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(422, $response->status);
|
|
}
|
|
|
|
public function testMarkProcessedMarksOwnRecordingDone(): void
|
|
{
|
|
$registration = $this->agents->register('Alice', 'entwickler');
|
|
$agentId = (int) $registration['agent']['id'];
|
|
$recording = $this->recordings->store($agentId, 'a', 'audio/webm');
|
|
|
|
$response = $this->controller->markProcessed(
|
|
Request::create('PATCH', '/x', body: ['status' => 'done']),
|
|
['id' => (string) $recording['id']],
|
|
$registration['agent']
|
|
);
|
|
|
|
self::assertSame(200, $response->status);
|
|
self::assertSame('done', $response->body()['recording']['status']);
|
|
}
|
|
}
|