🔥 **Remove Highscore and Bindle features**

- 🗑️ Deleted `Highscore` feature (Model, Controller, Factory, Tests, Routes, Migrations) and associated logic.
- 🗑️ Removed `BindleController` and its related test.
- 🧹 Cleaned up unused routes, database seeders, and localization references.
- 🚫 Deprecated inactive book rental guide component and associated views.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-08 01:08:07 +02:00
parent 351dd87fa9
commit 3875e127e4
17 changed files with 3 additions and 731 deletions
@@ -1,36 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\LibraryItem;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Support\Collection;
#[Group(name: 'Community', weight: 7)]
class BindleController extends Controller
{
/**
* Bindles (Bibliotheks-Einträge) auflisten
*
* Liefert die Bibliothekseinträge vom Typ 'bindle' mit id, name, link und image.
*
* @return Collection<int, array{id: int, name: string, link: string, image: string}>
*/
public function __invoke(): Collection
{
return LibraryItem::query()
->where('type', 'bindle')
->with([
'media',
])
->orderByDesc('id')
->get()
->map(fn ($item) => [
'id' => $item->id,
'name' => $item->name,
'link' => strtok($item->value, '?'),
'image' => $item->getFirstMediaUrl('main'),
]);
}
}
@@ -1,164 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreHighscoreRequest;
use App\Models\Highscore;
use Carbon\CarbonImmutable;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\Relay\RelaySet;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;
#[Group(name: 'Highscores', weight: 6)]
class HighscoreController extends Controller
{
/**
* Highscore-Bestenliste abrufen
*
* Öffentliche Bestenliste des Spiels, absteigend nach Satoshis (dann nach Zeitpunkt).
* Die Antwort hat die Form { data: [ { npub, name, satoshis, blocks, datetime } ] }.
*/
public function index(): JsonResponse
{
// npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6
$highscores = Highscore::query()
->orderByDesc('satoshis')
->orderBy('achieved_at')
->get()
->map(fn (Highscore $highscore) => [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
]);
return response()->json([
'data' => $highscores,
]);
}
/**
* Highscore einreichen
*
* Reicht einen Highscore ein (idempotent pro npub und Zeitpunkt).
* Zusätzlich auf 10 Anfragen pro Minute begrenzt.
* Fehlt ein Name, versucht der Server, ihn über das Nostr-Profil zu ergänzen.
* Antwortet mit HTTP 202.
*/
#[Response(status: 429, description: 'Zu viele Anfragen (Limit: 10 pro Minute überschritten).')]
public function store(StoreHighscoreRequest $request): JsonResponse
{
$validated = $request->validated();
$achievedAt = CarbonImmutable::parse($validated['datetime']);
$highscore = Highscore::query()->firstOrNew([
'npub' => $validated['npub'],
'achieved_at' => $achievedAt,
]);
$highscore->satoshis = (int) $validated['satoshis'];
$highscore->blocks = (int) $validated['blocks'];
if (array_key_exists('name', $validated)) {
$highscore->name = $validated['name'];
}
$highscore->save();
if (empty($highscore->name)) {
$fetchedName = $this->fetchNostrName($highscore->npub);
if ($fetchedName) {
$highscore->name = $fetchedName;
$highscore->save();
}
}
Log::info('Highscore submission received', [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
]);
return response()->json([
'message' => 'Highscore received',
'data' => [
'npub' => $highscore->npub,
'name' => $highscore->name,
'satoshis' => $highscore->satoshis,
'blocks' => $highscore->blocks,
'datetime' => $highscore->achieved_at->toIso8601String(),
],
], 202);
}
protected function fetchNostrName(string $npub): ?string
{
$author = trim($npub);
if (! str_starts_with($author, 'npub1')) {
return null;
}
$subscription = new Subscription;
$filter = new Filter;
$filter->setAuthors([$author]);
$filter->setKinds([0]);
$requestMessage = new RequestMessage($subscription->getId(), [$filter]);
$relaySet = new RelaySet;
$relaySet->setRelays([
new Relay('wss://nos.lol'),
]);
$request = new Request($relaySet, $requestMessage);
try {
$response = $request->send();
foreach ($response as $relayUrl => $relayResponses) {
foreach ($relayResponses as $message) {
if (! isset($message->event)) {
continue;
}
try {
$profile = json_decode($message->event->content, true, 512, JSON_THROW_ON_ERROR);
if (isset($profile['name']) && is_string($profile['name']) && $profile['name'] !== '') {
Log::info('Fetched nostr profile name for highscore', [
'npub' => $author,
'relay' => $relayUrl,
]);
return $profile['name'];
}
} catch (\JsonException $e) {
Log::warning('Failed to decode nostr profile for highscore', [
'npub' => $author,
'relay' => $relayUrl,
'error' => $e->getMessage(),
]);
}
}
}
} catch (\Throwable $e) {
Log::warning('Failed to fetch nostr profile for highscore', [
'npub' => $author,
'error' => $e->getMessage(),
]);
}
return null;
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreHighscoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function expectsJson(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'npub' => ['required', 'string', 'starts_with:npub1', 'max:100'],
'name' => ['nullable', 'string', 'max:255'],
'satoshis' => ['required', 'integer', 'min:0'],
'blocks' => ['required', 'integer', 'min:0'],
'datetime' => ['required', 'date'],
];
}
public function messages(): array
{
return [
'npub.required' => 'An npub is required to record the highscore.',
'npub.starts_with' => 'The npub must start with npub1.',
'name.string' => 'The name must be a valid string.',
'satoshis.required' => 'Please provide the earned satoshis amount.',
'satoshis.integer' => 'Satoshis must be a whole number.',
'blocks.required' => 'Please provide the number of blocks.',
'blocks.integer' => 'Blocks must be a whole number.',
'datetime.required' => 'Please provide when the score was achieved.',
'datetime.date' => 'The datetime value must be a valid date.',
];
}
}
@@ -1,23 +0,0 @@
<?php
namespace App\Livewire\BooksForPlebs;
use App\Traits\SeoTrait;
use Livewire\Component;
use RalphJSmit\Laravel\SEO\Support\SEOData;
class BookRentalGuide extends Component
{
use SeoTrait;
public function render()
{
return view('livewire.books-for-plebs.book-rental-guide')->with( [
'SEOData' => new SEOData(
title: __('BooksForPlebs'),
description: __('Lokale Buchausleihe für Bitcoin-Meetups.'),
image: asset('img/book-rental.jpg')
),
]);
}
}
-30
View File
@@ -1,30 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Highscore extends Model
{
use HasFactory;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'satoshis' => 'integer',
'blocks' => 'integer',
'achieved_at' => 'datetime',
];
}
+2 -3
View File
@@ -64,8 +64,7 @@ return [
## Rate Limiting ## Rate Limiting
Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt, das Einreichen von Öffentliche Endpunkte sind auf **60 Anfragen/Minute** begrenzt.
Highscores zusätzlich auf **10 Anfragen/Minute**.
MARKDOWN, MARKDOWN,
], ],
@@ -96,7 +95,7 @@ return [
'view' => 'scramble::scalar', 'view' => 'scramble::scalar',
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference', 'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
'theme' => 'laravel', 'theme' => 'laravel',
'proxyUrl' => 'https://proxy.scalar.com', 'proxyUrl' => '',
'darkMode' => true, 'darkMode' => true,
'showDeveloperTools' => 'never', 'showDeveloperTools' => 'never',
'agent' => ['disabled' => true], 'agent' => ['disabled' => true],
-26
View File
@@ -1,26 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\Highscore;
use Database\Factories\Helpers\NostrHelper;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Highscore>
*/
class HighscoreFactory extends Factory
{
protected $model = Highscore::class;
public function definition(): array
{
return [
'npub' => NostrHelper::randomNpub(),
'name' => fake()->name(),
'satoshis' => fake()->numberBetween(0, 100000),
'blocks' => fake()->numberBetween(0, 1000),
'achieved_at' => fake()->dateTimeBetween('-1 year', 'now'),
];
}
}
@@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('highscores', function (Blueprint $table) {
$table->id();
$table->string('npub', 100);
$table->string('name')->nullable();
$table->unsignedBigInteger('satoshis');
$table->unsignedInteger('blocks');
$table->dateTime('achieved_at');
$table->timestamps();
$table->unique(['npub', 'achieved_at']);
$table->index('satoshis');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('highscores');
}
};
+1 -14
View File
@@ -11,7 +11,6 @@ use App\Models\CourseEvent;
use App\Models\EmailCampaign; use App\Models\EmailCampaign;
use App\Models\EmailTexts; use App\Models\EmailTexts;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Highscore;
use App\Models\Lecturer; use App\Models\Lecturer;
use App\Models\Library; use App\Models\Library;
use App\Models\LibraryItem; use App\Models\LibraryItem;
@@ -28,7 +27,6 @@ use App\Models\TwitterAccount;
use App\Models\User; use App\Models\User;
use App\Models\Venue; use App\Models\Venue;
use App\Models\Vote; use App\Models\Vote;
use Database\Factories\Helpers\NostrHelper;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -167,7 +165,7 @@ class DatabaseSeeder extends Seeder
} }
}); });
$this->command->info('Phase 6: Voting & Highscores'); $this->command->info('Phase 6: Voting');
$proposals->each(function (ProjectProposal $proposal) use ($users) { $proposals->each(function (ProjectProposal $proposal) use ($users) {
foreach ($users->random(min(8, $users->count())) as $voter) { foreach ($users->random(min(8, $users->count())) as $voter) {
Vote::create([ Vote::create([
@@ -180,17 +178,6 @@ class DatabaseSeeder extends Seeder
} }
}); });
foreach (NostrHelper::realNpubs() as $i => $npub) {
for ($d = 0; $d < 5; $d++) {
Highscore::factory()->create([
'npub' => $npub,
'achieved_at' => now()->subDays(($i * 10) + $d),
'satoshis' => fake()->numberBetween(1000, 1_000_000),
'blocks' => fake()->numberBetween(1, 5000),
]);
}
}
$this->command->info('Phase 7: LoginKeys'); $this->command->info('Phase 7: LoginKeys');
LoginKey::factory()->count(5)->recycle($users)->create(); LoginKey::factory()->count(5)->recycle($users)->create();
@@ -1,213 +0,0 @@
<div class="h-screen w-full">
<div class="px-4 md:px-8 lg:px-24 py-5">
<div class="flex flex-col md:flex-row justify-between items-center mb-8">
<h1 class="text-4xl md:text-5xl text-orange-500 mb-4 md:mb-0">
Anleitung zum Bücherverleih
</h1>
<img src="{{ asset('/img/apple_touch_icon.png') }}" alt="Buch Etiketten"
class="object-cover h-32 rounded-md shadow-md">
</div>
<h2 class="text-2xl md:text-3xl mb-4 text-white">
Hallo Pleb,
</h2>
<p class="text-lg mb-8 text-white">
Vielen Dank, dass du dich dazu entschieden hast, deine
<span class="text-orange-500">
₿itcoin-Bücher
</span>
zur Verfügung zu stellen. Mit dieser Anleitung kannst du eine Bezahladresse
generieren und hast auch alle Materialien, die du benötigst. Wir haben
darauf geachtet, dass es für jedes Meetup geeignet ist. Deshalb stellen
wir dir die Quelldateien zur Verfügung, damit du deinen eigenen QR-Code
einfügen und das Logo eures Meetups verwenden kannst.
<br>
<p class="text-lg text-white mt-8">
Du hast keine ₿itcoin Wallet oder kein Programm zum Bearbeiten der Dateien?
Kein Problem! Schreib uns einfach und wir helfen dir.
</p>
<br>
<a href="https://t.me/Awesomo12" target="_blank" class="text-orange-500 underline telegram-blue">
<i class="fab fa-telegram mr-2">
</i>
@Awesomo12
</a>
<br>
<a href="https://t.me/LottiTheFuchs" target="_blank" class="text-orange-500 underline telegram-blue">
<i class="fab fa-telegram mr-2">
</i>
@LottiTheFuchs
</a>
<br>
<a href="https://t.me/Robin_Hodl21" target="_blank" class="text-orange-500 underline telegram-blue">
<i class="fab fa-telegram mr-2">
</i>
@Robin_Hodl21
</a>
</p>
<br>
<!-- Bücheretiketten -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
<div class="flex flex-col items-center">
<h2 class="text-2xl mb-2 text-orange-500">
Bücheretiketten
</h2>
<p class="text-lg text-gray-300 mb-2">
(Zum Editieren brauchst du
<a href="https://www.adobe.com/de/products/illustrator.html" target="_blank"
class="text-orange-500 underline link-gray">
Adobe Illustrator)
</a>
</p>
<img src="{{ asset('/img/etikett_bucherVerleih-min.jpg') }}" alt="Buch Etiketten"
class="mb-4 object-cover h-64 rounded-md shadow-md">
<div class="flex justify-center space-x-2">
<a download="true" href="{{ route('buecherverleih.download', ['filename' => 'buecherverleih.zip']) }}" class="btn bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700">
<p class="text-white">
Download .zip
</p>
</a>
</div>
</div>
<!-- Flyer -->
<div class="flex flex-col items-center">
<h2 class="text-2xl mb-2 text-orange-500">
Flyer
</h2>
<p class="text-lg text-gray-300 mb-2">
(Zum Editieren brauchst du
<a href="https://www.adobe.com/de/products/illustrator.html" target="_blank"
class="text-orange-500 underline link-gray">
Adobe Illustrator)
</a>
</p>
<img src="{{ asset('/img/flyerBuecherverleih-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
<div class="flex space-x-2">
<a download="true" href="{{ route('buecherverleih.download', ['filename' => 'buecherverleih.zip']) }}" class="btn bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700">
<p class="text-white">
Download .zip
</p>
</a>
</div>
</div>
<!-- Lesezeichen -->
<div class="flex flex-col items-center">
<h2 class="text-2xl mb-2 text-orange-500">
Lesezeichen
</h2>
<p class="text-lg text-gray-300 mb-2">
(Nicht editierbar)
</p>
<img src="{{ asset('/img/Lesezeichen-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
<div class="flex space-x-2">
<flux:button
variant="primary"
download="Lesezeichen"
:href="asset('/img/Lesezeichen-min.jpg')">
Download .jpg
</flux:button>
</div>
</div>
<!-- Bookring4Sats -->
<div class="flex flex-col items-center">
<h2 class="text-2xl mb-2 text-orange-500">
Bookring4Sats
</h2>
<p class="text-lg text-gray-300 mb-2">
(Nicht editierbar)
</p>
<img src="{{ asset('/img/B4S-min.jpg') }}" alt="Flyer" class="mb-4 object-cover h-64 rounded-md shadow-md">
<div class="flex space-x-2">
<flux:button
variant="primary"
download="B4S"
:href="asset('/img/B4S-min.jpg')">
Download .jpg
</flux:button>
</div>
</div>
</div>
<p class="text-lg mb-8 text-white">
Um deinen
<span class="text-orange-500">
₿itcoin
</span>
QR-Code zu erstellen, kopiere einfach die Empfangsadresse aus der Wallet
deiner Wahl und füge sie hier ein:
<br>
<a href="https://www.qr-code-generator.com/" target="_blank" class="text-orange-500 underline">
www.qr-code-generator.com
</a>
</p>
<p class="text-lg text-white font-bold mb-8">
Der QR-Code-Generator akzeptiert sowohl Lightning als auch Onchain-Empfangsadressen.
Wir empfehlen dir jedoch Lightning zu verwenden, da es schneller und zumeist deutlich günstiger als Onchain ist.
<br>
<span class="flex items-center text-red-500">
<i class="fas fa-exclamation-triangle mr-2">
</i>
Vorsicht: Bitte KEINE Lightning-Adressen,verwenden, da diese nach einem bestimmten Zeitraum ablaufen!
Stattdessen nutzt Ihr bitte eine LNURL, da diese Statisch sind.
</span>
<span>
Tipp: Der Lightning TipBot "LN.tips" erzeugt euch eine. Dazu müsst Ihr diesen nur aktivieren und mit dem Befehl
"/advanced" eure LNURL anzeigen lassen und diese copy-pasten.
Alternativ kann dies auch die WalletOfSatoshi, dort sehen Sie aus wie E-Mail Adessen, wie zum Beispiel:
"BitcoinKalle@walletofsatoshi.com"
</span>
</p>
<p class="text-lg text-white">
Für die sichere Lagerung deiner Bücher empfehlen wir einen Meetup-Ort,
an dem du regelmäßig bist und die Bücher auch sicher verstaut werden können.
Falls dem nicht der Fall ist, solltet Ihr die Bücher lieber jedes mal separat mit zum Meetup
nehmen und die nicht verliehenen Bücher auch wieder mit zurück.
</p>
<p class="text-lg text-white mt-8">
Du willst deine Bücher nicht nur deinem lokalen Meetup zur Verfügung stellen,
sondern online an die gesamte Community verschicken, dann komm in die Gruppe:
</p>
<p class="text-lg text-white">
<a href="https://t.me/BOOKRING4SATS" target="_blank" class="text-orange-500 underline telegram-blue">
<br>
<i class="fab fa-telegram mr-2">
</i>
@BOOKRING4SATS
</a>
</p>
<p class="text-lg mt-8 text-white font-bold">
<i class="fas fa-book mr-2 text-orange-500">
</i>
Vielen Dank, dass du deine Bücher zur Verfügung stellst und uns dabei
hilfst, das Wissen über ₿itcoin zu verbreiten!
</p>
<div class="flex items-center justify-center mt-4">
<img src="/img/btc-logo-6219386_1280.png" class="h-16" alt="">
<span class="text-orange-500">
Happy Stacking
</span>
</div>
</div>
</div>
-7
View File
@@ -1,12 +1,10 @@
<?php <?php
use App\Http\Controllers\Api\BindleController;
use App\Http\Controllers\Api\BtcMapCommunityController; use App\Http\Controllers\Api\BtcMapCommunityController;
use App\Http\Controllers\Api\CityController; use App\Http\Controllers\Api\CityController;
use App\Http\Controllers\Api\CountryController; use App\Http\Controllers\Api\CountryController;
use App\Http\Controllers\Api\CourseController; use App\Http\Controllers\Api\CourseController;
use App\Http\Controllers\Api\CourseEventController; use App\Http\Controllers\Api\CourseEventController;
use App\Http\Controllers\Api\HighscoreController;
use App\Http\Controllers\Api\LecturerController; use App\Http\Controllers\Api\LecturerController;
use App\Http\Controllers\Api\MeetupController; use App\Http\Controllers\Api\MeetupController;
use App\Http\Controllers\Api\MeetupEventController; use App\Http\Controllers\Api\MeetupEventController;
@@ -27,12 +25,7 @@ Route::middleware(['throttle:60,1'])
->only(['index', 'show']); ->only(['index', 'show']);
Route::resource('cities', CityController::class); Route::resource('cities', CityController::class);
Route::resource('venues', VenueController::class); Route::resource('venues', VenueController::class);
Route::get('highscores', [HighscoreController::class, 'index'])->name('highscores.index');
Route::post('highscores', [HighscoreController::class, 'store'])
->middleware('throttle:10,1')
->name('highscores.store');
Route::get('nostrplebs', NostrPlebController::class); Route::get('nostrplebs', NostrPlebController::class);
Route::get('bindles', BindleController::class);
Route::get('meetups', MeetupMapController::class); Route::get('meetups', MeetupMapController::class);
Route::get('meetup-events/{date?}', MeetupEventController::class); Route::get('meetup-events/{date?}', MeetupEventController::class);
Route::get('btc-map-communities', BtcMapCommunityController::class); Route::get('btc-map-communities', BtcMapCommunityController::class);
-21
View File
@@ -16,27 +16,6 @@ Route::get('error/{code}', function (string $code) {
abort($code >= 400 && $code <= 599 ? $code : 404); abort($code >= 400 && $code <= 599 ? $code : 404);
})->where('code', '[0-9]{3}'); })->where('code', '[0-9]{3}');
/*
* Commented out routes related to book rental download and display
* These are currently inactive but can be enabled if needed
*/
/*Route::get('/download-buecherverleih', function (Request $request) {
$filename = $request->input('filename');
// Get the file path from the storage folder
$filePath = storage_path('app/'.$filename);
dd($filePath);
// Check if the file exists
if (!file_exists($filePath)) {
abort(404);
}
// Generate a response with the file for download
return response()->download($filePath, $filename);
})->name('buecherverleih.download');
Route::middleware([])
->get('/buecherverleih', \App\Livewire\BooksForPlebs\BookRentalGuide::class)
->name('buecherverleih');*/
// Route for rabbit following helper page - Updated for Livewire v4 // Route for rabbit following helper page - Updated for Livewire v4
Route::livewire('/kaninchenbau', FollowTheRabbit::class) Route::livewire('/kaninchenbau', FollowTheRabbit::class)
->name('kaninchenbau'); ->name('kaninchenbau');
-71
View File
@@ -1,71 +0,0 @@
<?php
use App\Models\Highscore;
it('returns all highscores ordered by satoshis desc on GET /api/highscores', function () {
Highscore::factory()->create(['satoshis' => 100, 'achieved_at' => now()->subHours(1)]);
Highscore::factory()->create(['satoshis' => 5000, 'achieved_at' => now()->subHours(2)]);
Highscore::factory()->create(['satoshis' => 1000, 'achieved_at' => now()->subHours(3)]);
$response = $this->getJson('/api/highscores');
$response->assertSuccessful();
$data = $response->json('data');
expect(collect($data)->pluck('satoshis')->all())->toBe([5000, 1000, 100]);
});
it('accepts a valid highscore submission', function () {
$payload = [
'npub' => 'npub1'.str_repeat('a', 58),
'name' => 'Tester',
'satoshis' => 1234,
'blocks' => 5,
'datetime' => now()->subDay()->toIso8601String(),
];
$this->postJson('/api/highscores', $payload)
->assertStatus(202)
->assertJsonPath('data.satoshis', 1234)
->assertJsonPath('data.name', 'Tester');
expect(Highscore::query()->where('npub', $payload['npub'])->exists())->toBeTrue();
});
it('rejects a highscore submission missing npub', function () {
$this->postJson('/api/highscores', [
'satoshis' => 1234,
'blocks' => 5,
'datetime' => now()->toIso8601String(),
])->assertUnprocessable()
->assertJsonValidationErrors(['npub']);
});
it('rejects a highscore submission with an npub that does not start with npub1', function () {
$this->postJson('/api/highscores', [
'npub' => 'nsec1'.str_repeat('a', 58),
'satoshis' => 1234,
'blocks' => 5,
'datetime' => now()->toIso8601String(),
])->assertUnprocessable()
->assertJsonValidationErrors(['npub']);
});
it('rejects a highscore submission with negative satoshis', function () {
$this->postJson('/api/highscores', [
'npub' => 'npub1'.str_repeat('b', 58),
'satoshis' => -10,
'blocks' => 5,
'datetime' => now()->toIso8601String(),
])->assertUnprocessable()
->assertJsonValidationErrors(['satoshis']);
});
it('rejects a highscore submission with an invalid datetime', function () {
$this->postJson('/api/highscores', [
'npub' => 'npub1'.str_repeat('c', 58),
'satoshis' => 100,
'blocks' => 5,
'datetime' => 'not-a-date',
])->assertUnprocessable()
->assertJsonValidationErrors(['datetime']);
});
-14
View File
@@ -1,6 +1,5 @@
<?php <?php
use App\Models\LibraryItem;
use App\Models\User; use App\Models\User;
it('returns nostr-pubkeys in /api/nostrplebs', function () { it('returns nostr-pubkeys in /api/nostrplebs', function () {
@@ -15,16 +14,3 @@ it('returns nostr-pubkeys in /api/nostrplebs', function () {
->toHaveCount(2) ->toHaveCount(2)
->each->toStartWith('npub1'); ->each->toStartWith('npub1');
}); });
it('returns bindle-type library items in /api/bindles', function () {
LibraryItem::factory()->create(['type' => 'bindle', 'name' => 'My Bindle']);
LibraryItem::factory()->create(['type' => 'article', 'name' => 'My Article']);
$response = $this->getJson('/api/bindles');
$response->assertSuccessful();
$names = collect($response->json())->pluck('name');
expect($names->all())
->toContain('My Bindle')
->not->toContain('My Article');
});
@@ -1,14 +0,0 @@
<?php
use App\Livewire\BooksForPlebs\BookRentalGuide;
use Illuminate\View\ViewException;
use Livewire\Livewire;
it('mounts the BookRentalGuide component but its view references a route that is currently commented out in routes/web.php', function () {
expect(fn () => Livewire::test(BookRentalGuide::class)->assertStatus(200))
->toThrow(ViewException::class, 'Route [buecherverleih.download] not defined.');
})->skip('Component is unreachable: /buecherverleih route is commented out in routes/web.php — view references the missing buecherverleih.download route.');
it('confirms the BookRentalGuide component class still exists', function () {
expect(class_exists(BookRentalGuide::class))->toBeTrue();
});
-2
View File
@@ -9,7 +9,6 @@ use App\Models\CourseEvent;
use App\Models\EmailCampaign; use App\Models\EmailCampaign;
use App\Models\EmailTexts; use App\Models\EmailTexts;
use App\Models\Episode; use App\Models\Episode;
use App\Models\Highscore;
use App\Models\Lecturer; use App\Models\Lecturer;
use App\Models\Library; use App\Models\Library;
use App\Models\LibraryItem; use App\Models\LibraryItem;
@@ -62,7 +61,6 @@ it('creates a valid persisted record via the factory', function (string $modelCl
'Participant' => Participant::class, 'Participant' => Participant::class,
'EmailCampaign' => EmailCampaign::class, 'EmailCampaign' => EmailCampaign::class,
'EmailTexts' => EmailTexts::class, 'EmailTexts' => EmailTexts::class,
'Highscore' => Highscore::class,
'LoginKey' => LoginKey::class, 'LoginKey' => LoginKey::class,
'Tag' => Tag::class, 'Tag' => Tag::class,
]); ]);
-6
View File
@@ -4,9 +4,7 @@ use App\Models\City;
use App\Models\Country; use App\Models\Country;
use App\Models\Course; use App\Models\Course;
use App\Models\CourseEvent; use App\Models\CourseEvent;
use App\Models\Highscore;
use App\Models\Lecturer; use App\Models\Lecturer;
use App\Models\LibraryItem;
use App\Models\Meetup; use App\Models\Meetup;
use App\Models\MeetupEvent; use App\Models\MeetupEvent;
use App\Models\User; use App\Models\User;
@@ -21,8 +19,6 @@ beforeEach(function () {
Course::factory()->create(); Course::factory()->create();
CourseEvent::factory()->create(); CourseEvent::factory()->create();
Lecturer::factory()->create(); Lecturer::factory()->create();
Highscore::factory()->create();
LibraryItem::factory()->create(['type' => 'bindle']);
User::factory()->create(['nostr' => 'npub1'.str_repeat('a', 58)]); User::factory()->create(['nostr' => 'npub1'.str_repeat('a', 58)]);
}); });
@@ -34,12 +30,10 @@ it('returns a JSON response for the API GET endpoint', function (string $path) {
'meetup events' => '/api/meetup-events', 'meetup events' => '/api/meetup-events',
'btc-map communities' => '/api/btc-map-communities', 'btc-map communities' => '/api/btc-map-communities',
'nostrplebs' => '/api/nostrplebs', 'nostrplebs' => '/api/nostrplebs',
'bindles' => '/api/bindles',
'lecturers' => '/api/lecturers', 'lecturers' => '/api/lecturers',
'courses' => '/api/courses', 'courses' => '/api/courses',
'cities' => '/api/cities', 'cities' => '/api/cities',
'venues' => '/api/venues', 'venues' => '/api/venues',
'highscores' => '/api/highscores',
]); ]);
it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () { it('returns 404 for /api/meetup/ical (currently a stub that aborts)', function () {