chore: add tooling config, test bootstrap, env templates and docs

PHPUnit config (phpunit.dist.xml, bin/phpunit, bootstrap.php), PHP CS
Fixer config, .editorconfig. Separate .env.dev/.env.test templates.
Ollama tunnel setup script. Architecture and plan docs. Updated
application-layer unit tests to match current service signatures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-17 22:44:16 +00:00
parent 2cfc5e8f17
commit f55e96b094
21 changed files with 12348 additions and 25 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

4
.env.dev Normal file
View file

@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=7ac10a47af9d9582c01fe117c77e4c53
###< symfony/framework-bundle ###

3
.env.test Normal file
View file

@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'

17
.php-cs-fixer.dist.php Normal file
View file

@ -0,0 +1,17 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
->notPath([
'config/bundles.php',
'config/reference.php',
])
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;

4
bin/phpunit Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';

View file

@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Run this script on the LOCAL machine (where Ollama runs).
# It registers your SSH public key on the server and installs
# the autossh systemd service for a persistent tunnel.
#
# Usage: ./setup.sh <server-ip-or-hostname>
#
# Prerequisites (local machine):
# apt/brew: openssh-client autossh
# Ollama running on localhost:11434
set -euo pipefail
SERVER="${1:?Usage: $0 <server-ip-or-hostname>}"
TUNNEL_USER="ollama-tunnel"
REMOTE_PORT=11434
LOCAL_PORT=11434
KEY_FILE="${HOME}/.ssh/id_ed25519"
# Generate key if it doesn't exist
if [[ ! -f "${KEY_FILE}" ]]; then
echo "[+] Generating SSH key ${KEY_FILE} ..."
ssh-keygen -t ed25519 -f "${KEY_FILE}" -N "" -C "ollama-tunnel@$(hostname)"
fi
# Copy public key to server
echo "[+] Copying public key to ${TUNNEL_USER}@${SERVER} ..."
echo " You will be prompted for sudo on the server (or use the superseller account)."
PUBKEY=$(cat "${KEY_FILE}.pub")
ssh superseller@"${SERVER}" "sudo bash -c 'echo \"${PUBKEY}\" >> /home/${TUNNEL_USER}/.ssh/authorized_keys && sort -u /home/${TUNNEL_USER}/.ssh/authorized_keys -o /home/${TUNNEL_USER}/.ssh/authorized_keys'"
echo "[+] Testing tunnel connection ..."
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 \
-N -i "${KEY_FILE}" \
-R "172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT}" \
"${TUNNEL_USER}@${SERVER}" &
SSH_PID=$!
sleep 2
if kill -0 "${SSH_PID}" 2>/dev/null; then
echo "[+] Tunnel works! Stopping test connection."
kill "${SSH_PID}"
else
echo "[!] Tunnel test failed. Check sshd config and firewall on the server."
exit 1
fi
# Install systemd service
install_systemd_service() {
local service_file="${HOME}/.config/systemd/user/ollama-tunnel.service"
mkdir -p "$(dirname "${service_file}")"
cat > "${service_file}" << EOF
[Unit]
Description=Ollama SSH reverse tunnel to SuperSeller3000 server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/autossh -M 0 \\
-o "ServerAliveInterval=30" \\
-o "ServerAliveCountMax=3" \\
-o "ExitOnForwardFailure=yes" \\
-o "StrictHostKeyChecking=accept-new" \\
-N -i ${KEY_FILE} \\
-R 172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT} \\
${TUNNEL_USER}@${SERVER}
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable ollama-tunnel.service
systemctl --user start ollama-tunnel.service
echo "[+] systemd service installed and started."
echo " Status: systemctl --user status ollama-tunnel"
}
if command -v autossh &>/dev/null && command -v systemctl &>/dev/null; then
echo "[+] Installing autossh systemd user service ..."
install_systemd_service
else
echo "[!] autossh or systemd not found. Manual tunnel command:"
echo ""
echo " autossh -M 0 -o ServerAliveInterval=30 -N \\"
echo " -i ${KEY_FILE} \\"
echo " -R 172.18.0.1:${REMOTE_PORT}:localhost:${LOCAL_PORT} \\"
echo " ${TUNNEL_USER}@${SERVER}"
fi
echo ""
echo "Done. The server will see Ollama at http://172.18.0.1:${REMOTE_PORT}"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

44
phpunit.dist.xml Normal file
View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
</extensions>
</phpunit>

View file

