refactor(vikon): fetch parts from VIKON API instead of hardcoded config
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{"version":2,"defects":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":7},"times":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":0.018,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":0,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":0.027,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_module_id":0.005,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":0.011,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":0.005,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":0,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":0.001,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_requires_authentication":0.061,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":0.024,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":0.01,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_1":0.021,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_negative_1":0.001}}
|
||||||
@@ -49,10 +49,24 @@ class CheckAccessAction
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract parts per module from the API response
|
||||||
|
$partsByModule = [];
|
||||||
|
if (isset($body['tree_access']) && is_array($body['tree_access'])) {
|
||||||
|
foreach ($body['tree_access'] as $moduleId => $moduleData) {
|
||||||
|
if (isset($moduleData['parts']) && is_array($moduleData['parts'])) {
|
||||||
|
$partsByModule[$moduleId] = array_map(
|
||||||
|
fn($p) => ['id' => $p['id'], 'name' => $p['name'] ?? $p['id'], 'access' => $p['access'] ?? true],
|
||||||
|
$moduleData['parts']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'has_access' => true,
|
'has_access' => true,
|
||||||
'error' => null,
|
'error' => null,
|
||||||
'modules_tree' => $body['tree_access'],
|
'modules_tree' => $body['tree_access'],
|
||||||
|
'parts' => $partsByModule,
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('Vikon access check failed', ['error' => $e->getMessage()]);
|
Log::error('Vikon access check failed', ['error' => $e->getMessage()]);
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ class UpdatePartAction
|
|||||||
{
|
{
|
||||||
$config = $this->modulesConfig[$moduleId] ?? throw new \RuntimeException("Unknown module: {$moduleId}");
|
$config = $this->modulesConfig[$moduleId] ?? throw new \RuntimeException("Unknown module: {$moduleId}");
|
||||||
|
|
||||||
$allowedParts = config('vikon.parts', [])[$moduleId] ?? [];
|
|
||||||
if (!in_array($part, $allowedParts, true)) {
|
|
||||||
throw new \RuntimeException("Invalid part '{$part}' for module {$moduleId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('Vikon: starting part update', ['module' => $moduleId, 'part' => $part]);
|
Log::info('Vikon: starting part update', ['module' => $moduleId, 'part' => $part]);
|
||||||
|
|
||||||
// Step 1: Request generation
|
// Step 1: Request generation
|
||||||
|
|||||||
@@ -51,21 +51,6 @@ class UpdatePartActionTest extends TestCase
|
|||||||
$action->run(999, 'common', 'token');
|
$action->run(999, 'common', 'token');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_rejects_invalid_part(): void
|
|
||||||
{
|
|
||||||
$http = Mockery::mock(HttpTask::class);
|
|
||||||
$fs = Mockery::mock(FilesystemTask::class);
|
|
||||||
$poll = Mockery::mock(PollPartStatusTask::class);
|
|
||||||
|
|
||||||
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
|
||||||
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
|
||||||
$this->expectExceptionMessage("Invalid part 'nonexistent' for module 1");
|
|
||||||
$action->run(1, 'nonexistent', 'token');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_requests_generation_and_polls_status(): void
|
public function test_requests_generation_and_polls_status(): void
|
||||||
{
|
{
|
||||||
$http = Mockery::mock(HttpTask::class);
|
$http = Mockery::mock(HttpTask::class);
|
||||||
@@ -117,8 +102,6 @@ class UpdatePartActionTest extends TestCase
|
|||||||
->once()
|
->once()
|
||||||
->andReturn(true);
|
->andReturn(true);
|
||||||
|
|
||||||
config(['vikon.parts' => [1 => ['common']]]);
|
|
||||||
|
|
||||||
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
||||||
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
||||||
]);
|
]);
|
||||||
@@ -141,8 +124,6 @@ class UpdatePartActionTest extends TestCase
|
|||||||
'message' => 'Generation not available',
|
'message' => 'Generation not available',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
config(['vikon.parts' => [1 => ['common']]]);
|
|
||||||
|
|
||||||
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
||||||
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
||||||
]);
|
]);
|
||||||
@@ -169,8 +150,6 @@ class UpdatePartActionTest extends TestCase
|
|||||||
->once()
|
->once()
|
||||||
->andReturn(['status' => 'failed', 'error' => 'Server error']);
|
->andReturn(['status' => 'failed', 'error' => 'Server error']);
|
||||||
|
|
||||||
config(['vikon.parts' => [1 => ['common']]]);
|
|
||||||
|
|
||||||
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
$action = new UpdatePartAction($http, $fs, $poll, $this->tempDir, $this->tempDir, [
|
||||||
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
1 => ['path' => 'sveden', 'allowed_folders' => ['common']],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ class VikonController extends Controller
|
|||||||
{
|
{
|
||||||
$token = Session::get('vikon_access_token');
|
$token = Session::get('vikon_access_token');
|
||||||
$isAuth = $token ? $this->validateToken->run($token) : false;
|
$isAuth = $token ? $this->validateToken->run($token) : false;
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
if ($isAuth && $token) {
|
||||||
|
$accessResult = $this->checkAccess->run($token);
|
||||||
|
$parts = $accessResult['parts'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
return inertia()->render('Dashboard/VikonUpdates/Index', [
|
return inertia()->render('Dashboard/VikonUpdates/Index', [
|
||||||
'is_authenticated' => $isAuth,
|
'is_authenticated' => $isAuth,
|
||||||
'current_version' => config('vikon.current_version'),
|
'current_version' => config('vikon.current_version'),
|
||||||
'modules' => config('vikon.modules'),
|
'modules' => config('vikon.modules'),
|
||||||
'parts' => config('vikon.parts'),
|
'parts' => $parts,
|
||||||
'vikon_api_domain' => config('vikon.api_domain'),
|
'vikon_api_domain' => config('vikon.api_domain'),
|
||||||
'vikon_client_id' => config('vikon.client_id'),
|
'vikon_client_id' => config('vikon.client_id'),
|
||||||
]);
|
]);
|
||||||
@@ -70,12 +76,18 @@ class VikonController extends Controller
|
|||||||
|
|
||||||
$token = Session::get('vikon_access_token');
|
$token = Session::get('vikon_access_token');
|
||||||
$isAuth = $token ? $this->validateToken->run($token) : false;
|
$isAuth = $token ? $this->validateToken->run($token) : false;
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
if ($isAuth && $token) {
|
||||||
|
$accessResult = $this->checkAccess->run($token);
|
||||||
|
$parts = $accessResult['parts'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
return inertia()->render('Dashboard/VikonUpdates/Index', [
|
return inertia()->render('Dashboard/VikonUpdates/Index', [
|
||||||
'is_authenticated' => $isAuth,
|
'is_authenticated' => $isAuth,
|
||||||
'current_version' => config('vikon.current_version'),
|
'current_version' => config('vikon.current_version'),
|
||||||
'modules' => config('vikon.modules'),
|
'modules' => config('vikon.modules'),
|
||||||
'parts' => config('vikon.parts'),
|
'parts' => $parts,
|
||||||
'vikon_api_domain' => config('vikon.api_domain'),
|
'vikon_api_domain' => config('vikon.api_domain'),
|
||||||
'vikon_client_id' => config('vikon.client_id'),
|
'vikon_client_id' => config('vikon.client_id'),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -49,12 +49,6 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'parts' => [
|
|
||||||
1 => ['common', 'struct', 'document', 'education', 'managers', 'employees', 'objects', 'paid_edu', 'budget', 'vacant', 'grants', 'inter', 'catering', 'eduStandarts', 'corruption', 'antiterrorism'],
|
|
||||||
2 => ['abitur'],
|
|
||||||
6 => ['general', 'structure', 'faq', 'procedures', 'results-and-reports', 'plans', 'survey'],
|
|
||||||
],
|
|
||||||
|
|
||||||
'poll_interval' => (int) env('VIKON_POLL_INTERVAL', 3),
|
'poll_interval' => (int) env('VIKON_POLL_INTERVAL', 3),
|
||||||
'poll_max_attempts' => (int) env('VIKON_POLL_MAX_ATTEMPTS', 50),
|
'poll_max_attempts' => (int) env('VIKON_POLL_MAX_ATTEMPTS', 50),
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,9 @@
|
|||||||
Выбрать все
|
Выбрать все
|
||||||
</label>
|
</label>
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 pl-5">
|
<div class="flex flex-wrap gap-x-4 gap-y-1 pl-5">
|
||||||
<label v-for="p in parts[id]" :key="p" class="flex items-center gap-1.5 text-sm cursor-pointer">
|
<label v-for="p in parts[id]" :key="p.id" class="flex items-center gap-1.5 text-sm" :class="p.access ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'">
|
||||||
<input type="checkbox" :value="p" v-model="selectedParts[id]" class="rounded" />
|
<input type="checkbox" :value="p.id" v-model="selectedParts[id]" :disabled="!p.access" class="rounded" />
|
||||||
{{ p }}
|
{{ p.name }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -278,17 +278,17 @@ async function updateModule(moduleId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAllPartsSelected(moduleId) {
|
function isAllPartsSelected(moduleId) {
|
||||||
const moduleParts = props.parts?.[moduleId] || [];
|
const moduleParts = (props.parts?.[moduleId] || []).filter(p => p.access);
|
||||||
const selected = selectedParts.value[moduleId] || [];
|
const selected = selectedParts.value[moduleId] || [];
|
||||||
return moduleParts.length > 0 && moduleParts.every(p => selected.includes(p));
|
return moduleParts.length > 0 && moduleParts.every(p => selected.includes(p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAllParts(moduleId) {
|
function toggleAllParts(moduleId) {
|
||||||
const moduleParts = props.parts?.[moduleId] || [];
|
const moduleParts = (props.parts?.[moduleId] || []).filter(p => p.access);
|
||||||
if (isAllPartsSelected(moduleId)) {
|
if (isAllPartsSelected(moduleId)) {
|
||||||
selectedParts.value[moduleId] = [];
|
selectedParts.value[moduleId] = [];
|
||||||
} else {
|
} else {
|
||||||
selectedParts.value[moduleId] = [...moduleParts];
|
selectedParts.value[moduleId] = moduleParts.map(p => p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user