Compare commits
29 changed files with 8 additions and 571 deletions
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\About;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class AboutPageController extends Controller
|
|
||||||
{
|
|
||||||
public function show(): View
|
|
||||||
{
|
|
||||||
return view('about', [
|
|
||||||
'about' => About::first()?->content,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\About;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class AboutController extends Controller
|
|
||||||
{
|
|
||||||
public function show(): View
|
|
||||||
{
|
|
||||||
$about = About::first();
|
|
||||||
|
|
||||||
return view('admin.about.show', [
|
|
||||||
'aboutEntry' => $about,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$about = About::firstOrNew();
|
|
||||||
$about->content = $request->input('content');
|
|
||||||
$about->save();
|
|
||||||
|
|
||||||
return redirect()->route('admin.about.show');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,9 +6,7 @@ namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\MicropubToken;
|
use App\Models\MicropubToken;
|
||||||
use App\Services\TokenService;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class TokensController extends Controller
|
class TokensController extends Controller
|
||||||
|
|
@ -23,36 +21,6 @@ class TokensController extends Controller
|
||||||
return view('admin.tokens.index', compact('tokens'));
|
return view('admin.tokens.index', compact('tokens'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form to manually generate a new Micropub token.
|
|
||||||
*
|
|
||||||
* This is for clients (e.g. iA Writer) that don't support the IndieAuth
|
|
||||||
* PKCE flow and instead expect to be given a token directly.
|
|
||||||
*/
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
return view('admin.tokens.create');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually generate a new Micropub token.
|
|
||||||
*/
|
|
||||||
public function store(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'client_id' => 'required|string',
|
|
||||||
'scope' => 'required|array|min:1',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$token = resolve(TokenService::class)->getNewToken([
|
|
||||||
'me' => config('app.url'),
|
|
||||||
'client_id' => $validated['client_id'],
|
|
||||||
'scope' => implode(' ', $validated['scope']),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect('/admin/tokens')->with('new_token', $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke a Micropub token.
|
* Revoke a Micropub token.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,7 @@ class MicropubController extends Controller
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
'error_description' => 'No known note with given ID',
|
'error_description' => 'No known note with given ID',
|
||||||
], 404);
|
], 404);
|
||||||
} catch (MicropubUnsupportedModelException $e) {
|
} catch (MicropubUnsupportedModelException) {
|
||||||
report($e);
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'invalid',
|
'error' => 'invalid',
|
||||||
'error_description' => 'This implementation currently only supports the updating of notes',
|
'error_description' => 'This implementation currently only supports the updating of notes',
|
||||||
|
|
@ -82,16 +80,12 @@ class MicropubController extends Controller
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
'error_description' => $e->getMessage(),
|
'error_description' => $e->getMessage(),
|
||||||
], 400);
|
], 400);
|
||||||
} catch (MicropubHandlerException $e) {
|
} catch (MicropubHandlerException) {
|
||||||
report($e);
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'unsupported_operation',
|
'error' => 'unsupported_operation',
|
||||||
'error_description' => 'The request could not be processed by this server',
|
'error_description' => 'The request could not be processed by this server',
|
||||||
], 500);
|
], 500);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Exception $e) {
|
||||||
report($e);
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'server_error',
|
'error' => 'server_error',
|
||||||
'error_description' => 'An error occurred processing the request',
|
'error_description' => 'An error occurred processing the request',
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class About extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'about';
|
|
||||||
}
|
|
||||||
|
|
@ -40,7 +40,6 @@ class Article extends Model
|
||||||
return [
|
return [
|
||||||
'titleurl' => [
|
'titleurl' => [
|
||||||
'source' => 'title',
|
'source' => 'title',
|
||||||
'includeTrashed' => true,
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -94,13 +93,6 @@ class Article extends Model
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function uri(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::get(
|
|
||||||
get: fn () => config('app.url').$this->link,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to only include articles from a particular year/month.
|
* Scope a query to only include articles from a particular year/month.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,12 @@ use App\Models\Article;
|
||||||
|
|
||||||
class ArticleService
|
class ArticleService
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @throws \InvalidArgumentException if a published article already has this title
|
|
||||||
*/
|
|
||||||
public function create(array $data): Article
|
public function create(array $data): Article
|
||||||
{
|
{
|
||||||
$attributes = [
|
return Article::create([
|
||||||
'title' => $data['name'],
|
'title' => $data['name'],
|
||||||
'main' => $data['content'],
|
'main' => $data['content'],
|
||||||
'published' => ($data['post-status'] ?? null) !== 'draft',
|
'published' => ($data['post-status'] ?? null) !== 'draft',
|
||||||
];
|
]);
|
||||||
|
|
||||||
$existing = Article::where('title', $data['name'])->first();
|
|
||||||
|
|
||||||
if ($existing !== null) {
|
|
||||||
if ($existing->published) {
|
|
||||||
throw new \InvalidArgumentException("An article titled \"{$data['name']}\" has already been published");
|
|
||||||
}
|
|
||||||
|
|
||||||
$existing->update($attributes);
|
|
||||||
|
|
||||||
return $existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Article::create($attributes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class EntryHandler implements MicropubHandlerInterface
|
||||||
$location = match (true) {
|
$location = match (true) {
|
||||||
isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url,
|
isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url,
|
||||||
isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri,
|
isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri,
|
||||||
isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->uri,
|
isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->link,
|
||||||
default => resolve(NoteService::class)->create($dataArray)->uri,
|
default => resolve(NoteService::class)->create($dataArray)->uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Factories;
|
|
||||||
|
|
||||||
use App\Models\About;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends Factory<About>
|
|
||||||
*/
|
|
||||||
class AboutFactory extends Factory
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Define the model's default state.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function definition(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'content' => $this->faker->paragraph,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +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('about', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->text('content');
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('about');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -134,30 +134,4 @@
|
||||||
.token-list button.revoke:hover {
|
.token-list button.revoke:hover {
|
||||||
background: light-dark(oklch(80% 0.2 25deg), oklch(45% 0.18 25deg));
|
background: light-dark(oklch(80% 0.2 25deg), oklch(45% 0.18 25deg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-reveal {
|
|
||||||
margin-block-end: 1em;
|
|
||||||
padding: 1em 1.2em;
|
|
||||||
border: 1px solid var(--clr-border);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: light-dark(
|
|
||||||
oklch(96% 0.08 145deg),
|
|
||||||
oklch(28% 0.08 145deg)
|
|
||||||
);
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
font-family: monospace;
|
|
||||||
padding: 0.5em 0.7em;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--clr-border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope-checkboxes {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
@extends('master')
|
|
||||||
@section('title')About « @stop
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<h2>About</h2>
|
|
||||||
<div class="e-content">
|
|
||||||
{!! $about !!}
|
|
||||||
</div>
|
|
||||||
@stop
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
@extends('master')
|
|
||||||
|
|
||||||
@section('title')Edit About « Admin CP « @stop
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<h1>Edit About</h1>
|
|
||||||
<form action="/admin/about" method="post" accept-charset="utf-8" class="admin-form form">
|
|
||||||
{{ csrf_field() }}
|
|
||||||
{{ method_field('PUT') }}
|
|
||||||
<div>
|
|
||||||
<label for="content">Content:</label>
|
|
||||||
<br>
|
|
||||||
<textarea name="content" id="content" rows="10" cols="50">{{ old('content', $aboutEntry?->content) }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="submit" name="save">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@stop
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
@extends('master')
|
|
||||||
|
|
||||||
@section('title')New Token « Admin CP « @stop
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
<h1>Generate a new token</h1>
|
|
||||||
<p>Use this for clients that can't complete the IndieAuth authorization flow (e.g. they don't support PKCE) and instead let you paste in a token directly.</p>
|
|
||||||
|
|
||||||
<form action="/admin/tokens" method="post" accept-charset="utf-8" class="admin-form form">
|
|
||||||
{{ csrf_field() }}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="client_id">Client</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="client_id"
|
|
||||||
id="client_id"
|
|
||||||
value="{{ old('client_id') }}"
|
|
||||||
placeholder="https://ia.net/writer"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="scope-checkboxes">
|
|
||||||
<span>Scope</span>
|
|
||||||
<label for="scope_create">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="scope[]"
|
|
||||||
id="scope_create"
|
|
||||||
value="create"
|
|
||||||
@checked(in_array('create', old('scope', []), true))
|
|
||||||
>
|
|
||||||
create
|
|
||||||
</label>
|
|
||||||
<label for="scope_update">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="scope[]"
|
|
||||||
id="scope_update"
|
|
||||||
value="update"
|
|
||||||
@checked(in_array('update', old('scope', []), true))
|
|
||||||
>
|
|
||||||
update
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit" name="save">Generate token</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@stop
|
|
||||||
|
|
@ -4,15 +4,6 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<h1>Micropub Tokens</h1>
|
<h1>Micropub Tokens</h1>
|
||||||
<p><a href="/admin/tokens/create">Generate new token</a></p>
|
|
||||||
|
|
||||||
@if(session('new_token'))
|
|
||||||
<div class="token-reveal">
|
|
||||||
<p>Here's your new token. <strong>Copy it now</strong> — it won't be shown again.</p>
|
|
||||||
<input type="text" readonly value="{{ session('new_token') }}" onclick="this.select()">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($tokens->isEmpty())
|
@if($tokens->isEmpty())
|
||||||
<p>No tokens have been issued.</p>
|
<p>No tokens have been issued.</p>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,6 @@
|
||||||
Edit your <a href="/admin/bio">bio</a>.
|
Edit your <a href="/admin/bio">bio</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>About</h2>
|
|
||||||
<p>
|
|
||||||
Edit your <a href="/admin/about">about page</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Passkeys</h2>
|
<h2>Passkeys</h2>
|
||||||
<p>
|
<p>
|
||||||
Manager <a href="/admin/passkeys">your passkeys</a>.
|
Manager <a href="/admin/passkeys">your passkeys</a>.
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
<a href="/likes">Likes</a>
|
<a href="/likes">Likes</a>
|
||||||
<a href="/contacts">Contacts</a>
|
<a href="/contacts">Contacts</a>
|
||||||
<a href="/projects">Projects</a>
|
<a href="/projects">Projects</a>
|
||||||
<a href="/about">About</a>
|
|
||||||
<a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.json-feed', ['title' => 'JSON Feed'])</a>
|
<a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.json-feed', ['title' => 'JSON Feed'])</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="theme-selector" role="region" aria-label="Theme switcher">
|
<div id="theme-selector" role="region" aria-label="Theme switcher">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\AboutPageController;
|
|
||||||
use App\Http\Controllers\Admin\AboutController;
|
|
||||||
use App\Http\Controllers\Admin\ArticlesController as AdminArticlesController;
|
use App\Http\Controllers\Admin\ArticlesController as AdminArticlesController;
|
||||||
use App\Http\Controllers\Admin\BioController;
|
use App\Http\Controllers\Admin\BioController;
|
||||||
use App\Http\Controllers\Admin\ClientsController;
|
use App\Http\Controllers\Admin\ClientsController;
|
||||||
|
|
@ -53,9 +51,6 @@ Route::view('projects', 'projects');
|
||||||
// Static colophon page
|
// Static colophon page
|
||||||
Route::view('colophon', 'colophon');
|
Route::view('colophon', 'colophon');
|
||||||
|
|
||||||
// About page
|
|
||||||
Route::get('about', [AboutPageController::class, 'show']);
|
|
||||||
|
|
||||||
// The login routes to get auth’d for admin
|
// The login routes to get auth’d for admin
|
||||||
Route::get('login', [AuthController::class, 'showLogin'])->name('login');
|
Route::get('login', [AuthController::class, 'showLogin'])->name('login');
|
||||||
Route::post('login', [AuthController::class, 'login']);
|
Route::post('login', [AuthController::class, 'login']);
|
||||||
|
|
@ -157,8 +152,6 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
|
||||||
// Micropub Tokens
|
// Micropub Tokens
|
||||||
Route::prefix('tokens')->group(function () {
|
Route::prefix('tokens')->group(function () {
|
||||||
Route::get('/', [TokensController::class, 'index']);
|
Route::get('/', [TokensController::class, 'index']);
|
||||||
Route::get('/create', [TokensController::class, 'create']);
|
|
||||||
Route::post('/', [TokensController::class, 'store']);
|
|
||||||
Route::put('/{token}/revoke', [TokensController::class, 'revoke']);
|
Route::put('/{token}/revoke', [TokensController::class, 'revoke']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -174,12 +167,6 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
|
||||||
Route::put('/', [SettingsController::class, 'update']);
|
Route::put('/', [SettingsController::class, 'update']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// About
|
|
||||||
Route::prefix('about')->group(function () {
|
|
||||||
Route::get('/', [AboutController::class, 'show'])->name('admin.about.show');
|
|
||||||
Route::put('/', [AboutController::class, 'update']);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Passkeys
|
// Passkeys
|
||||||
Route::prefix('passkeys')->group(function () {
|
Route::prefix('passkeys')->group(function () {
|
||||||
Route::get('/', [PasskeysController::class, 'index']);
|
Route::get('/', [PasskeysController::class, 'index']);
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\About;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AboutPageTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function about_page_shows_content(): void
|
|
||||||
{
|
|
||||||
About::factory()->create([
|
|
||||||
'content' => 'This is the about page content.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->get('/about')
|
|
||||||
->assertSee('This is the about page content.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Feature\Admin;
|
|
||||||
|
|
||||||
use App\Models\About;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AboutTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function admin_about_page_loads(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
|
||||||
->get('/admin/about');
|
|
||||||
$response->assertSeeText('Edit About');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function admin_can_create_about(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post('/admin/about', [
|
|
||||||
'_method' => 'PUT',
|
|
||||||
'content' => 'About content',
|
|
||||||
]);
|
|
||||||
$this->assertDatabaseHas('about', ['content' => 'About content']);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function admin_can_load_existing_about(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$about = About::factory()->create([
|
|
||||||
'content' => 'This is <em>my</em> about page. It uses <strong>HTML</strong>.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
|
||||||
->get('/admin/about');
|
|
||||||
$response->assertSeeText('This is <em>my</em> about page. It uses <strong>HTML</strong>.');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function admin_can_edit_about(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
$about = About::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post('/admin/about', [
|
|
||||||
'_method' => 'PUT',
|
|
||||||
'content' => 'This about page has been edited',
|
|
||||||
]);
|
|
||||||
$this->assertDatabaseHas('about', [
|
|
||||||
'content' => 'This about page has been edited',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -37,73 +37,6 @@ class TokensTest extends TestCase
|
||||||
$response->assertSeeText($token->client_id);
|
$response->assertSeeText($token->client_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function create_requires_authentication(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/admin/tokens/create');
|
|
||||||
$response->assertRedirect();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function create_shows_form(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/admin/tokens/create');
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertSee('name="client_id"', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function store_requires_authentication(): void
|
|
||||||
{
|
|
||||||
$response = $this->post('/admin/tokens', [
|
|
||||||
'client_id' => 'https://ia.net/writer',
|
|
||||||
'scope' => ['create'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect();
|
|
||||||
$this->assertDatabaseCount('micropub_tokens', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function store_creates_a_new_token_and_redirects_with_it_flashed(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/admin/tokens', [
|
|
||||||
'client_id' => 'https://ia.net/writer',
|
|
||||||
'scope' => ['create', 'update'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect('/admin/tokens');
|
|
||||||
$response->assertSessionHas('new_token');
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('micropub_tokens', [
|
|
||||||
'client_id' => 'https://ia.net/writer',
|
|
||||||
'scope' => 'create update',
|
|
||||||
'me' => config('app.url'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$token = $response->getSession()->get('new_token');
|
|
||||||
$this->assertNotNull(MicropubToken::findActive($token));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function store_requires_at_least_one_scope(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->make();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/admin/tokens', [
|
|
||||||
'client_id' => 'https://ia.net/writer',
|
|
||||||
'scope' => [],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertSessionHasErrors('scope');
|
|
||||||
$this->assertDatabaseCount('micropub_tokens', 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function revoke_requires_authentication(): void
|
public function revoke_requires_authentication(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Exceptions\MicropubHandlerException;
|
|
||||||
use App\Jobs\SendWebMentions;
|
use App\Jobs\SendWebMentions;
|
||||||
use App\Jobs\SyndicateNoteToBluesky;
|
use App\Jobs\SyndicateNoteToBluesky;
|
||||||
use App\Jobs\SyndicateNoteToMastodon;
|
use App\Jobs\SyndicateNoteToMastodon;
|
||||||
use App\Models\Article;
|
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
use App\Models\SyndicationTarget;
|
use App\Models\SyndicationTarget;
|
||||||
use Faker\Factory;
|
use Faker\Factory;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Queue;
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
@ -460,11 +457,6 @@ class MicropubControllerTest extends TestCase
|
||||||
#[Test]
|
#[Test]
|
||||||
public function micropub_client_api_request_for_unsupported_post_type_returns_error(): void
|
public function micropub_client_api_request_for_unsupported_post_type_returns_error(): void
|
||||||
{
|
{
|
||||||
$this->mock(ExceptionHandler::class)
|
|
||||||
->shouldReceive('report')
|
|
||||||
->once()
|
|
||||||
->with(\Mockery::type(MicropubHandlerException::class));
|
|
||||||
|
|
||||||
$response = $this->postJson(
|
$response = $this->postJson(
|
||||||
'/api/post',
|
'/api/post',
|
||||||
[
|
[
|
||||||
|
|
@ -879,8 +871,6 @@ class MicropubControllerTest extends TestCase
|
||||||
'main' => $content,
|
'main' => $content,
|
||||||
'published' => true,
|
'published' => true,
|
||||||
]);
|
]);
|
||||||
$response->assertHeader('Location');
|
|
||||||
$this->assertStringStartsWith(config('app.url').'/blog/', $response->headers->get('Location'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|
@ -912,64 +902,4 @@ class MicropubControllerTest extends TestCase
|
||||||
'published' => false,
|
'published' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function micropub_client_api_request_updates_an_existing_draft_article_with_the_same_name(): void
|
|
||||||
{
|
|
||||||
$draft = Article::create([
|
|
||||||
'title' => 'WireGuard',
|
|
||||||
'main' => 'Early draft content',
|
|
||||||
'published' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->postJson(
|
|
||||||
'/api/post',
|
|
||||||
[
|
|
||||||
'type' => ['h-entry'],
|
|
||||||
'properties' => [
|
|
||||||
'name' => ['WireGuard'],
|
|
||||||
'content' => ['Finished content'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
['HTTP_Authorization' => 'Bearer '.$this->getToken()]
|
|
||||||
);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertJson(['response' => 'created'])
|
|
||||||
->assertStatus(201);
|
|
||||||
$this->assertSame(1, Article::where('title', 'WireGuard')->count());
|
|
||||||
$this->assertDatabaseHas('articles', [
|
|
||||||
'id' => $draft->id,
|
|
||||||
'title' => 'WireGuard',
|
|
||||||
'main' => 'Finished content',
|
|
||||||
'published' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function micropub_client_api_request_errors_when_an_article_with_the_same_name_is_already_published(): void
|
|
||||||
{
|
|
||||||
Article::create([
|
|
||||||
'title' => 'WireGuard',
|
|
||||||
'main' => 'Published content',
|
|
||||||
'published' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->postJson(
|
|
||||||
'/api/post',
|
|
||||||
[
|
|
||||||
'type' => ['h-entry'],
|
|
||||||
'properties' => [
|
|
||||||
'name' => ['WireGuard'],
|
|
||||||
'content' => ['Some other content'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
['HTTP_Authorization' => 'Bearer '.$this->getToken()]
|
|
||||||
);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertJson(['error' => 'invalid_request'])
|
|
||||||
->assertStatus(400);
|
|
||||||
$this->assertSame(1, Article::where('title', 'WireGuard')->count());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,28 +63,6 @@ class ArticlesTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function uri_is_the_absolute_form_of_the_link(): void
|
|
||||||
{
|
|
||||||
$article = Article::create([
|
|
||||||
'title' => 'Test',
|
|
||||||
'main' => 'Test',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertEquals(config('app.url').$article->link, $article->uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
|
||||||
public function slug_is_suffixed_when_a_trashed_article_already_used_it(): void
|
|
||||||
{
|
|
||||||
$original = Article::create(['title' => 'My Title', 'main' => 'Content']);
|
|
||||||
$original->delete();
|
|
||||||
|
|
||||||
$newArticle = Article::create(['title' => 'My Title', 'main' => 'Other content']);
|
|
||||||
|
|
||||||
$this->assertEquals('my-title-2', $newArticle->titleurl);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function date_scope_returns_expected_articles(): void
|
public function date_scope_returns_expected_articles(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue