From 31fb04caaab914054b34bc9c64e88ea2625ef187 Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode Date: Sun, 18 Jan 2026 22:23:23 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20outdated=20mig?= =?UTF-8?q?ration=20files=20for=20`einundzwanzig=5Fplebs`=20and=20`pulse`?= =?UTF-8?q?=20tables,=20restructure=20directory,=20and=20update=20testing?= =?UTF-8?q?=20suite=20with=20new=20factories=20and=20Livewire=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .junie/guidelines.md | 127 ++++++++++ AGENTS.md | 127 ++++++++++ CLAUDE.md | 127 ++++++++++ app/Models/EinundzwanzigPleb.php | 6 +- .../2024_08_29_190127_create_pulse_tables.php | 0 composer.json | 1 + composer.lock | 73 +++++- .../factories/EinundzwanzigPlebFactory.php | 26 ++ ...0259_create_einundzwanzig_plebs_table.php} | 0 tests/Feature/ExampleTest.php | 7 +- .../Livewire/Association/ElectionTest.php | 145 +++++++++++ .../Association/Members/AdminTest.php | 73 ++++++ .../Feature/Livewire/Association/NewsTest.php | 110 +++++++++ .../Livewire/Association/ProfileTest.php | 116 +++++++++ .../Association/ProjectSupportTest.php | 228 ++++++++++++++++++ tests/Pest.php | 2 +- 16 files changed, 1161 insertions(+), 7 deletions(-) rename {database/migrations => backup_migrations}/2024_08_29_190127_create_pulse_tables.php (100%) create mode 100644 database/factories/EinundzwanzigPlebFactory.php rename database/migrations/{2024_08_29_120928_create_einundzwanzig_plebs_table.php => 2023_03_10_190259_create_einundzwanzig_plebs_table.php} (100%) create mode 100644 tests/Feature/Livewire/Association/ElectionTest.php create mode 100644 tests/Feature/Livewire/Association/Members/AdminTest.php create mode 100644 tests/Feature/Livewire/Association/NewsTest.php create mode 100644 tests/Feature/Livewire/Association/ProfileTest.php create mode 100644 tests/Feature/Livewire/Association/ProjectSupportTest.php diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 93dab41..a6c42d1 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -17,6 +17,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/flux (FLUXUI_FREE) - v2 - livewire/flux-pro (FLUXUI_PRO) - v2 - livewire/livewire (LIVEWIRE) - v4 +- livewire/volt (VOLT) - v1 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v3 @@ -267,6 +268,132 @@ accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, ca ->assertSeeLivewire(CreatePost::class); +=== volt/core rules === + +## Livewire Volt + +- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. +- Make new Volt components using `vendor/bin/sail artisan make:volt [name] [--test] [--pest]`. +- Volt is a class-based and functional API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file. +- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@volt` directive. +- You must check existing Volt components to determine if they're functional or class-based. If you can't detect that, ask the user which they prefer before writing a Volt component. + +### Volt Functional Component Example + + +@volt + 0]); + +$increment = fn () => $this->count++; +$decrement = fn () => $this->count--; + +$double = computed(fn () => $this->count * 2); +?> + +
+

Count: {{ $count }}

+

Double: {{ $this->double }}

+ + +
+@endvolt +
+ +### Volt Class Based Component Example +To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: + + +use Livewire\Volt\Component; + +new class extends Component { + public $count = 0; + + public function increment() + { + $this->count++; + } +} ?> + +
+

{{ $count }}