@ -23,7 +23,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->service = new ArticleTypeService($this->repo); $this->service = new ArticleTypeService($this->repo);
} }
public function test_create_saves_article_type(): void public function testCreateSavesArticleType(): void
{ {
$this->repo->expects($this->once())->method('save'); $this->repo->expects($this->once())->method('save');
@ -32,7 +32,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->assertSame('Notebook', $type->getName()); $this->assertSame('Notebook', $type->getName());
} }
public function test_rename_updates_name(): void public function testRenameUpdatesName(): void
{ {
$type = new ArticleType('Notebook'); $type = new ArticleType('Notebook');
$this->repo->method('findById')->willReturn($type); $this->repo->method('findById')->willReturn($type);
@ -43,7 +43,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->assertSame('Laptop', $type->getName()); $this->assertSame('Laptop', $type->getName());
} }
public function test_rename_throws_when_not_found(): void public function testRenameThrowsWhenNotFound(): void
{ {
$this->repo->method('findById')->willReturn(null); $this->repo->method('findById')->willReturn(null);
@ -52,7 +52,7 @@ final class ArticleTypeServiceTest extends TestCase
$this->service->rename(\Symfony\Component\Uid\Uuid::v7(), 'X'); $this->service->rename(\Symfony\Component\Uid\Uuid::v7(), 'X');
} }
public function test_add_attribute_links_definition(): void public function testAddAttributeLinksDefinition(): void
{ {
$type = new ArticleType('Notebook'); $type = new ArticleType('Notebook');
$def = new AttributeDefinition('RAM', AttributeType::String); $def = new AttributeDefinition('RAM', AttributeType::String);

View file

@ -30,7 +30,7 @@ final class ArticleValidatorTest extends TestCase
$this->type->addAttributeDefinition($this->cpuDef); $this->type->addAttributeDefinition($this->cpuDef);
} }
public function test_valid_when_all_attributes_set(): void public function testValidWhenAllAttributesSet(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB')); $article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB'));
@ -42,7 +42,7 @@ final class ArticleValidatorTest extends TestCase
$this->assertTrue($this->validator->isValid($article)); $this->assertTrue($this->validator->isValid($article));
} }
public function test_returns_missing_attribute_names(): void public function testReturnsMissingAttributeNames(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB')); $article->setAttributeValue(new AttributeValue($article, $this->ramDef, '16 GB'));
@ -55,7 +55,7 @@ final class ArticleValidatorTest extends TestCase
$this->assertFalse($this->validator->isValid($article)); $this->assertFalse($this->validator->isValid($article));
} }
public function test_all_missing_when_no_values_set(): void public function testAllMissingWhenNoValuesSet(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);

View file

