feat(integration): administrar instancias y asociaciones con permisos
Agrega endpoints para crear, editar y asociar instancias a clientes y tipos de sitio. Valida las credenciales contra el esquema del catalogo y devuelve metadatos sin secretos. Restringe la administracion al rol admin autenticado y mantiene inmutables los codigos de integracion. Incluye pruebas de API, permisos, prioridad de resolucion, edicion compartida y renovacion de tokens.
This commit is contained in:
@@ -4,8 +4,12 @@ namespace App\Domains\Integration\Controllers;
|
||||
|
||||
use App\Domains\Client\Models\Client;
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Models\IntegrationInstance;
|
||||
use App\Domains\Integration\Requests\AssociateIntegrationInstanceRequest;
|
||||
use App\Domains\Integration\Requests\StoreClientIntegrationRequest;
|
||||
use App\Domains\Integration\Resources\IntegrationAssociationResource;
|
||||
use App\Domains\Integration\Services\ClientIntegrationService;
|
||||
use App\Domains\Integration\Services\IntegrationAssociationService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
@@ -61,4 +65,18 @@ class ClientIntegrationController extends Controller
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function associate(AssociateIntegrationInstanceRequest $request, Client $client, string $integrationCode, IntegrationAssociationService $service)
|
||||
{
|
||||
return new IntegrationAssociationResource($service->associate(
|
||||
$client, $integrationCode, IntegrationInstance::findOrFail($request->validated('integration_instance_id')),
|
||||
));
|
||||
}
|
||||
|
||||
public function destroy(Client $client, string $integrationCode, IntegrationAssociationService $service)
|
||||
{
|
||||
$service->detach($client, $integrationCode);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class IntegrationController extends Controller
|
||||
|
||||
public function destroy(Integration $integration)
|
||||
{
|
||||
abort_if($integration->instances()->exists(), 409, 'Delete the integration instances first.');
|
||||
$integration->delete();
|
||||
|
||||
return response()->noContent();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Controllers;
|
||||
|
||||
use App\Domains\Integration\Models\IntegrationInstance;
|
||||
use App\Domains\Integration\Requests\StoreIntegrationInstanceRequest;
|
||||
use App\Domains\Integration\Resources\IntegrationInstanceResource;
|
||||
use App\Domains\Integration\Services\IntegrationInstanceService;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class IntegrationInstanceController extends Controller
|
||||
{
|
||||
public function __construct(private readonly IntegrationInstanceService $service) {}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return IntegrationInstanceResource::collection(IntegrationInstance::query()->orderBy('id')->paginate());
|
||||
}
|
||||
|
||||
public function store(StoreIntegrationInstanceRequest $request)
|
||||
{
|
||||
return (new IntegrationInstanceResource($this->service->create($request->validated())))
|
||||
->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(IntegrationInstance $integrationInstance)
|
||||
{
|
||||
return new IntegrationInstanceResource($integrationInstance);
|
||||
}
|
||||
|
||||
public function update(StoreIntegrationInstanceRequest $request, IntegrationInstance $integrationInstance)
|
||||
{
|
||||
return new IntegrationInstanceResource($this->service->update($integrationInstance, $request->validated()));
|
||||
}
|
||||
|
||||
public function destroy(IntegrationInstance $integrationInstance)
|
||||
{
|
||||
$this->service->delete($integrationInstance);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Controllers;
|
||||
|
||||
use App\Domains\Integration\Models\IntegrationInstance;
|
||||
use App\Domains\Integration\Requests\AssociateIntegrationInstanceRequest;
|
||||
use App\Domains\Integration\Resources\IntegrationAssociationResource;
|
||||
use App\Domains\Integration\Services\IntegrationAssociationService;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class WebsiteTypeIntegrationController extends Controller
|
||||
{
|
||||
public function __construct(private readonly IntegrationAssociationService $service) {}
|
||||
|
||||
public function index(WebsiteType $websiteType)
|
||||
{
|
||||
return IntegrationAssociationResource::collection($websiteType->integrations()->with('integrationInstance')->get());
|
||||
}
|
||||
|
||||
public function show(WebsiteType $websiteType, string $integrationCode)
|
||||
{
|
||||
return new IntegrationAssociationResource($websiteType->integrations()->with('integrationInstance')->where('integration_code', $integrationCode)->firstOrFail());
|
||||
}
|
||||
|
||||
public function store(AssociateIntegrationInstanceRequest $request, WebsiteType $websiteType, string $integrationCode)
|
||||
{
|
||||
return new IntegrationAssociationResource($this->service->associate(
|
||||
$websiteType, $integrationCode, IntegrationInstance::findOrFail($request->validated('integration_instance_id')),
|
||||
));
|
||||
}
|
||||
|
||||
public function destroy(WebsiteType $websiteType, string $integrationCode)
|
||||
{
|
||||
$this->service->detach($websiteType, $integrationCode);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
14
app/Domains/Integration/Policies/IntegrationPolicy.php
Normal file
14
app/Domains/Integration/Policies/IntegrationPolicy.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Policies;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Authorization\Enums\RoleCode;
|
||||
|
||||
class IntegrationPolicy
|
||||
{
|
||||
public function manage(User $user): bool
|
||||
{
|
||||
return $user->rol_codigo === RoleCode::Admin->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AssociateIntegrationInstanceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage', Integration::class) ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'integration_instance_id' => [
|
||||
'required', 'integer',
|
||||
Rule::exists('integration_instances', 'id')->where('integration_code', $this->route('integration_code')),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class StoreClientIntegrationRequest extends FormRequest
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
return $this->user()?->can('manage', Integration::class) ?? false;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
@@ -31,7 +31,7 @@ class StoreClientIntegrationRequest extends FormRequest
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [];
|
||||
$rules = ['integration_data' => ['present', 'array']];
|
||||
|
||||
foreach ($this->integrationModel?->integration_data_schema ?? [] as $field => $rule) {
|
||||
$rules['integration_data.'.$field] = $rule;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreIntegrationInstanceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage', Integration::class) ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$instance = $this->route('integration_instance');
|
||||
$code = $instance?->integration_code ?? $this->input('integration_code');
|
||||
$integration = is_string($code) ? Integration::where('integration_code', $code)->first() : null;
|
||||
$rules = [
|
||||
'integration_code' => $instance ? ['prohibited'] : ['required', 'string', 'exists:integrations,integration_code'],
|
||||
'name' => [$instance ? 'sometimes' : 'required', 'required', 'string', 'max:255'],
|
||||
// PUT/PATCH replaces the complete credential object when supplied.
|
||||
'integration_data' => [$instance ? 'sometimes' : 'present', 'array'],
|
||||
];
|
||||
|
||||
if (! $instance || $this->exists('integration_data')) {
|
||||
foreach ($integration?->integration_data_schema ?? [] as $field => $rule) {
|
||||
$rules['integration_data.'.$field] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateIntegrationRequest extends FormRequest
|
||||
{
|
||||
@@ -20,8 +21,7 @@ class UpdateIntegrationRequest extends FormRequest
|
||||
'url' => ['nullable', 'url', 'max:255'],
|
||||
'integration_data_schema' => ['nullable', 'array'],
|
||||
'requires_configuration' => ['sometimes', 'boolean'],
|
||||
// the code shouldn't ideally be updatable, but if it is:
|
||||
'integration_code' => ['sometimes', 'required', 'string', 'unique:integrations,integration_code,'.($integration->id ?? '')],
|
||||
'integration_code' => ['sometimes', 'required', 'string', Rule::in([$integration->integration_code])],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class IntegrationAssociationResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'client_id' => $this->when(isset($this->client_id), $this->client_id),
|
||||
'website_type_code' => $this->when(isset($this->website_type_code), $this->website_type_code),
|
||||
'integration_code' => $this->integration_code,
|
||||
'integration_instance_id' => $this->integration_instance_id,
|
||||
'integration_instance' => new IntegrationInstanceResource($this->whenLoaded('integrationInstance')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class IntegrationInstanceResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'integration_code' => $this->integration_code,
|
||||
'name' => $this->name,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,37 @@
|
||||
|
||||
use App\Domains\Integration\Controllers\ClientIntegrationController;
|
||||
use App\Domains\Integration\Controllers\IntegrationController;
|
||||
use App\Domains\Integration\Controllers\IntegrationInstanceController;
|
||||
use App\Domains\Integration\Controllers\TelepagosWebhookController;
|
||||
use App\Domains\Integration\Controllers\WebsiteTypeIntegrationController;
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::group(['prefix' => 'integrations'], function () {
|
||||
Route::get('/', [IntegrationController::class, 'index']);
|
||||
Route::post('/', [IntegrationController::class, 'store']);
|
||||
Route::get('/{integration}', [IntegrationController::class, 'show']);
|
||||
Route::put('/{integration}', [IntegrationController::class, 'update']);
|
||||
Route::delete('/{integration}', [IntegrationController::class, 'destroy']);
|
||||
});
|
||||
Route::middleware(['auth:sanctum', 'can:manage,'.Integration::class])->group(function (): void {
|
||||
Route::apiResource('integration-instances', IntegrationInstanceController::class);
|
||||
|
||||
Route::group(['prefix' => 'clients/{client}/integrations'], function () {
|
||||
Route::get('/', [ClientIntegrationController::class, 'index']);
|
||||
Route::get('/{integration_code}', [ClientIntegrationController::class, 'show']);
|
||||
Route::put('/{integration_code}', [ClientIntegrationController::class, 'store']);
|
||||
Route::prefix('website-types/{websiteType:codigo}/integrations')->group(function (): void {
|
||||
Route::get('/', [WebsiteTypeIntegrationController::class, 'index']);
|
||||
Route::get('/{integration_code}', [WebsiteTypeIntegrationController::class, 'show']);
|
||||
Route::put('/{integration_code}', [WebsiteTypeIntegrationController::class, 'store']);
|
||||
Route::delete('/{integration_code}', [WebsiteTypeIntegrationController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'integrations'], function () {
|
||||
Route::get('/', [IntegrationController::class, 'index']);
|
||||
Route::post('/', [IntegrationController::class, 'store']);
|
||||
Route::get('/{integration}', [IntegrationController::class, 'show']);
|
||||
Route::put('/{integration}', [IntegrationController::class, 'update']);
|
||||
Route::delete('/{integration}', [IntegrationController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'clients/{client}/integrations'], function () {
|
||||
Route::get('/', [ClientIntegrationController::class, 'index']);
|
||||
Route::get('/{integration_code}', [ClientIntegrationController::class, 'show']);
|
||||
Route::put('/{integration_code}', [ClientIntegrationController::class, 'store']);
|
||||
Route::put('/{integration_code}/instance', [ClientIntegrationController::class, 'associate']);
|
||||
Route::delete('/{integration_code}', [ClientIntegrationController::class, 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::post('webhooks/telepagos/{client}', [TelepagosWebhookController::class, 'handle']);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Policies\IntegrationPolicy;
|
||||
use App\Domains\Notification\Events\PasswordResetRequested;
|
||||
use App\Domains\Notification\Events\UserRegistered;
|
||||
use App\Domains\Notification\Listeners\SendPasswordResetEmail;
|
||||
@@ -13,6 +15,7 @@ use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@@ -31,6 +34,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Gate::policy(
|
||||
Integration::class,
|
||||
IntegrationPolicy::class,
|
||||
);
|
||||
Event::listen(PurchasePaid::class, GenerateTicketsForPaidPurchase::class);
|
||||
Event::listen(PurchasePaid::class, SendPurchaseConfirmedEmail::class);
|
||||
Event::listen(UserRegistered::class, SendWelcomeEmail::class);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Tests\Feature\Integration;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Client\Models\Client;
|
||||
use App\Domains\Integration\Models\ClientIntegration;
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Services\ClientIntegrationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -16,6 +18,7 @@ class ClientIntegrationControllerTest extends TestCase
|
||||
|
||||
public function test_store_returns_success_message_without_integration_data(): void
|
||||
{
|
||||
Sanctum::actingAs(User::factory()->create(['rol_codigo' => 'admin']));
|
||||
$integration = Integration::create([
|
||||
'integration_code' => 'test_integration',
|
||||
'name' => 'Test Integration',
|
||||
@@ -36,7 +39,7 @@ class ClientIntegrationControllerTest extends TestCase
|
||||
->andReturn(new ClientIntegration);
|
||||
});
|
||||
|
||||
$this->putJson('/api/clients/test-client/integrations/test_integration', [
|
||||
$this->withHeader('Accept-Language', 'es')->putJson('/api/clients/test-client/integrations/test_integration', [
|
||||
'integration_data' => [
|
||||
'api_key' => 'secret',
|
||||
],
|
||||
|
||||
200
tests/Feature/Integration/IntegrationInstanceTest.php
Normal file
200
tests/Feature/Integration/IntegrationInstanceTest.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Integration;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Client\Models\Client;
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Models\IntegrationInstance;
|
||||
use App\Domains\Integration\Services\BaseIntegrationService;
|
||||
use App\Domains\Integration\Services\ClientIntegrationService;
|
||||
use App\Domains\Integration\Services\IntegrationAssociationService;
|
||||
use App\Domains\Integration\Services\IntegrationInstanceService;
|
||||
use App\Domains\Integration\Services\TelepagosIntegrationService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IntegrationInstanceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Integration $integration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config(['services.integrations.secret' => 'instance-test-secret']);
|
||||
$this->integration = Integration::create([
|
||||
'integration_code' => 'test', 'name' => 'Test', 'requires_configuration' => false,
|
||||
'integration_data_schema' => ['api_key' => 'required|string'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_management_requires_an_authenticated_global_admin(): void
|
||||
{
|
||||
$this->getJson('/api/integration-instances')->assertUnauthorized();
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
$this->getJson('/api/integration-instances')->assertForbidden();
|
||||
$this->postJson('/api/integration-instances', [])->assertForbidden();
|
||||
$this->getJson('/api/integrations')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_instance_crud_validates_configuration_and_never_returns_secrets(): void
|
||||
{
|
||||
$this->admin();
|
||||
$this->postJson('/api/integration-instances', [
|
||||
'integration_code' => 'test', 'name' => 'Invalid', 'integration_data' => [],
|
||||
])->assertUnprocessable()->assertJsonValidationErrors('integration_data.api_key');
|
||||
$response = $this->postJson('/api/integration-instances', [
|
||||
'integration_code' => 'test', 'name' => 'Shared', 'integration_data' => ['api_key' => 'secret'],
|
||||
])->assertCreated()->assertJsonMissingPath('data.integration_data');
|
||||
$id = $response->json('data.id');
|
||||
$this->getJson('/api/integration-instances/'.$id)->assertOk()->assertJsonMissingPath('data.integration_data');
|
||||
$this->patchJson('/api/integration-instances/'.$id, ['name' => 'Renamed'])->assertOk();
|
||||
self::assertSame('secret', IntegrationInstance::findOrFail($id)->integration_data['api_key']);
|
||||
$this->patchJson('/api/integration-instances/'.$id, ['integration_code' => 'test'])->assertUnprocessable();
|
||||
$this->patchJson('/api/integration-instances/'.$id, ['integration_data' => []])->assertUnprocessable();
|
||||
$this->patchJson('/api/integration-instances/'.$id, ['integration_data' => ['api_key' => 'new']])->assertOk();
|
||||
self::assertSame('new', IntegrationInstance::findOrFail($id)->integration_data['api_key']);
|
||||
$this->deleteJson('/api/integration-instances/'.$id)->assertNoContent();
|
||||
}
|
||||
|
||||
public function test_association_api_checks_integration_and_protects_linked_instances(): void
|
||||
{
|
||||
$this->admin();
|
||||
$client = Client::create(['code' => 'acme', 'name' => 'Acme']);
|
||||
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
|
||||
$instance = $this->makeInstance('shared');
|
||||
$this->putJson('/api/clients/acme/integrations/wrong/instance', ['integration_instance_id' => $instance->id])->assertUnprocessable();
|
||||
$this->putJson('/api/clients/acme/integrations/test/instance', ['integration_instance_id' => $instance->id])
|
||||
->assertCreated()->assertJsonPath('data.integration_instance.name', 'shared')->assertJsonMissingPath('data.integration_instance.integration_data');
|
||||
$this->putJson('/api/website-types/demo/integrations/test', ['integration_instance_id' => $instance->id])->assertCreated();
|
||||
$this->getJson('/api/website-types/demo/integrations/test')->assertOk()->assertJsonPath('data.integration_instance_id', $instance->id);
|
||||
$this->deleteJson('/api/integration-instances/'.$instance->id)->assertConflict();
|
||||
$this->deleteJson('/api/clients/acme/integrations/test')->assertNoContent();
|
||||
$this->deleteJson('/api/website-types/demo/integrations/test')->assertNoContent();
|
||||
self::assertTrue($instance->fresh()->exists);
|
||||
$this->deleteJson('/api/integration-instances/'.$instance->id)->assertNoContent();
|
||||
}
|
||||
|
||||
public function test_tenant_prefers_client_then_type_and_client_context_does_not_inherit(): void
|
||||
{
|
||||
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
|
||||
$tenant = $this->tenantForType($type);
|
||||
$associations = new IntegrationAssociationService;
|
||||
$associations->associate($type, 'test', $this->makeInstance('type'));
|
||||
$service = $this->probe();
|
||||
self::assertSame('type', $service->forTenant($tenant->codigo)->setting());
|
||||
self::assertNull($service->forClient($tenant->client)->setting());
|
||||
$associations->associate($tenant->client, 'test', $this->makeInstance('client'));
|
||||
self::assertSame('client', $service->forTenant($tenant->codigo)->setting());
|
||||
$associations->detach($tenant->client, 'test');
|
||||
self::assertSame('type', $service->forTenant($tenant->codigo)->setting());
|
||||
$associations->detach($type, 'test');
|
||||
self::assertNull($service->forTenant($tenant->codigo)->setting());
|
||||
$this->integration->update(['requires_configuration' => true]);
|
||||
$this->expectException(\Exception::class);
|
||||
$service->forTenant($tenant->codigo);
|
||||
}
|
||||
|
||||
public function test_required_configuration_can_come_from_the_website_type(): void
|
||||
{
|
||||
$this->integration->update(['requires_configuration' => true]);
|
||||
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
|
||||
$tenant = $this->tenantForType($type);
|
||||
(new IntegrationAssociationService)->associate($type, 'test', $this->makeInstance('type'));
|
||||
self::assertSame('type', $this->probe()->forTenant($tenant->codigo)->setting());
|
||||
}
|
||||
|
||||
public function test_legacy_save_does_not_modify_a_shared_instance(): void
|
||||
{
|
||||
$a = Client::create(['code' => 'a', 'name' => 'A']);
|
||||
$b = Client::create(['code' => 'b', 'name' => 'B']);
|
||||
$shared = $this->makeInstance('shared');
|
||||
$associations = new IntegrationAssociationService;
|
||||
$associations->associate($a, 'test', $shared);
|
||||
$associations->associate($b, 'test', $shared);
|
||||
(new ClientIntegrationService)->updateOrCreateIntegration($a, $this->integration, ['api_key' => 'private']);
|
||||
self::assertSame('private', $this->probe()->forClient($a)->setting());
|
||||
self::assertSame('shared', $this->probe()->forClient($b)->setting());
|
||||
(new IntegrationInstanceService)->update($shared, ['integration_data' => ['api_key' => 'changed']]);
|
||||
self::assertSame('changed', $this->probe()->forClient($b)->setting());
|
||||
}
|
||||
|
||||
public function test_telepagos_shares_tokens_by_instance_and_refreshes_after_credential_changes(): void
|
||||
{
|
||||
Integration::create(['integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://payments.test']);
|
||||
$instance = IntegrationInstance::create([
|
||||
'integration_code' => 'telepagos_homo', 'name' => 'Payments',
|
||||
'integration_data' => ['username' => 'first', 'password' => 'secret'],
|
||||
]);
|
||||
$a = Client::create(['code' => 'a', 'name' => 'A']);
|
||||
$b = Client::create(['code' => 'b', 'name' => 'B']);
|
||||
$associations = new IntegrationAssociationService;
|
||||
$associations->associate($a, 'telepagos_homo', $instance);
|
||||
$associations->associate($b, 'telepagos_homo', $instance);
|
||||
Cache::flush();
|
||||
Http::fake(['https://payments.test/v2/auth/token' => Http::sequence()
|
||||
->push(['status' => 'ok', 'token' => 'first-token', 'expires_at' => now()->addHour()->toDateTimeString()])
|
||||
->push(['status' => 'ok', 'token' => 'new-token', 'expires_at' => now()->addHour()->toDateTimeString()])]);
|
||||
self::assertSame('first-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($a)->getToken());
|
||||
self::assertSame('first-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($b)->getToken());
|
||||
Http::assertSentCount(1);
|
||||
(new IntegrationInstanceService)->update($instance, ['integration_data' => ['username' => 'second', 'password' => 'new-secret']]);
|
||||
self::assertSame('new-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($b)->getToken());
|
||||
Http::assertSentCount(2);
|
||||
}
|
||||
|
||||
private function tenantForType(WebsiteType $type): Tenant
|
||||
{
|
||||
$logo = Attachment::create([
|
||||
'path' => 'tenants/logo.png', 'filename' => 'logo.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
|
||||
return Tenant::create([
|
||||
'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test',
|
||||
'website_type_code' => $type->codigo,
|
||||
'primary_color' => '#112233', 'secondary_color' => '#445566',
|
||||
'danger_color' => '#ff0000', 'success_color' => '#00ff00',
|
||||
'header_bg_color' => '#112233', 'footer_bg_color' => '#112233',
|
||||
'header_logo_id' => $logo->id, 'footer_logo_id' => $logo->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeInstance(string $name): IntegrationInstance
|
||||
{
|
||||
return IntegrationInstance::create(['integration_code' => 'test', 'name' => $name, 'integration_data' => ['api_key' => $name]]);
|
||||
}
|
||||
|
||||
private function admin(): void
|
||||
{
|
||||
Sanctum::actingAs(User::factory()->create(['rol_codigo' => 'admin']));
|
||||
}
|
||||
|
||||
private function probe(): BaseIntegrationService
|
||||
{
|
||||
return new class extends BaseIntegrationService
|
||||
{
|
||||
protected string $integrationCode = 'test';
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function setting(): mixed
|
||||
{
|
||||
return $this->getIntegrationSetting('api_key');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user