Initial commit

This commit is contained in:
Julien Nahum
2022-09-20 21:59:52 +02:00
commit f8e6cd4dd6
479 changed files with 77078 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
<?php
use App\Models\User;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\Login;
uses(\Tests\DuskTestCase::class);
/** @test */
it('can login onboarded users', function () {
$user = User::factory()->create();
$this->createUserWorkspace($user);
$this->browse(function ($browser) use ($user) {
$browser->visit(new Login)
->submit($user->email, 'password')
->assertPageIs(Home::class);
});
});
it('cannot login with invalid credentials',function()
{
$this->browse(function ($browser) {
$browser->visit(new Login)
->submit('test@test.app', 'password')
->pause(100)
->assertSee('These credentials do not match our records.');
});
});
it('can log out the user',function()
{
$user = User::factory()->create();
$this->browse(function ($browser) use ($user) {
$browser->visit(new Login)
->submit($user->email, 'password')
->on(new Home)
->clickLogout()
->assertPageIs(Login::class);
});
});

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class Home extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/home';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->waitForLocation($this->url())->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [];
}
/**
* Click on the log out link.
*
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
public function clickLogout($browser)
{
$browser->click('@nav-dropdown-button')
->waitFor('@nav-dropdown')
->waitForText('Logout')
->clickLink('Logout')
->pause(100);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class HomePage extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/';
}
/**
* Assert that the browser is on the page.
*
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
//
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@element' => '#selector',
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Browser\Pages;
class Login extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/login';
}
/**
* Submit the form with the given credentials.
*
* @param \Laravel\Dusk\Browser $browser
* @param string $email
* @param string $password
* @return void
*/
public function submit($browser, $email, $password)
{
$browser->type('email', $email)
->type('password', $password)
->press('@btn_login')
->pause(500);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class Onboarding extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/onboarding';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->waitForLocation($this->url())->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page as BasePage;
abstract class Page extends BasePage
{
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
/**
* Get the global element shortcuts for the site.
*
* @return array
*/
public static function siteElements()
{
return [
'@element' => '#selector',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Tests\Browser\Pages;
class Register extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/register';
}
/**
* Submit the form with the given data.
*
* @param \Laravel\Dusk\Browser $browser
* @param array $data
* @return void
*/
public function submit($browser, array $data = [])
{
foreach ($data as $key => $value) {
$browser->type($key, $value);
}
$browser->press('Register')
->pause(1000);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\Onboarding;
use Tests\Browser\Pages\Register;
use Tests\DuskTestCase;
class RegisterTest extends DuskTestCase
{
use DatabaseMigrations;
public function setUp(): void
{
parent::setup();
static::closeAll();
}
/**
* Pick Random option from custom select
* @param Browser $browser
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function selectHearAboutUsReason(Browser $browser)
{
$browser->waitFor('@hear_about_us')
->click('@hear_about_us')
->waitFor('@hear_about_us_dropdown');
$options = $browser->elements('@hear_about_us_option');
shuffle($options);
$options[0]->click();
}
/** @test */
public function register_with_valid_data()
{
$this->browse(function (Browser $browser) {
$browser->visit(new Register);
$this->selectHearAboutUsReason($browser);
$browser->submit([
'name' => 'Test User',
'email' => 'testuser@test.test',
'password' => 'password',
'password_confirmation' => 'password',
])
->assertPageIs(Onboarding::class);
});
}
/** @test */
public function can_not_register_with_the_same_twice()
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->visit(new Register);
$this->selectHearAboutUsReason($browser);
$browser->submit([
'name' => 'Test User',
'email' => $user->email,
'password' => 'password',
'password_confirmation' => 'password',
])
->pause(3000)
->assertSee('The email has already been taken.');
});
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
class WelcomeTest extends DuskTestCase
{
/** @test */
public function basic_test()
{
$this->browse(function ($browser) {
$browser->visit('/')
->waitFor('@title', 1)
->assertSee('Forms for Notion');
});
}
}

2
tests/Browser/console/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
tests/Browser/screenshots/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
tests/Browser/source/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,22 @@
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
trait CreatesApplication
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}

57
tests/DuskTestCase.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace Tests;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;
use Laravel\Dusk\TestCase as BaseTestCase;
Browser::macro('assertPageIs', function ($page) {
if (! $page instanceof Page) {
$page = new $page;
}
// waiting for location before asserting, because window.location.pathname may be updated asynchronously
return $this->waitForLocation($page->url())->assertPathIs($page->url());
});
abstract class DuskTestCase extends BaseTestCase
{
use DatabaseMigrations;
use CreatesApplication;
use TestHelpers;
/**
* Prepare for Dusk test execution.
*
* @beforeClass
* @return void
*/
public static function prepare()
{
static::startChromeDriver();
}
/**
* Create the RemoteWebDriver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function driver()
{
$options = (new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless',
'--window-size=1920,1080',
]);
return RemoteWebDriver::create(
'http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
use Tests\Helpers\FormSubmissionDataFactory;
it('can answer a form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
// TODO: generate random response given a form and un-skip
})->skip('Need to finish writing a class to generated random responses');
it('can submit form if close date is in future', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'closes_at' => \Carbon\Carbon::now()->addDays(1)->toDateTimeString(),
]);
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});
it('can not submit closed form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'closes_at' => \Carbon\Carbon::now()->subDays(1)->toDateTimeString(),
]);
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertStatus(403);
});
it('can submit form till max submissions count is not reached at limit', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'max_submissions_count' => 3,
]);
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
// Can submit form
for($i=1;$i<=3;$i++){
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
}
// Now, can not submit form, Because it's reached at submission limit
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertStatus(403);
});

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Support\Facades\Artisan;
use Spatie\Mailcoach\Domain\TransactionalMail\Models\TransactionalMail;
it('check form statistic for views & submissions counts', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, []);
// Create 10 views & submissions (in the past of 1 day so that it's cleaned)
for($i=1;$i<=10;$i++){
$submission = $form->submissions()->create();
$submission->created_at = now()->subDay();
$submission->save();
$view = $form->views()->create();
$view->created_at = now()->subDay();
$view->save();
}
// Create a submission & a view for another date
$submission = $form->submissions()->create();
$submission->created_at = now()->subDays(2);
$submission->save();
$view = $form->views()->create();
$view->created_at = now()->subDays(2);
$view->save();
// Run Command
Artisan::call('forms:database-cleanup');
// Create 5 views & submissions
for($i=1;$i<=5;$i++){
$form->views()->create();
$form->submissions()->create();
}
// Now check counters
$statistics = $form->statistics()->get();
expect($form->views_count)->toBe(16);
expect($form->submissions_count)->toBe(16);
expect($form->views()->count())->toBe(5);
expect($form->submissions()->count())->toBe(16);
expect(count($statistics))->toBe(2); // 1 per day for 2 different dates
expect($statistics[0]["date"])->toBe(now()->subDays(2)->toDateString());
expect($statistics[0]["data"])->toBe(["views"=>1,"submissions"=>1]);
expect($statistics[1]["date"])->toBe(now()->subDay()->toDateString());
expect($statistics[1]["data"])->toBe(["views"=>10,"submissions"=>10]);
});

View File

@@ -0,0 +1,109 @@
<?php
use Illuminate\Support\Facades\Mail;
use App\Mail\Forms\SubmissionConfirmationMail;
it('creates confirmation emails with the submitted data', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'send_submission_confirmation' => true,
'notifications_include_submission' => true,
'notification_sender' => 'Custom Sender',
'notification_subject' => 'Test subject',
'notification_body' => 'Test body',
]);
$formData = [
collect($form->properties)->first(function ($property) {
return $property['type'] == 'email';
})["id"] => "test@test.com",
];
$event = new \App\Events\Forms\FormSubmitted($form, $formData);
$mailable = new SubmissionConfirmationMail($event);
$mailable->assertSeeInHtml('Test body')
->assertSeeInHtml('As a reminder, here are your answers:')
->assertSeeInHtml('You are receiving this email because you answered the form:');
});
it('creates confirmation emails without the submitted data', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'send_submission_confirmation' => true,
'notifications_include_submission' => false,
'notification_sender' => 'Custom Sender',
'notification_subject' => 'Test subject',
'notification_body' => 'Test body',
]);
$formData = [
collect($form->properties)->first(function ($property) {
return $property['type'] == 'email';
})["id"] => "test@test.com",
];
$event = new \App\Events\Forms\FormSubmitted($form, $formData);
$mailable = new SubmissionConfirmationMail($event);
$mailable->assertSeeInHtml('Test body')
->assertDontSeeInHtml('As a reminder, here are your answers:')
->assertSeeInHtml('You are receiving this email because you answered the form:');
});
it('sends a confirmation email if needed', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'send_submission_confirmation' => true,
'notifications_include_submission' => true,
'notification_subject' => 'Test subject',
]);
$emailProperty = collect($form->properties)->first(function ($property) {
return $property['type'] == 'email';
});
$formData = [
$emailProperty["id"] => "test@test.com",
];
Mail::fake();
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
Mail::assertQueued(SubmissionConfirmationMail::class,
function (SubmissionConfirmationMail $mail) use ($formData, $emailProperty) {
return $mail->hasTo("test@test.com");
});
});
it('does not send a confirmation email if not needed', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'send_submission_confirmation' => false,
]);
$emailProperty = collect($form->properties)->first(function ($property) {
return $property['type'] == 'email';
});
$formData = [
$emailProperty["id"] => "test@test.com",
];
Mail::fake();
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
Mail::assertNotQueued(SubmissionConfirmationMail::class,
function (SubmissionConfirmationMail $mail) use ($formData, $emailProperty) {
return $mail->hasTo("test@test.com");
});
});

View File

@@ -0,0 +1,26 @@
<?php
use Tests\Helpers\FormSubmissionDataFactory;
it('can submit form with dyanamic select option', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$selectionsPreData = [];
$form->properties = collect($form->properties)->map(function ($property) use (&$selectionsPreData) {
if(in_array($property['type'], ['select','multi_select'])){
$property["allow_creation"] = true;
$selectionsPreData[$property['id']] = ($property['type'] == "select") ? "New single select - ".time() : ["New multi select - ".time()];
}
return $property;
})->toArray();
$form->update();
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $selectionsPreData);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});

View File

@@ -0,0 +1,20 @@
<?php
it('create form with captcha and raise validation issue', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'use_captcha' => true,
]);
$this->postJson(route('forms.answer', $form->slug), [])
->assertStatus(422)
->assertJson([
'message' => 'Please complete the captcha.',
'errors' => [
'h-captcha-response' => [
'Please complete the captcha.',
],
],
]);
});

View File

@@ -0,0 +1,58 @@
<?php
use Illuminate\Testing\Fluent\AssertableJson;
it('create form with logic', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'properties' => [
[
'id' => "title",
'name' => "Name",
'type' => 'title',
'hidden' => false,
'required' => true,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "and",
"children"=> [
[
"identifier"=> "email",
"value"=> [
"operator"=> "is_empty",
"property_meta"=> [
'id'=> "93ea3198-353f-440b-8dc9-2ac9a7bee124",
"type"=> "email",
],
"value"=> true
]
]
]
],
"actions" => ['make-it-optional']
]
]
],
]);
$this->getJson(route('forms.show', $form->slug))
->assertSuccessful()
->assertJson(function (AssertableJson $json) use ($form) {
return $json->where('id', $form->id)
->where('properties', function($values){
return (count($values[0]['logic']) > 0);
})
->etc();
});
// Should submit form
$forData = ['93ea3198-353f-440b-8dc9-2ac9a7bee124' => ""];
$this->postJson(route('forms.answer', $form->slug), $forData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});

View File

@@ -0,0 +1,97 @@
<?php
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\Helpers\FormSubmissionDataFactory;
beforeEach(function () {
$this->password = "12345";
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$this->form = $this->createForm($user, $workspace, [
'password' => $this->password,
]);
$this->formData = FormSubmissionDataFactory::generateSubmissionData($this->form);
});
it('can allow form owner to access and submit form without password', function () {
// As Form Owner so can access form without password
$this->getJson(route('forms.show', $this->form->slug))
->assertSuccessful()
->assertJson(function (AssertableJson $json) {
return $json->where('id', $this->form->id)
->where('has_password', true)
->where('is_password_protected', false)
->etc();
});
// As Form Owner so can submit form without password
$this->postJson(route('forms.answer', $this->form->slug), $this->formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});
it('can not access form without password for guest user', function () {
$this->actingAsGuest();
$this->getJson(route('forms.show', $this->form->slug))
->assertSuccessful()
->assertJson(function (AssertableJson $json) {
return $json->where('id', $this->form->id)
->where('has_password', true)
->where('is_password_protected', true)
->etc();
});
});
it('can not submit form without password for guest user', function () {
$this->actingAsGuest();
$this->postJson(route('forms.answer', $this->form->slug), $this->formData)
->assertStatus(403)
->assertJson([
'status' => 'Unauthorized',
'message' => 'Form is password protected.'
]);
});
it('can not submit form with wrong password for guest user', function () {
$this->actingAsGuest();
$this->withHeaders(['form-password'=>hash('sha256', 'WRONGPASSWORD')])
->postJson(route('forms.answer', $this->form->slug), $this->formData)
->assertStatus(403)
->assertJson([
'status' => 'Unauthorized',
'message' => 'Form is password protected.'
]);
});
it('can access form with right password for guest user', function () {
$this->actingAsGuest();
$this->withHeaders(['form-password'=>hash('sha256', $this->password)])
->getJson(route('forms.show', $this->form->slug))
->assertSuccessful()
->assertJson(function (AssertableJson $json) {
return $json->where('id', $this->form->id)
->where('has_password', true)
->where('is_password_protected', false)
->etc();
});
});
it('can submit form with right password for guest user', function () {
$this->actingAsGuest();
$this->withHeaders(['form-password'=>hash('sha256', $this->password)])
->postJson(route('forms.answer', $this->form->slug), $this->formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});

View File

@@ -0,0 +1,138 @@
<?php
use App\Rules\FormPropertyLogicRule;
it('can validate form logic rules for actions', function () {
$rules = [
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()]
];
$data = [
'properties' => [
[
'id' => "title",
'name' => "Name",
'type' => 'title',
'hidden' => false,
'required' => false,
'logic' => [
"conditions" => null,
"actions" => []
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertTrue($validatorObj->passes());
$data = [
'properties' => [
[
'id' => "title",
'name' => "Name",
'type' => 'title',
'hidden' => true,
'required' => false,
'logic' => [
"conditions" => null,
"actions" => ['hide-block']
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertFalse($validatorObj->passes());
expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe("The logic actions for Name are not valid.");
$data = [
'properties' => [
[
'id' => "text",
'name' => "Custom Test",
'type' => 'nf-text',
'logic' => [
"conditions" => null,
"actions" => ['require-answer']
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertFalse($validatorObj->passes());
expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe("The logic actions for Custom Test are not valid.");
});
it('can validate form logic rules for conditions', function () {
$rules = [
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()]
];
$data = [
'properties' => [
[
'id' => "title",
'name' => "Name",
'type' => 'text',
'hidden' => false,
'required' => false,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "and",
"children"=> [
[
"identifier"=> "title",
"value"=> [
"operator"=> "equals",
"property_meta"=> [
"id"=> "title",
"type"=> "text"
],
"value"=> "TEST"
]
]
]
],
"actions" => []
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertTrue($validatorObj->passes());
$data = [
'properties' => [
[
'id' => "title",
'name' => "Name",
'type' => 'text',
'hidden' => false,
'required' => false,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "and",
"children"=> [
[
"identifier"=> "title",
"value"=> [
"operator"=> "starts_with",
"property_meta"=> [
"id"=> "title",
"type"=> "text"
],
]
]
]
],
"actions" => []
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertFalse($validatorObj->passes());
expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe("The logic conditions for Name are not complete.");
});

View File

@@ -0,0 +1,65 @@
<?php
use Illuminate\Support\Facades\Artisan;
it('check formstat chart data', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, []);
$views = [];
$submissions = [];
// Create 10 views & submissions for past days
for($i=1;$i<=10;$i++){
$date = now()->subDays($i);
$dateString = $date->format('d-m-Y');
$submission = $form->submissions()->create();
$submission->created_at = $date;
$submission->save();
$view = $form->views()->create();
$view->created_at = $date;
$view->save();
$views[$dateString] = isset($views[$dateString]) ? ($views[$dateString] + 1) : 1;
$submissions[$dateString] = isset($submissions[$dateString]) ? ($submissions[$dateString] + 1) : 1;
}
// Run Command
Artisan::call('forms:database-cleanup');
// Create 5 views & submissions
for($i=1;$i<=5;$i++){
$form->views()->create();
$form->submissions()->create();
$dateString = now()->format('d-m-Y');
$views[$dateString] = isset($views[$dateString]) ? ($views[$dateString] + 1) : 1;
$submissions[$dateString] = isset($submissions[$dateString]) ? ($submissions[$dateString] + 1) : 1;
}
// Now check chart data
$this->getJson(route('open.workspaces.form.stats', [$workspace->id, $form->id]))
->assertSuccessful()
->assertJson(function (\Illuminate\Testing\Fluent\AssertableJson $json) use ($views, $submissions) {
return $json->whereType('views', 'array')
->whereType('submissions', 'array')
->where('views', function($values) use ($views) {
foreach($values as $date=>$count){
if((isset($views[$date]) && $views[$date] != $count) || (!isset($views[$date]) && $count != 0)){
return false;
}
}
return true;
})
->where('submissions', function($values) use ($submissions) {
foreach($values as $date=>$count){
if((isset($submissions[$date]) && $submissions[$date] != $count) || (!isset($submissions[$date]) && $count != 0)){
return false;
}
}
return true;
})
->etc();
});
});

View File

@@ -0,0 +1,155 @@
<?php
use Illuminate\Testing\Fluent\AssertableJson;
it('can create a contact form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->makeForm($user, $workspace);
$formData = new \App\Http\Resources\FormResource($form);
$response = $this->postJson(route('open.forms.store', $formData->toArray(request())))
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form created.'
]);
expect($workspace->forms()->count())->toBe(1);
$this->assertDatabaseHas('forms', [
'id' => $response->json('form.id'),
]);
});
it('can fetch forms', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$this->getJson(route('open.workspaces.forms.index', $workspace->id))
->assertSuccessful()
->assertJsonCount(1)
->assertJson(function (AssertableJson $json) use ($form) {
return $json->where('0.id', $form->id)
->whereType('0.title', 'string')
->whereType('0.properties', 'array');
});
});
it('can update a form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$newFormData = $this->makeForm($user, $workspace);
$form->fill((new \App\Http\Resources\FormResource($newFormData))->toArray(request()));
$formData = (new \App\Http\Resources\FormResource($form))->toArray(request());
$this->putJson(route('open.forms.update', $form->id), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form updated.'
]);
$this->assertDatabaseHas('forms', [
'id' => $form->id,
'title' => $form->title,
'description' => $form->description,
]);
});
it('can regenerate a form url', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$newFormData = $this->makeForm($user, $workspace);
$form->update([
'title' => $newFormData->title,
]);
$form->generateSlug();
$newSlug = $form->slug;
$this->putJson(route('open.forms.regenerate-link', [$form->id, 'uuid']))
->assertSuccessful()
->assertJson(function (AssertableJson $json) {
return $json->where('type', 'success')
->where('form.slug', function ($value) {
if (!is_string($value) || (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/',
$value) !== 1)) {
return false;
}
return true;
})
->etc();
});
$this->putJson(route('open.forms.regenerate-link', [$form->id, 'slug']))
->assertSuccessful()
->assertJson(function (AssertableJson $json) use ($newSlug) {
return $json->where('type', 'success')
->where('form.slug', $newSlug)
->etc();
});
});
it('can duplicate a form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$response = $this->postJson(route('open.forms.duplicate', $form->id))
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form successfully duplicated.'
]);
expect($user->forms()->count())->toBe(2);
expect($workspace->forms()->count())->toBe(2);
$this->assertDatabaseHas('forms', [
'id' => $response->json('new_form.id'),
'title' => 'Copy of '.$form->title,
'description' => $form->description,
]);
});
it('can delete a form', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$this->deleteJson(route('open.forms.destroy', $form->id))
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form was deleted.'
]);
expect($user->forms()->count())->toBe(0);
expect($workspace->forms()->count())->toBe(0);
});
it('can create form with dark mode', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'dark_mode' => "dark",
]);
$formData = (new \App\Http\Resources\FormResource($form))->toArray(request());
$this->postJson(route('open.forms.store', $formData))
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form created.'
]);
$this->getJson(route('forms.show', $form->slug))
->assertSuccessful()
->assertJson(function (AssertableJson $json) use ($form) {
return $json->where('id', $form->id)
->where('dark_mode', 'dark')
->etc();
});
});

View File

@@ -0,0 +1,38 @@
<?php
it('can see form without counting view for form owner', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$this->getJson(route('forms.show', $form->slug))
->assertSuccessful()
->assertJson(function (\Illuminate\Testing\Fluent\AssertableJson $json) use ($form) {
return $json->where('id', $form->id)
->where('title', $form->title)
->whereType('properties', 'array')
->etc();
});
expect($form->views()->count())->toBe(0);
});
it('can see form and count view for guest', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$this->actingAsGuest();
$this->getJson(route('forms.show', $form->slug))
->assertSuccessful()
->assertJson(function (\Illuminate\Testing\Fluent\AssertableJson $json) use ($form) {
return $json->where('id', $form->id)
->where('title', $form->title)
->whereType('properties', 'array')
->etc();
});
expect($form->views()->count())->toBe(1);
});

View File

@@ -0,0 +1,35 @@
<?php
use Tests\Helpers\FormSubmissionDataFactory;
use App\Models\User;
it('can validate Update Workspace Select Option Job', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.'
]);
});
it('can validate scope with active subscription', function () {
$this->createProUser();
$this->createUser();
$this->createProUser();
$this->createProUser();
$this->createUser();
expect(User::WithActiveSubscription()->count())->toBe(3);
});

View File

@@ -0,0 +1,38 @@
<?php
use App\Models\User;
it('can login to Forms', function () {
$user = User::factory()->create();
$this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password',
])
->assertSuccessful()
->assertJsonStructure(['token', 'expires_in'])
->assertJson(['token_type' => 'bearer']);
});
it('can fetch current user', function () {
$this->actingAs(User::factory()->create())
->getJson('/api/user')
->assertSuccessful()
->assertJsonStructure(['id', 'name', 'email']);
});
it('can log out', function () {
$this->postJson('/api/login', [
'email' => User::factory()->create()->email,
'password' => 'password',
])->assertSuccessful();
$this->assertAuthenticated();
$this->postJson("/api/logout")
->assertSuccessful();
$this->assertGuest();
$this->getJson("/api/user")
->assertStatus(401);
});

View File

@@ -0,0 +1,33 @@
<?php
use App\Models\User;
use Tests\TestCase;
it('can register', function () {
$this->postJson('/api/register', [
'name' => 'Test User',
'email' => 'test@test.app',
'hear_about_us' => 'google',
'password' => 'secret',
'password_confirmation' => 'secret',
])
->assertSuccessful()
->assertJsonStructure(['id', 'name', 'email']);
$this->assertDatabaseHas('users', [
'name' => 'Test User',
'email' => 'test@test.app'
]);
});
it('cannot register with existing email', function () {
User::factory()->create(['email' => 'test@test.app']);
$this->postJson('/api/register', [
'name' => 'Test User',
'email' => 'test@test.app',
'password' => 'secret',
'password_confirmation' => 'secret',
])
->assertStatus(422)
->assertJsonValidationErrors(['email']);
});

View File

@@ -0,0 +1,32 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
it('update profile info',function() {
$this->actingAs($user = User::factory()->create())
->patchJson('/api/settings/profile', [
'name' => 'Test User',
'email' => 'test@test.app',
])
->assertSuccessful()
->assertJsonStructure(['id', 'name', 'email']);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Test User',
'email' => 'test@test.app',
]);
});
it('update password',function() {
$this->actingAs($user = User::factory()->create())
->patchJson('/api/settings/password', [
'password' => 'updated',
'password_confirmation' => 'updated',
])
->assertSuccessful();
$this->assertTrue(Hash::check('updated', $user->password));
});

View File

@@ -0,0 +1,24 @@
<?php
use App\Models\User;
use App\Notifications\VerifyEmail;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
it('can verify email', function () {
$user = User::factory()->create(['email_verified_at' => null]);
$url = URL::temporarySignedRoute('verification.verify', now()->addMinutes(60), ['user' => $user->id]);
Event::fake();
$this->postJson($url)
->assertSuccessful()
->assertJsonFragment(['status' => 'Your email has been verified!']);
Event::assertDispatched(Verified::class, function (Verified $e) use ($user) {
return $e->user->is($user);
});
});

View File

@@ -0,0 +1,36 @@
<?php
it('can create and delete Workspace', function () {
$user = $this->actingAsUser();
for($i=1;$i<=3;$i++){
$this->postJson(route('open.workspaces.create'), [
'name' => 'Workspace Test - '.$i,
'icon' => '🧪',
])
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Workspace created.'
]);
}
expect($user->workspaces()->count())->toBe(3);
$i=0;
foreach($user->workspaces as $workspace){
$i++;
if($i !== 3){
$this->deleteJson(route('open.workspaces.delete', $workspace->id))
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Workspace deleted.'
]);
}else{
// Last workspace can not delete
$this->deleteJson(route('open.workspaces.delete', $workspace->id))
->assertStatus(403);
}
}
});

View File

@@ -0,0 +1,91 @@
<?php
namespace Tests\Helpers;
use App\Models\Forms\Form;
use Faker;
class FormSubmissionDataFactory
{
private Faker\Generator|null $faker;
public function __construct(private Form $form)
{
$this->faker = Faker\Factory::create();
}
public static function generateSubmissionData(Form $form, array $data = [])
{
return (new self($form))->createSubmissionData($data);
}
public function createSubmissionData($mergeData = [])
{
$data = [];
// for all non-hidden fields in form, create some fake data
collect($this->form->properties)->each(function ($property) use (&$data) {
$value = null;
switch ($property['type']) {
case 'text':
$value = $this->faker->name();
break;
case 'email':
$value = $this->faker->unique()->email();
break;
case 'checkbox':
$value = $this->faker->randomElement([true, false]);
break;
case 'number':
$value = $this->faker->numberBetween();
break;
case 'url':
$value = $this->faker->url();
break;
case 'phone_number':
$value = $this->faker->phoneNumber();
break;
case 'date':
$value = $this->faker->date();
break;
case 'select':
$value = $this->generateSelectValue($property);
break;
case 'multi_select':
$value = $this->generateMultiSelectValues($property);
break;
case 'files':
$value = null; // TODO: Will do this in future
break;
}
$data[$property['id']] = $value;
});
return array_merge($data, $mergeData);
}
private function generateSelectValue($property)
{
$values = [];
if (isset($property["select"]["options"]) && count($property["select"]["options"]) > 0) {
$values = collect($property["select"]["options"])->map(function ($option) {
return $option['name'];
})->toArray();
}
return ($values) ? $this->faker->randomElement($values) : null;
}
private function generateMultiSelectValues($property)
{
$values = [];
if (isset($property["multi_select"]["options"]) && count($property["multi_select"]["options"]) > 0) {
$values = collect($property["multi_select"]["options"])->map(function ($option) {
return $option['name'];
})->toArray();
}
return ($values) ? $this->faker->randomElements(
$values,
$this->faker->numberBetween(1, count($values))) : null;
}
}

40
tests/Pest.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
uses(\Tests\TestCase::class)->in('Feature');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

15
tests/TestCase.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace Tests;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Str;
abstract class TestCase extends BaseTestCase
{
use RefreshDatabase;
use CreatesApplication;
use TestHelpers;
}

195
tests/TestHelpers.php Normal file
View File

@@ -0,0 +1,195 @@
<?php
namespace Tests;
use App\Models\Forms\Form;
use App\Models\Workspace;
use App\Models\User;
use Database\Factories\FormFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
trait TestHelpers
{
/**
* Creates a workspace for a user
* @param User $user
*/
public function createUserWorkspace(User $user)
{
if(!$user){ return null; }
$workspace = Workspace::create([
'name' => 'Form Testing',
'icon' => '🧪',
]);
$user->workspaces()->sync([
$workspace->id => [
'role' => 'admin'
]
], false);
return $workspace;
}
/**
* Generates a Form instance (not saved)
* @param User $user
* @param Workspace $workspace
* @return array
*/
public function makeForm(User $user, Workspace $workspace, array $data = [])
{
$dbProperties = [
[
'name' => 'Name',
'type' => 'text',
'hidden' => false,
'required' => false
],
[
'name' => 'Message',
'type' => 'text',
'hidden' => false,
'required' => false,
'multi_lines' => true
],
[
'name' => 'Number',
'type' => 'number',
'hidden' => false,
'required' => false
],
[
'name' => 'Select',
'type' => 'select',
'hidden' => false,
'required' => false,
'allow_creation' => false,
'select' => [
'options' => [['id' => 'First','name' => 'First'], ['id' => 'Second','name' => 'Second']]
]
],
[
'name' => 'Multi Select',
'type' => 'multi_select',
'hidden' => false,
'required' => false,
'allow_creation' => false,
'multi_select' => [
'options' => [['id' => 'One','name' => 'One'], ['id' => 'Two','name' => 'Two'], ['id' => 'Three','name' => 'Three']]
]
],
[
'name' => 'Date',
'type' => 'date',
'hidden' => false,
'required' => false
],
[
'name' => 'Checkbox',
'type' => 'checkbox',
'hidden' => false,
'required' => false
],
[
'name' => 'URL',
'type' => 'url',
'hidden' => false,
'required' => false
],
[
'name' => 'Email',
'type' => 'email',
'hidden' => false,
'required' => false
],
[
'name' => 'Phone Number',
'type' => 'phone_number',
'hidden' => false,
'required' => false
],
[
'name' => 'Files',
'type' => 'files',
'hidden' => false,
'required' => false,
]
];
return Form::factory()
->withProperties(FormFactory::formatProperties($dbProperties))
->forWorkspace($workspace)
->createdBy($user)
->make($data);
}
public function createForm(User $user, Workspace $workspace, array $data = [])
{
$form = $this->makeForm($user, $workspace, $data);
$form->save();
return $form;
}
public function createUser()
{
return \App\Models\User::factory()->create();
}
public function createProUser()
{
$user = $this->createUser();
$user->subscriptions()->create([
'name' => 'default',
'stripe_id' => Str::random(),
'stripe_status' => 'active',
'stripe_price' => Str::random(),
'quantity' => 1,
]);
return $user;
}
public function actingAsUser(User $user = null)
{
if ($user == null) {
$user = $this->createUser();
}
$this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password',
])->assertSuccessful();
return $user;
}
/**
* Creates a user with a Pro subscription
*
* @param User|null $user
* @return User|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null
*/
public function actingAsProUser(User $user = null)
{
if ($user == null) {
$user = $this->createProUser();
}
return $this->actingAsUser($user);
}
public function actingAsGuest()
{
if (Auth::check()) {
Auth::logout();
}
$this->assertGuest();
}
}

