Add encrypted Etsy app credential settings
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 23s
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 23s
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EtsyAppCredential;
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Services\Etsy;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EtsySettingsController extends Controller
|
||||
{
|
||||
public function index(Request $request, Etsy $etsy): View
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
|
||||
return view('settings.index', [
|
||||
'storedCredentials' => EtsyAppCredential::find(1) !== null,
|
||||
'configured' => $etsy->configured(),
|
||||
'redirectUri' => $etsy->redirectUri(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
|
||||
$credentials = EtsyAppCredential::find(1);
|
||||
$requireCredentials = $credentials === null ? 'required' : 'nullable';
|
||||
$validated = $request->validate([
|
||||
'keystring' => [$requireCredentials, 'string', 'max:255'],
|
||||
'shared_secret' => [$requireCredentials, 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$previousKeystring = $credentials?->keystring ?: config('services.etsy.keystring');
|
||||
$credentials ??= new EtsyAppCredential;
|
||||
$credentials->id = 1;
|
||||
$credentials->fill(array_filter($validated, fn (?string $value): bool => filled($value)));
|
||||
$credentials->save();
|
||||
|
||||
$keystringChanged = $previousKeystring !== $credentials->keystring;
|
||||
if ($keystringChanged) {
|
||||
EtsyConnection::query()->delete();
|
||||
}
|
||||
|
||||
return redirect()->route('settings.index')->with('status', $keystringChanged
|
||||
? 'Etsy credentials saved. Reconnect your shop with the new app key.'
|
||||
: 'Etsy credentials saved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\EtsyAppCredentialFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EtsyAppCredential extends Model
|
||||
{
|
||||
/** @use HasFactory<EtsyAppCredentialFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['keystring', 'shared_secret'];
|
||||
|
||||
protected $hidden = ['keystring', 'shared_secret'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'keystring' => 'encrypted',
|
||||
'shared_secret' => 'encrypted',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ class EtsyConnection extends Model
|
||||
{
|
||||
protected $fillable = ['shop_id', 'shop_name', 'access_token', 'refresh_token', 'expires_at'];
|
||||
|
||||
protected $hidden = ['access_token', 'refresh_token'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
+29
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\EtsyAppCredential;
|
||||
use App\Models\EtsyConnection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
@@ -10,9 +11,14 @@ class Etsy
|
||||
{
|
||||
private const API_URL = 'https://api.etsy.com/v3';
|
||||
|
||||
/** @var array{keystring: ?string, shared_secret: ?string}|null */
|
||||
private ?array $credentials = null;
|
||||
|
||||
public function configured(): bool
|
||||
{
|
||||
return filled(config('services.etsy.keystring')) && filled(config('services.etsy.shared_secret'));
|
||||
$credentials = $this->credentials();
|
||||
|
||||
return filled($credentials['keystring']) && filled($credentials['shared_secret']);
|
||||
}
|
||||
|
||||
public function authorizationUrl(string $state, string $verifier): string
|
||||
@@ -24,7 +30,7 @@ class Etsy
|
||||
|
||||
return 'https://www.etsy.com/oauth/connect?'.http_build_query([
|
||||
'response_type' => 'code', 'redirect_uri' => $this->redirectUri(),
|
||||
'scope' => 'transactions_r listings_r', 'client_id' => config('services.etsy.keystring'),
|
||||
'scope' => 'transactions_r listings_r', 'client_id' => $this->credentials()['keystring'],
|
||||
'state' => $state, 'code_challenge' => $challenge, 'code_challenge_method' => 'S256',
|
||||
], '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
@@ -33,7 +39,7 @@ class Etsy
|
||||
{
|
||||
$tokens = Http::asForm()->acceptJson()->connectTimeout(3)->timeout(10)
|
||||
->post(self::API_URL.'/public/oauth/token', [
|
||||
'grant_type' => 'authorization_code', 'client_id' => config('services.etsy.keystring'),
|
||||
'grant_type' => 'authorization_code', 'client_id' => $this->credentials()['keystring'],
|
||||
'redirect_uri' => $this->redirectUri(), 'code' => $code, 'code_verifier' => $verifier,
|
||||
])->throw()->json();
|
||||
$userId = explode('.', $tokens['access_token'] ?? '')[0];
|
||||
@@ -77,7 +83,7 @@ class Etsy
|
||||
}
|
||||
$tokens = Http::asForm()->acceptJson()->connectTimeout(3)->timeout(10)
|
||||
->post(self::API_URL.'/public/oauth/token', [
|
||||
'grant_type' => 'refresh_token', 'client_id' => config('services.etsy.keystring'),
|
||||
'grant_type' => 'refresh_token', 'client_id' => $this->credentials()['keystring'],
|
||||
'refresh_token' => $connection->refresh_token,
|
||||
])->throw()->json();
|
||||
if (empty($tokens['access_token']) || empty($tokens['refresh_token'])) {
|
||||
@@ -93,13 +99,30 @@ class Etsy
|
||||
|
||||
private function get(string $path, string $accessToken, array $query = []): array
|
||||
{
|
||||
return Http::withHeaders(['x-api-key' => config('services.etsy.keystring').':'.config('services.etsy.shared_secret')])
|
||||
$credentials = $this->credentials();
|
||||
|
||||
return Http::withHeaders(['x-api-key' => $credentials['keystring'].':'.$credentials['shared_secret']])
|
||||
->withToken($accessToken)->acceptJson()->connectTimeout(3)->timeout(10)
|
||||
->get(self::API_URL.'/'.$path, $query)->throw()->json() ?? [];
|
||||
}
|
||||
|
||||
private function redirectUri(): string
|
||||
public function redirectUri(): string
|
||||
{
|
||||
return config('services.etsy.redirect_uri') ?: route('etsy.callback');
|
||||
}
|
||||
|
||||
/** @return array{keystring: ?string, shared_secret: ?string} */
|
||||
private function credentials(): array
|
||||
{
|
||||
if ($this->credentials !== null) {
|
||||
return $this->credentials;
|
||||
}
|
||||
|
||||
$stored = EtsyAppCredential::find(1);
|
||||
|
||||
return $this->credentials = [
|
||||
'keystring' => $stored?->keystring ?: config('services.etsy.keystring'),
|
||||
'shared_secret' => $stored?->shared_secret ?: config('services.etsy.shared_secret'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\EtsyAppCredential;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<EtsyAppCredential>
|
||||
*/
|
||||
class EtsyAppCredentialFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'keystring' => fake()->bothify('etsy-key-????????'),
|
||||
'shared_secret' => fake()->sha256(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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('etsy_app_credentials', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->text('keystring');
|
||||
$table->text('shared_secret');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('etsy_app_credentials');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\EtsyAppCredential;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class EtsyAppCredentialSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$keystring = config('services.etsy.keystring');
|
||||
$sharedSecret = config('services.etsy.shared_secret');
|
||||
|
||||
if (filled($keystring) && filled($sharedSecret)) {
|
||||
if (EtsyAppCredential::find(1) === null) {
|
||||
$credentials = new EtsyAppCredential;
|
||||
$credentials->id = 1;
|
||||
$credentials->keystring = $keystring;
|
||||
$credentials->shared_secret = $sharedSecret;
|
||||
$credentials->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Etsy shop')
|
||||
@section('content')
|
||||
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Sales</p><h2 class="mt-2 text-2xl font-bold">{{ $connection?->shop_name ?? 'Connect your Etsy shop' }}</h2><p class="mt-1 text-slate-400">Paid orders awaiting dispatch and active listings.</p></div><form method="post" action="{{ route('etsy.connect') }}">@csrf<button class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">{{ $connection ? 'Reconnect Etsy' : 'Connect Etsy' }}</button></form></div>
|
||||
@if(! $configured)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">Add ETSY_KEYSTRING and ETSY_SHARED_SECRET to the application environment, then register {{ route('etsy.callback') }} as the Etsy redirect URI.</div>@endif
|
||||
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Sales</p><h2 class="mt-2 text-2xl font-bold">{{ $connection?->shop_name ?? 'Connect your Etsy shop' }}</h2><p class="mt-1 text-slate-400">Paid orders awaiting dispatch and active listings.</p></div>@if($configured)<form method="post" action="{{ route('etsy.connect') }}">@csrf<button class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">{{ $connection ? 'Reconnect Etsy' : 'Connect Etsy' }}</button></form>@else<a href="{{ route('settings.index') }}" class="rounded-xl bg-brand px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Set up Etsy</a>@endif</div>
|
||||
@if(! $configured)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">Add your Etsy app key and shared secret in <a class="underline" href="{{ route('settings.index') }}">Settings</a>, then connect your shop.</div>@endif
|
||||
@if($integrationError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $integrationError }}</div>@endif
|
||||
@if($connection)
|
||||
<div class="grid gap-4 sm:grid-cols-2"><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Awaiting dispatch</p><p class="mt-3 text-4xl font-bold">{{ count($receipts) }}</p></div><div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">Active listings shown</p><p class="mt-3 text-4xl font-bold">{{ count($listings) }}</p></div></div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
@endif
|
||||
@endforeach
|
||||
@if(auth()->user()->role === 'admin')
|
||||
<a href="{{ route('settings.index') }}" @class(['shrink-0 rounded-xl px-4 py-3 text-sm font-medium hover:bg-slate-800', 'bg-brand/10 text-brand' => request()->routeIs('settings.*'), 'text-slate-400' => !request()->routeIs('settings.*')])>Settings</a>
|
||||
<a href="{{ route('users.index') }}" @class(['shrink-0 rounded-xl px-4 py-3 text-sm font-medium hover:bg-slate-800', 'bg-brand/10 text-brand' => request()->routeIs('users.*'), 'text-slate-400' => !request()->routeIs('users.*')])>Users</a>
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Settings')
|
||||
@section('content')
|
||||
<div>
|
||||
<p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Integrations</p>
|
||||
<h2 class="mt-2 text-2xl font-bold">Etsy app credentials</h2>
|
||||
<p class="mt-1 text-sm text-slate-400">Save the credentials from your Etsy developer app, then connect your shop with OAuth.</p>
|
||||
</div>
|
||||
|
||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">API access</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">{{ $storedCredentials ? 'Credentials saved in the database.' : ($configured ? 'Using server credentials. Save them here to manage them in the dashboard.' : 'No credentials saved yet.') }}</p>
|
||||
</div>
|
||||
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $configured ? 'bg-emerald-500/10 text-emerald-300' : 'bg-amber-500/10 text-amber-200' }}">{{ $configured ? 'Configured' : 'Setup needed' }}</span>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('settings.etsy.update') }}" class="mt-6 grid gap-5 md:grid-cols-2">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<label class="grid gap-2 text-sm text-slate-300">
|
||||
Etsy app keystring
|
||||
<input name="keystring" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $storedCredentials ? 'Leave blank to keep saved key' : 'Enter your app keystring' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $storedCredentials ? '' : 'required' }}>
|
||||
</label>
|
||||
<label class="grid gap-2 text-sm text-slate-300">
|
||||
Etsy shared secret
|
||||
<input name="shared_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $storedCredentials ? 'Leave blank to keep saved secret' : 'Enter your shared secret' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $storedCredentials ? '' : 'required' }}>
|
||||
</label>
|
||||
<div class="md:col-span-2">
|
||||
<button class="rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save credentials</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mt-4 text-xs text-slate-500">Saved values are encrypted and are never shown again. Changing the app key disconnects the current Etsy shop.</p>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||
<h3 class="text-lg font-semibold">OAuth callback</h3>
|
||||
<p class="mt-1 text-sm text-slate-400">Register this exact HTTPS URL in your Etsy developer app.</p>
|
||||
<code class="mt-4 block overflow-x-auto rounded-xl border border-slate-800 bg-slate-950 px-4 py-3 text-sm text-slate-200">{{ $redirectUri }}</code>
|
||||
@if($configured)<a href="{{ route('etsy.index') }}" class="mt-5 inline-flex rounded-xl border border-slate-700 px-4 py-3 text-sm font-semibold text-slate-200 hover:border-brand hover:text-white">Go to Etsy shop</a>@endif
|
||||
</section>
|
||||
@endsection
|
||||
@@ -5,6 +5,7 @@ use App\Http\Controllers\BambuddyController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\DryerController;
|
||||
use App\Http\Controllers\EtsyController;
|
||||
use App\Http\Controllers\EtsySettingsController;
|
||||
use App\Http\Controllers\FilamentController;
|
||||
use App\Http\Controllers\PrinterController;
|
||||
use App\Http\Controllers\ShippingController;
|
||||
@@ -32,6 +33,8 @@ Route::middleware(['auth', EnsureAccountActive::class])->group(function (): void
|
||||
Route::get('/etsy', [EtsyController::class, 'index'])->name('etsy.index');
|
||||
Route::post('/etsy/connect', [EtsyController::class, 'connect'])->name('etsy.connect');
|
||||
Route::get('/etsy/callback', [EtsyController::class, 'callback'])->name('etsy.callback');
|
||||
Route::get('/settings', [EtsySettingsController::class, 'index'])->name('settings.index');
|
||||
Route::put('/settings/etsy', [EtsySettingsController::class, 'update'])->name('settings.etsy.update');
|
||||
Route::get('/dryer', [DryerController::class, 'index'])->name('dryer');
|
||||
Route::get('/dryer/status', [DryerController::class, 'status'])->name('dryer.status');
|
||||
Route::post('/dryer/control', [DryerController::class, 'control'])->name('dryer.control');
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\EtsyAppCredential;
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EtsySettingsTest extends TestCase
|
||||
{
|
||||
use LazilyRefreshDatabase;
|
||||
|
||||
public function test_only_administrators_can_view_and_change_etsy_credentials(): void
|
||||
{
|
||||
$this->get(route('settings.index'))->assertRedirect(route('login'));
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user)->get(route('settings.index'))->assertForbidden();
|
||||
$this->actingAs($user)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'key', 'shared_secret' => 'secret',
|
||||
])->assertForbidden();
|
||||
$this->assertDatabaseCount('etsy_app_credentials', 0);
|
||||
}
|
||||
|
||||
public function test_administrator_saves_encrypted_credentials_for_oauth_without_exposing_them(): void
|
||||
{
|
||||
config()->set('services.etsy.keystring', 'old-server-key');
|
||||
config()->set('services.etsy.shared_secret', 'old-server-secret');
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'saved-key', 'shared_secret' => 'saved-secret',
|
||||
])->assertRedirect(route('settings.index'))->assertSessionHas('status');
|
||||
|
||||
$credentials = EtsyAppCredential::firstOrFail();
|
||||
$this->assertSame('saved-key', $credentials->keystring);
|
||||
$this->assertSame('saved-secret', $credentials->shared_secret);
|
||||
$this->assertNotSame('saved-key', $credentials->getRawOriginal('keystring'));
|
||||
$this->assertNotSame('saved-secret', $credentials->getRawOriginal('shared_secret'));
|
||||
$this->assertArrayNotHasKey('shared_secret', $credentials->toArray());
|
||||
|
||||
$this->actingAs($admin)->get(route('settings.index'))
|
||||
->assertOk()->assertSee('Credentials saved in the database.')
|
||||
->assertDontSee('saved-key')->assertDontSee('saved-secret');
|
||||
$this->actingAs($admin)->post(route('etsy.connect'))
|
||||
->assertRedirectContains('client_id=saved-key');
|
||||
}
|
||||
|
||||
public function test_oauth_api_requests_use_the_saved_shared_secret(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://api.etsy.com/v3/public/oauth/token' => Http::response(['access_token' => '123.token', 'refresh_token' => '123.refresh', 'expires_in' => 3600]),
|
||||
'https://api.etsy.com/v3/application/users/123/shops' => Http::response(['shop_id' => 42, 'shop_name' => 'HickLab3D']),
|
||||
]);
|
||||
config()->set('services.etsy.keystring', 'old-server-key');
|
||||
config()->set('services.etsy.shared_secret', 'old-server-secret');
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'saved-key', 'shared_secret' => 'saved-secret',
|
||||
])->assertRedirect(route('settings.index'));
|
||||
|
||||
$this->actingAs($admin)->withSession(['etsy_oauth' => [
|
||||
'state' => 'expected', 'verifier' => 'verifier', 'started' => time(),
|
||||
]])->get(route('etsy.callback', ['state' => 'expected', 'code' => 'code']))
|
||||
->assertRedirect(route('etsy.index'));
|
||||
|
||||
Http::assertSent(fn ($request) => $request->url() === 'https://api.etsy.com/v3/application/users/123/shops'
|
||||
&& $request->hasHeader('x-api-key', 'saved-key:saved-secret'));
|
||||
}
|
||||
|
||||
public function test_initial_credentials_require_both_values(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'key',
|
||||
])->assertSessionHasErrors('shared_secret');
|
||||
$this->assertDatabaseCount('etsy_app_credentials', 0);
|
||||
}
|
||||
|
||||
public function test_blank_fields_keep_saved_credentials_and_new_key_disconnects_old_shop(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'first-key', 'shared_secret' => 'first-secret',
|
||||
])->assertRedirect(route('settings.index'));
|
||||
EtsyConnection::create([
|
||||
'shop_id' => 42,
|
||||
'shop_name' => 'HickLab3D',
|
||||
'access_token' => '123.token',
|
||||
'refresh_token' => '123.refresh',
|
||||
'expires_at' => now()->addHour(),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => '', 'shared_secret' => '',
|
||||
])->assertRedirect(route('settings.index'));
|
||||
$this->assertSame('first-key', EtsyAppCredential::firstOrFail()->keystring);
|
||||
$this->assertDatabaseCount('etsy_connections', 1);
|
||||
|
||||
$this->actingAs($admin)->put(route('settings.etsy.update'), [
|
||||
'keystring' => 'second-key', 'shared_secret' => '',
|
||||
])->assertRedirect(route('settings.index'))->assertSessionHas('status');
|
||||
$this->assertSame('second-key', EtsyAppCredential::firstOrFail()->keystring);
|
||||
$this->assertSame('first-secret', EtsyAppCredential::firstOrFail()->shared_secret);
|
||||
$this->assertDatabaseCount('etsy_connections', 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user