Zapier integration (#491)
* create zapier app * install sanctum * move OAuthProviderController * make `api-external` middleware * add zapier endpoints * add tests * token management * zapier event handler * add policy * use `slug` instead of `id` * wip * check policies * change api prefix to `external` * ui tweaks * validate token abilities * open zapier URL * zapier ui tweaks * update zap * Fix linting * Added sample endpoints + minor UI changes * Run PHP code linter --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
@@ -4,6 +4,7 @@ use App\Mail\Forms\SubmissionConfirmationMail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
it('creates confirmation emails with the submitted data', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
$user = $this->actingAsUser();
|
||||
$workspace = $this->createUserWorkspace($user);
|
||||
$form = $this->createForm($user, $workspace);
|
||||
|
||||
244
tests/Feature/Zapier/IntegrationsTest.php
Normal file
244
tests/Feature/Zapier/IntegrationsTest.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use App\Models\User;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\Helpers\FormSubmissionDataFactory;
|
||||
|
||||
use function Pest\Laravel\assertDatabaseCount;
|
||||
use function Pest\Laravel\delete;
|
||||
use function Pest\Laravel\post;
|
||||
use function PHPUnit\Framework\assertEquals;
|
||||
|
||||
test('create an integration', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||
|
||||
Sanctum::actingAs(
|
||||
$user,
|
||||
['manage-integrations']
|
||||
);
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
post(route('zapier.webhooks.store'), [
|
||||
'form_id' => $form->slug,
|
||||
'hookUrl' => $hookUrl = 'https://zapier.com/hook/test'
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
assertDatabaseCount('form_integrations', 1);
|
||||
|
||||
$integration = FormIntegration::first();
|
||||
|
||||
assertEquals($form->id, $integration->form_id);
|
||||
assertEquals($hookUrl, $integration->data->hook_url);
|
||||
});
|
||||
|
||||
test('cannot create an integration without a corresponding ability', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
post(route('zapier.webhooks.store'), [
|
||||
'form_id' => $form->slug,
|
||||
'hookUrl' => 'https://zapier.com/hook/test'
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
assertDatabaseCount('form_integrations', 0);
|
||||
});
|
||||
|
||||
test('cannot create an integration for other users form', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user2 = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user2);
|
||||
|
||||
$form = createForm($user2, $workspace, ['title' => 'First form']);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
post(route('zapier.webhooks.store'), [
|
||||
'form_id' => $form->slug,
|
||||
'hookUrl' => 'https://zapier.com/hook/test'
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
assertDatabaseCount('form_integrations', 0);
|
||||
});
|
||||
|
||||
test('delete an integration', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||
|
||||
Sanctum::actingAs(
|
||||
$user,
|
||||
['manage-integrations']
|
||||
);
|
||||
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->create([
|
||||
'data' => [
|
||||
'hook_url' => $hookUrl = 'https://zapier.com/hook/test'
|
||||
]
|
||||
]);
|
||||
|
||||
assertDatabaseCount('form_integrations', 1);
|
||||
|
||||
delete(route('zapier.webhooks.destroy', $integration), [
|
||||
'form_id' => $form->slug,
|
||||
'hookUrl' => $hookUrl,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
assertDatabaseCount('form_integrations', 0);
|
||||
});
|
||||
|
||||
test('cannot delete an integration with an incorrect hook url', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||
|
||||
Sanctum::actingAs(
|
||||
$user,
|
||||
['manage-integrations']
|
||||
);
|
||||
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->create([
|
||||
'data' => [
|
||||
'hook_url' => 'https://zapier.com/hook/test'
|
||||
]
|
||||
]);
|
||||
|
||||
delete(route('zapier.webhooks.destroy', $integration), [
|
||||
'form_id' => $form->slug,
|
||||
'hookUrl' => 'https://google.com',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
assertDatabaseCount('form_integrations', 1);
|
||||
});
|
||||
|
||||
test('poll for the latest submission', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
$form = createForm($user, $workspace, [
|
||||
'properties' => [
|
||||
[
|
||||
'id' => 'title',
|
||||
'name' => 'Name',
|
||||
'type' => 'text',
|
||||
'hidden' => false,
|
||||
'required' => true,
|
||||
'logic' => [
|
||||
'conditions' => null,
|
||||
'actions' => [],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'age',
|
||||
'name' => 'Age',
|
||||
'type' => 'number',
|
||||
'hidden' => false,
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create a submission for the form
|
||||
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
|
||||
|
||||
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||
->assertSuccessful()
|
||||
->assertJson([
|
||||
'type' => 'success',
|
||||
'message' => 'Form submission saved.',
|
||||
]);
|
||||
|
||||
// Create a webhook integration for the form
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->create([
|
||||
'data' => [
|
||||
'hook_url' => 'https://zapier.com/hook/test'
|
||||
]
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user, ['view', 'manage-integrations']);
|
||||
|
||||
// Call the poll endpoint
|
||||
$response = $this->getJson(route('zapier.webhooks.poll', ['form_id' => $form->slug]));
|
||||
|
||||
// Assert the response status is OK
|
||||
$response->assertOk();
|
||||
|
||||
// Decode the response data
|
||||
$responseData = $response->json()[0];
|
||||
$receivedData = collect($responseData['data'])->values()->pluck('value')->toArray();
|
||||
|
||||
$this->assertEmpty(array_diff(array_values($formData), $receivedData));
|
||||
});
|
||||
|
||||
test('make up a submission when polling without any submission', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
$form = createForm($user, $workspace, [
|
||||
'properties' => [
|
||||
[
|
||||
'id' => 'title',
|
||||
'name' => 'Name',
|
||||
'type' => 'text',
|
||||
'hidden' => false,
|
||||
'required' => true,
|
||||
'logic' => [
|
||||
'conditions' => null,
|
||||
'actions' => [],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'age',
|
||||
'name' => 'Age',
|
||||
'type' => 'number',
|
||||
'hidden' => false,
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create a webhook integration for the form
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->create([
|
||||
'data' => [
|
||||
'hook_url' => 'https://zapier.com/hook/test'
|
||||
]
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user, ['view', 'manage-integrations']);
|
||||
|
||||
// Call the poll endpoint
|
||||
$this->withoutExceptionHandling();
|
||||
$response = $this->getJson(route('zapier.webhooks.poll', ['form_id' => $form->slug]));
|
||||
// Assert the response status is OK
|
||||
$response->assertOk();
|
||||
|
||||
// Decode the response data
|
||||
$responseData = $response->json()[0];
|
||||
|
||||
ray($responseData);
|
||||
|
||||
ray($responseData);
|
||||
$this->assertNotEmpty($responseData['data']);
|
||||
$this->assertTrue(count($responseData['data']) == 2);
|
||||
});
|
||||
61
tests/Feature/Zapier/ListFormsTest.php
Normal file
61
tests/Feature/Zapier/ListFormsTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
use function Pest\Laravel\get;
|
||||
|
||||
test('list all forms of a given workspace', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||
|
||||
Sanctum::actingAs(
|
||||
$user,
|
||||
['list-forms']
|
||||
);
|
||||
|
||||
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||
->assertOk()
|
||||
->assertJsonCount(2)
|
||||
->assertJson([
|
||||
[
|
||||
'id' => $form1->slug,
|
||||
'name' => $form1->title,
|
||||
],
|
||||
[
|
||||
'id' => $form2->slug,
|
||||
'name' => $form2->title,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('cannot list forms without a corresponding ability', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('cannot other users forms', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user2 = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user2);
|
||||
|
||||
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||
->assertForbidden();
|
||||
});
|
||||
39
tests/Feature/Zapier/ListWorkspacesTest.php
Normal file
39
tests/Feature/Zapier/ListWorkspacesTest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
use function Pest\Laravel\get;
|
||||
|
||||
test('list all workspaces of a user', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
$anotherUser = User::factory()->create();
|
||||
$anotherWorkspace = createUserWorkspace($anotherUser);
|
||||
|
||||
Sanctum::actingAs(
|
||||
$user,
|
||||
['list-workspaces']
|
||||
);
|
||||
|
||||
get(route('zapier.workspaces'))
|
||||
->assertOk()
|
||||
->assertJsonCount(1)
|
||||
->assertJson([
|
||||
[
|
||||
'id' => $workspace->id,
|
||||
'name' => $workspace->name,
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
test('cannot list workspaces without a corresponding ability', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = createUserWorkspace($user);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
get(route('zapier.workspaces'))
|
||||
->assertForbidden();
|
||||
});
|
||||
24
tests/Feature/Zapier/ValidateAuthTest.php
Normal file
24
tests/Feature/Zapier/ValidateAuthTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
use function Pest\Laravel\get;
|
||||
|
||||
test('validate auth', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
get(route('zapier.validate'))
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
]);
|
||||
});
|
||||
|
||||
test('cannot validate auth with incorrect credentials', function () {
|
||||
get(route('zapier.validate'))
|
||||
->assertUnauthorized();
|
||||
});
|
||||
@@ -9,6 +9,12 @@ class FormSubmissionDataFactory
|
||||
{
|
||||
private ?Faker\Generator $faker;
|
||||
|
||||
/**
|
||||
* If true, then format expected by answer endpoint
|
||||
* otherwise, format of answer as we store it in the FormSubmission's data
|
||||
*/
|
||||
private bool $answerFormat = true;
|
||||
|
||||
public function __construct(private Form $form)
|
||||
{
|
||||
$this->faker = Faker\Factory::create();
|
||||
@@ -19,6 +25,12 @@ class FormSubmissionDataFactory
|
||||
return (new self($form))->createSubmissionData($data);
|
||||
}
|
||||
|
||||
public function asFormSubmissionData()
|
||||
{
|
||||
$this->answerFormat = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createSubmissionData($mergeData = [])
|
||||
{
|
||||
$data = [];
|
||||
@@ -68,9 +80,23 @@ class FormSubmissionDataFactory
|
||||
$data[$property['id']] = $value;
|
||||
});
|
||||
|
||||
if (!$this->answerFormat) {
|
||||
$data = $this->formatAsSubmissionData($data);
|
||||
}
|
||||
|
||||
return array_merge($data, $mergeData);
|
||||
}
|
||||
|
||||
private function formatAsSubmissionData($data)
|
||||
{
|
||||
collect($this->form->properties)->each(function ($property) use (&$data) {
|
||||
if ($property['type'] === 'phone_number') {
|
||||
$data[$property['id']] = '+33749119783';
|
||||
}
|
||||
});
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function generateSelectValue($property)
|
||||
{
|
||||
$values = [];
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
|
|
||||
*/
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
uses(\Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
@@ -38,3 +42,13 @@ expect()->extend('toBeOne', function () {
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function createUserWorkspace(User $user): Workspace
|
||||
{
|
||||
return test()->createUserWorkspace($user);
|
||||
}
|
||||
|
||||
function createForm(User $user, Workspace $workspace, array $data = []): Form
|
||||
{
|
||||
return test()->createForm($user, $workspace, $data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user