View File

@@ -0,0 +1,109 @@
<?php
use App\Service\Forms\FormLogicPropertyResolver;
it('can validate form logic property resolver', function ($property, $formData, $expectedResult) {
$isRequired = FormLogicPropertyResolver::isRequired($property, $formData);
expect($isRequired)->toBe($expectedResult);
})->with([
[
[
'id' => "title",
'name' => "Name",
'type' => 'text',
'hidden' => false,
'required' => true,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "and",
"children"=> [
[
"identifier"=> "user",
"value"=> [
"operator"=> "is_not_empty",
"property_meta"=> [
'id'=> "93ea3198-353f-440b-8dc9-2ac9a7bee124",
"type"=> "select",
],
"value"=> true
]
]
]
],
"actions" => ['make-it-optional']
]
],
['93ea3198-353f-440b-8dc9-2ac9a7bee124'=>["One"]],
false
],
[
[
'id' => "title",
'name' => "Name",
'type' => 'text',
'hidden' => false,
'required' => true,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "and",
"children"=> [
[
"identifier"=> "user",
"value"=> [
"operator"=> "is_not_empty",
"property_meta"=> [
'id'=> "93ea3198-353f-440b-8dc9-2ac9a7bee124",
"type"=> "select",
],
"value"=> true
]
]
]
],
"actions" => ['make-it-optional']
]
],
['93ea3198-353f-440b-8dc9-2ac9a7bee124'=>[]],
true
],
[
[
'id' => "title",
'name' => "Name",
'type' => 'text',
'hidden' => false,
'required' => true,
'logic' => [
"conditions" => [
"operatorIdentifier"=> "or",
"children"=> [
[
"identifier"=> "user",
"value"=> [
"operator"=> "is_not_empty",
"property_meta"=> [
'id'=> "93ea3198-353f-440b-8dc9-2ac9a7bee124",
"type"=> "select",
],
"value"=> true
]
],
[
"identifier"=> "email",
"value"=> [
"operator"=> "contains",
"property_meta"=> [
'id'=> "93ea3198-353f-440b-8dc9-2ac9a7bee222",
"type"=> "email",
],
"value"=> "abc"
]
]
]
],
"actions" => ['make-it-optional']
]
],
['93ea3198-353f-440b-8dc9-2ac9a7bee124'=>[], '93ea3198-353f-440b-8dc9-2ac9a7bee222'=>['abc']],
false
]
]);

