🗑️ Remove outdated migration files for einundzwanzig_plebs and pulse tables, restructure directory, and update testing suite with new factories and Livewire tests.

This commit is contained in:
HolgerHatGarKeineNode
2026-01-18 22:23:23 +01:00
parent 00707797e5
commit 31fb04caaa
16 changed files with 1161 additions and 7 deletions

View File

@@ -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);
</code-snippet>
=== 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
<code-snippet name="Volt Functional Component Example" lang="php">
@volt
<?php
use function Livewire\Volt\{state, computed};
state(['count' => 0]);
$increment = fn () => $this->count++;
$decrement = fn () => $this->count--;
$double = computed(fn () => $this->count * 2);
?>
<div>
<h1>Count: {{ $count }}</h1>
<h2>Double: {{ $this->double }}</h2>
<button wire:click="increment">+</button>
<button wire:click="decrement">-</button>
</div>
@endvolt
</code-snippet>
### 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:
<code-snippet name="Volt Class-based Volt Component Example" lang="php">
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
</code-snippet>
### Testing Volt & Volt Components
- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`.
<code-snippet name="Livewire Test Example" lang="php">
use Livewire\Volt\Volt;
test('counter increments', function () {
Volt::test('counter')
->assertSee('Count: 0')
->call('increment')
->assertSee('Count: 1');
});
</code-snippet>
<code-snippet name="Volt Component Test Using Pest" lang="php">
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();
});
</code-snippet>
### Common Patterns
<code-snippet name="CRUD With Volt" lang="php">
<?php
use App\Models\Product;
use function Livewire\Volt\{state, computed};
state(['editing' => 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();
?>
<!-- HTML / UI Here -->
</code-snippet>
<code-snippet name="Real-Time Search With Volt" lang="php">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search..."
/>
</code-snippet>
<code-snippet name="Loading States With Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving...</span>
</flux:button>
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter

127
AGENTS.md
View File

@@ -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);
</code-snippet>
=== 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
<code-snippet name="Volt Functional Component Example" lang="php">
@volt
<?php
use function Livewire\Volt\{state, computed};
state(['count' => 0]);
$increment = fn () => $this->count++;
$decrement = fn () => $this->count--;
$double = computed(fn () => $this->count * 2);
?>
<div>
<h1>Count: {{ $count }}</h1>
<h2>Double: {{ $this->double }}</h2>
<button wire:click="increment">+</button>
<button wire:click="decrement">-</button>
</div>
@endvolt
</code-snippet>
### 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:
<code-snippet name="Volt Class-based Volt Component Example" lang="php">
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
</code-snippet>
### Testing Volt & Volt Components
- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`.
<code-snippet name="Livewire Test Example" lang="php">
use Livewire\Volt\Volt;
test('counter increments', function () {
Volt::test('counter')
->assertSee('Count: 0')
->call('increment')
->assertSee('Count: 1');
});
</code-snippet>
<code-snippet name="Volt Component Test Using Pest" lang="php">
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();
});
</code-snippet>
### Common Patterns
<code-snippet name="CRUD With Volt" lang="php">
<?php
use App\Models\Product;
use function Livewire\Volt\{state, computed};
state(['editing' => 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();
?>
<!-- HTML / UI Here -->
</code-snippet>
<code-snippet name="Real-Time Search With Volt" lang="php">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search..."
/>
</code-snippet>
<code-snippet name="Loading States With Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving...</span>
</flux:button>
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter

127
CLAUDE.md
View File

@@ -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);
</code-snippet>
=== 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
<code-snippet name="Volt Functional Component Example" lang="php">
@volt
<?php
use function Livewire\Volt\{state, computed};
state(['count' => 0]);
$increment = fn () => $this->count++;
$decrement = fn () => $this->count--;
$double = computed(fn () => $this->count * 2);
?>
<div>
<h1>Count: {{ $count }}</h1>
<h2>Double: {{ $this->double }}</h2>
<button wire:click="increment">+</button>
<button wire:click="decrement">-</button>
</div>
@endvolt
</code-snippet>
### 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:
<code-snippet name="Volt Class-based Volt Component Example" lang="php">
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
</code-snippet>
### Testing Volt & Volt Components
- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`.
<code-snippet name="Livewire Test Example" lang="php">
use Livewire\Volt\Volt;
test('counter increments', function () {
Volt::test('counter')
->assertSee('Count: 0')
->call('increment')
->assertSee('Count: 1');
});
</code-snippet>
<code-snippet name="Volt Component Test Using Pest" lang="php">
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();
});
</code-snippet>
### Common Patterns
<code-snippet name="CRUD With Volt" lang="php">
<?php
use App\Models\Product;
use function Livewire\Volt\{state, computed};
state(['editing' => 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();
?>
<!-- HTML / UI Here -->
</code-snippet>
<code-snippet name="Real-Time Search With Volt" lang="php">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search..."
/>
</code-snippet>
<code-snippet name="Loading States With Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving...</span>
</flux:button>
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter

View File

@@ -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 = [];

View File

@@ -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",

73
composer.lock generated
View File

@@ -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",

View File

@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EinundzwanzigPleb>
*/
class EinundzwanzigPlebFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'pubkey' => $this->faker->sha256(),
'npub' => $this->faker->word(),
'email' => $this->faker->safeEmail(),
'association_status' => \App\Enums\AssociationStatus::DEFAULT,
];
}
}

View File

@@ -1,7 +1,8 @@
<?php
it('returns a successful response', function () {
$response = $this->get('/');
use Livewire\Livewire;
$response->assertStatus(200);
it('returns a successful response', function () {
Livewire::test('association.profile')
->assertStatus(200);
});

View File

@@ -0,0 +1,145 @@
<?php
use App\Models\EinundzwanzigPleb;
use App\Models\Election;
use App\Support\NostrAuth;
use Livewire\Livewire;
it('loads elections on mount', function () {
$election1 = Election::factory()->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);
});

View File

@@ -0,0 +1,73 @@
<?php
use App\Models\EinundzwanzigPleb;
use App\Support\NostrAuth;
use Livewire\Livewire;
it('denies access to unauthorized users', function () {
$pleb = EinundzwanzigPleb::factory()->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');
});

View File

@@ -0,0 +1,110 @@
<?php
use App\Enums\AssociationStatus;
use App\Enums\NewsCategory;
use App\Models\EinundzwanzigPleb;
use App\Models\Notification;
use App\Support\NostrAuth;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
beforeEach(function () {
Storage::fake('public');
});
it('denies access when pleb has insufficient association status', function () {
$pleb = EinundzwanzigPleb::factory()->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);
});

View File

@@ -0,0 +1,116 @@
<?php
use App\Enums\AssociationStatus;
use App\Models\EinundzwanzigPleb;
use App\Support\NostrAuth;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
it('handles nostr login correctly', function () {
$pleb = EinundzwanzigPleb::factory()->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();
});

View File

@@ -0,0 +1,228 @@
<?php
use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal;
use App\Support\NostrAuth;
use Livewire\Livewire;
it('loads projects on mount', function () {
$project1 = ProjectProposal::factory()->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');
});

View File

@@ -13,7 +13,7 @@
uses(
Tests\TestCase::class,
// Illuminate\Foundation\Testing\RefreshDatabase::class,
Illuminate\Foundation\Testing\RefreshDatabase::class,
)->in('Feature');
/*