@ -31,7 +31,7 @@ final class LocalStorageManagerTest extends TestCase
} }
} }
public function test_store_picks_active_path_with_quota(): void public function testStorePicksActivePathWithQuota(): void
{ {
$path = new StoragePath('Main', sys_get_temp_dir().'/storage-test-'.uniqid(), 1_000_000, 10); $path = new StoragePath('Main', sys_get_temp_dir().'/storage-test-'.uniqid(), 1_000_000, 10);
mkdir($path->getBasePath(), recursive: true); mkdir($path->getBasePath(), recursive: true);
@ -51,7 +51,7 @@ final class LocalStorageManagerTest extends TestCase
rmdir($path->getBasePath()); rmdir($path->getBasePath());
} }
public function test_throws_when_no_active_path(): void public function testThrowsWhenNoActivePath(): void
{ {
$this->repo->method('findActiveSortedByPriority')->willReturn([]); $this->repo->method('findActiveSortedByPriority')->willReturn([]);
@ -61,7 +61,7 @@ final class LocalStorageManagerTest extends TestCase
$this->manager->store($this->tmpFile, 'photo.jpg'); $this->manager->store($this->tmpFile, 'photo.jpg');
} }
public function test_skips_full_path_and_uses_next(): void public function testSkipsFullPathAndUsesNext(): void
{ {
$fullPath = new StoragePath('Full', sys_get_temp_dir().'/full-'.uniqid(), 50, 20); $fullPath = new StoragePath('Full', sys_get_temp_dir().'/full-'.uniqid(), 50, 20);
$okPath = new StoragePath('OK', sys_get_temp_dir().'/ok-'.uniqid(), 1_000_000, 10); $okPath = new StoragePath('OK', sys_get_temp_dir().'/ok-'.uniqid(), 1_000_000, 10);
@ -80,7 +80,7 @@ final class LocalStorageManagerTest extends TestCase
rmdir($okPath->getBasePath()); rmdir($okPath->getBasePath());
} }
public function test_get_full_path(): void public function testGetFullPath(): void
{ {
$path = new StoragePath('Main', '/srv/storage', 1_000_000, 10); $path = new StoragePath('Main', '/srv/storage', 1_000_000, 10);
$this->assertSame('/srv/storage/photo.jpg', $this->manager->getFullPath($path, 'photo.jpg')); $this->assertSame('/srv/storage/photo.jpg', $this->manager->getFullPath($path, 'photo.jpg'));

View file

@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
final class ArticleStatusTest extends TestCase final class ArticleStatusTest extends TestCase
{ {
public function test_valid_transitions(): void public function testValidTransitions(): void
{ {
$this->assertTrue(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Draft)); $this->assertTrue(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Draft));
$this->assertTrue(ArticleStatus::Draft->canTransitionTo(ArticleStatus::Active)); $this->assertTrue(ArticleStatus::Draft->canTransitionTo(ArticleStatus::Active));
@ -19,7 +19,7 @@ final class ArticleStatusTest extends TestCase
$this->assertTrue(ArticleStatus::Listed->canTransitionTo(ArticleStatus::Sold)); $this->assertTrue(ArticleStatus::Listed->canTransitionTo(ArticleStatus::Sold));
} }
public function test_invalid_transitions(): void public function testInvalidTransitions(): void
{ {
$this->assertFalse(ArticleStatus::Sold->canTransitionTo(ArticleStatus::Draft)); $this->assertFalse(ArticleStatus::Sold->canTransitionTo(ArticleStatus::Draft));
$this->assertFalse(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Sold)); $this->assertFalse(ArticleStatus::Ingesting->canTransitionTo(ArticleStatus::Sold));

View file

@ -19,7 +19,7 @@ final class ArticleTest extends TestCase
$this->type = new ArticleType('Notebook'); $this->type = new ArticleType('Notebook');
} }
public function test_new_article_has_ingesting_status(): void public function testNewArticleHasIngestingStatus(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
@ -27,7 +27,7 @@ final class ArticleTest extends TestCase
$this->assertSame(1, $article->getStock()); $this->assertSame(1, $article->getStock());
} }
public function test_valid_status_transition(): void public function testValidStatusTransition(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->transitionTo(ArticleStatus::Draft); $article->transitionTo(ArticleStatus::Draft);
@ -35,7 +35,7 @@ final class ArticleTest extends TestCase
$this->assertSame(ArticleStatus::Draft, $article->getStatus()); $this->assertSame(ArticleStatus::Draft, $article->getStatus());
} }
public function test_invalid_status_transition_throws(): void public function testInvalidStatusTransitionThrows(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
@ -43,7 +43,7 @@ final class ArticleTest extends TestCase
$article->transitionTo(ArticleStatus::Sold); $article->transitionTo(ArticleStatus::Sold);
} }
public function test_decrement_stock(): void public function testDecrementStock(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 3, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 3, ArticleCondition::Good);
$article->decrementStock(); $article->decrementStock();
@ -52,7 +52,7 @@ final class ArticleTest extends TestCase
$this->assertFalse($article->isOutOfStock()); $this->assertFalse($article->isOutOfStock());
} }
public function test_decrement_to_zero_marks_out_of_stock(): void public function testDecrementToZeroMarksOutOfStock(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
$article->decrementStock(); $article->decrementStock();
@ -60,7 +60,7 @@ final class ArticleTest extends TestCase
$this->assertTrue($article->isOutOfStock()); $this->assertTrue($article->isOutOfStock());
} }
public function test_decrement_below_zero_throws(): void public function testDecrementBelowZeroThrows(): void
{ {
$article = new Article($this->type, 'NB-001', 'INV-001', 0, ArticleCondition::Good); $article = new Article($this->type, 'NB-001', 'INV-001', 0, ArticleCondition::Good);

View file

@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
final class CustomerTest extends TestCase final class CustomerTest extends TestCase
{ {
public function test_new_customer_has_empty_platform_ids(): void public function testNewCustomerHasEmptyPlatformIds(): void
{ {
$customer = new Customer('Max Mustermann', 'max@example.com', []); $customer = new Customer('Max Mustermann', 'max@example.com', []);
@ -17,7 +17,7 @@ final class CustomerTest extends TestCase
$this->assertNull($customer->getPlatformId('ebay')); $this->assertNull($customer->getPlatformId('ebay'));
} }
public function test_add_platform_id(): void public function testAddPlatformId(): void
{ {
$customer = new Customer('Max Mustermann', 'max@example.com', []); $customer = new Customer('Max Mustermann', 'max@example.com', []);
$customer->addPlatformId('ebay', 'ebay-user-123'); $customer->addPlatformId('ebay', 'ebay-user-123');
@ -25,7 +25,7 @@ final class CustomerTest extends TestCase
$this->assertSame('ebay-user-123', $customer->getPlatformId('ebay')); $this->assertSame('ebay-user-123', $customer->getPlatformId('ebay'));
} }
public function test_matching_key_is_lowercase_normalized(): void public function testMatchingKeyIsLowercaseNormalized(): void
{ {
$customer = new Customer('Max Mustermann', 'max@example.com', [ $customer = new Customer('Max Mustermann', 'max@example.com', [
'street' => 'Musterstraße 1', 'street' => 'Musterstraße 1',

View file

@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1);
use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php'; require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); (new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) { if ($_SERVER['APP_DEBUG']) {
umask(0000); umask(0000);

0
translations/.gitignore vendored Normal file
View file