From 7a39798e8b6c6ab1624721f495a950873cc4a61a Mon Sep 17 00:00:00 2001
From: philipp lang <philipp@aweos.de>
Date: Sun, 20 Oct 2024 21:19:07 +0200
Subject: [PATCH] Add mailgateway

---
 app/Setting/SettingServiceProvider.php        |   2 -
 modules/Mailgateway/Components/Form.php       | 119 ++++++++++++++++
 .../Mailgateway/Components/SettingView.php    |  21 +++
 .../Components/setting-view.blade.php         |  37 +++++
 modules/Mailgateway/IndexTest.php             |  39 ++++++
 .../MailgatewayServiceProvider.php            |  37 +++++
 modules/Mailgateway/MailgatewaySettings.php   |  26 ++++
 modules/Mailgateway/StoreTest.php             | 128 ++++++++++++++++++
 modules/Mailgateway/UpdateTest.php            | 104 ++++++++++++++
 resources/js/components/ui/BooleanDisplay.vue |  32 -----
 10 files changed, 511 insertions(+), 34 deletions(-)
 create mode 100644 modules/Mailgateway/Components/Form.php
 create mode 100644 modules/Mailgateway/Components/SettingView.php
 create mode 100644 modules/Mailgateway/Components/setting-view.blade.php
 create mode 100644 modules/Mailgateway/IndexTest.php
 create mode 100644 modules/Mailgateway/MailgatewayServiceProvider.php
 create mode 100644 modules/Mailgateway/MailgatewaySettings.php
 create mode 100644 modules/Mailgateway/StoreTest.php
 create mode 100644 modules/Mailgateway/UpdateTest.php
 delete mode 100644 resources/js/components/ui/BooleanDisplay.vue

diff --git a/app/Setting/SettingServiceProvider.php b/app/Setting/SettingServiceProvider.php
index 4889a3f9..c452886b 100644
--- a/app/Setting/SettingServiceProvider.php
+++ b/app/Setting/SettingServiceProvider.php
@@ -4,7 +4,6 @@ namespace App\Setting;
 
 use App\Fileshare\FileshareSettings;
 use App\Form\FormSettings;
-use App\Mailgateway\MailgatewaySettings;
 use Modules\Module\ModuleSettings;
 use App\Prevention\PreventionSettings;
 use App\Setting\Data\SettingSynthesizer;