View File

@@ -0,0 +1,27 @@
<?php
uses(\Tests\TestCase::class);
it('can parse filenames', function () {
$fileName = 'Notion_app_logo_85e16d7b-58ed-43bc-8dce-7d3ff7d69f41.png';
$parsedFilename = \App\Service\Storage\StorageFileNameParser::parse($fileName);
expect($parsedFilename->fileName)->toBe('Notion_app_logo');
expect($parsedFilename->uuid)->toBe('85e16d7b-58ed-43bc-8dce-7d3ff7d69f41');
expect($parsedFilename->extension)->toBe('png');
expect($parsedFilename->getMovedFileName())->toBe($fileName);
$uuid = Str::uuid()->toString();
$parsedFilename = \App\Service\Storage\StorageFileNameParser::parse($uuid);
expect($parsedFilename->uuid)->toBe($uuid);
expect($parsedFilename->fileName)->toBeNull();
expect($parsedFilename->extension)->toBeNull();
expect($parsedFilename->getMovedFileName())->toBe($uuid);
$randomString = Str::random(20);
$parsedFilename = \App\Service\Storage\StorageFileNameParser::parse($randomString);
expect($parsedFilename->fileName)->toBeNull();
expect($parsedFilename->uuid)->toBeNull();
expect($parsedFilename->extension)->toBeNull();
expect($parsedFilename->getMovedFileName())->toBeNull();
});

View File

@@ -0,0 +1,26 @@
<?php
uses(\Tests\TestCase::class);
use function Pest\Faker\faker;
use Tests\Helpers\FormSubmissionDataFactory;
it('can create pro user who are subscribed', function () {
$user = $this->actingAsProUser();
expect($user->is_subscribed)->toBeTrue();
});
it('can create test workspace', function () {
$user = $this->actingAsProUser();
$this->createUserWorkspace($user);
expect($user->workspaces()->count())->toBe(1);
});
it('can make a form for a database', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->makeForm($user,$workspace);
expect($form->title)->not()->toBeNull();
expect($form->description)->not()->toBeNull();
expect(count($form->properties))->not()->toBe(0);
});