+ +
+
+ +### Testing Volt & Volt Components +- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. + + +use Livewire\Volt\Volt; + +test('counter increments', function () { + Volt::test('counter') + ->assertSee('Count: 0') + ->call('increment') + ->assertSee('Count: 1'); +}); + + + +declare(strict_types=1); + +use App\Models\{User, Product}; +use Livewire\Volt\Volt; + +test('product form creates product', function () { + $user = User::factory()->create(); + + Volt::test('pages.products.create') + ->actingAs($user) + ->set('form.name', 'Test Product') + ->set('form.description', 'Test Description') + ->set('form.price', 99.99) + ->call('create') + ->assertHasNoErrors(); + + expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); +}); + + +### Common Patterns + + + null, 'search' => '']); + +$products = computed(fn() => Product::when($this->search, + fn($q) => $q->where('name', 'like', "%{$this->search}%") +)->get()); + +$edit = fn(Product $product) => $this->editing = $product->id; +$delete = fn(Product $product) => $product->delete(); + +?> + + + + + + + + + + + Save + Saving... + + + === pint/core rules === ## Laravel Pint Code Formatter diff --git a/AGENTS.md b/AGENTS.md index 93dab41..a6c42d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/flux (FLUXUI_FREE) - v2 - livewire/flux-pro (FLUXUI_PRO) - v2 - livewire/livewire (LIVEWIRE) - v4 +- livewire/volt (VOLT) - v1 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v3 @@ -267,6 +268,132 @@ accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, ca ->assertSeeLivewire(CreatePost::class); +=== volt/core rules === + +## Livewire Volt + +- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. +- Make new Volt components using `vendor/bin/sail artisan make:volt [name] [--test] [--pest]`. +- Volt is a class-based and functional API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file. +- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@volt` directive. +- You must check existing Volt components to determine if they're functional or class-based. If you can't detect that, ask the user which they prefer before writing a Volt component. + +### Volt Functional Component Example + + +@volt + 0]); + +$increment = fn () => $this->count++; +$decrement = fn () => $this->count--; + +$double = computed(fn () => $this->count * 2); +?> + +
+

Count: {{ $count }}

+

Double: {{ $this->double }}

+ + +
+@endvolt +
+ +### Volt Class Based Component Example +To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: + + +use Livewire\Volt\Component; + +new class extends Component { + public $count = 0; + + public function increment() + { + $this->count++; + } +} ?> + +
+

{{ $count }}

+ +
+
+ +### Testing Volt & Volt Components +- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. + + +use Livewire\Volt\Volt; + +test('counter increments', function () { + Volt::test('counter') + ->assertSee('Count: 0') + ->call('increment') + ->assertSee('Count: 1'); +}); + + + +declare(strict_types=1); + +use App\Models\{User, Product}; +use Livewire\Volt\Volt; + +test('product form creates product', function () { + $user = User::factory()->create(); + + Volt::test('pages.products.create') + ->actingAs($user) + ->set('form.name', 'Test Product') + ->set('form.description', 'Test Description') + ->set('form.price', 99.99) + ->call('create') + ->assertHasNoErrors(); + + expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); +}); + + +### Common Patterns + + + null, 'search' => '']); + +$products = computed(fn() => Product::when($this->search, + fn($q) => $q->where('name', 'like', "%{$this->search}%") +)->get()); + +$edit = fn(Product $product) => $this->editing = $product->id; +$delete = fn(Product $product) => $product->delete(); + +?> + + + + + + + + + + + Save + Saving... + + + === pint/core rules === ## Laravel Pint Code Formatter diff --git a/CLAUDE.md b/CLAUDE.md index 93dab41..a6c42d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/flux (FLUXUI_FREE) - v2 - livewire/flux-pro (FLUXUI_PRO) - v2 - livewire/livewire (LIVEWIRE) - v4 +- livewire/volt (VOLT) - v1 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - pestphp/pest (PEST) - v3 @@ -267,6 +268,132 @@ accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, ca ->assertSeeLivewire(CreatePost::class); +=== volt/core rules === + +## Livewire Volt + +- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. +- Make new Volt components using `vendor/bin/sail artisan make:volt [name] [--test] [--pest]`. +- Volt is a class-based and functional API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file. +- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@volt` directive. +- You must check existing Volt components to determine if they're functional or class-based. If you can't detect that, ask the user which they prefer before writing a Volt component. + +### Volt Functional Component Example + + +@volt + 0]); + +$increment = fn () => $this->count++; +$decrement = fn () => $this->count--; + +$double = computed(fn () => $this->count * 2); +?> + +
+

Count: {{ $count }}

+

Double: {{ $this->double }}

+ + +
+@endvolt +
+ +### Volt Class Based Component Example +To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: + + +use Livewire\Volt\Component; + +new class extends Component { + public $count = 0; + + public function increment() + { + $this->count++; + } +} ?> + +
+