@@ -35,7 +34,6 @@ class SettingServiceProvider extends ServiceProvider
     {
         app(SettingFactory::class)->register(ModuleSettings::class);
         app(SettingFactory::class)->register(InvoiceSettings::class);
-        app(SettingFactory::class)->register(MailgatewaySettings::class);
         app(SettingFactory::class)->register(NamiSettings::class);
         app(SettingFactory::class)->register(FormSettings::class);
         app(SettingFactory::class)->register(FileshareSettings::class);
diff --git a/modules/Mailgateway/Components/Form.php b/modules/Mailgateway/Components/Form.php
new file mode 100644
index 00000000..9eb57979
--- /dev/null
+++ b/modules/Mailgateway/Components/Form.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Modules\Mailgateway\Components;
+
+use App\Mailgateway\Models\Mailgateway;
+use Illuminate\Support\Collection;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\ValidationException;
+use Livewire\Attributes\On;
+use Livewire\Attributes\Validate;
+use Livewire\Component;
+
+class Form extends Component
+{
+
+    public string $id = '';
+    public string $name = '';
+    public string $domain = '';
+    public array $params = [];
+    #[Validate('required')]
+    public ?string $cls = null;
+    public Collection $types;
+
+    public function rules()
+    {
+        return [
+            'name' => 'required|string|max:255',
+            'domain' => 'required|string|max:255',
+            'cls' => ['required', 'string', 'max:255', Rule::in(app('mail-gateways'))],
+            'params' => 'present|array',
+            ...$this->cls ? collect($this->cls::rules($this->id ? 'updateValidator' : 'storeValidator'))->mapWithKeys(fn ($rules, $key) => ["params.{$key}" => $rules]) : [],
+        ];
+    }
+
+    public function validationAttributes(): array
+    {
+        return [
+            'cls' => 'Typ',
+            'name' => 'Beschreibung',
+            'domain' => 'Domain',
+            ...$this->cls ? collect($this->cls::fieldNames())->mapWithKeys(fn ($attribute, $key) => ["params.{$key}" => $attribute]) : [],
+        ];
+    }
+
+    public function mount(?string $model = null): void
+    {
+        $this->types = app('mail-gateways')->map(fn ($gateway) => [
+            'name' => $gateway::name(),
+            'id' => $gateway,
+        ]);
+
+        $model = Mailgateway::find($model);
+
+        if ($model) {
+            $this->id = $model->id;
+            $this->name = $model->name;
+            $this->domain = $model->domain;
+            $this->cls = get_class($model->type);
+            $this->params = (array) $model->type;
+        }
+    }
+
+    public function updatedType(string $type): void
+    {
+        $this->params = $type::defaults();
+    }
+
+    public function fields(): array
+    {
+        return $this->cls ? $this->cls::fields() : [];
+    }
+
+    #[On('onStoreFromModal')]
+    public function onSave(): void
+    {
+        $this->validate();
+
+        if (!app($this->cls)->setParams($this->params)->works()) {
+            throw ValidationException::withMessages(['connection' => 'Verbindung fehlgeschlagen.']);
+        }
+
+        $payload = [
+            'name' => $this->name,
+            'domain' => $this->domain,
+            'type' => ['cls' => $this->cls, 'params' => $this->params],
+        ];
+        if ($this->id) {
+            Mailgateway::find($this->id)->update($payload);
+        } else {
+            Mailgateway::create($payload);
+        }
+        $this->dispatch('closeModal');
+        $this->dispatch('refresh');
+        $this->dispatch('success', 'Erfolgreich gespeichert.');
+    }
+
+    public function render()
+    {
+        return <<<'HTML'
+            <div>
+                <form class="grid grid-cols-2 gap-3">
+                    <x-form::text name="name" wire:model="name" label="Beschreibung" required />
+                    <x-form::text name="domain" wire:model="domain" label="Domain" required />
+                    <x-form::select name="cls" wire:model.live="cls" label="Typ" :options="$types" required />
+                    @foreach($this->fields() as $index => $field)
+                        <x-form::text
+                            wire:key="index"
+                            wire:model="params.{{$field['name']}}"
+                            :label="$field['label']"
+                            :type="$field['type']"
+                            :name="$field['name']"
+                            :required="str_contains('required', $field['storeValidator'])"
+                        ></x-form::text>
+                    @endforeach
+                </form>
+            </div>
+        HTML;
+    }
+}
diff --git a/modules/Mailgateway/Components/SettingView.php b/modules/Mailgateway/Components/SettingView.php
new file mode 100644
index 00000000..2ec0c789
--- /dev/null
+++ b/modules/Mailgateway/Components/SettingView.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Modules\Mailgateway\Components;
+
+use App\Mailgateway\Models\Mailgateway;
+use Livewire\Component;
+use Modules\Mailgateway\MailgatewaySettings;
+
+class SettingView extends Component
+{
+    public string $settingClass = MailgatewaySettings::class;
+
+    public $listeners = ['refresh' => '$refresh'];
+
+    public function render()
+    {
+        return view('mailgateway::setting-view', [
+            'data' => Mailgateway::get(),
+        ]);
+    }
+}
diff --git a/modules/Mailgateway/Components/setting-view.blade.php b/modules/Mailgateway/Components/setting-view.blade.php
new file mode 100644
index 00000000..d4b2c45f
--- /dev/null
+++ b/modules/Mailgateway/Components/setting-view.blade.php
@@ -0,0 +1,37 @@
+<x-page::setting-layout :active="$settingClass">
+    <div>
+        <x-ui::table>
+            <thead>
+                <th>Bezeichnung</th>
+                <th>Domain</th>
+                <th>Typ</th>
+                <th>Prüfung</th>
+                <th>Aktion</th>
+            </thead>
+
+            <x-ui::action wire:click.prevent="$dispatch('openModal', {component: 'modules.mailgateway.components.form', props: {model: ''}, title: 'Verbindung erstellen'})" icon="plus" variant="danger">Neu</x-ui::action>
+
+            @foreach ($data as $index => $gateway)
+            <tr wire:key="$index">
+                <td>{{ $gateway->name }}</td>
+                <td>{{ $gateway->domain }}</td>
+                <td>{{ $gateway->type::name() }}</td>
+                <td>
+                    <x-ui::boolean-display :value="$gateway->type->works()"
+                        hint="Verbindungsstatus"
+                        right="Verbindung erfolgreich"
+                        wrong="Verbindung fehlgeschlagen"
+                    ></x-ui::boolean-display>
+                </td>
+                <td>
+                    <x-ui::action wire:click="$dispatch('openModal', {
+                        component: 'modules.mailgateway.components.form',
+                        props: {model: '{{$gateway->id}}'},
+                        title: 'Verbindung {{$gateway->name}} bearbeiten'}
+                    )" icon="pencil" variant="warning">Bearbeiten</x-ui::action>
+                </td>
+            </tr>
+            @endforeach
+        </x-ui::table>
+    </div>
+</x-page::setting-layout>
diff --git a/modules/Mailgateway/IndexTest.php b/modules/Mailgateway/IndexTest.php
new file mode 100644
index 00000000..31419d47
--- /dev/null
+++ b/modules/Mailgateway/IndexTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Tests\Feature\Mailgateway;
+
+use App\Mailgateway\Models\Mailgateway;
+use App\Mailgateway\Types\LocalType;
+use App\Mailgateway\Types\MailmanType;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Livewire\Livewire;
+use Modules\Mailgateway\Components\SettingView;
+use Tests\RequestFactories\MailmanTypeRequest;
+use Tests\TestCase;
+
+uses(DatabaseTransactions::class);
+uses(TestCase::class);
+
+it('test it can view index page', function () {
+    test()->login()->loginNami();
+    test()->get('/setting/mailgateway')->assertSeeLivewire(SettingView::class);
+});
+
+it('test it displays local gateways', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+    Mailgateway::factory()->type(LocalType::class, [])->name('Lore')->domain('example.com')->create();
+
+    Livewire::test(SettingView::class)
+        ->assertSeeHtml('example.com')
+        ->assertSeeHtml('Lore')
+        ->assertSeeHtml('Lokal')
+        ->assertSeeHtml('Verbindung erfolgreich');
+});
+
+it('displays mailman gateways', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+    $typeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner']);
+    Mailgateway::factory()->type(MailmanType::class, $typeParams)->create();
+
+    Livewire::test(SettingView::class)->assertSeeHtml('Verbindung erfolgreich');
+});
diff --git a/modules/Mailgateway/MailgatewayServiceProvider.php b/modules/Mailgateway/MailgatewayServiceProvider.php
new file mode 100644
index 00000000..f0f2b64a
--- /dev/null
+++ b/modules/Mailgateway/MailgatewayServiceProvider.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Modules\Mailgateway;
+
+use App\Setting\SettingFactory;
+use Illuminate\Routing\Router;
+use Illuminate\Support\Facades\View;
+use Illuminate\Support\ServiceProvider;
+use Modules\Mailgateway\Components\SettingView;
+
+class MailgatewayServiceProvider extends ServiceProvider
+{
+    /**
+     * Register services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+    }
+
+    /**
+     * Bootstrap services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        app(SettingFactory::class)->register(MailgatewaySettings::class);
+
+        app(Router::class)->middleware(['web', 'auth:web'])->group(function ($router) {
+            $router->get('/setting/mailgateway', SettingView::class)->name('setting.mailgateway');
+        });
+
+        View::addNamespace('mailgateway', __DIR__ . '/Components');
+    }
+}
diff --git a/modules/Mailgateway/MailgatewaySettings.php b/modules/Mailgateway/MailgatewaySettings.php
new file mode 100644
index 00000000..419fa797
--- /dev/null
+++ b/modules/Mailgateway/MailgatewaySettings.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Modules\Mailgateway;
+
+use App\Setting\LocalSettings;
+
+class MailgatewaySettings extends LocalSettings
+{
+    public static function group(): string
+    {
+        return 'mailgateway';
+    }
+
+    public static function title(): string
+    {
+        return 'E-Mail-Verbindungen';
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function viewData(): array
+    {
+        return [];
+    }
+}
diff --git a/modules/Mailgateway/StoreTest.php b/modules/Mailgateway/StoreTest.php
new file mode 100644
index 00000000..b2c2f7dd
--- /dev/null
+++ b/modules/Mailgateway/StoreTest.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Tests\Feature\Mailgateway;
+
+use App\Mailgateway\Types\LocalType;
+use App\Mailgateway\Types\MailmanType;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Livewire\Livewire;
+use Modules\Mailgateway\Components\Form;
+use Tests\RequestFactories\MailmanTypeRequest;
+use Tests\TestCase;
+
+uses(DatabaseTransactions::class);
+uses(TestCase::class);
+
+it('test it saves a mail gateway', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    Livewire::test(Form::class)
+        ->set('name', 'lala')
+        ->set('domain', 'example.com')
+        ->set('cls', LocalType::class)
+        ->call('onSave')
+        ->assertDispatched('closeModal')
+        ->assertDispatched('refresh')
+        ->assertDispatched('success');
+
+    $this->assertDatabaseHas('mailgateways', [
+        'domain' => 'example.com',
+        'name' => 'lala',
+        'type' => json_encode([
+            'cls' => LocalType::class,
+            'params' => [],
+        ]),
+    ]);
+});
+
+it('validates type', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    Livewire::test(Form::class)
+        ->set('cls', '')
+        ->assertHasErrors(['cls' => 'required']);
+});
+
+it('test it validates mail gateway', function (array $attributes, array $errors) {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    Livewire::test(Form::class)
+        ->set('name', 'lala')
+        ->set('domain', 'example.com')
+        ->set('cls', LocalType::class)
+        ->setArray($attributes)
+        ->call('onSave')
+        ->assertHasErrors($errors)
+        ->assertNotDispatched('closeModal')
+        ->assertNotDispatched('refresh')
+        ->assertNotDispatched('success');
+})->with([
+    [['name' => ''], ['name' => 'required']],
+    [['domain' => ''], ['domain' => 'required']],
+]);
+
+it('test it validates mailman type', function (array $attributes, array $errors) {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    Livewire::test(Form::class)
+        ->set('name', 'lala')
+        ->set('domain', 'example.com')
+        ->set('cls', MailmanType::class)
+        ->set('params.url', 'exampl.com')
+        ->set('params.user', '::user::')
+        ->set('params.password', 'password')
+        ->setArray($attributes)
+        ->call('onSave')
+        ->assertHasErrors($errors)
+        ->assertNotDispatched('closeModal');
+})->with([
+    [['params.url' => ''], ['params.url' => 'required']],
+    [['params.user' => ''], ['params.user' => 'required']],
+    [['params.password' => ''], ['params.password' => 'required']],
+    [['params.owner' => ''], ['params.owner' => 'required']],
+    [['params.owner' => 'aaa'], ['params.owner' => 'email']],
+]);
+
+it('test it stores mailman gateway', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com']);
+
+    Livewire::test(Form::class)
+        ->setArray([
+            'name' => 'lala',
+            'domain' => 'https://example.com',
+            'cls' => MailmanType::class,
+            'params' => $typeParams
+        ])
+        ->call('onSave')
+        ->assertDispatched('closeModal');
+
+    $this->assertDatabaseHas('mailgateways', [
+        'type' => json_encode([
+            'cls' => MailmanType::class,
+            'params' => $typeParams,
+        ]),
+        'name' => 'lala',
+        'domain' => 'https://example.com',
+    ]);
+});
+
+it('test it checks mailman connection', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->fails()->create(['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret', 'owner' => 'owner@example.com']);
+
+    Livewire::test(Form::class)
+        ->setArray([
+            'name' => 'lala',
+            'domain' => 'https://example.com',
+            'cls' => MailmanType::class,
+            'params' => $typeParams
+        ])
+        ->call('onSave')
+        ->assertHasErrors('connection')
+        ->assertNotDispatched('closeModal');
+
+    $this->assertDatabaseCount('mailgateways', 0);
+});
diff --git a/modules/Mailgateway/UpdateTest.php b/modules/Mailgateway/UpdateTest.php
new file mode 100644
index 00000000..790af3fb
--- /dev/null
+++ b/modules/Mailgateway/UpdateTest.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Tests\Feature\Mailgateway;
+
+use App\Mailgateway\Models\Mailgateway;
+use App\Mailgateway\Types\LocalType;
+use App\Mailgateway\Types\MailmanType;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Livewire\Livewire;
+use Modules\Mailgateway\Components\Form;
+use Phake;
+use Tests\RequestFactories\MailmanTypeRequest;
+use Tests\TestCase;
+
+uses(DatabaseTransactions::class);
+uses(TestCase::class);
+
+it('test it sets attributes for mailman', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner']);
+    $mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create(['name' => '::name::', 'domain' => 'example.com']);
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->assertSet('id', $mailgateway->id)
+        ->assertSet('name', '::name::')
+        ->assertSet('domain', 'example.com')
+        ->assertSet('cls', MailmanType::class)
+        ->assertSet('params.url', 'https://mailman.example.com')
+        ->assertSet('params.user', 'user')
+        ->assertSet('params.password', 'password')
+        ->assertSet('params.owner', 'owner');
+});
+
+it('test it sets attributes for local', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $mailgateway = Mailgateway::factory()->type(LocalType::class, [])->create(['name' => '::name::', 'domain' => 'example.com']);
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->assertSet('name', '::name::')
+        ->assertSet('domain', 'example.com')
+        ->assertSet('cls', LocalType::class)
+        ->assertSet('params', []);
+});
+
+it('test it validates type', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $mailgateway = Mailgateway::factory()->type(LocalType::class, [])->create(['name' => '::name::', 'domain' => 'example.com']);
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->set('cls', '')
+        ->assertHasErrors(['cls' => 'required']);
+});
+
+it('test it updates a mailman gateway without updating password', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
+    $mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create(['name' => '::name::', 'domain' => 'example.com']);
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->set('name', '::newname::')
+        ->call('onSave')
+        ->assertHasNoErrors();
+
+    $this->assertDatabaseCount('mailgateways', 1);
+    $this->assertDatabaseHas('mailgateways', [
+        'name' => '::newname::',
+        'type' => json_encode(['cls' => MailmanType::class, 'params' => $typeParams]),
+    ]);
+});
+
+it('test it updates a mailman gateway with password', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
+    $newTypeParams = MailmanTypeRequest::new()->succeeds()->create(['url' => 'https://mailman.example.com', 'user' => 'newuser', 'password' => 'password', 'owner' => 'owner@example.com']);
+    $mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create();
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->set('params.user', 'newuser')
+        ->call('onSave')
+        ->assertHasNoErrors();
+
+    $this->assertDatabaseCount('mailgateways', 1);
+    $this->assertDatabaseHas('mailgateways', [
+        'type' => json_encode(['cls' => MailmanType::class, 'params' => $newTypeParams]),
+    ]);
+});
+
+it('test it checks mailgateway connection when updating', function () {
+    test()->withoutExceptionHandling()->login()->loginNami();
+
+    $typeParams = MailmanTypeRequest::new()->create(['url' => 'https://mailman.example.com', 'user' => 'user', 'password' => 'password', 'owner' => 'owner@example.com']);
+    MailmanTypeRequest::new()->fails()->create(['url' => 'https://mailman.example.com', 'user' => 'newuser', 'password' => 'password', 'owner' => 'owner@example.com']);
+    $mailgateway = Mailgateway::factory()->type(MailmanType::class, $typeParams)->create();
+
+    Livewire::test(Form::class, ['model' => $mailgateway->id])
+        ->set('params.user', 'newuser')
+        ->call('onSave')
+        ->assertHasErrors('connection');
+});
diff --git a/resources/js/components/ui/BooleanDisplay.vue b/resources/js/components/ui/BooleanDisplay.vue
deleted file mode 100644
index 4536b3ed..00000000
--- a/resources/js/components/ui/BooleanDisplay.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-    <div v-tooltip="longLabel" class="flex space-x-2 items-center">
-        <div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? (dark ? 'border-green-500' : 'border-green-700') : dark ? 'border-red-500' : 'border-red-700'">
-            <ui-sprite :src="value ? 'check' : 'close'" :class="value ? (dark ? 'text-green-600' : 'text-green-800') : dark ? 'text-red-600' : 'text-red-800'" class="w-3 h-3 flex-none"></ui-sprite>
-        </div>
-        <div class="text-gray-400 text-xs" v-text="label"></div>
-    </div>
-</template>
-
-<script>
-export default {
-    props: {
-        value: {
-            required: true,
-            type: Boolean,
-        },
-        label: {
-            type: String,
-            default: () => '',
-        },
-        longLabel: {
-            default: function () {
-                return null;
-            },
-        },
-        dark: {
-            type: Boolean,
-            default: () => false,
-        },
-    },
-};
-</script>