Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

View File

@@ -0,0 +1,96 @@
<?php
namespace Tests\Feature\Admin;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
public function test_admin_can_list_users(): void
{
$admin = $this->actingAsAdmin();
$user = $this->createUser();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonFragment(['id' => $admin->id])
->assertJsonFragment(['id' => $user->id]);
}
public function test_non_admin_cannot_list_users(): void
{
$this->actingAsUser();
$this->getJson('/api/users')->assertStatus(403);
}
public function test_admin_can_create_user(): void
{
$this->actingAsAdmin();
$response = $this->postJson('/api/users', [
'name' => 'Test User',
'email' => 'test-user@example.com',
'password' => 'secretpass',
'is_admin' => true,
'is_active' => true,
]);
$response->assertStatus(201)
->assertJsonFragment(['email' => 'test-user@example.com']);
$this->assertDatabaseHas('users', [
'email' => 'test-user@example.com',
'is_admin' => 1,
'is_active' => 1,
]);
}
public function test_admin_can_update_user_and_password(): void
{
$this->actingAsAdmin();
$user = $this->createUser(['password' => 'oldpass']);
$response = $this->putJson("/api/users/{$user->id}", [
'name' => 'Updated Name',
'email' => 'updated@example.com',
'password' => 'newpass123',
'is_admin' => false,
'is_active' => true,
]);
$response->assertStatus(200)
->assertJsonFragment(['email' => 'updated@example.com']);
$user->refresh();
$this->assertSame('Updated Name', $user->name);
$this->assertTrue(Hash::check('newpass123', $user->password));
}
public function test_admin_can_deactivate_user(): void
{
$this->actingAsAdmin();
$user = $this->createUser(['is_active' => true]);
$response = $this->deleteJson("/api/users/{$user->id}");
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'is_active' => 0,
]);
}
public function test_admin_cannot_deactivate_self(): void
{
$admin = $this->actingAsAdmin();
$this->deleteJson("/api/users/{$admin->id}")
->assertStatus(422);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LoginControllerTest extends TestCase
{
use RefreshDatabase;
public function test_login_success_returns_user(): void
{
$user = $this->createUser([
'password' => 'demodemo',
]);
$response = $this->withSession([])->postJson('/api/login', [
'email' => $user->email,
'password' => 'demodemo',
]);
$response->assertStatus(200)
->assertJsonFragment([
'id' => $user->id,
'email' => $user->email,
]);
}
public function test_login_inactive_user_fails(): void
{
$user = $this->createInactiveUser([
'password' => 'demodemo',
]);
$response = $this->withSession([])->postJson('/api/login', [
'email' => $user->email,
'password' => 'demodemo',
]);
$response->assertStatus(422);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BandControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_bands(): void
{
$band = $this->createBand();
$response = $this->getJson('/api/bands');
$response->assertStatus(200)
->assertJsonFragment(['id' => $band->id]);
}
public function test_show_returns_band(): void
{
$band = $this->createBand();
$response = $this->getJson("/api/bands/{$band->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $band->id]);
}
public function test_admin_can_create_update_and_delete_band(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/bands', [
'name' => '144 MHz',
'order' => 1,
'edi_band_begin' => 144000000,
'edi_band_end' => 146000000,
'has_power_category' => true,
]);
$createResponse->assertStatus(201);
$bandId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/bands/{$bandId}", [
'name' => '144 MHz (upd)',
'order' => 2,
'edi_band_begin' => 144000000,
'edi_band_end' => 146000000,
'has_power_category' => false,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $bandId]);
$this->deleteJson("/api/bands/{$bandId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_band(): void
{
$this->actingAsUser();
$this->postJson('/api/bands', [
'name' => '144 MHz',
'order' => 1,
'edi_band_begin' => 144000000,
'edi_band_end' => 146000000,
'has_power_category' => true,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CategoryControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_categories(): void
{
$category = $this->createCategory();
$response = $this->getJson('/api/categories');
$response->assertStatus(200)
->assertJsonFragment(['id' => $category->id]);
}
public function test_show_returns_category(): void
{
$category = $this->createCategory();
$response = $this->getJson("/api/categories/{$category->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $category->id]);
}
public function test_admin_can_create_update_and_delete_category(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/categories', [
'name' => 'CAT-A',
'order' => 1,
]);
$createResponse->assertStatus(201);
$categoryId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/categories/{$categoryId}", [
'name' => 'CAT-B',
'order' => 2,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $categoryId]);
$this->deleteJson("/api/categories/{$categoryId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_category(): void
{
$this->actingAsUser();
$this->postJson('/api/categories', [
'name' => 'CAT-A',
'order' => 1,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ContestParameterControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_contest_parameters(): void
{
$item = $this->createContestParameter();
$response = $this->getJson('/api/contest-parameters');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_contest_parameter(): void
{
$item = $this->createContestParameter();
$response = $this->getJson("/api/contest-parameters/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_contest_parameter(): void
{
$this->actingAsAdmin();
$contest = $this->createContest();
$createResponse = $this->postJson('/api/contest-parameters', [
'contest_id' => $contest->id,
'log_type' => 'STANDARD',
'ignore_slash_part' => true,
'ignore_third_part' => true,
'letters_in_rst' => true,
'discard_qso_rec_diff_call' => true,
'discard_qso_sent_diff_call' => false,
'discard_qso_rec_diff_rst' => true,
'discard_qso_sent_diff_rst' => false,
'discard_qso_rec_diff_code' => true,
'discard_qso_sent_diff_code' => false,
'unique_qso' => true,
'time_tolerance' => 600,
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/contest-parameters/{$itemId}", [
'contest_id' => $contest->id,
'log_type' => 'CHECK',
'ignore_slash_part' => false,
'ignore_third_part' => true,
'letters_in_rst' => false,
'discard_qso_rec_diff_call' => true,
'discard_qso_sent_diff_call' => false,
'discard_qso_rec_diff_rst' => true,
'discard_qso_sent_diff_rst' => false,
'discard_qso_rec_diff_code' => true,
'discard_qso_sent_diff_code' => false,
'unique_qso' => false,
'time_tolerance' => 300,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/contest-parameters/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_contest_parameter(): void
{
$this->actingAsUser();
$contest = $this->createContest();
$this->postJson('/api/contest-parameters', [
'contest_id' => $contest->id,
'log_type' => 'STANDARD',
'ignore_slash_part' => true,
'ignore_third_part' => true,
'letters_in_rst' => true,
'discard_qso_rec_diff_call' => true,
'discard_qso_sent_diff_call' => false,
'discard_qso_rec_diff_rst' => true,
'discard_qso_sent_diff_rst' => false,
'discard_qso_rec_diff_code' => true,
'discard_qso_sent_diff_code' => false,
'unique_qso' => true,
'time_tolerance' => 600,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CountryWwlControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_country_wwl_records(): void
{
$item = $this->createCountryWwl();
$response = $this->getJson('/api/countries-wwl');
$response->assertStatus(200)
->assertJsonFragment([
'country_name' => $item->country_name,
'wwl' => $item->wwl,
]);
}
public function test_admin_can_create_country_wwl(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/countries-wwl', [
'country_name' => 'Test Country',
'wwl' => 'AA00',
]);
$createResponse->assertStatus(201);
}
public function test_non_admin_cannot_create_country_wwl(): void
{
$this->actingAsUser();
$this->postJson('/api/countries-wwl', [
'country_name' => 'Test Country',
'wwl' => 'AA00',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CtyControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_cty_records(): void
{
$item = $this->createCty();
$response = $this->getJson('/api/cty');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_cty_record(): void
{
$item = $this->createCty();
$response = $this->getJson("/api/cty/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_cty_record(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/cty', [
'country_name' => 'Test Country',
'dxcc' => 999,
'cq_zone' => 10,
'itu_zone' => 20,
'continent' => 'EU',
'latitude' => 10.0,
'longitude' => 20.0,
'time_offset' => 1.0,
'prefix' => 'TST',
'prefix_norm' => 'TST',
'precise' => true,
'source' => 'test',
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/cty/{$itemId}", [
'country_name' => 'Test Country 2',
'dxcc' => 999,
'cq_zone' => 10,
'itu_zone' => 20,
'continent' => 'EU',
'latitude' => 10.0,
'longitude' => 20.0,
'time_offset' => 1.0,
'prefix' => 'TST2',
'prefix_norm' => 'TST2',
'precise' => false,
'source' => 'test',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/cty/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_cty_record(): void
{
$this->actingAsUser();
$this->postJson('/api/cty', [
'country_name' => 'Test Country',
'dxcc' => 999,
'cq_zone' => 10,
'itu_zone' => 20,
'continent' => 'EU',
'latitude' => 10.0,
'longitude' => 20.0,
'time_offset' => 1.0,
'prefix' => 'TST',
'precise' => true,
'source' => 'test',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EdiBandControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_edi_bands(): void
{
$item = $this->createEdiBand();
$response = $this->getJson('/api/edi-bands');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_edi_band(): void
{
$item = $this->createEdiBand();
$response = $this->getJson("/api/edi-bands/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_edi_band(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/edi-bands', [
'value' => 'EDI-144',
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/edi-bands/{$itemId}", [
'value' => 'EDI-432',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/edi-bands/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_edi_band(): void
{
$this->actingAsUser();
$this->postJson('/api/edi-bands', [
'value' => 'EDI-144',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EdiCategoryControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_edi_categories(): void
{
$item = $this->createEdiCategory();
$response = $this->getJson('/api/edi-categories');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_edi_category(): void
{
$item = $this->createEdiCategory();
$response = $this->getJson("/api/edi-categories/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_edi_category(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/edi-categories', [
'value' => 'EDI-CAT-A',
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/edi-categories/{$itemId}", [
'value' => 'EDI-CAT-B',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/edi-categories/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_edi_category(): void
{
$this->actingAsUser();
$this->postJson('/api/edi-categories', [
'value' => 'EDI-CAT-A',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EvaluationRuleSetControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_rule_sets(): void
{
$item = $this->createRuleSet();
$response = $this->getJson('/api/evaluation-rule-sets');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_rule_set(): void
{
$item = $this->createRuleSet();
$response = $this->getJson("/api/evaluation-rule-sets/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_rule_set(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/evaluation-rule-sets', [
'name' => 'Test ruleset',
'code' => 'TEST_RULES',
'scoring_mode' => 'DISTANCE',
'multiplier_type' => 'WWL',
'dup_qso_policy' => 'ZERO_POINTS',
'nil_qso_policy' => 'ZERO_POINTS',
'busted_call_policy' => 'ZERO_POINTS',
'busted_exchange_policy' => 'ZERO_POINTS',
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/evaluation-rule-sets/{$itemId}", [
'name' => 'Updated ruleset',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/evaluation-rule-sets/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_rule_set(): void
{
$this->actingAsUser();
$this->postJson('/api/evaluation-rule-sets', [
'name' => 'Test ruleset',
'code' => 'TEST_RULES',
'scoring_mode' => 'DISTANCE',
'multiplier_type' => 'WWL',
'dup_qso_policy' => 'ZERO_POINTS',
'nil_qso_policy' => 'ZERO_POINTS',
'busted_call_policy' => 'ZERO_POINTS',
'busted_exchange_policy' => 'ZERO_POINTS',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\Feature\Catalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PowerCategoryControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_power_categories(): void
{
$item = $this->createPowerCategory();
$response = $this->getJson('/api/power-categories');
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_show_returns_power_category(): void
{
$item = $this->createPowerCategory();
$response = $this->getJson("/api/power-categories/{$item->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $item->id]);
}
public function test_admin_can_create_update_and_delete_power_category(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/power-categories', [
'name' => 'PWR-A',
'order' => 1,
'power_level' => 100,
]);
$createResponse->assertStatus(201);
$itemId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/power-categories/{$itemId}", [
'name' => 'PWR-B',
'order' => 2,
'power_level' => 200,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $itemId]);
$this->deleteJson("/api/power-categories/{$itemId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_power_category(): void
{
$this->actingAsUser();
$this->postJson('/api/power-categories', [
'name' => 'PWR-A',
'order' => 1,
'power_level' => 100,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature\Contests;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ContestControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_contests(): void
{
$contest = $this->createContest();
$response = $this->getJson('/api/contests');
$response->assertStatus(200)
->assertJsonFragment(['id' => $contest->id]);
}
public function test_index_can_filter_only_active(): void
{
$active = $this->createContest(['is_active' => true]);
$inactive = $this->createContest(['is_active' => false]);
$response = $this->getJson('/api/contests?only_active=1');
$response->assertStatus(200)
->assertJsonFragment(['id' => $active->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertFalse(in_array($inactive->id, $ids, true));
}
public function test_show_returns_contest(): void
{
$contest = $this->createContest();
$response = $this->getJson("/api/contests/{$contest->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $contest->id]);
}
public function test_admin_can_create_update_and_delete_contest(): void
{
$this->actingAsAdmin();
$ruleSet = $this->createRuleSet();
$createResponse = $this->postJson('/api/contests', [
'name' => ['cs' => 'Test soutěž', 'en' => 'Test contest'],
'description' => ['cs' => 'Popis', 'en' => 'Description'],
'rule_set_id' => $ruleSet->id,
'is_active' => true,
]);
$createResponse->assertStatus(201);
$contestId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/contests/{$contestId}", [
'name' => ['cs' => 'Upraveno', 'en' => 'Updated'],
'description' => ['cs' => 'Popis', 'en' => 'Description'],
'is_active' => false,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $contestId]);
$this->deleteJson("/api/contests/{$contestId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_contest(): void
{
$this->actingAsUser();
$this->postJson('/api/contests', [
'name' => ['cs' => 'Test soutěž', 'en' => 'Test contest'],
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Tests\Feature\Evaluation;
use App\Jobs\AggregateLogResultsJob;
use App\Models\LogResult;
use App\Models\QsoResult;
use App\Models\WorkingQso;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AggregateLogResultsJobTest extends TestCase
{
use RefreshDatabase;
private function createQso(int $runId, int $logId, int $bandId, Carbon $ts, int $points): int
{
$logQso = $this->createLogQso(['log_id' => $logId]);
$this->createQsoResult([
'evaluation_run_id' => $runId,
'log_qso_id' => $logQso->id,
'is_valid' => true,
'points' => $points,
]);
WorkingQso::create([
'evaluation_run_id' => $runId,
'log_qso_id' => $logQso->id,
'log_id' => $logId,
'ts_utc' => $ts,
'band_id' => $bandId,
'call_norm' => 'OK1AAA',
'rcall_norm' => 'OK1BBB',
'loc_norm' => 'JN00AA',
'rloc_norm' => 'JN00AA',
]);
return $logQso->id;
}
public function test_aggregate_applies_operating_window_for_6h(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
'operating_window_mode' => 'BEST_CONTIGUOUS',
'operating_window_hours' => 6,
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun([
'round_id' => $round->id,
'rule_set_id' => $ruleSet->id,
]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC');
$t2 = Carbon::create(2025, 1, 1, 7, 0, 0, 'UTC');
$t3 = Carbon::create(2025, 1, 1, 8, 0, 0, 'UTC');
$firstA = $this->createQso($run->id, $log->id, $band->id, $t0, 10);
$firstB = $this->createQso($run->id, $log->id, $band->id, $t1, 10);
$secondA = $this->createQso($run->id, $log->id, $band->id, $t2, 10);
$secondB = $this->createQso($run->id, $log->id, $band->id, $t3, 10);
$this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'sixhr_category' => true,
'status' => 'OK',
]);
(new AggregateLogResultsJob($run->id, $log->id))->handle();
$result = LogResult::where('evaluation_run_id', $run->id)
->where('log_id', $log->id)
->first();
$this->assertNotNull($result);
$this->assertSame(40, $result->official_score);
$this->assertSame($t0->toDateTimeString(), $result->operating_window_start_utc?->toDateTimeString());
$this->assertSame($t1->toDateTimeString(), $result->operating_window_end_utc?->toDateTimeString());
$this->assertSame($t2->toDateTimeString(), $result->operating_window_2_start_utc?->toDateTimeString());
$this->assertSame($t3->toDateTimeString(), $result->operating_window_2_end_utc?->toDateTimeString());
$this->assertSame(6, $result->operating_window_hours);
$this->assertSame(4, $result->operating_window_qso_count);
$included = QsoResult::where('evaluation_run_id', $run->id)
->whereIn('log_qso_id', [$firstA, $firstB, $secondA, $secondB])
->pluck('is_operating_window_excluded')
->all();
$this->assertSame([false, false, false, false], $included);
$excludedCount = QsoResult::where('evaluation_run_id', $run->id)
->where('is_operating_window_excluded', true)
->count();
$this->assertSame(0, $excludedCount);
}
public function test_aggregate_keeps_all_qso_for_non_6h(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
'operating_window_mode' => 'BEST_CONTIGUOUS',
'operating_window_hours' => 6,
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun([
'round_id' => $round->id,
'rule_set_id' => $ruleSet->id,
]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC');
$t2 = Carbon::create(2025, 1, 1, 7, 0, 0, 'UTC');
$t3 = Carbon::create(2025, 1, 1, 8, 0, 0, 'UTC');
$this->createQso($run->id, $log->id, $band->id, $t0, 10);
$this->createQso($run->id, $log->id, $band->id, $t1, 10);
$this->createQso($run->id, $log->id, $band->id, $t2, 10);
$this->createQso($run->id, $log->id, $band->id, $t3, 10);
$this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'sixhr_category' => false,
'status' => 'OK',
]);
(new AggregateLogResultsJob($run->id, $log->id))->handle();
$result = LogResult::where('evaluation_run_id', $run->id)
->where('log_id', $log->id)
->first();
$this->assertNotNull($result);
$this->assertSame(40, $result->official_score);
$this->assertNull($result->operating_window_start_utc);
$this->assertNull($result->operating_window_end_utc);
$this->assertNull($result->operating_window_2_start_utc);
$this->assertNull($result->operating_window_2_end_utc);
$this->assertNull($result->operating_window_hours);
$this->assertNull($result->operating_window_qso_count);
$excludedCount = QsoResult::where('evaluation_run_id', $run->id)
->where('is_operating_window_excluded', true)
->count();
$this->assertSame(0, $excludedCount);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Tests\Feature\Evaluation;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EvaluationRunControllerTest extends TestCase
{
use RefreshDatabase;
public function test_cancel_requires_authentication(): void
{
$run = $this->createEvaluationRun();
$this->postJson("/api/evaluation-runs/{$run->id}/cancel")
->assertStatus(401);
}
public function test_non_admin_cannot_cancel(): void
{
$this->actingAsUser();
$run = $this->createEvaluationRun(['status' => 'RUNNING']);
$this->postJson("/api/evaluation-runs/{$run->id}/cancel")
->assertStatus(403);
}
public function test_admin_can_cancel_running_run(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun(['status' => 'RUNNING']);
$response = $this->postJson("/api/evaluation-runs/{$run->id}/cancel");
$response->assertStatus(200)
->assertJsonFragment(['status' => 'canceled']);
$this->assertDatabaseHas('evaluation_runs', [
'id' => $run->id,
'status' => 'CANCELED',
]);
}
public function test_cancel_rejects_finished_runs(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun(['status' => 'SUCCEEDED']);
$this->postJson("/api/evaluation-runs/{$run->id}/cancel")
->assertStatus(409);
}
public function test_admin_can_set_result_type_and_update_round(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun(['result_type' => null]);
$round = $run->round;
$response = $this->postJson("/api/evaluation-runs/{$run->id}/result-type", [
'result_type' => 'PRELIMINARY',
]);
$response->assertStatus(200)
->assertJsonFragment(['result_type' => 'PRELIMINARY']);
$round->refresh();
$this->assertSame($run->id, $round->preliminary_evaluation_run_id);
$this->assertNull($round->official_evaluation_run_id);
$this->assertNull($round->test_evaluation_run_id);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Tests\Feature\Evaluation;
use App\Jobs\RecalculateOfficialRanksJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RecalculateOfficialRanksSixhrModeTest extends TestCase
{
use RefreshDatabase;
public function test_iaru_sixhr_merges_single_and_multi(): void
{
$ruleSet = $this->createRuleSet(['sixhr_ranking_mode' => 'IARU']);
$round = $this->createRound();
$run = $this->createEvaluationRun(['round_id' => $round->id, 'rule_set_id' => $ruleSet->id]);
$band = $this->createBand();
$single = $this->createCategory(['name' => 'SINGLE']);
$multi = $this->createCategory(['name' => 'MULTI']);
$logSingle = $this->createLog(['round_id' => $round->id]);
$logMulti = $this->createLog(['round_id' => $round->id]);
$resSingle = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logSingle->id,
'band_id' => $band->id,
'category_id' => $single->id,
'official_score' => 200,
'valid_qso_count' => 10,
'status' => 'OK',
'sixhr_category' => true,
]);
$resMulti = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logMulti->id,
'band_id' => $band->id,
'category_id' => $multi->id,
'official_score' => 150,
'valid_qso_count' => 8,
'status' => 'OK',
'sixhr_category' => true,
]);
(new RecalculateOfficialRanksJob($run->id))->handle();
$resSingle->refresh();
$resMulti->refresh();
$this->assertSame(1, $resSingle->rank_overall);
$this->assertSame(2, $resMulti->rank_overall);
$this->assertSame('ALL', $resSingle->sixhr_ranking_bucket);
$this->assertSame('ALL', $resMulti->sixhr_ranking_bucket);
}
public function test_crk_sixhr_keeps_single_and_multi_separate(): void
{
$ruleSet = $this->createRuleSet(['sixhr_ranking_mode' => 'CRK']);
$round = $this->createRound();
$run = $this->createEvaluationRun(['round_id' => $round->id, 'rule_set_id' => $ruleSet->id]);
$band = $this->createBand();
$single = $this->createCategory(['name' => 'SINGLE']);
$multi = $this->createCategory(['name' => 'MULTI']);
$logSingle = $this->createLog(['round_id' => $round->id]);
$logMulti = $this->createLog(['round_id' => $round->id]);
$resSingle = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logSingle->id,
'band_id' => $band->id,
'category_id' => $single->id,
'official_score' => 200,
'valid_qso_count' => 10,
'status' => 'OK',
'sixhr_category' => true,
]);
$resMulti = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logMulti->id,
'band_id' => $band->id,
'category_id' => $multi->id,
'official_score' => 150,
'valid_qso_count' => 8,
'status' => 'OK',
'sixhr_category' => true,
]);
(new RecalculateOfficialRanksJob($run->id))->handle();
$resSingle->refresh();
$resMulti->refresh();
$this->assertSame(1, $resSingle->rank_overall);
$this->assertSame(1, $resMulti->rank_overall);
$this->assertSame('SINGLE', $resSingle->sixhr_ranking_bucket);
$this->assertSame('MULTI', $resMulti->sixhr_ranking_bucket);
}
}

View File

@@ -0,0 +1,7 @@
<?php
test('the application returns a successful response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});

View File

@@ -0,0 +1,86 @@
<?php
namespace Tests\Feature\Files;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FileControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_files(): void
{
$file = $this->createFile();
$response = $this->getJson('/api/files');
$response->assertStatus(200)
->assertJsonFragment(['id' => $file->id]);
}
public function test_show_returns_file_metadata(): void
{
$file = $this->createFile();
$response = $this->getJson("/api/files/{$file->id}");
$response->assertStatus(200)
->assertJsonFragment([
'id' => $file->id,
'filename' => $file->filename,
]);
}
public function test_store_allows_guest_before_deadline(): void
{
Storage::fake();
Queue::fake();
$round = $this->createRound(['logs_deadline' => now()->addDay()]);
$response = $this->postJson('/api/files', [
'round_id' => $round->id,
'file' => UploadedFile::fake()->create('log.edi', 5, 'text/plain'),
]);
$response->assertStatus(201);
}
public function test_content_returns_stored_file(): void
{
Storage::fake();
$file = $this->createFile(['path' => 'uploads/test/file.edi', 'mimetype' => 'text/plain']);
Storage::put($file->path, 'TEST');
$response = $this->get("/api/files/{$file->id}/content");
$response->assertStatus(200);
$this->assertSame('TEST', $response->getContent());
}
public function test_delete_requires_auth(): void
{
$file = $this->createFile();
$this->deleteJson("/api/files/{$file->id}")
->assertStatus(401);
}
public function test_admin_can_delete_file(): void
{
Storage::fake();
$this->actingAsAdmin();
$file = $this->createFile(['path' => 'uploads/test/file.edi']);
Storage::put($file->path, 'TEST');
$this->deleteJson("/api/files/{$file->id}")
->assertStatus(204);
Storage::assertMissing($file->path);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature\Logs;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LogControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_logs(): void
{
$log = $this->createLog();
$response = $this->getJson('/api/logs');
$response->assertStatus(200)
->assertJsonFragment(['id' => $log->id]);
}
public function test_index_can_filter_by_round(): void
{
$roundA = $this->createRound();
$roundB = $this->createRound();
$logA = $this->createLog(['round_id' => $roundA->id]);
$this->createLog(['round_id' => $roundB->id]);
$response = $this->getJson("/api/logs?round_id={$roundA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $logA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_log(): void
{
$log = $this->createLog();
$response = $this->getJson("/api/logs/{$log->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $log->id]);
}
public function test_admin_can_create_update_and_delete_log(): void
{
$this->actingAsAdmin();
$round = $this->createRound();
$createResponse = $this->postJson('/api/logs', [
'round_id' => $round->id,
'pcall' => 'OK1ABC',
]);
$createResponse->assertStatus(201);
$logId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/logs/{$logId}", [
'pcall' => 'OK1DEF',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $logId]);
$this->deleteJson("/api/logs/{$logId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_log(): void
{
$this->actingAsUser();
$round = $this->createRound();
$this->postJson('/api/logs', [
'round_id' => $round->id,
'pcall' => 'OK1ABC',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Tests\Feature\Logs;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LogQsoControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_log_qsos(): void
{
$qso = $this->createLogQso();
$response = $this->getJson('/api/log-qsos');
$response->assertStatus(200)
->assertJsonFragment(['id' => $qso->id]);
}
public function test_index_can_filter_by_log_id(): void
{
$logA = $this->createLog();
$logB = $this->createLog();
$qsoA = $this->createLogQso(['log_id' => $logA->id]);
$this->createLogQso(['log_id' => $logB->id]);
$response = $this->getJson("/api/log-qsos?log_id={$logA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $qsoA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_log_qso(): void
{
$qso = $this->createLogQso();
$response = $this->getJson("/api/log-qsos/{$qso->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $qso->id]);
}
public function test_admin_can_create_update_and_delete_log_qso(): void
{
$this->actingAsAdmin();
$log = $this->createLog();
$createResponse = $this->postJson('/api/log-qsos', [
'log_id' => $log->id,
'my_call' => 'OK1ABC',
'dx_call' => 'OK2ABC',
]);
$createResponse->assertStatus(201);
$qsoId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/log-qsos/{$qsoId}", [
'dx_call' => 'OK9XYZ',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $qsoId]);
$this->deleteJson("/api/log-qsos/{$qsoId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_log_qso(): void
{
$this->actingAsUser();
$log = $this->createLog();
$this->postJson('/api/log-qsos', [
'log_id' => $log->id,
'my_call' => 'OK1ABC',
'dx_call' => 'OK2ABC',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Tests\Feature\Logs;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LogQsoTableTest extends TestCase
{
use RefreshDatabase;
public function test_qso_table_uses_latest_succeeded_non_claimed_run_by_default(): void
{
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$qsoA = $this->createLogQso([
'log_id' => $log->id,
'qso_index' => 1,
'dx_call' => 'OK1AAA',
]);
$qsoB = $this->createLogQso([
'log_id' => $log->id,
'qso_index' => 2,
'dx_call' => 'OK1BBB',
]);
$claimedRun = $this->createEvaluationRun([
'round_id' => $round->id,
'rules_version' => 'CLAIMED',
'status' => 'SUCCEEDED',
]);
$this->createQsoResult([
'evaluation_run_id' => $claimedRun->id,
'log_qso_id' => $qsoA->id,
'error_code' => 'DUP',
]);
$officialRun = $this->createEvaluationRun([
'round_id' => $round->id,
'rules_version' => 'OFFICIAL',
'status' => 'SUCCEEDED',
]);
$this->createQsoResult([
'evaluation_run_id' => $officialRun->id,
'log_qso_id' => $qsoA->id,
'error_code' => 'OK',
'penalty_points' => 5,
]);
$this->createQsoOverride([
'evaluation_run_id' => $officialRun->id,
'log_qso_id' => $qsoB->id,
'forced_status' => 'AUTO',
'reason' => 'Manual override',
]);
$response = $this->getJson("/api/logs/{$log->id}/qso-table");
$response->assertStatus(200)
->assertJsonFragment(['evaluation_run_id' => $officialRun->id]);
$rows = collect($response->json('data'));
$this->assertCount(2, $rows);
$rowA = $rows->firstWhere('id', $qsoA->id);
$rowB = $rows->firstWhere('id', $qsoB->id);
$this->assertSame('OK1AAA', $rowA['dx_call']);
$this->assertSame('OK', $rowA['result']['error_code']);
$this->assertSame(5, $rowA['result']['penalty_points']);
$this->assertNull($rowA['override']);
$this->assertSame('OK1BBB', $rowB['dx_call']);
$this->assertNull($rowB['result']);
$this->assertSame('Manual override', $rowB['override']['reason']);
}
public function test_qso_table_respects_explicit_evaluation_run_id(): void
{
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$qso = $this->createLogQso([
'log_id' => $log->id,
'qso_index' => 1,
]);
$claimedRun = $this->createEvaluationRun([
'round_id' => $round->id,
'rules_version' => 'CLAIMED',
'status' => 'SUCCEEDED',
]);
$this->createQsoResult([
'evaluation_run_id' => $claimedRun->id,
'log_qso_id' => $qso->id,
'error_code' => 'DUP',
]);
$officialRun = $this->createEvaluationRun([
'round_id' => $round->id,
'rules_version' => 'OFFICIAL',
'status' => 'SUCCEEDED',
]);
$this->createQsoResult([
'evaluation_run_id' => $officialRun->id,
'log_qso_id' => $qso->id,
'error_code' => 'OK',
]);
$response = $this->getJson("/api/logs/{$log->id}/qso-table?evaluation_run_id={$claimedRun->id}");
$response->assertStatus(200)
->assertJsonFragment(['evaluation_run_id' => $claimedRun->id]);
$rows = collect($response->json('data'));
$row = $rows->firstWhere('id', $qso->id);
$this->assertSame('DUP', $row['result']['error_code']);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Tests\Feature\News;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class NewsPostControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_only_published_by_default(): void
{
$published = $this->createNewsPost();
$unpublished = $this->createNewsPost(['is_published' => false, 'published_at' => null]);
$response = $this->getJson('/api/news');
$response->assertStatus(200)
->assertJsonFragment(['id' => $published->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertFalse(in_array($unpublished->id, $ids, true));
}
public function test_show_returns_404_for_unpublished(): void
{
$news = $this->createNewsPost(['is_published' => false, 'published_at' => null]);
$this->getJson("/api/news/{$news->slug}")
->assertStatus(404);
}
public function test_admin_can_create_update_and_delete_news(): void
{
$this->actingAsAdmin();
$createResponse = $this->postJson('/api/news', [
'title' => ['cs' => 'Novinka', 'en' => 'News'],
'content' => ['cs' => 'Obsah', 'en' => 'Content'],
'excerpt' => ['cs' => 'Krátce', 'en' => 'Short'],
'is_published' => true,
]);
$createResponse->assertStatus(201);
$slug = $createResponse->json('slug');
$updateResponse = $this->putJson("/api/news/{$slug}", [
'title' => ['cs' => 'Novinka 2', 'en' => 'News 2'],
'content' => ['cs' => 'Obsah', 'en' => 'Content'],
'excerpt' => ['cs' => 'Krátce', 'en' => 'Short'],
]);
$updateResponse->assertStatus(200)
->assertJsonStructure(['slug']);
$updatedSlug = $updateResponse->json('slug');
$this->deleteJson("/api/news/{$updatedSlug}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_news(): void
{
$this->actingAsUser();
$this->postJson('/api/news', [
'title' => ['cs' => 'Novinka', 'en' => 'News'],
'content' => ['cs' => 'Obsah', 'en' => 'Content'],
'excerpt' => ['cs' => 'Krátce', 'en' => 'Short'],
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Tests\Feature\Results;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LogOverrideControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_log_overrides(): void
{
$override = $this->createLogOverride();
$response = $this->getJson('/api/log-overrides');
$response->assertStatus(200)
->assertJsonFragment(['id' => $override->id]);
}
public function test_index_can_filter_by_evaluation_run(): void
{
$runA = $this->createEvaluationRun();
$runB = $this->createEvaluationRun();
$overrideA = $this->createLogOverride(['evaluation_run_id' => $runA->id]);
$this->createLogOverride(['evaluation_run_id' => $runB->id]);
$response = $this->getJson("/api/log-overrides?evaluation_run_id={$runA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $overrideA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_log_override(): void
{
$override = $this->createLogOverride();
$response = $this->getJson("/api/log-overrides/{$override->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $override->id]);
}
public function test_admin_can_create_update_and_delete_override(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun();
$log = $this->createLog(['round_id' => $run->round_id]);
$createResponse = $this->postJson('/api/log-overrides', [
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'forced_log_status' => 'CHECK',
'reason' => 'Test důvod',
]);
$createResponse->assertStatus(201);
$overrideId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/log-overrides/{$overrideId}", [
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'forced_log_status' => 'OK',
'reason' => 'Aktualizace',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $overrideId]);
$this->deleteJson("/api/log-overrides/{$overrideId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_override(): void
{
$this->actingAsUser();
$run = $this->createEvaluationRun();
$log = $this->createLog(['round_id' => $run->round_id]);
$this->postJson('/api/log-overrides', [
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'forced_log_status' => 'CHECK',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Tests\Feature\Results;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LogResultControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_filters_by_evaluation_run(): void
{
$runA = $this->createEvaluationRun();
$runB = $this->createEvaluationRun();
$logA = $this->createLog(['round_id' => $runA->round_id]);
$logB = $this->createLog(['round_id' => $runB->round_id]);
$resultA = $this->createLogResult([
'evaluation_run_id' => $runA->id,
'log_id' => $logA->id,
]);
$this->createLogResult([
'evaluation_run_id' => $runB->id,
'log_id' => $logB->id,
]);
$response = $this->getJson("/api/log-results?evaluation_run_id={$runA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $resultA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_index_resolves_claimed_run_for_round(): void
{
$round = $this->createRound();
$run = $this->createEvaluationRun([
'round_id' => $round->id,
'rules_version' => 'CLAIMED',
]);
$log = $this->createLog(['round_id' => $round->id]);
$result = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
]);
$response = $this->getJson("/api/log-results?round_id={$round->id}&status=CLAIMED");
$response->assertStatus(200)
->assertJsonFragment(['id' => $result->id]);
}
public function test_index_resolves_auto_result_type_from_round(): void
{
$round = $this->createRound();
$run = $this->createEvaluationRun(['round_id' => $round->id]);
$round->update(['official_evaluation_run_id' => $run->id]);
$log = $this->createLog(['round_id' => $round->id]);
$result = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
]);
$response = $this->getJson("/api/log-results?round_id={$round->id}&result_type=AUTO");
$response->assertStatus(200)
->assertJsonFragment(['id' => $result->id]);
}
public function test_index_only_ok_filters_by_callsign(): void
{
$round = $this->createRound();
$run = $this->createEvaluationRun(['round_id' => $round->id]);
$logOk = $this->createLog([
'round_id' => $round->id,
'pcall' => 'OK1ABC',
]);
$logOther = $this->createLog([
'round_id' => $round->id,
'pcall' => 'DL1ABC',
]);
$resultOk = $this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logOk->id,
]);
$this->createLogResult([
'evaluation_run_id' => $run->id,
'log_id' => $logOther->id,
]);
$response = $this->getJson("/api/log-results?evaluation_run_id={$run->id}&round_id={$round->id}&only_ok=1");
$response->assertStatus(200)
->assertJsonFragment(['id' => $resultOk->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_admin_can_create_update_and_delete_log_result(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun();
$log = $this->createLog(['round_id' => $run->round_id]);
$createResponse = $this->postJson('/api/log-results', [
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
'claimed_qso_count' => 10,
'claimed_score' => 100,
]);
$createResponse->assertStatus(201);
$resultId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/log-results/{$resultId}", [
'claimed_score' => 200,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $resultId]);
$this->deleteJson("/api/log-results/{$resultId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_log_result(): void
{
$this->actingAsUser();
$run = $this->createEvaluationRun();
$log = $this->createLog(['round_id' => $run->round_id]);
$this->postJson('/api/log-results', [
'evaluation_run_id' => $run->id,
'log_id' => $log->id,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Tests\Feature\Results;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class QsoOverrideControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_qso_overrides(): void
{
$override = $this->createQsoOverride();
$response = $this->getJson('/api/qso-overrides');
$response->assertStatus(200)
->assertJsonFragment(['id' => $override->id]);
}
public function test_index_can_filter_by_evaluation_run(): void
{
$runA = $this->createEvaluationRun();
$runB = $this->createEvaluationRun();
$overrideA = $this->createQsoOverride(['evaluation_run_id' => $runA->id]);
$this->createQsoOverride(['evaluation_run_id' => $runB->id]);
$response = $this->getJson("/api/qso-overrides?evaluation_run_id={$runA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $overrideA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_qso_override(): void
{
$override = $this->createQsoOverride();
$response = $this->getJson("/api/qso-overrides/{$override->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $override->id]);
}
public function test_admin_can_create_update_and_delete_override(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun();
$logQso = $this->createLogQso();
$createResponse = $this->postJson('/api/qso-overrides', [
'evaluation_run_id' => $run->id,
'log_qso_id' => $logQso->id,
'forced_status' => 'VALID',
'reason' => 'Test důvod',
]);
$createResponse->assertStatus(201);
$overrideId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/qso-overrides/{$overrideId}", [
'evaluation_run_id' => $run->id,
'log_qso_id' => $logQso->id,
'forced_status' => 'INVALID',
'reason' => 'Aktualizace',
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $overrideId]);
$this->deleteJson("/api/qso-overrides/{$overrideId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_override(): void
{
$this->actingAsUser();
$run = $this->createEvaluationRun();
$logQso = $this->createLogQso();
$this->postJson('/api/qso-overrides', [
'evaluation_run_id' => $run->id,
'log_qso_id' => $logQso->id,
'forced_status' => 'VALID',
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Tests\Feature\Results;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class QsoResultControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_qso_results(): void
{
$result = $this->createQsoResult();
$response = $this->getJson('/api/qso-results');
$response->assertStatus(200)
->assertJsonFragment(['id' => $result->id]);
}
public function test_index_can_filter_by_evaluation_run(): void
{
$runA = $this->createEvaluationRun();
$runB = $this->createEvaluationRun();
$resultA = $this->createQsoResult(['evaluation_run_id' => $runA->id]);
$this->createQsoResult(['evaluation_run_id' => $runB->id]);
$response = $this->getJson("/api/qso-results?evaluation_run_id={$runA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $resultA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_qso_result(): void
{
$result = $this->createQsoResult();
$response = $this->getJson("/api/qso-results/{$result->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $result->id]);
}
public function test_admin_can_create_update_and_delete_qso_result(): void
{
$this->actingAsAdmin();
$run = $this->createEvaluationRun();
$logQso = $this->createLogQso();
$createResponse = $this->postJson('/api/qso-results', [
'evaluation_run_id' => $run->id,
'log_qso_id' => $logQso->id,
'points' => 50,
]);
$createResponse->assertStatus(201);
$resultId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/qso-results/{$resultId}", [
'points' => 75,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $resultId]);
$this->deleteJson("/api/qso-results/{$resultId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_qso_result(): void
{
$this->actingAsUser();
$run = $this->createEvaluationRun();
$logQso = $this->createLogQso();
$this->postJson('/api/qso-results', [
'evaluation_run_id' => $run->id,
'log_qso_id' => $logQso->id,
])->assertStatus(403);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Tests\Feature\Rounds;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RoundControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_rounds(): void
{
$round = $this->createRound();
$response = $this->getJson('/api/rounds');
$response->assertStatus(200)
->assertJsonFragment(['id' => $round->id]);
}
public function test_index_can_filter_by_contest(): void
{
$contestA = $this->createContest();
$contestB = $this->createContest();
$roundA = $this->createRound(['contest_id' => $contestA->id]);
$this->createRound(['contest_id' => $contestB->id]);
$response = $this->getJson("/api/rounds?contest_id={$contestA->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $roundA->id]);
$ids = collect($response->json('data'))->pluck('id')->all();
$this->assertCount(1, $ids);
}
public function test_show_returns_round(): void
{
$round = $this->createRound();
$response = $this->getJson("/api/rounds/{$round->id}");
$response->assertStatus(200)
->assertJsonFragment(['id' => $round->id]);
}
public function test_admin_can_create_update_and_delete_round(): void
{
$this->actingAsAdmin();
$contest = $this->createContest();
$ruleSet = $this->createRuleSet();
$start = now()->addDay();
$end = (clone $start)->addHours(2);
$deadline = (clone $end)->addDays(2);
$createResponse = $this->postJson('/api/rounds', [
'contest_id' => $contest->id,
'rule_set_id' => $ruleSet->id,
'name' => ['cs' => 'Kolo 1', 'en' => 'Round 1'],
'description' => ['cs' => 'Popis', 'en' => 'Description'],
'start_time' => $start->toDateTimeString(),
'end_time' => $end->toDateTimeString(),
'logs_deadline' => $deadline->toDateTimeString(),
'is_active' => true,
]);
$createResponse->assertStatus(201);
$roundId = $createResponse->json('id');
$updateResponse = $this->putJson("/api/rounds/{$roundId}", [
'contest_id' => $contest->id,
'name' => ['cs' => 'Kolo 1B', 'en' => 'Round 1B'],
'description' => ['cs' => 'Popis', 'en' => 'Description'],
'start_time' => $start->toDateTimeString(),
'end_time' => $end->toDateTimeString(),
'logs_deadline' => $deadline->toDateTimeString(),
'is_active' => false,
]);
$updateResponse->assertStatus(200)
->assertJsonFragment(['id' => $roundId]);
$this->deleteJson("/api/rounds/{$roundId}")
->assertStatus(204);
}
public function test_non_admin_cannot_create_round(): void
{
$this->actingAsUser();
$contest = $this->createContest();
$start = now()->addDay();
$end = (clone $start)->addHours(2);
$deadline = (clone $end)->addDays(2);
$this->postJson('/api/rounds', [
'contest_id' => $contest->id,
'name' => ['cs' => 'Kolo 1', 'en' => 'Round 1'],
'start_time' => $start->toDateTimeString(),
'end_time' => $end->toDateTimeString(),
'logs_deadline' => $deadline->toDateTimeString(),
])->assertStatus(403);
}
}

47
tests/Pest.php Normal file
View File

@@ -0,0 +1,47 @@
<?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 "pest()" function to bind a different classes or traits.
|
*/
pest()->extend(Tests\TestCase::class)
// ->use(Illuminate\Foundation\Testing\RefreshDatabase::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.
|
*/
function something()
{
// ..
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Tests\Support;
use App\Models\User;
use Laravel\Sanctum\Sanctum;
trait ActsAsUser
{
protected function createUser(array $overrides = []): User
{
return User::factory()->create($overrides);
}
protected function createAdminUser(array $overrides = []): User
{
return User::factory()->admin()->create($overrides);
}
protected function createInactiveUser(array $overrides = []): User
{
return User::factory()->inactive()->create($overrides);
}
protected function actingAsUser(array $overrides = []): User
{
$user = $this->createUser($overrides);
Sanctum::actingAs($user);
return $user;
}
protected function actingAsAdmin(array $overrides = []): User
{
$user = $this->createAdminUser($overrides);
Sanctum::actingAs($user);
return $user;
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Tests\Support;
use App\Models\Band;
use App\Models\Category;
use App\Models\Contest;
use App\Models\EvaluationRuleSet;
use App\Models\EvaluationRun;
use App\Models\EdiBand;
use App\Models\EdiCategory;
use App\Models\Cty;
use App\Models\CountryWwl;
use App\Models\ContestParameter;
use App\Models\Log;
use App\Models\LogQso;
use App\Models\LogResult;
use App\Models\LogOverride;
use App\Models\QsoOverride;
use App\Models\QsoResult;
use App\Models\NewsPost;
use App\Models\File;
use App\Models\PowerCategory;
use App\Models\Round;
trait CreatesDomainData
{
protected function createRuleSet(array $overrides = []): EvaluationRuleSet
{
return EvaluationRuleSet::factory()->create($overrides);
}
protected function createContest(array $overrides = []): Contest
{
return Contest::factory()->create($overrides);
}
protected function createRound(array $overrides = []): Round
{
return Round::factory()->create($overrides);
}
protected function createLog(array $overrides = []): Log
{
return Log::factory()->create($overrides);
}
protected function createBand(array $overrides = []): Band
{
return Band::factory()->create($overrides);
}
protected function createCategory(array $overrides = []): Category
{
return Category::factory()->create($overrides);
}
protected function createPowerCategory(array $overrides = []): PowerCategory
{
return PowerCategory::factory()->create($overrides);
}
protected function createEdiBand(array $overrides = []): EdiBand
{
return EdiBand::factory()->create($overrides);
}
protected function createEdiCategory(array $overrides = []): EdiCategory
{
return EdiCategory::factory()->create($overrides);
}
protected function createCty(array $overrides = []): Cty
{
return Cty::factory()->create($overrides);
}
protected function createCountryWwl(array $overrides = []): CountryWwl
{
return CountryWwl::factory()->create($overrides);
}
protected function createContestParameter(array $overrides = []): ContestParameter
{
return ContestParameter::factory()->create($overrides);
}
protected function createLogQso(array $overrides = []): LogQso
{
return LogQso::factory()->create($overrides);
}
protected function createLogResult(array $overrides = []): LogResult
{
return LogResult::factory()->create($overrides);
}
protected function createLogOverride(array $overrides = []): LogOverride
{
return LogOverride::factory()->create($overrides);
}
protected function createQsoOverride(array $overrides = []): QsoOverride
{
return QsoOverride::factory()->create($overrides);
}
protected function createQsoResult(array $overrides = []): QsoResult
{
return QsoResult::factory()->create($overrides);
}
protected function createNewsPost(array $overrides = []): NewsPost
{
return NewsPost::factory()->create($overrides);
}
protected function createFile(array $overrides = []): File
{
return File::factory()->create($overrides);
}
protected function createEvaluationRun(array $overrides = []): EvaluationRun
{
return EvaluationRun::factory()->create($overrides);
}
protected function attachRoundRelations(
Round $round,
array $bands = [],
array $categories = [],
array $powerCategories = []
): void {
if ($bands) {
$round->bands()->sync($bands);
}
if ($categories) {
$round->categories()->sync($categories);
}
if ($powerCategories) {
$round->powerCategories()->sync($powerCategories);
}
}
}

13
tests/TestCase.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Support\ActsAsUser;
use Tests\Support\CreatesDomainData;
abstract class TestCase extends BaseTestCase
{
use ActsAsUser;
use CreatesDomainData;
}

View File

@@ -0,0 +1,5 @@
<?php
test('that true is true', function () {
expect(true)->toBeTrue();
});

View File

@@ -0,0 +1,160 @@
<?php
namespace Tests\Unit;
use App\Models\EvaluationRuleSet;
use App\Models\WorkingQso;
use App\Services\Evaluation\OperatingWindowService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OperatingWindowServiceTest extends TestCase
{
use RefreshDatabase;
private function createQso(int $runId, int $logId, int $bandId, Carbon $ts, int $points, bool $valid = true): int
{
$logQso = $this->createLogQso(['log_id' => $logId]);
$this->createQsoResult([
'evaluation_run_id' => $runId,
'log_qso_id' => $logQso->id,
'is_valid' => $valid,
'points' => $points,
]);
WorkingQso::create([
'evaluation_run_id' => $runId,
'log_qso_id' => $logQso->id,
'log_id' => $logId,
'ts_utc' => $ts,
'band_id' => $bandId,
'call_norm' => 'OK1AAA',
'rcall_norm' => 'OK1BBB',
'loc_norm' => 'JN00AA',
'rloc_norm' => 'JN00AA',
]);
return $logQso->id;
}
public function test_pick_best_window_prefers_higher_score(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC');
$t2 = Carbon::create(2025, 1, 1, 7, 0, 0, 'UTC');
$t3 = Carbon::create(2025, 1, 1, 8, 0, 0, 'UTC');
$firstA = $this->createQso($run->id, $log->id, $band->id, $t0, 10);
$firstB = $this->createQso($run->id, $log->id, $band->id, $t1, 10);
$bestA = $this->createQso($run->id, $log->id, $band->id, $t2, 25);
$bestB = $this->createQso($run->id, $log->id, $band->id, $t3, 25);
$service = new OperatingWindowService();
$result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet);
$this->assertNotNull($result);
$this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString());
$this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString());
$this->assertSame($t2->toDateTimeString(), $result['secondStartUtc']?->toDateTimeString());
$this->assertSame($t3->toDateTimeString(), $result['secondEndUtc']?->toDateTimeString());
$this->assertSame([$firstA, $firstB, $bestA, $bestB], $result['includedLogQsoIds']);
$this->assertSame(4, $result['qsoCount']);
}
public function test_tie_break_prefers_earlier_start(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$t1 = Carbon::create(2025, 1, 1, 1, 0, 0, 'UTC');
$t2 = Carbon::create(2025, 1, 1, 15, 0, 0, 'UTC');
$t3 = Carbon::create(2025, 1, 1, 21, 0, 0, 'UTC');
$firstA = $this->createQso($run->id, $log->id, $band->id, $t0, 10);
$firstB = $this->createQso($run->id, $log->id, $band->id, $t1, 10);
$this->createQso($run->id, $log->id, $band->id, $t2, 10);
$this->createQso($run->id, $log->id, $band->id, $t3, 10);
$service = new OperatingWindowService();
$result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet);
$this->assertNotNull($result);
$this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString());
$this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString());
$this->assertNull($result['secondStartUtc']);
$this->assertNull($result['secondEndUtc']);
$this->assertSame([$firstA, $firstB], $result['includedLogQsoIds']);
}
public function test_window_includes_boundary_at_six_hours(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$t1 = Carbon::create(2025, 1, 1, 6, 0, 0, 'UTC');
$this->createQso($run->id, $log->id, $band->id, $t0, 10);
$this->createQso($run->id, $log->id, $band->id, $t1, 10);
$service = new OperatingWindowService();
$result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet);
$this->assertNotNull($result);
$this->assertSame($t0->toDateTimeString(), $result['startUtc']->toDateTimeString());
$this->assertSame($t1->toDateTimeString(), $result['endUtc']->toDateTimeString());
$this->assertSame(2, $result['qsoCount']);
}
public function test_returns_null_when_no_valid_qso(): void
{
$ruleSet = $this->createRuleSet([
'use_multipliers' => false,
'multiplier_type' => 'NONE',
'multiplier_scope' => 'OVERALL',
'multiplier_source' => 'VALID_ONLY',
]);
$round = $this->createRound();
$log = $this->createLog(['round_id' => $round->id]);
$run = $this->createEvaluationRun(['rule_set_id' => $ruleSet->id, 'round_id' => $round->id]);
$band = $this->createBand();
$t0 = Carbon::create(2025, 1, 1, 0, 0, 0, 'UTC');
$this->createQso($run->id, $log->id, $band->id, $t0, 10, false);
$service = new OperatingWindowService();
$result = $service->pickBestOperatingWindow($run->id, $log->id, 6, $ruleSet);
$this->assertNull($result);
}
}