some vibe tests
better than none
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\DatabaseTestCase;
|
||||
|
||||
class AuthenticationTest extends DatabaseTestCase
|
||||
{
|
||||
/** @dataProvider protectedRoutes */
|
||||
public function test_guests_are_redirected_to_login($method, $path)
|
||||
{
|
||||
$this->call($method, $path)->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function protectedRoutes()
|
||||
{
|
||||
return [
|
||||
['GET', '/bookings/year-2020'], ['GET', '/people'],
|
||||
['GET', '/projects'], ['GET', '/targets'],
|
||||
['GET', '/reports/balances/2020'], ['GET', '/reports/scopes/compact/2020'],
|
||||
['POST', '/bookings'], ['PATCH', '/bookings/1'], ['DELETE', '/bookings/1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_login_form_is_available()
|
||||
{
|
||||
$this->get('/login')->assertStatus(200)->assertSee('password');
|
||||
}
|
||||
|
||||
public function test_guests_cannot_change_or_delete_an_existing_booking()
|
||||
{
|
||||
$booking = $this->booking('2020-01-15', 100);
|
||||
$this->patch('/bookings/' . $booking->id, [
|
||||
'booking' => ['reference' => 'Unauthorized change'],
|
||||
])->assertRedirect('/login');
|
||||
$this->delete('/bookings/' . $booking->id)->assertRedirect('/login');
|
||||
$this->assertSame('Test booking', $booking->fresh()->reference);
|
||||
$this->assertCount(1, $booking->fresh()->subBookings);
|
||||
}
|
||||
|
||||
public function test_valid_credentials_log_in_and_redirect_to_bookings()
|
||||
{
|
||||
$this->post('/login', ['email' => $this->user->email, 'password' => 'test-password'])
|
||||
->assertRedirect('/bookings');
|
||||
$this->assertAuthenticatedAs($this->user);
|
||||
}
|
||||
|
||||
public function test_wrong_password_is_rejected()
|
||||
{
|
||||
$this->from('/login')->post('/login', [
|
||||
'email' => $this->user->email, 'password' => 'wrong-password',
|
||||
])->assertRedirect('/login')->assertSessionHasErrors('email');
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_logout_ends_authentication()
|
||||
{
|
||||
$this->actingAs($this->user)->post('/logout')->assertRedirect('/login');
|
||||
$this->assertGuest();
|
||||
$this->get('/bookings/year-2020')->assertRedirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Booking;
|
||||
use App\PersonTarget;
|
||||
use App\SubBooking;
|
||||
use Tests\DatabaseTestCase;
|
||||
|
||||
class BookingsTest extends DatabaseTestCase
|
||||
{
|
||||
protected function payload(array $attributes = [])
|
||||
{
|
||||
return array_replace([
|
||||
'date' => '2020-02-15', 'reference' => 'Membership payment',
|
||||
'person_or_company' => 'Test company', 'note' => 'Test note', 'amount' => '125.50',
|
||||
'account_type_id' => $this->incomeAccount->id, 'project_id' => $this->project->id,
|
||||
'person_target_id' => null, 'personTarget' => ['person_id' => null, 'date' => null],
|
||||
], $attributes);
|
||||
}
|
||||
|
||||
public function test_user_can_create_a_booking_with_its_account_and_project()
|
||||
{
|
||||
$this->actingAs($this->user)->post('/bookings', $this->payload())
|
||||
->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors();
|
||||
|
||||
$booking = Booking::where('reference', 'Membership payment')->firstOrFail();
|
||||
$this->assertSame('2020-02-15', $booking->date);
|
||||
$this->assertSame('Test company', $booking->person_or_company);
|
||||
$this->assertSame('Test note', $booking->note);
|
||||
$this->assertCount(1, $booking->subBookings);
|
||||
$this->assertDatabaseHas('sub_bookings', [
|
||||
'booking_id' => $booking->id, 'amount' => 125.50, 'reference' => 'Membership payment',
|
||||
'account_type_id' => $this->incomeAccount->id, 'project_id' => $this->project->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_booking_forms_render_with_account_project_and_target_choices()
|
||||
{
|
||||
$target = $this->target();
|
||||
$booking = $this->booking('2020-01-15', 100, null, ['person_target_id' => $target->id]);
|
||||
$this->actingAs($this->user)->get('/bookings/create')->assertStatus(200)
|
||||
->assertSee($this->project->name)->assertSee($this->incomeAccount->name);
|
||||
// A fully paid target must still be selectable when editing its booking.
|
||||
$this->get('/bookings/' . $booking->id . '/edit')->assertStatus(200)
|
||||
->assertSee($target->reference)->assertSee($booking->reference);
|
||||
$this->get('/bookings/' . $booking->id . '/delete')->assertStatus(200);
|
||||
}
|
||||
|
||||
/** @dataProvider invalidBookings */
|
||||
public function test_invalid_bookings_do_not_write_any_records($field, $value)
|
||||
{
|
||||
$this->actingAs($this->user)->from('/bookings/create')
|
||||
->post('/bookings', $this->payload([$field => $value]))
|
||||
->assertRedirect('/bookings/create')->assertSessionHasErrors($field);
|
||||
$this->assertSame(0, Booking::count());
|
||||
$this->assertSame(0, SubBooking::count());
|
||||
}
|
||||
|
||||
public function invalidBookings()
|
||||
{
|
||||
return [
|
||||
['date', ''], ['reference', ''], ['person_or_company', ''],
|
||||
['amount', ''], ['amount', 'not money'], ['amount', '-10'], ['account_type_id', ''],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_booking_can_pay_an_existing_target()
|
||||
{
|
||||
$target = $this->target(125.50);
|
||||
$this->actingAs($this->user)->post('/bookings', $this->payload(['person_target_id' => $target->id]))
|
||||
->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors();
|
||||
$this->assertDatabaseHas('sub_bookings', ['person_target_id' => $target->id, 'amount' => 125.50]);
|
||||
$this->assertTrue($target->fresh()->isPaid());
|
||||
$this->assertSame(1, PersonTarget::count());
|
||||
}
|
||||
|
||||
public function test_booking_can_create_a_target_using_the_booking_date_by_default()
|
||||
{
|
||||
$person = $this->target()->person;
|
||||
$this->actingAs($this->user)->post('/bookings', $this->payload([
|
||||
'personTarget' => ['person_id' => $person->id, 'date' => null],
|
||||
]))->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors();
|
||||
$target = SubBooking::firstOrFail()->personTarget;
|
||||
$this->assertSame($person->id, $target->person_id);
|
||||
$this->assertSame('2020-02-15', $target->date);
|
||||
$this->assertEquals(125.50, $target->amount);
|
||||
$this->assertTrue($target->isPaid());
|
||||
}
|
||||
|
||||
public function test_user_cannot_choose_an_existing_target_and_create_one_together()
|
||||
{
|
||||
$target = $this->target();
|
||||
$this->actingAs($this->user)->post('/bookings', $this->payload([
|
||||
'person_target_id' => $target->id,
|
||||
'personTarget' => ['person_id' => $target->person_id, 'date' => '2020-01-01'],
|
||||
]))->assertSessionHasErrors('person_target_id');
|
||||
$this->assertSame(0, Booking::count());
|
||||
$this->assertSame(1, PersonTarget::count());
|
||||
}
|
||||
|
||||
public function test_user_can_update_booking_and_split_it_into_income_and_expense_lines()
|
||||
{
|
||||
$booking = $this->booking('2019-12-31', 100);
|
||||
$line = $booking->subBookings->first();
|
||||
$this->actingAs($this->user)->patch('/bookings/' . $booking->id, [
|
||||
'booking' => ['date' => '2020-01-15', 'reference' => 'Corrected', 'person_or_company' => 'Updated company'],
|
||||
'sub_booking' => [
|
||||
$line->id => ['reference' => 'Income', 'amount' => '120.50', 'account_type_id' => $this->incomeAccount->id],
|
||||
0 => ['reference' => 'Fee', 'amount' => '20.25', 'account_type_id' => $this->expenseAccount->id],
|
||||
],
|
||||
])->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors();
|
||||
$booking = $booking->fresh();
|
||||
$this->assertSame('Corrected', $booking->reference);
|
||||
$this->assertSame('2020-01-15', $booking->date);
|
||||
$this->assertSame('Updated company', $booking->person_or_company);
|
||||
$this->assertCount(2, $booking->subBookings);
|
||||
$this->assertEquals(100.25, $booking->signedAmount());
|
||||
$this->assertDatabaseHas('sub_bookings', ['id' => $line->id, 'amount' => 120.50]);
|
||||
$this->assertDatabaseHas('sub_bookings', ['booking_id' => $booking->id, 'reference' => 'Fee', 'amount' => 20.25]);
|
||||
}
|
||||
|
||||
public function test_invalid_update_leaves_booking_and_lines_unchanged()
|
||||
{
|
||||
$booking = $this->booking('2020-01-15', 100);
|
||||
$line = $booking->subBookings->first();
|
||||
$this->actingAs($this->user)->patch('/bookings/' . $booking->id, [
|
||||
'booking' => ['date' => '2020-02-01', 'reference' => 'Changed', 'person_or_company' => 'Company'],
|
||||
'sub_booking' => [$line->id => ['reference' => 'Invalid', 'amount' => -1, 'account_type_id' => $this->incomeAccount->id]],
|
||||
])->assertSessionHasErrors('sub_booking.' . $line->id . '.amount');
|
||||
$this->assertSame('2020-01-15', $booking->fresh()->date);
|
||||
$this->assertEquals(100, $line->fresh()->amount);
|
||||
}
|
||||
|
||||
public function test_empty_line_is_removed_while_other_lines_are_kept()
|
||||
{
|
||||
$booking = $this->booking('2020-01-15', 100);
|
||||
$line = $booking->subBookings->first();
|
||||
$this->actingAs($this->user)->patch('/bookings/' . $booking->id, [
|
||||
'booking' => ['date' => $booking->date, 'reference' => $booking->reference, 'person_or_company' => $booking->person_or_company],
|
||||
'sub_booking' => [
|
||||
$line->id => ['reference' => null, 'amount' => null, 'account_type_id' => null],
|
||||
0 => ['reference' => 'Replacement', 'amount' => 75, 'account_type_id' => $this->incomeAccount->id],
|
||||
],
|
||||
])->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors();
|
||||
$this->assertDatabaseMissing('sub_bookings', ['id' => $line->id]);
|
||||
$this->assertCount(1, $booking->fresh()->subBookings);
|
||||
$this->assertEquals(75, $booking->fresh()->signedAmount());
|
||||
}
|
||||
|
||||
public function test_deleting_booking_cascades_to_its_lines_but_keeps_target_and_other_bookings()
|
||||
{
|
||||
$target = $this->target();
|
||||
$booking = $this->booking('2020-01-15', 100, null, ['person_target_id' => $target->id]);
|
||||
$other = $this->booking('2020-02-15', 50);
|
||||
$this->actingAs($this->user)->delete('/bookings/' . $booking->id, ['date' => $booking->date])
|
||||
->assertRedirect('/bookings/year-2020');
|
||||
$this->assertDatabaseMissing('bookings', ['id' => $booking->id]);
|
||||
$this->assertDatabaseMissing('sub_bookings', ['booking_id' => $booking->id]);
|
||||
$this->assertDatabaseHas('bookings', ['id' => $other->id]);
|
||||
$this->assertDatabaseHas('person_targets', ['id' => $target->id]);
|
||||
$this->assertEquals(100, $target->fresh()->difference());
|
||||
}
|
||||
|
||||
public function test_clearing_all_lines_redirects_to_delete_confirmation_without_deleting()
|
||||
{
|
||||
$booking = $this->booking('2020-01-15', 100);
|
||||
$line = $booking->subBookings->first();
|
||||
$this->actingAs($this->user)->patch('/bookings/' . $booking->id, [
|
||||
'booking' => ['date' => $booking->date, 'reference' => $booking->reference, 'person_or_company' => $booking->person_or_company],
|
||||
'sub_booking' => [$line->id => ['reference' => null, 'amount' => null, 'account_type_id' => null]],
|
||||
])->assertRedirect('/bookings/' . $booking->id . '/delete')->assertSessionHasNoErrors();
|
||||
$this->assertDatabaseHas('bookings', ['id' => $booking->id]);
|
||||
$this->assertDatabaseHas('sub_bookings', ['id' => $line->id, 'amount' => 100]);
|
||||
}
|
||||
|
||||
public function test_unknown_booking_returns_not_found()
|
||||
{
|
||||
$this->actingAs($this->user)->get('/bookings/999999/edit')->assertStatus(404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\PersonTarget;
|
||||
use Tests\DatabaseTestCase;
|
||||
|
||||
class PersonTargetsTest extends DatabaseTestCase
|
||||
{
|
||||
/** @dataProvider paymentStates */
|
||||
public function test_payment_states_and_outstanding_balances($expense, $payment, $difference, $state)
|
||||
{
|
||||
$account = $expense ? $this->expenseAccount : $this->incomeAccount;
|
||||
$target = $this->target(100, $account);
|
||||
if ($payment > 0) {
|
||||
$this->booking('2020-01-15', $payment, $account, ['person_target_id' => $target->id]);
|
||||
}
|
||||
$this->assertEquals($difference, $target->difference());
|
||||
$this->assertSame($state === 'paid', $target->isPaid());
|
||||
$this->assertTrue(PersonTarget::$state()->contains('id', $target->id));
|
||||
$this->assertSame($state !== 'paid', PersonTarget::open()->contains('id', $target->id));
|
||||
$this->assertSame($difference > 0, PersonTarget::receivable()->contains('id', $target->id));
|
||||
$this->assertSame($difference < 0, PersonTarget::debt()->contains('id', $target->id));
|
||||
}
|
||||
|
||||
public function paymentStates()
|
||||
{
|
||||
return [
|
||||
'unpaid income' => [false, 0, 100, 'underpaid'],
|
||||
'partial income' => [false, 40, 60, 'underpaid'],
|
||||
'paid income' => [false, 100, 0, 'paid'],
|
||||
'overpaid income' => [false, 120, -20, 'overpaid'],
|
||||
'unpaid expense' => [true, 0, -100, 'underpaid'],
|
||||
'partial expense' => [true, 40, -60, 'underpaid'],
|
||||
'paid expense' => [true, 100, 0, 'paid'],
|
||||
'overpaid expense' => [true, 120, 20, 'overpaid'],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_multiple_payments_accumulate_and_targets_page_displays_remaining_total()
|
||||
{
|
||||
$receivable = $this->target(100);
|
||||
$payable = $this->target(80, $this->expenseAccount);
|
||||
$paid = $this->target(25);
|
||||
$this->booking('2020-01-10', 30, null, ['person_target_id' => $receivable->id]);
|
||||
$this->booking('2020-01-20', 20, null, ['person_target_id' => $receivable->id]);
|
||||
$this->booking('2020-01-20', 15, $this->expenseAccount, ['person_target_id' => $payable->id]);
|
||||
$this->booking('2020-01-20', 25, null, ['person_target_id' => $paid->id]);
|
||||
$response = $this->actingAs($this->user)->get('/targets')->assertStatus(200);
|
||||
$data = $response->original->getData();
|
||||
$this->assertEquals(['receivables' => 50, 'debts' => -65], $data['sums']);
|
||||
$this->assertSame([$receivable->id], $data['personTargets']['receivables']->pluck('id')->all());
|
||||
$this->assertSame([$payable->id], $data['personTargets']['debts']->pluck('id')->all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\SubBooking;
|
||||
use Tests\DatabaseTestCase;
|
||||
|
||||
class ReportsTest extends DatabaseTestCase
|
||||
{
|
||||
protected function seedCashFlows()
|
||||
{
|
||||
$this->booking('2019-12-31', 100);
|
||||
$this->booking('2020-01-01', 50.25);
|
||||
$this->booking('2020-01-31', 20.10, $this->expenseAccount);
|
||||
$this->booking('2020-03-01', 10, $this->expenseAccount);
|
||||
$this->booking('2020-12-31', 5.50);
|
||||
$this->booking('2021-01-01', 999);
|
||||
$this->actingAs($this->user);
|
||||
}
|
||||
|
||||
public function test_balance_includes_cutoff_day_and_excludes_future_bookings()
|
||||
{
|
||||
$this->seedCashFlows();
|
||||
$this->assertEquals(0, SubBooking::balanceAt('2019-12-30'));
|
||||
$this->assertEquals(100, SubBooking::balanceAt('2019-12-31'));
|
||||
$this->assertEquals(130.15, SubBooking::balanceAt('2020-01-31'), '', 0.001);
|
||||
$this->assertEquals(125.65, SubBooking::balanceAt('2020-12-31'), '', 0.001);
|
||||
}
|
||||
|
||||
public function test_interval_totals_include_both_endpoints_and_sign_expenses()
|
||||
{
|
||||
$this->seedCashFlows();
|
||||
$this->assertEquals(55.75, SubBooking::incomesInInterval('2020-01-01', '2020-12-31')->first()->incomes, '', 0.001);
|
||||
$this->assertEquals(-30.10, SubBooking::expedituresInInterval('2020-01-01', '2020-12-31')->first()->expeditures, '', 0.001);
|
||||
}
|
||||
|
||||
public function test_monthly_totals_fill_empty_months_across_a_year_boundary()
|
||||
{
|
||||
$this->booking('2019-12-31', 10);
|
||||
$this->booking('2020-02-01', 25);
|
||||
$months = SubBooking::incomesInInterval('2019-12-01', '2020-02-29', true);
|
||||
$this->assertSame([[2019, 12], [2020, 1], [2020, 2]], $months->map(function ($month) {
|
||||
return [(int) $month->year, (int) $month->month];
|
||||
})->all());
|
||||
$this->assertEquals([10, 0, 25], $months->pluck('incomes')->all());
|
||||
}
|
||||
|
||||
public function test_monthly_balance_report_carries_opening_balance_through_empty_months()
|
||||
{
|
||||
$this->seedCashFlows();
|
||||
$response = $this->get('/reports/balances/2020')->assertStatus(200)->assertViewIs('reports.balances');
|
||||
$data = $response->original->getData();
|
||||
$this->assertEquals(100, $data['startBalance']);
|
||||
$balances = $data['balances'];
|
||||
$this->assertCount(12, $balances);
|
||||
$this->assertEquals(50.25, $balances[0]->incomes);
|
||||
$this->assertEquals(-20.10, $balances[0]->expeditures, '', 0.001);
|
||||
$this->assertEquals(30.15, $balances[0]->monthBalance, '', 0.001);
|
||||
$this->assertEquals(130.15, $balances[0]->accountBalance, '', 0.001);
|
||||
$this->assertEquals(0, $balances[1]->monthBalance);
|
||||
$this->assertEquals(130.15, $balances[1]->accountBalance, '', 0.001);
|
||||
$this->assertEquals(120.15, $balances[2]->accountBalance, '', 0.001);
|
||||
$this->assertEquals(125.65, $balances[11]->accountBalance, '', 0.001);
|
||||
$this->assertSame('01.01.2021', $balances[11]->date);
|
||||
}
|
||||
|
||||
/** @dataProvider cashbookPaths */
|
||||
public function test_cashbook_and_print_view_filter_year_and_show_correct_totals($path)
|
||||
{
|
||||
$this->seedCashFlows();
|
||||
$response = $this->get($path)->assertStatus(200);
|
||||
$data = $response->original->getData();
|
||||
$this->assertCount(4, $data['bookings']->flatten());
|
||||
$this->assertEquals(100, $data['sums'][0]['balance']);
|
||||
$this->assertEquals(-20.10, $data['sums'][1]['expeditures'], '', 0.001);
|
||||
$this->assertEquals(150.25, $data['sums'][1]['incomes'], '', 0.001);
|
||||
$this->assertEquals(130.15, $data['sums'][1]['balance'], '', 0.001);
|
||||
$this->assertEquals(130.15, $data['sums'][2]['balance'], '', 0.001);
|
||||
$this->assertEquals(125.65, $data['sums'][12]['balance'], '', 0.001);
|
||||
}
|
||||
|
||||
public function cashbookPaths()
|
||||
{
|
||||
return [['/bookings/year-2020'], ['/bookings/year-2020/print']];
|
||||
}
|
||||
|
||||
/** @dataProvider compactReportPaths */
|
||||
public function test_scope_report_totals_match_cashbook_and_exclude_other_years($path)
|
||||
{
|
||||
$this->seedCashFlows();
|
||||
$response = $this->get($path)->assertStatus(200);
|
||||
$data = $response->original->getData();
|
||||
$this->assertEquals(100, $data['startBalance']);
|
||||
$this->assertEquals(25.65, $data['scopes']->sum('balance'), '', 0.001);
|
||||
$this->assertEquals(125.65, $data['endBalance'], '', 0.001);
|
||||
}
|
||||
|
||||
public function compactReportPaths()
|
||||
{
|
||||
return [['/reports/scopes/compact/2020'], ['/reports/scopes/compact/2020/print']];
|
||||
}
|
||||
|
||||
public function test_project_report_excludes_other_projects_and_orders_bookings_by_date()
|
||||
{
|
||||
$late = $this->booking('2020-03-01', 80);
|
||||
$early = $this->booking('2020-01-01', 20, $this->expenseAccount);
|
||||
$this->booking('2020-02-01', 999, null, ['project_id' => null]);
|
||||
$response = $this->actingAs($this->user)->get('/reports/projects/' . $this->project->id)
|
||||
->assertStatus(200)->assertSee($this->project->name);
|
||||
$data = $response->original->getData();
|
||||
$this->assertEquals(80, $data['incomes']);
|
||||
$this->assertEquals(20, $data['expeditures']);
|
||||
$this->assertSame([$early->id, $late->id], $data['projectBookings']->pluck('booking_id')->values()->all());
|
||||
}
|
||||
|
||||
public function test_reports_render_with_an_empty_database()
|
||||
{
|
||||
$this->actingAs($this->user);
|
||||
$response = $this->get('/reports/balances/2020')->assertStatus(200);
|
||||
$data = $response->original->getData();
|
||||
$this->assertEquals(0, $data['startBalance']);
|
||||
$this->assertCount(12, $data['balances']);
|
||||
foreach ($data['balances'] as $balance) {
|
||||
$this->assertEquals(0, $balance->accountBalance);
|
||||
}
|
||||
$this->get('/reports/scopes/compact/2020')->assertStatus(200)->assertViewHas('endBalance', 0);
|
||||
$this->get('/bookings/year-2020')->assertStatus(200);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user