Laravel Hetzner Storagebox

PHP MIT

Production-ready Laravel package for integrating Hetzner Storage Box into Laravel applications using the native Storage facade and filesystem API.

Stars
19
Forks
0
Downloads
2,357
Open Issues
0
Files main

Repository Files

Loading file structure...
tests/Feature/MockedHttpTest.php
<?php

namespace GhostCompiler\Hetzner\StorageBox\Tests\Feature;

use GhostCompiler\Hetzner\StorageBox\Collections\StorageBoxCollection;
use GhostCompiler\Hetzner\StorageBox\DTOs\StorageBox;
use GhostCompiler\Hetzner\StorageBox\Exceptions\AuthenticationException;
use GhostCompiler\Hetzner\StorageBox\Exceptions\NetworkException;
use GhostCompiler\Hetzner\StorageBox\Exceptions\ValidationException;
use GhostCompiler\Hetzner\StorageBox\Http\Client\HetznerClient;
use GhostCompiler\Hetzner\StorageBox\Http\Middleware\RetryMiddleware;
use GhostCompiler\Hetzner\StorageBox\Managers\HetznerStorageBoxManager;
use GhostCompiler\Hetzner\StorageBox\Tests\TestCase;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;

class MockedHttpTest extends TestCase
{
    private function createMockClient(array $responses, int $maxRetries = 3, int $retryBackoff = 0): HetznerClient
    {
        $mock = new MockHandler($responses);
        $stack = HandlerStack::create($mock);

        // Add retry middleware
        $retry = new RetryMiddleware($maxRetries, $retryBackoff);
        $stack->push(Middleware::retry($retry->decider(), $retry->delay()));

        $guzzle = new GuzzleClient([
            'handler' => $stack,
            'base_uri' => 'https://api.hetzner.com/v1/',
        ]);

        $client = new HetznerClient('mock-token', [
            'base_url' => 'https://api.hetzner.com/v1',
            'retries' => $maxRetries,
            'retry_backoff' => $retryBackoff,
        ]);
        $client->setGuzzleClient($guzzle);

        return $client;
    }