{{ $count }}

+ +
+
+ +### Testing Volt & Volt Components +- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. + + +use Livewire\Volt\Volt; + +test('counter increments', function () { + Volt::test('counter') + ->assertSee('Count: 0') + ->call('increment') + ->assertSee('Count: 1'); +}); + + + +declare(strict_types=1); + +use App\Models\{User, Product}; +use Livewire\Volt\Volt; + +test('product form creates product', function () { + $user = User::factory()->create(); + + Volt::test('pages.products.create') + ->actingAs($user) + ->set('form.name', 'Test Product') + ->set('form.description', 'Test Description') + ->set('form.price', 99.99) + ->call('create') + ->assertHasNoErrors(); + + expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); +}); + + +### Common Patterns + + + null, 'search' => '']); + +$products = computed(fn() => Product::when($this->search, + fn($q) => $q->where('name', 'like', "%{$this->search}%") +)->get()); + +$edit = fn(Product $product) => $this->editing = $product->id; +$delete = fn(Product $product) => $product->delete(); + +?> + + + + + + + + + + + Save + Saving... + + + === pint/core rules === ## Laravel Pint Code Formatter diff --git a/app/Models/EinundzwanzigPleb.php b/app/Models/EinundzwanzigPleb.php index f5a53a2..fefbdcd 100644 --- a/app/Models/EinundzwanzigPleb.php +++ b/app/Models/EinundzwanzigPleb.php @@ -3,14 +3,16 @@ namespace App\Models; use App\Enums\AssociationStatus; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Foundation\Auth\User as Authenticatable; use ParagonIE\CipherSweet\BlindIndex; use ParagonIE\CipherSweet\EncryptedRow; use Spatie\LaravelCipherSweet\Concerns\UsesCipherSweet; use Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted; -class EinundzwanzigPleb extends Model implements CipherSweetEncrypted +class EinundzwanzigPleb extends Authenticatable implements CipherSweetEncrypted { + use HasFactory; use UsesCipherSweet; protected $guarded = []; diff --git a/database/migrations/2024_08_29_190127_create_pulse_tables.php b/backup_migrations/2024_08_29_190127_create_pulse_tables.php similarity index 100% rename from database/migrations/2024_08_29_190127_create_pulse_tables.php rename to backup_migrations/2024_08_29_190127_create_pulse_tables.php diff --git a/composer.json b/composer.json index dce247b..2739624 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "livewire/flux": "^2.10", "livewire/flux-pro": "^2.10", "livewire/livewire": "^4.0", + "livewire/volt": "^1.0", "openspout/openspout": "^4.24", "power-components/livewire-powergrid": "^6.7", "pusher/pusher-php-server": "^7.2.2", diff --git a/composer.lock b/composer.lock index cbadb66..0777bba 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39a9ce519dbfeb237966b7441b59a562", + "content-hash": "7a60c8e828d100018e8703dd85753739", "packages": [ { "name": "akuechler/laravel-geoly", @@ -3240,6 +3240,77 @@ ], "time": "2026-01-14T18:40:41+00:00" }, + { + "name": "livewire/volt", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/livewire/volt.git", + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", + "shasum": "" + }, + "require": { + "laravel/framework": "^10.38.2|^11.0|^12.0", + "livewire/livewire": "^3.6.1|^4.0", + "php": "^8.1" + }, + "require-dev": { + "laravel/folio": "^1.1", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.9.5|^3.0|^4.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Livewire\\Volt\\VoltServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Livewire\\Volt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "An elegantly crafted functional API for Laravel Livewire.", + "homepage": "https://github.com/livewire/volt", + "keywords": [ + "laravel", + "livewire", + "volt" + ], + "support": { + "issues": "https://github.com/livewire/volt/issues", + "source": "https://github.com/livewire/volt" + }, + "time": "2025-11-25T16:19:15+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.2.1", diff --git a/database/factories/EinundzwanzigPlebFactory.php b/database/factories/EinundzwanzigPlebFactory.php new file mode 100644 index 0000000..e7f2114 --- /dev/null +++ b/database/factories/EinundzwanzigPlebFactory.php @@ -0,0 +1,26 @@ + + */ +class EinundzwanzigPlebFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'pubkey' => $this->faker->sha256(), + 'npub' => $this->faker->word(), + 'email' => $this->faker->safeEmail(), + 'association_status' => \App\Enums\AssociationStatus::DEFAULT, + ]; + } +} diff --git a/database/migrations/2024_08_29_120928_create_einundzwanzig_plebs_table.php b/database/migrations/2023_03_10_190259_create_einundzwanzig_plebs_table.php similarity index 100% rename from database/migrations/2024_08_29_120928_create_einundzwanzig_plebs_table.php rename to database/migrations/2023_03_10_190259_create_einundzwanzig_plebs_table.php diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8b5843f..75a170f 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -1,7 +1,8 @@ get('/'); +use Livewire\Livewire; - $response->assertStatus(200); +it('returns a successful response', function () { + Livewire::test('association.profile') + ->assertStatus(200); }); diff --git a/tests/Feature/Livewire/Association/ElectionTest.php b/tests/Feature/Livewire/Association/ElectionTest.php new file mode 100644 index 0000000..cea3345 --- /dev/null +++ b/tests/Feature/Livewire/Association/ElectionTest.php @@ -0,0 +1,145 @@ +create(['year' => 2024]); + $election2 = Election::factory()->create(['year' => 2025]); + + Livewire::test('association.election.index') + ->assertSet('elections', function ($elections) { + return count($elections) >= 2; + }); +}); + +it('denies access to unauthorized users in election index', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $election = Election::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.election.index', ['election' => $election]) + ->assertSet('isAllowed', false); +}); + +it('grants access to authorized users in election index', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create(['pubkey' => $allowedPubkey]); + $election = Election::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.election.index', ['election' => $election]) + ->assertSet('isAllowed', true); +}); + +// Election Admin Tests +it('renders election admin component', function () { + $election = Election::factory()->create(); + + Livewire::test('association.election.admin', ['election' => $election]) + ->assertStatus(200); +}); + +it('denies access to unauthorized users in election admin', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $election = Election::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.election.admin', ['election' => $election]) + ->assertSet('isAllowed', false); +}); + +it('grants access to authorized users in election admin', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create(['pubkey' => $allowedPubkey]); + $election = Election::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.election.admin', ['election' => $election]) + ->assertSet('isAllowed', true); +}); + +it('can save election candidates', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create(['pubkey' => $allowedPubkey]); + $election = Election::factory()->create([ + 'candidates' => json_encode([['type' => 'presidency', 'c' => []]]), + ]); + + NostrAuth::login($pleb->pubkey); + + $newCandidates = json_encode([['type' => 'presidency', 'c' => ['test-pubkey']]]); + + Livewire::test('association.election.admin', ['election' => $election]) + ->set('elections.0.candidates', $newCandidates) + ->call('saveElection', 0); + + expect($election->fresh()->candidates)->toBe($newCandidates); +}); + +// Election Show Tests +it('renders election show component', function () { + $election = Election::factory()->create(); + + Livewire::test('association.election.show', ['election' => $election]) + ->assertStatus(200); +}); + +it('loads election data on mount in show', function () { + $election = Election::factory()->create(); + + Livewire::test('association.election.show', ['election' => $election]) + ->assertSet('election.id', $election->id); +}); + +it('handles search in election show', function () { + $election = Election::factory()->create(); + $pleb1 = EinundzwanzigPleb::factory()->active()->create(); + $pleb2 = EinundzwanzigPleb::factory()->boardMember()->create(); + + Livewire::test('association.election.show', ['election' => $election]) + ->set('search', $pleb1->pubkey) + ->assertSet('plebs', function ($plebs) use ($pleb1) { + return collect($plebs)->contains('pubkey', $pleb1->pubkey); + }); +}); + +it('can create vote event', function () { + $election = Election::factory()->create(); + $pleb = EinundzwanzigPleb::factory()->active()->create(); + $candidatePubkey = 'test-candidate-pubkey'; + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.election.show', ['election' => $election]) + ->call('vote', $candidatePubkey, 'presidency', false) + ->assertSet('signThisEvent', function ($event) use ($candidatePubkey) { + return str_contains($event, $candidatePubkey); + }); +}); + +it('checks election closure status', function () { + $election = Election::factory()->create([ + 'end_time' => now()->subDay(), + ]); + + Livewire::test('association.election.show', ['election' => $election]) + ->call('checkElection') + ->assertSet('isNotClosed', false); +}); + +it('displays log for authorized users', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create(['pubkey' => $allowedPubkey]); + $election = Election::factory()->create(); + + Livewire::test('association.election.show', ['election' => $election]) + ->call('handleNostrLoggedIn', $allowedPubkey) + ->assertSet('showLog', true); +}); diff --git a/tests/Feature/Livewire/Association/Members/AdminTest.php b/tests/Feature/Livewire/Association/Members/AdminTest.php new file mode 100644 index 0000000..5ec4132 --- /dev/null +++ b/tests/Feature/Livewire/Association/Members/AdminTest.php @@ -0,0 +1,73 @@ +create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.members.admin') + ->assertSet('isAllowed', false) + ->assertSee('Du bist nicht berechtigt, Mitglieder zu bearbeiten.'); +}); + +it('grants access to authorized pubkeys', function () { + $allowedPubkeys = [ + '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033', + '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279', + '7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d', + 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba', + '19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3', + 'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec', + ]; + + $pleb = EinundzwanzigPleb::factory()->create([ + 'pubkey' => $allowedPubkeys[0], + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.members.admin') + ->assertSet('isAllowed', true); +}); + +it('handles nostr login for authorized user', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create([ + 'pubkey' => $allowedPubkey, + ]); + + Livewire::test('association.members.admin') + ->call('handleNostrLoggedIn', $allowedPubkey) + ->assertSet('isAllowed', true) + ->assertSet('currentPubkey', $allowedPubkey); +}); + +it('handles nostr logout', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create([ + 'pubkey' => $allowedPubkey, + ]); + + Livewire::test('association.members.admin') + ->call('handleNostrLoggedIn', $allowedPubkey) + ->call('handleNostrLoggedOut') + ->assertSet('isAllowed', false) + ->assertSet('currentPubkey', null); +}); + +it('displays einundzwanzig pleb table when authorized', function () { + $allowedPubkey = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033'; + $pleb = EinundzwanzigPleb::factory()->create([ + 'pubkey' => $allowedPubkey, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.members.admin') + ->assertSet('isAllowed', true) + ->assertSee('einundzwanzig-pleb-table'); +}); diff --git a/tests/Feature/Livewire/Association/NewsTest.php b/tests/Feature/Livewire/Association/NewsTest.php new file mode 100644 index 0000000..e5f9b23 --- /dev/null +++ b/tests/Feature/Livewire/Association/NewsTest.php @@ -0,0 +1,110 @@ +create([ + 'association_status' => AssociationStatus::PASSIVE, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->assertSet('isAllowed', false); +}); + +it('denies access when pleb has not paid for current year', function () { + $pleb = EinundzwanzigPleb::factory()->create([ + 'association_status' => AssociationStatus::ACTIVE, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->assertSet('isAllowed', false); +}); + +it('grants access when pleb is active and has paid', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->assertSet('isAllowed', true); +}); + +it('allows board member to edit news', function () { + $pleb = EinundzwanzigPleb::factory()->boardMember()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->assertSet('canEdit', true); +}); + +it('can create news entry with pdf', function () { + $pleb = EinundzwanzigPleb::factory()->boardMember()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + $file = UploadedFile::fake()->create('document.pdf', 100); + + Livewire::test('association.news') + ->set('file', $file) + ->set('form.category', NewsCategory::ORGANISATION->value) + ->set('form.name', 'Test News') + ->set('form.description', 'Test Description') + ->call('save') + ->assertHasNoErrors(); + + expect(Notification::where('name', 'Test News')->exists())->toBeTrue(); +}); + +it('validates news entry creation', function () { + $pleb = EinundzwanzigPleb::factory()->boardMember()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->call('save') + ->assertHasErrors(['file', 'form.category', 'form.name']); +}); + +it('can delete news entry', function () { + $pleb = EinundzwanzigPleb::factory()->boardMember()->withPaidCurrentYear()->create(); + $news = Notification::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->call('delete', $news->id) + ->assertHasNoErrors(); + + expect(Notification::find($news->id))->toBeNull(); +}); + +it('displays news list', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + $news1 = Notification::factory()->create(); + $news2 = Notification::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.news') + ->assertSet('isAllowed', true) + ->assertSee($news1->name) + ->assertSee($news2->name); +}); diff --git a/tests/Feature/Livewire/Association/ProfileTest.php b/tests/Feature/Livewire/Association/ProfileTest.php new file mode 100644 index 0000000..3eaa90d --- /dev/null +++ b/tests/Feature/Livewire/Association/ProfileTest.php @@ -0,0 +1,116 @@ +create(); + + Livewire::test('association.profile') + ->call('handleNostrLoggedIn', $pleb->pubkey) + ->assertSet('currentPubkey', $pleb->pubkey) + ->assertSet('currentPleb.pubkey', $pleb->pubkey); +}); + +it('handles nostr logout correctly', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + Livewire::test('association.profile') + ->call('handleNostrLoggedIn', $pleb->pubkey) + ->call('handleNostrLoggedOut') + ->assertSet('currentPubkey', null) + ->assertSet('currentPleb', null); +}); + +it('can save email address', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->set('email', 'test@example.com') + ->call('saveEmail') + ->assertHasNoErrors(); + + expect($pleb->fresh()->email)->toBe('test@example.com'); +}); + +it('validates email format', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->set('email', 'invalid-email') + ->call('saveEmail') + ->assertHasErrors(['email']); +}); + +it('can update no email preference', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->set('no', true) + ->assertSet('showEmail', false); + + expect($pleb->fresh()->no_email)->toBeTrue(); +}); + +it('can save membership application', function () { + $pleb = EinundzwanzigPleb::factory()->create([ + 'association_status' => AssociationStatus::DEFAULT, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->set('form.check', true) + ->call('save', AssociationStatus::PASSIVE->value) + ->assertHasNoErrors(); + + expect($pleb->fresh()->association_status)->toBe(AssociationStatus::PASSIVE); +}); + +it('creates payment event when pleb becomes active', function () { + $pleb = EinundzwanzigPleb::factory()->active()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->assertSet('amountToPay', config('app.env') === 'production' ? 21000 : 1); + + expect($pleb->paymentEvents()->count())->toBeGreaterThan(0); +}); + +it('displays paid status for current year', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.profile') + ->call('listenForPayment') + ->assertSet('currentYearIsPaid', true); +}); + +it('can initiate payment', function () { + Http::fake([ + 'pay.einundzwanzig.space/*' => Http::response([ + 'id' => 'invoice123', + 'checkoutLink' => 'https://pay.einundzwanzig.space/checkout/invoice123', + ], 200), + ]); + + $pleb = EinundzwanzigPleb::factory()->active()->create(); + + NostrAuth::login($pleb->pubkey); + + $response = Livewire::test('association.profile') + ->call('pay', 'test-comment'); + + $response->assertRedirect(); +}); diff --git a/tests/Feature/Livewire/Association/ProjectSupportTest.php b/tests/Feature/Livewire/Association/ProjectSupportTest.php new file mode 100644 index 0000000..761a683 --- /dev/null +++ b/tests/Feature/Livewire/Association/ProjectSupportTest.php @@ -0,0 +1,228 @@ +create(); + $project2 = ProjectProposal::factory()->create(); + + Livewire::test('association.project-support.index') + ->assertSet('projects', function ($projects) { + return $projects->count() >= 2; + }); +}); + +it('can search projects', function () { + $project = ProjectProposal::factory()->create(['name' => 'Unique Project Name']); + + Livewire::test('association.project-support.index') + ->set('search', 'Unique') + ->assertSet('projects', function ($projects) use ($project) { + return $projects->contains('id', $project->id); + }); +}); + +it('can filter projects', function () { + Livewire::test('association.project-support.index') + ->call('setFilter', 'new') + ->assertSet('activeFilter', 'new'); +}); + +it('can confirm delete', function () { + $project = ProjectProposal::factory()->create(); + + Livewire::test('association.project-support.index') + ->call('confirmDelete', $project->id) + ->assertSet('confirmDeleteId', $project->id); +}); + +it('can delete project', function () { + $pleb = EinundzwanzigPleb::factory()->boardMember()->create(); + $project = ProjectProposal::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.index') + ->set('confirmDeleteId', $project->id) + ->call('delete'); + + expect(ProjectProposal::find($project->id))->toBeNull(); +}); + +it('handles nostr login', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + Livewire::test('association.project-support.index') + ->call('handleNostrLoggedIn', $pleb->pubkey) + ->assertSet('currentPubkey', $pleb->pubkey) + ->assertSet('isAllowed', true); +}); + +it('handles nostr logout', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + + Livewire::test('association.project-support.index') + ->call('handleNostrLoggedIn', $pleb->pubkey) + ->call('handleNostrLoggedOut') + ->assertSet('currentPubkey', null) + ->assertSet('isAllowed', false); +}); + +it('denies access to create when not authenticated', function () { + Livewire::test('association.project-support.form.create') + ->assertSet('isAllowed', false) + ->assertSee('Du bist nicht berechtigt, eine Projektförderung anzulegen.'); +}); + +it('denies access to create when pleb has not paid', function () { + $pleb = EinundzwanzigPleb::factory()->active()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.create') + ->assertSet('isAllowed', false); +}); + +it('grants access to create when pleb is active and paid', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.create') + ->assertSet('isAllowed', true); +}); + +it('can create project proposal', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.create') + ->set('form.name', 'Test Project') + ->set('form.description', 'Test Description') + ->call('save') + ->assertHasNoErrors(); + + expect(ProjectProposal::where('name', 'Test Project')->exists())->toBeTrue(); +}); + +it('validates project proposal creation', function () { + $pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.create') + ->call('save') + ->assertHasErrors(['form.name', 'form.description']); +}); + +// Project Support Edit Tests +it('renders project support edit component', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + ]); + + Livewire::test('association.project-support.form.edit', ['project' => $project]) + ->assertStatus(200); +}); + +it('denies access to edit when not owner', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.edit', ['project' => $project]) + ->assertSet('isAllowed', false); +}); + +it('grants access to edit when owner', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.edit', ['project' => $project]) + ->assertSet('isAllowed', true); +}); + +it('can update project proposal', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + 'name' => 'Old Name', + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.edit', ['project' => $project]) + ->set('form.name', 'New Name') + ->set('form.description', 'Updated Description') + ->call('update') + ->assertHasNoErrors(); + + expect($project->fresh()->name)->toBe('New Name'); +}); + +it('validates project proposal update', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create([ + 'einundzwanzig_pleb_id' => $pleb->id, + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.form.edit', ['project' => $project]) + ->set('form.name', '') + ->call('update') + ->assertHasErrors(['form.name']); +}); + +// Project Support Show Tests +it('renders project support show component', function () { + $project = ProjectProposal::factory()->create(); + + Livewire::test('association.project-support.show', ['project' => $project]) + ->assertStatus(200); +}); + +it('denies access to show when not authenticated', function () { + $project = ProjectProposal::factory()->create(); + + Livewire::test('association.project-support.show', ['project' => $project]) + ->assertSet('isAllowed', false) + ->assertSee('Du bist nicht berechtigt, die Projektförderung einzusehen.'); +}); + +it('grants access to show when authenticated', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create(); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.show', ['project' => $project]) + ->assertSet('isAllowed', true); +}); + +it('displays project details', function () { + $pleb = EinundzwanzigPleb::factory()->create(); + $project = ProjectProposal::factory()->create([ + 'name' => 'Test Project Name', + 'description' => 'Test Project Description', + ]); + + NostrAuth::login($pleb->pubkey); + + Livewire::test('association.project-support.show', ['project' => $project]) + ->assertSet('project.name', 'Test Project Name') + ->assertSee('Test Project Name') + ->assertSee('Test Project Description'); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 50ab1e4..3d4d875 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,7 +13,7 @@ uses( Tests\TestCase::class, - // Illuminate\Foundation\Testing\RefreshDatabase::class, + Illuminate\Foundation\Testing\RefreshDatabase::class, )->in('Feature'); /*