    public function test_get_storage_boxes_success()
    {
        $responseBody = json_encode([
            'storage_boxes' => [
                ['id' => 1, 'name' => 'box-1', 'status' => 'active'],
                ['id' => 2, 'name' => 'box-2', 'status' => 'locked'],
            ],
        ]);

        $client = $this->createMockClient([
            new Response(
                200,
                [
                    'RateLimit-Limit' => '3600',
                    'RateLimit-Remaining' => '3599',
                    'RateLimit-Reset' => '1718115600',
                ],
                $responseBody
            ),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $boxes = $manager->storageBoxes()->all();

        $this->assertCount(2, $boxes);
        $this->assertEquals('box-1', $boxes->first()->name);

        $limits = $manager->rateLimit();
        $this->assertEquals(3600, $limits['limit']);
        $this->assertEquals(3599, $limits['remaining']);
        $this->assertEquals(1718115600, $limits['reset']);
    }

    public function test_auth_exception_mapping()
    {
        $client = $this->createMockClient([
            new Response(401, [], json_encode([
                'error' => [
                    'code' => 'unauthorized',
                    'message' => 'Invalid token',
                ],
            ])),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $this->expectException(AuthenticationException::class);
        $this->expectExceptionMessage('Invalid token');

        $manager->storageBoxes()->all();
    }

    public function test_validation_exception_mapping()
    {
        $client = $this->createMockClient([
            new Response(422, [], json_encode([
                'error' => [
                    'code' => 'invalid_input',
                    'message' => 'Validation failed',
                    'details' => [
                        'fields' => [
                            ['name' => 'name', 'message' => ['must be unique']],
                        ],
                    ],
                ],
            ])),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        try {
            $manager->storageBoxes()->create(['name' => 'box-1']);
            $this->fail('Expected ValidationException was not thrown');
        } catch (ValidationException $e) {
            $this->assertEquals('Validation failed', $e->getMessage());
            $this->assertEquals('invalid_input', $e->getErrorCode());
            $this->assertEquals(['name' => ['must be unique']], $e->getErrors());
        }
    }

    public function test_network_exception_mapping()
    {
        $client = $this->createMockClient([
            new ConnectException('Connection timed out', new Request('GET', 'storage_boxes')),
        ], 0); // No retries

        $manager = new HetznerStorageBoxManager($client);

        $this->expectException(NetworkException::class);
        $this->expectExceptionMessage('Connection timed out');

        $manager->storageBoxes()->all();
    }

    public function test_rate_limit_exception_mapping_and_retries()
    {
        $responseBody = json_encode([
            'storage_boxes' => [['id' => 1, 'name' => 'box-1', 'status' => 'active']],
        ]);

        // First call: 429 Too Many Requests
        // Second call: 200 OK
        $client = $this->createMockClient([
            new Response(429, [
                'RateLimit-Limit' => '3600',
                'RateLimit-Remaining' => '0',
                'RateLimit-Reset' => (string) (time() + 1),
            ], json_encode([
                'error' => ['code' => 'rate_limit_exceeded', 'message' => 'Rate limit exceeded'],
            ])),
            new Response(200, [], $responseBody),
        ], 3, 0); // 3 retries, 0ms backoff multiplier for test speed

        $manager = new HetznerStorageBoxManager($client);

        // Should retry once and succeed
        $boxes = $manager->storageBoxes()->all();

        $this->assertCount(1, $boxes);
        $this->assertEquals('box-1', $boxes->first()->name);
    }

    public function test_async_requests()
    {
        $responseBody = json_encode([
            'storage_boxes' => [['id' => 1, 'name' => 'async-box', 'status' => 'active']],
        ]);

        $client = $this->createMockClient([
            new Response(200, [], $responseBody),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $promise = $manager->storageBoxes()->async()->all();

        $this->assertInstanceOf(PromiseInterface::class, $promise);

        // Wait for resolution
        $boxes = $promise->wait();

        $this->assertInstanceOf(StorageBoxCollection::class, $boxes);
        $this->assertEquals('async-box', $boxes->first()->name);
    }

    public function test_batch_operations()
    {
        $responseBody1 = json_encode(['storage_box' => ['id' => 1, 'name' => 'box-1', 'status' => 'active']]);
        $responseBody2 = json_encode(['storage_box' => ['id' => 2, 'name' => 'box-2', 'status' => 'active']]);

        $client = $this->createMockClient([
            new Response(200, [], $responseBody1),
            new Response(200, [], $responseBody2),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $results = $manager->batch([
            fn () => $manager->storageBoxes()->find(1),
            fn () => $manager->storageBoxes()->find(2),
        ]);

        $this->assertCount(2, $results);
        $this->assertInstanceOf(StorageBox::class, $results[0]);
        $this->assertInstanceOf(StorageBox::class, $results[1]);
        $this->assertEquals('box-1', $results[0]->name);
        $this->assertEquals('box-2', $results[1]->name);
    }

    public function test_folders_list()
    {
        $responseBody = json_encode([
            'folders' => ['folder1', 'folder2'],
        ]);

        $client = $this->createMockClient([
            new Response(200, [], $responseBody),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $folders = $manager->storageBoxes()->folders(1, '/path');

        $this->assertEquals(['folder1', 'folder2'], $folders['folders']);
    }

    public function test_reset_password_action()
    {
        $responseBody = json_encode([
            'action' => [
                'id' => 456,
                'command' => 'reset_password',
                'status' => 'running',
                'progress' => 0,
                'started' => '2026-06-11T12:00:00Z',
            ],
        ]);

        $client = $this->createMockClient([
            new Response(200, [], $responseBody),
        ]);

        $manager = new HetznerStorageBoxManager($client);

        $action = $manager->storageBoxes()->resetPassword(1, 'new-password');

        $this->assertEquals(456, $action->id);
        $this->assertEquals('reset_password', $action->command);
        $this->assertEquals('running', $action->status);
    }

    public function test_subaccounts_lifecycle_and_actions()
    {
        $listResponse = json_encode([
            'subaccounts' => [
                [
                    'id' => 10,
                    'name' => 'sub-1',
                    'username' => 'sb1-sub',
                    'home_directory' => '/sub',
                    'access_settings' => ['samba' => true],
                    'created' => '2026-06-12T10:00:00Z',
                ],
            ],
        ]);

        $actionResponse = json_encode([
            'action' => [
                'id' => 789,
                'command' => 'reset_subaccount_password',
                'status' => 'success',
                'progress' => 100,
                'started' => '2026-06-12T11:00:00Z',
            ],
        ]);

        $client = $this->createMockClient([
            new Response(200, [], $listResponse),
            new Response(200, [], $actionResponse),
        ]);

        $manager = new HetznerStorageBoxManager($client);
        $subaccounts = $manager->storageBoxes()->subaccounts(1)->all();

        $this->assertCount(1, $subaccounts);
        $this->assertEquals('sub-1', $subaccounts->first()->name);

        $action = $manager->storageBoxes()->subaccounts(1)->resetPassword(10, 'secret');
        $this->assertEquals(789, $action->id);
        $this->assertEquals('reset_subaccount_password', $action->command);
    }

    public function test_snapshots_lifecycle()
    {
        $snapResponse = json_encode([
            'snapshot' => [
                'id' => 20,
                'name' => 'snap-1',
                'is_automatic' => false,
                'created' => '2026-06-12T12:00:00Z',
            ],
        ]);

        $client = $this->createMockClient([
            new Response(200, [], $snapResponse),
        ]);

        $manager = new HetznerStorageBoxManager($client);
        $snapshot = $manager->storageBoxes()->snapshots(1)->create(['description' => 'Backup']);

        $this->assertEquals(20, $snapshot->id);
        $this->assertEquals('snap-1', $snapshot->name);
        $this->assertFalse($snapshot->isAutomatic);
    }
}