diff --git a/Report/images/IMAGES_REGISTRY.md b/Report/images/IMAGES_REGISTRY.md index b6a1be0..4b2c08a 100644 --- a/Report/images/IMAGES_REGISTRY.md +++ b/Report/images/IMAGES_REGISTRY.md @@ -4,29 +4,29 @@ | № | Имя файла | Статус | Где используется | Typst label | |---|-----------|--------|------------------|-------------| -| 1 | fig_01_start_sync.png | ready | 3.3.3, 4.1.4, прил. Г | fig-01 | -| 2 | fig_02_vault_lifecycle.png | ready | 3.3.3, 4.1.4, прил. Г | fig-02 | -| 3 | fig_03_navigation_hub.png | ready | 3.3.3, 4.1.4, прил. Г | fig-03 | -| 4 | fig_04_domain_class.png | ready | 2.3.2, 4.2.1, прил. Г | fig-04 | -| 5 | fig_05_local_vaults.jpg | placeholder | 3.4, 4.2.3, 5.2.2, прил. В, РП | fig-05 | -| 6 | fig_06_encrypt_dialog.jpg | placeholder | 3.4, 4.1.1, 5.2.2, прил. В, РП | fig-06 | -| 7 | fig_07_open_close_dialog.jpg | placeholder | 3.4, 4.1.2, прил. В, РП | fig-07 | -| 8 | fig_08_rename_delete_dialog.jpg | placeholder | 3.4, 5.2.2, прил. В, РП | fig-08 | -| 9 | fig_09_remote_vaults.jpg | placeholder | 3.4, 4.2.3, прил. В, РП | fig-09 | -| 10 | fig_10_yandex_oauth.jpg | placeholder | 3.4, 4.2.3, 1.2.2.4, прил. В, РП | fig-10 | -| 11 | fig_11_room_schema.png | placeholder | 2.3.2, 4.2.2, прил. В, РП | fig-11 | +| 1 | fig_01_start_sync.png | ready | 3.3.3, 4.1.4 | fig-01 | +| 2 | fig_02_vault_lifecycle.png | ready | 3.3.3, 4.1.4 | fig-02 | +| 3 | fig_03_navigation_hub.png | ready | 3.3.3, 4.1.4 | fig-03 | +| 4 | fig_04_domain_class.png | ready | 2.3.2, 4.2.1 | fig-04 | +| 5 | fig_05_local_vaults.jpg | placeholder | 3.4, 4.2.3, 5.2.2, прил. В | fig-05 | +| 6 | fig_06_encrypt_dialog.jpg | placeholder | 3.4, 4.1.1, 5.2.2, прил. В | fig-06 | +| 7 | fig_07_open_close_dialog.jpg | placeholder | 3.4, 4.1.2, прил. В | fig-07 | +| 8 | fig_08_rename_delete_dialog.jpg | placeholder | 3.4, 5.2.2, прил. В | fig-08 | +| 9 | fig_09_remote_vaults.jpg | placeholder | 3.4, 4.2.3, прил. В | fig-09 | +| 10 | fig_10_yandex_oauth.jpg | placeholder | 3.4, 4.2.3, 1.2.2.4, прил. В | fig-10 | +| 11 | fig_11_room_schema.png | ready | 2.3.2, 4.2.2, прил. В | fig-11 | | 12 | fig_12_tasks_screen.jpg | placeholder | 5.2.3, прил. В | fig-12 | | 13 | fig_13_tasks_notification.jpg | placeholder | 5.2.3, прил. В | fig-13 | -| 14 | fig_14_context_system.png | placeholder | 1.2.1, 2.1.4 | fig-14 | -| 15 | fig_15_bpmn_vault.png | placeholder | 2.1.3 | fig-15 | -| 16 | fig_16_dfd_level0.png | placeholder | 2.2 | fig-16 | -| 17 | fig_17_use_case.png | placeholder | 2.3.1 | fig-17 | -| 18 | fig_18_deployment.png | placeholder | 2.3.3 | fig-18 | -| 19 | fig_19_clean_architecture.png | placeholder | 2.3, 4.3 | fig-19 | -| 20 | fig_20_oauth_sequence.png | placeholder | 1.5.2, 4.2.3 | fig-20 | -| 21 | fig_21_encrypt_flow.png | placeholder | 4.1.1, 5.2.1 | fig-21 | -| 22 | fig_22_cjm_vault.png | placeholder | 3.3.2 | fig-22 | -| 23 | fig_23_module_deps.png | placeholder | 4.3 | fig-23 | +| 14 | fig_14_context_system.png | ready | 1.2.1, 2.1.4 | fig-14 | +| 15 | fig_15_bpmn_vault.png | ready | 2.1.3 | fig-15 | +| 16 | fig_16_dfd_level0.png | ready | 2.2 | fig-16 | +| 17 | fig_17_use_case.png | ready | 2.3.1 | fig-17 | +| 18 | fig_18_deployment.png | ready | 2.3.3 | fig-18 | +| 19 | fig_19_clean_architecture.png | ready | 2.3, 4.3 | fig-19 | +| 20 | fig_20_oauth_sequence.png | ready | 1.5.2, 4.2.3 | fig-20 | +| 21 | fig_21_encrypt_flow.png | ready | 4.1.1, 5.2.1 | fig-21 | +| 22 | fig_22_cjm_vault.png | ready | 3.3.2 | fig-22 | +| 23 | fig_23_module_deps.png | ready | 4.3 | fig-23 | | 27 | fig_27_gradle_domain_test.png | ready | 5.2.1 | fig-27 | | 28 | fig_28_gradle_usecases_test.png | ready | 5.2.2 | fig-28 | | 29 | fig_29_gradle_ui_test.png | ready | 5.2.3 | fig-29 | @@ -34,4 +34,6 @@ | 31 | fig_31_gradle_connected_test.png | ready | 5.3 | fig-31 | | 32 | fig_32_manual_test_checklist.png | ready | 5.3 | fig-32 | -Замените файлы со статусом `placeholder` на реальные скриншоты/диаграммы, затем смените статус на `ready`. Рис. 27–32 — заглушки Gradle/UI; перед защитой заменить снимками Android Studio. +Диаграммы fig_01–04, fig_11, fig_14–23: исходники `Report/puml/fig_*.puml` (`@startuml` = имя PNG), растр — `Report/scripts/render_puml.sh` сразу в `Report/images/` (без копирования). + +Скриншоты UI (fig_05–fig_10, fig_12–13) и Gradle (fig_27–32) — заменить placeholder на реальные снимки перед защитой. diff --git a/Report/images/fig_01_start_sync.png b/Report/images/fig_01_start_sync.png index 8b82b3f..91f95e9 100644 Binary files a/Report/images/fig_01_start_sync.png and b/Report/images/fig_01_start_sync.png differ diff --git a/Report/images/fig_02_vault_lifecycle.png b/Report/images/fig_02_vault_lifecycle.png index 89bce54..afa548a 100644 Binary files a/Report/images/fig_02_vault_lifecycle.png and b/Report/images/fig_02_vault_lifecycle.png differ diff --git a/Report/images/fig_03_navigation_hub.png b/Report/images/fig_03_navigation_hub.png index 3581dc4..5901e16 100644 Binary files a/Report/images/fig_03_navigation_hub.png and b/Report/images/fig_03_navigation_hub.png differ diff --git a/Report/images/fig_04_domain_class.png b/Report/images/fig_04_domain_class.png index c410e31..3abad9a 100644 Binary files a/Report/images/fig_04_domain_class.png and b/Report/images/fig_04_domain_class.png differ diff --git a/Report/images/fig_11_room_schema.png b/Report/images/fig_11_room_schema.png index d66727f..ad0d201 100644 Binary files a/Report/images/fig_11_room_schema.png and b/Report/images/fig_11_room_schema.png differ diff --git a/Report/images/fig_14_context_system.png b/Report/images/fig_14_context_system.png index 2594788..f514d89 100644 Binary files a/Report/images/fig_14_context_system.png and b/Report/images/fig_14_context_system.png differ diff --git a/Report/images/fig_15_bpmn_vault.png b/Report/images/fig_15_bpmn_vault.png index 5be2472..7b27d66 100644 Binary files a/Report/images/fig_15_bpmn_vault.png and b/Report/images/fig_15_bpmn_vault.png differ diff --git a/Report/images/fig_16_dfd_level0.png b/Report/images/fig_16_dfd_level0.png index ebdfc71..3e0b0d0 100644 Binary files a/Report/images/fig_16_dfd_level0.png and b/Report/images/fig_16_dfd_level0.png differ diff --git a/Report/images/fig_17_use_case.png b/Report/images/fig_17_use_case.png index fea1721..6ae655e 100644 Binary files a/Report/images/fig_17_use_case.png and b/Report/images/fig_17_use_case.png differ diff --git a/Report/images/fig_18_deployment.png b/Report/images/fig_18_deployment.png index a53b867..3472170 100644 Binary files a/Report/images/fig_18_deployment.png and b/Report/images/fig_18_deployment.png differ diff --git a/Report/images/fig_19_clean_architecture.png b/Report/images/fig_19_clean_architecture.png index 824e288..c67179d 100644 Binary files a/Report/images/fig_19_clean_architecture.png and b/Report/images/fig_19_clean_architecture.png differ diff --git a/Report/images/fig_20_oauth_sequence.png b/Report/images/fig_20_oauth_sequence.png index 921392d..3d39bf8 100644 Binary files a/Report/images/fig_20_oauth_sequence.png and b/Report/images/fig_20_oauth_sequence.png differ diff --git a/Report/images/fig_21_encrypt_flow.png b/Report/images/fig_21_encrypt_flow.png index c6eb0d3..90959f7 100644 Binary files a/Report/images/fig_21_encrypt_flow.png and b/Report/images/fig_21_encrypt_flow.png differ diff --git a/Report/images/fig_22_cjm_vault.png b/Report/images/fig_22_cjm_vault.png index e0ba21a..92317e7 100644 Binary files a/Report/images/fig_22_cjm_vault.png and b/Report/images/fig_22_cjm_vault.png differ diff --git a/Report/images/fig_23_module_deps.png b/Report/images/fig_23_module_deps.png index 0e4e11d..5cddbff 100644 Binary files a/Report/images/fig_23_module_deps.png and b/Report/images/fig_23_module_deps.png differ diff --git a/Report/includes/abstract-en.typ b/Report/includes/abstract-en.typ index 1431872..a5269a5 100644 --- a/Report/includes/abstract-en.typ +++ b/Report/includes/abstract-en.typ @@ -2,8 +2,8 @@ #v(0.5em) -This explanatory note describes the development of the Wallenc mobile application — a client-side wallet for secure storage of user data on untrusted storage backends without a dedicated application server. Security is ensured by client-side encryption before data leaves the device; decryption is performed only inside the application when the user supplies a valid key. The work covers analysis of the problem domain and competing products, requirements specification, system and user-interface design, Kotlin implementation for Android, software testing, and a brief economic assessment. +The thesis describes the Wallenc mobile application — a client wallet for storing data on untrusted backends without a dedicated application server. Data is encrypted on the device before upload; decryption is performed only when the user enters a valid key. The work includes analysis of analogues, requirements, architecture and UI design, Kotlin implementation for Android, testing, and a brief economic assessment. -The project applies MVVM and Clean Architecture with Gradle modules (`:app`, `:domain`, `:usecases`, `:ui`, `:domain-vault`, `:infrastructure-android`, `:vault-contracts`, `:task-runtime`). Functional capabilities include local and remote vault management, AES-based encryption, Room metadata storage, Yandex OAuth integration, and a planned synchronization model that does not expose encryption keys to external providers. +The application uses MVVM and Clean Architecture. Main features are local and remote vault management, client-side AES encryption, Room metadata storage, and Yandex OAuth integration. -Keywords: mobile application, client-side encryption, Android, vault, zero-knowledge, OAuth, Room, Jetpack Compose. +Keywords: mobile application, client-side encryption, Android, vault, OAuth, Room. diff --git a/Report/includes/ch04.typ b/Report/includes/ch04.typ index 41fb62d..4b693a0 100644 --- a/Report/includes/ch04.typ +++ b/Report/includes/ch04.typ @@ -87,11 +87,11 @@ viewModel.yandexSignIn.launch { outcome -> == Взаимодействие подсистем и итоговая архитектура -Зависимости модулей Gradle показаны на рисунке @fig-23. Полный исходный код всех модулей сборки приведён в *приложении А* (307 файлов, сгенерировано скриптом `gen_listings.py`). +Зависимости модулей Gradle показаны на рисунке @fig-23. Полный исходный код модулей сборки приведён в приложении А. #pz-fig("fig_23_module_deps.png", [Зависимости модулей Gradle], "fig-23") -В основном тексте приведены показательные фрагменты; листинги по модулям `:domain`, `:infrastructure-android`, `:app` — в приложении А, разделы «Модуль :domain» и др. +В основном тексте приведены показательные фрагменты; полные листинги — в приложении А. #include "ch04-expand.typ" #include "ch04-modules.typ" diff --git a/Report/includes/ch05-encryptor.typ b/Report/includes/ch05-encryptor.typ deleted file mode 100644 index 01894e5..0000000 --- a/Report/includes/ch05-encryptor.typ +++ /dev/null @@ -1,25 +0,0 @@ -#import "common.typ": pz-table - -=== Детальное описание тестов EncryptorTest - -Класс `EncryptorTest` — эталонный набор для приёмки криптографического ядра. Каждый метод изолирован и не зависит от порядка выполнения. - -#pz-table( - [Методы EncryptorTest], - 3, - table.header([Метод], [Проверяемое поведение], [OK]), - [test correct key for StorageEncryptionInfo], [`checkKey` → true], [+], - [test incorrect key for StorageEncryptionInfo], [`checkKey` → false], [+], - [test string encryption with the same key], [Симметрия encrypt/decrypt], [+], - [test string encryption with the wrong key], [Исключение при decrypt], [+], - [test bytes encryption with the same key], [Восстановление 512 байт], [+], - [test bytes encryption with the wrong key], [Исключение], [+], - [test stream encryption with the same key], [Поток 1500 байт], [+], - [test stream encryption with the wrong key], [Исключение decryptStream], [+], -) - -Методика: для потоков используется `ByteArrayOutputStream` с запасом ёмкости `dataLen*3`, чтобы учесть расширение ciphertext. - -=== Детальное описание тестов StorageSyncEngineTest - -Движок синхронизации тестируется на in-memory двойниках хранилищ. Полный реестр методов — в таблице модуля `:usecases` выше. diff --git a/Report/includes/ch05-sync-tests.typ b/Report/includes/ch05-sync-tests.typ deleted file mode 100644 index be17cf7..0000000 --- a/Report/includes/ch05-sync-tests.typ +++ /dev/null @@ -1,24 +0,0 @@ -#import "common.typ": pz-table - -=== Каталог тестов StorageSyncEngineTest - -#pz-table( - [StorageSyncEngineTest — 12 методов], - 3, - table.header([Метод], [Поведение], [+]), - [syncAllGroupsReportsNoGroupsWhenEmpty], [Нет групп → отчёт], [+], - [syncGroupCopiesFileFromSourceToTarget], [Копия на target], [+], - [syncGroupSkippedWhenFewerThanTwoStorages], [Skip при менее 2 storage], [+], - [syncGroupDeleteRemovesFileOnTarget], [Удаление на target], [+], - [syncSkipsWhenTargetRevisionAlreadyWinner], [Ревизия-победитель], [+], - [openReadDoesNotChangeJournal], [Чтение без журнала], [+], - [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [Delete без журнала], [+], - [syncGroupTrashSoftDeletesOnTarget], [Trash на target], [+], - [syncGroupStopsWhenLockCannotBeAcquired], [Стоп при lock], [+], - [syncGroupReleasesLocksAfterSuccessfulSync], [Unlock после успеха], [+], - [syncGroupReleasesLocksWhenJournalReadFails], [Unlock при ошибке], [+], - [syncGroupCooperativeCancellationReleasesLocks], [Unlock при отмене], [+], - [syncGroupReleasesLocksWhenJournalEmpty], [Пустой журнал], [+], -) - -Тесты `StorageSyncJournalMergeTest` и `StorageSyncEncryptionCompatTest` дополняют движок проверкой слияния журнала и совместимости шифрования в группе. diff --git a/Report/includes/ch05-tests-generated.typ b/Report/includes/ch05-tests-generated.typ index 69928be..a27d0fa 100644 --- a/Report/includes/ch05-tests-generated.typ +++ b/Report/includes/ch05-tests-generated.typ @@ -2,209 +2,81 @@ #import "common.typ": pz-test-table #pz-test-table( - [Сводка модульных unit-тестов (src/test)], + [Реестр модульных unit-тестов], 4, table.header( + [№], [Модуль], - [Класс], [Метод], - [Файл], + [Проверяемое поведение], ), - [domain], [EncryptorTest], [test correct key for StorageEncryptionInfo], [EncryptorTest.kt], - [domain], [EncryptorTest], [test incorrect key for StorageEncryptionInfo], [EncryptorTest.kt], - [domain], [EncryptorTest], [test string encryption with the same key], [EncryptorTest.kt], - [domain], [EncryptorTest], [test string encryption with the wrong key], [EncryptorTest.kt], - [domain], [EncryptorTest], [test bytes encryption with the same key], [EncryptorTest.kt], - [domain], [EncryptorTest], [test bytes encryption with the wrong key], [EncryptorTest.kt], - [domain], [EncryptorTest], [test stream encryption with the same key], [EncryptorTest.kt], - [domain], [EncryptorTest], [test stream encryption with the wrong key], [EncryptorTest.kt], - [domain], [WallencExceptionMappingTest], [preservesWallencException], [WallencExceptionMappingTest.kt], - [domain], [WallencExceptionMappingTest], [mapsFileNotFoundException], [WallencExceptionMappingTest.kt], - [domain], [WallencExceptionMappingTest], [mapsIOExceptionToIoFailed], [WallencExceptionMappingTest.kt], - [domain], [WallencExceptionMappingTest], [mapsGenericExceptionToUnknown], [WallencExceptionMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsYandexDiskAuthToAuthFailed], [VaultThrowableMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsHttpExceptionToNetworkHttpFailed], [VaultThrowableMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsMissingOAuthTokenIoToTokenMissing], [VaultThrowableMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsSocketTimeoutToOperationTimedOut], [VaultThrowableMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsFileNotFoundToStorageFileNotFound], [VaultThrowableMappingTest.kt], - [domain-vault], [VaultThrowableMappingTest], [mapsIllegalStateNotAFile], [VaultThrowableMappingTest.kt], - [domain-vault], [YandexDiskRepositoryTest], [diskInfoParsesResponse], [YandexDiskRepositoryTest.kt], - [domain-vault], [YandexDiskRepositoryTest], [listReturnsEmptyEmbeddedOn404], [YandexDiskRepositoryTest.kt], - [domain-vault], [YandexDiskRepositoryTest], [diskInfoThrowsAuthExceptionOn401], [YandexDiskRepositoryTest.kt], - [domain-vault], [StorageSyncJournalBufferTest], [flushRestoresPendingOnWriteFailure], [StorageSyncJournalBufferTest.kt], - [task-runtime], [TaskOrchestratorTest], [enqueueCompletesTask], [TaskOrchestratorTest.kt], - [task-runtime], [TaskOrchestratorTest], [cancelAllMarksRunningTaskCancelled], [TaskOrchestratorTest.kt], - [task-runtime], [TaskOrchestratorTest], [cancelMarksTaskCancelled], [TaskOrchestratorTest.kt], - [task-runtime], [TaskOrchestratorTest], [failRecordsFailedState], [TaskOrchestratorTest.kt], - [task-runtime], [TaskOrchestratorTest], [progressUpdatesRunningState], [TaskOrchestratorTest.kt], - [task-runtime], [TaskOrchestratorTest], [logAppendsLine], [TaskOrchestratorTest.kt], - [ui], [WallencDeepLinksTest], [matchesWallencViewIntent], [WallencDeepLinksTest.kt], - [ui], [WallencDeepLinksTest], [rejectsUnrelatedIntent], [WallencDeepLinksTest.kt], - [ui], [WallencDeepLinksTest], [matchesTasksAndSettingsHosts], [WallencDeepLinksTest.kt], - [ui], [TaskProgressLabelsTest], [syncNoGroups_mapsToStringRes], [TaskProgressLabelsTest.kt], - [ui], [TaskProgressLabelsTest], [vaultTask_mapsToStringRes], [TaskProgressLabelsTest.kt], - [ui], [TaskProgressLabelsTest], [clearContentProgress_mapsToStringRes], [TaskProgressLabelsTest.kt], - [ui], [WallencUserNotificationMappingTest], [mapsFeatureStorageNotFound], [WallencUserNotificationMappingTest.kt], - [ui], [WallencUserNotificationMappingTest], [mapsStorageIncorrectKey], [WallencUserNotificationMappingTest.kt], - [ui], [WallencUserNotificationMappingTest], [mapsUnknown], [WallencUserNotificationMappingTest.kt], - [ui], [StorageNavigationRoutesSmokeTest], [storageHomeRouteCarriesVaultAndStorageIds], [StorageNavigationRoutesSmokeTest.kt], - [ui], [StorageNavigationRoutesSmokeTest], [textSecretsRoutesCarryRequiredArguments], [StorageNavigationRoutesSmokeTest.kt], - [ui], [OtpAuthUriParserTest], [parsesStandardTotpUri], [OtpAuthUriParserTest.kt], - [ui], [OtpAuthUriParserTest], [rejectsNonOtpauthScheme], [OtpAuthUriParserTest.kt], - [ui], [OtpAuthUriParserTest], [rejectsMissingSecret], [OtpAuthUriParserTest.kt], - [ui], [TaskPipelineViewModelTest], [startTestTaskEnqueuesWork], [TaskPipelineViewModelTest.kt], - [usecases], [StorageDomainUseCasesTest], [twoFaCrudWorksAndPersists], [StorageDomainUseCasesTest.kt], - [usecases], [StorageDomainUseCasesTest], [twoFaInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt], - [usecases], [StorageDomainUseCasesTest], [textSecretsCrudWorksWithOptionalLabels], [StorageDomainUseCasesTest.kt], - [usecases], [StorageDomainUseCasesTest], [textSecretsInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt], - [usecases], [StorageSyncEncryptionCompatTest], [storageWithoutEncInfoIsCompatible], [StorageSyncEncryptionCompatTest.kt], - [usecases], [StorageSyncEncryptionCompatTest], [storageWithEncInfoIsIncompatible], [StorageSyncEncryptionCompatTest.kt], - [usecases], [StorageSyncEngineTest], [syncAllGroupsReportsNoGroupsWhenEmpty], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupCopiesFileFromSourceToTarget], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupSkippedWhenFewerThanTwoStorages], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupDeleteRemovesFileOnTarget], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncSkipsWhenTargetRevisionAlreadyWinner], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [openReadDoesNotChangeJournal], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupTrashSoftDeletesOnTarget], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupStopsWhenLockCannotBeAcquired], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksAfterSuccessfulSync], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalReadFails], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupCooperativeCancellationReleasesLocks], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalEmpty], [StorageSyncEngineTest.kt], - [usecases], [StorageSyncJournalMergeTest], [mergeKeepsSingleEntryPerPath], [StorageSyncJournalMergeTest.kt], - [usecases], [StorageSyncJournalMergeTest], [isSyncableUserPathExcludesEncDirAndJournal], [StorageSyncJournalMergeTest.kt], - [usecases], [TwoFaTotpTest], [buildTwoFaCodeStateMatchesJavaOtpForKnownSecret], [TwoFaTotpTest.kt], - [usecases], [TwoFaTotpTest], [totpPeriodProgressIsContinuousWithinPeriod], [TwoFaTotpTest.kt], - [usecases], [TwoFaTotpTest], [totpSecondsUntilRefreshCountsDownWithinPeriod], [TwoFaTotpTest.kt], - [usecases], [TwoFaTotpTest], [buildTwoFaCodeStateReturnsNullForInvalidSecret], [TwoFaTotpTest.kt], + [1], [domain], [mapsFileNotFoundException], [исключение преобразуется в типизированную ошибку Wallenc], + [2], [domain], [mapsGenericExceptionToUnknown], [исключение преобразуется в типизированную ошибку Wallenc], + [3], [domain], [mapsIOExceptionToIoFailed], [исключение преобразуется в типизированную ошибку Wallenc], + [4], [domain], [preservesWallencException], [сохранение уже типизированного WallencException], + [5], [domain], [test bytes encryption with the same key], [симметрия шифрования и дешифрования при верном ключе], + [6], [domain], [test bytes encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой], + [7], [domain], [test correct key for StorageEncryptionInfo], [верный ключ проходит проверку checkKey], + [8], [domain], [test incorrect key for StorageEncryptionInfo], [верный ключ проходит проверку checkKey], + [9], [domain], [test stream encryption with the same key], [симметрия шифрования и дешифрования при верном ключе], + [10], [domain], [test stream encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой], + [11], [domain], [test string encryption with the same key], [симметрия шифрования и дешифрования при верном ключе], + [12], [domain], [test string encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой], + [13], [domain-vault], [diskInfoParsesResponse], [разбор ответа API diskInfo], + [14], [domain-vault], [diskInfoThrowsAuthExceptionOn401], [AuthException при HTTP 401], + [15], [domain-vault], [flushRestoresPendingOnWriteFailure], [откат буфера журнала при сбое записи], + [16], [domain-vault], [listReturnsEmptyEmbeddedOn404], [пустой список при HTTP 404], + [17], [domain-vault], [mapsFileNotFoundToStorageFileNotFound], [исключение преобразуется в типизированную ошибку Wallenc], + [18], [domain-vault], [mapsHttpExceptionToNetworkHttpFailed], [исключение преобразуется в типизированную ошибку Wallenc], + [19], [domain-vault], [mapsIllegalStateNotAFile], [исключение преобразуется в типизированную ошибку Wallenc], + [20], [domain-vault], [mapsMissingOAuthTokenIoToTokenMissing], [исключение преобразуется в типизированную ошибку Wallenc], + [21], [domain-vault], [mapsSocketTimeoutToOperationTimedOut], [исключение преобразуется в типизированную ошибку Wallenc], + [22], [domain-vault], [mapsYandexDiskAuthToAuthFailed], [исключение преобразуется в типизированную ошибку Wallenc], + [23], [task-runtime], [cancelAllMarksRunningTaskCancelled], [жизненный цикл фоновой задачи], + [24], [task-runtime], [cancelMarksTaskCancelled], [жизненный цикл фоновой задачи], + [25], [task-runtime], [enqueueCompletesTask], [жизненный цикл фоновой задачи], + [26], [task-runtime], [failRecordsFailedState], [жизненный цикл фоновой задачи], + [27], [task-runtime], [logAppendsLine], [log appends line], + [28], [task-runtime], [progressUpdatesRunningState], [жизненный цикл фоновой задачи], + [29], [ui], [clearContentProgress_mapsToStringRes], [маршрутизация, deep link или подписи UI], + [30], [ui], [mapsFeatureStorageNotFound], [исключение преобразуется в типизированную ошибку Wallenc], + [31], [ui], [mapsStorageIncorrectKey], [исключение преобразуется в типизированную ошибку Wallenc], + [32], [ui], [mapsUnknown], [исключение преобразуется в типизированную ошибку Wallenc], + [33], [ui], [matchesTasksAndSettingsHosts], [matches tasks and settings hosts], + [34], [ui], [matchesWallencViewIntent], [маршрутизация, deep link или подписи UI], + [35], [ui], [parsesStandardTotpUri], [корректность TOTP/OTP: parses standard totp uri], + [36], [ui], [rejectsMissingSecret], [разбор и валидация входных данных], + [37], [ui], [rejectsNonOtpauthScheme], [корректность TOTP/OTP: rejects non otpauth scheme], + [38], [ui], [rejectsUnrelatedIntent], [разбор и валидация входных данных], + [39], [ui], [startTestTaskEnqueuesWork], [постановка тестовой задачи в очередь orchestrator], + [40], [ui], [storageHomeRouteCarriesVaultAndStorageIds], [маршрутизация, deep link или подписи UI], + [41], [ui], [syncNoGroups_mapsToStringRes], [сценарий синхронизации: no groups_maps to string res], + [42], [ui], [textSecretsRoutesCarryRequiredArguments], [маршрутизация, deep link или подписи UI], + [43], [ui], [vaultTask_mapsToStringRes], [маршрутизация, deep link или подписи UI], + [44], [usecases], [buildTwoFaCodeStateMatchesJavaOtpForKnownSecret], [корректность TOTP/OTP: build two fa code state matches java otp for known secret], + [45], [usecases], [buildTwoFaCodeStateReturnsNullForInvalidSecret], [build two fa code state returns null for invalid secret], + [46], [usecases], [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [удаление без записи в журнал не увеличивает sequence], + [47], [usecases], [isSyncableUserPathExcludesEncDirAndJournal], [пользовательский путь исключает служебные каталоги], + [48], [usecases], [mergeKeepsSingleEntryPerPath], [слияние журнала оставляет одну запись на путь], + [49], [usecases], [openReadDoesNotChangeJournal], [чтение без записи не изменяет журнал синхронизации], + [50], [usecases], [storageWithEncInfoIsIncompatible], [хранилище с шифрованием несовместимо в одной группе sync], + [51], [usecases], [storageWithoutEncInfoIsCompatible], [хранилище без метаданных шифрования совместимо с синхронизацией], + [52], [usecases], [syncAllGroupsReportsNoGroupsWhenEmpty], [сценарий синхронизации: all groups reports no groups when empty], + [53], [usecases], [syncGroupCooperativeCancellationReleasesLocks], [снятие блокировок при отмене задачи пользователем], + [54], [usecases], [syncGroupCopiesFileFromSourceToTarget], [копирование файла с источника на целевое хранилище в группе], + [55], [usecases], [syncGroupDeleteRemovesFileOnTarget], [удаление файла на целевом хранилище при синхронизации], + [56], [usecases], [syncGroupReleasesLocksAfterSuccessfulSync], [снятие блокировок после успешной синхронизации], + [57], [usecases], [syncGroupReleasesLocksWhenJournalEmpty], [снятие блокировок при пустом журнале], + [58], [usecases], [syncGroupReleasesLocksWhenJournalReadFails], [снятие блокировок при ошибке чтения журнала], + [59], [usecases], [syncGroupSkippedWhenFewerThanTwoStorages], [синхронизация пропускается, если в группе меньше двух хранилищ], + [60], [usecases], [syncGroupStopsWhenLockCannotBeAcquired], [остановка при невозможности захватить блокировку группы], + [61], [usecases], [syncGroupTrashSoftDeletesOnTarget], [мягкое удаление (trash) на целевом хранилище], + [62], [usecases], [syncSkipsWhenTargetRevisionAlreadyWinner], [пропуск синхронизации, если ревизия цели уже новее], + [63], [usecases], [textSecretsCrudWorksWithOptionalLabels], [CRUD-операции и сохранение данных], + [64], [usecases], [textSecretsInvalidJsonFallsBackToEmptyList], [text secrets invalid json falls back to empty list], + [65], [usecases], [totpPeriodProgressIsContinuousWithinPeriod], [корректность TOTP/OTP: totp period progress is continuous within period], + [66], [usecases], [totpSecondsUntilRefreshCountsDownWithinPeriod], [корректность TOTP/OTP: totp seconds until refresh counts down within period], + [67], [usecases], [twoFaCrudWorksAndPersists], [CRUD-операции и сохранение данных], + [68], [usecases], [twoFaInvalidJsonFallsBackToEmptyList], [two fa invalid json falls back to empty list], ) -=== Реестр тестов модуля :domain - -#pz-test-table( - [Unit-тесты модуля :domain], - 3, - table.header( - [Класс], - [Метод], - [Файл], - ), - [EncryptorTest], [test correct key for StorageEncryptionInfo], [EncryptorTest.kt], - [EncryptorTest], [test incorrect key for StorageEncryptionInfo], [EncryptorTest.kt], - [EncryptorTest], [test string encryption with the same key], [EncryptorTest.kt], - [EncryptorTest], [test string encryption with the wrong key], [EncryptorTest.kt], - [EncryptorTest], [test bytes encryption with the same key], [EncryptorTest.kt], - [EncryptorTest], [test bytes encryption with the wrong key], [EncryptorTest.kt], - [EncryptorTest], [test stream encryption with the same key], [EncryptorTest.kt], - [EncryptorTest], [test stream encryption with the wrong key], [EncryptorTest.kt], - [WallencExceptionMappingTest], [preservesWallencException], [WallencExceptionMappingTest.kt], - [WallencExceptionMappingTest], [mapsFileNotFoundException], [WallencExceptionMappingTest.kt], - [WallencExceptionMappingTest], [mapsIOExceptionToIoFailed], [WallencExceptionMappingTest.kt], - [WallencExceptionMappingTest], [mapsGenericExceptionToUnknown], [WallencExceptionMappingTest.kt], -) - -=== Реестр тестов модуля :domain-vault - -#pz-test-table( - [Unit-тесты модуля :domain-vault], - 3, - table.header( - [Класс], - [Метод], - [Файл], - ), - [VaultThrowableMappingTest], [mapsYandexDiskAuthToAuthFailed], [VaultThrowableMappingTest.kt], - [VaultThrowableMappingTest], [mapsHttpExceptionToNetworkHttpFailed], [VaultThrowableMappingTest.kt], - [VaultThrowableMappingTest], [mapsMissingOAuthTokenIoToTokenMissing], [VaultThrowableMappingTest.kt], - [VaultThrowableMappingTest], [mapsSocketTimeoutToOperationTimedOut], [VaultThrowableMappingTest.kt], - [VaultThrowableMappingTest], [mapsFileNotFoundToStorageFileNotFound], [VaultThrowableMappingTest.kt], - [VaultThrowableMappingTest], [mapsIllegalStateNotAFile], [VaultThrowableMappingTest.kt], - [YandexDiskRepositoryTest], [diskInfoParsesResponse], [YandexDiskRepositoryTest.kt], - [YandexDiskRepositoryTest], [listReturnsEmptyEmbeddedOn404], [YandexDiskRepositoryTest.kt], - [YandexDiskRepositoryTest], [diskInfoThrowsAuthExceptionOn401], [YandexDiskRepositoryTest.kt], - [StorageSyncJournalBufferTest], [flushRestoresPendingOnWriteFailure], [StorageSyncJournalBufferTest.kt], -) - -=== Реестр тестов модуля :task-runtime - -#pz-test-table( - [Unit-тесты модуля :task-runtime], - 3, - table.header( - [Класс], - [Метод], - [Файл], - ), - [TaskOrchestratorTest], [enqueueCompletesTask], [TaskOrchestratorTest.kt], - [TaskOrchestratorTest], [cancelAllMarksRunningTaskCancelled], [TaskOrchestratorTest.kt], - [TaskOrchestratorTest], [cancelMarksTaskCancelled], [TaskOrchestratorTest.kt], - [TaskOrchestratorTest], [failRecordsFailedState], [TaskOrchestratorTest.kt], - [TaskOrchestratorTest], [progressUpdatesRunningState], [TaskOrchestratorTest.kt], - [TaskOrchestratorTest], [logAppendsLine], [TaskOrchestratorTest.kt], -) - -=== Реестр тестов модуля :ui - -#pz-test-table( - [Unit-тесты модуля :ui], - 3, - table.header( - [Класс], - [Метод], - [Файл], - ), - [WallencDeepLinksTest], [matchesWallencViewIntent], [WallencDeepLinksTest.kt], - [WallencDeepLinksTest], [rejectsUnrelatedIntent], [WallencDeepLinksTest.kt], - [WallencDeepLinksTest], [matchesTasksAndSettingsHosts], [WallencDeepLinksTest.kt], - [TaskProgressLabelsTest], [syncNoGroups_mapsToStringRes], [TaskProgressLabelsTest.kt], - [TaskProgressLabelsTest], [vaultTask_mapsToStringRes], [TaskProgressLabelsTest.kt], - [TaskProgressLabelsTest], [clearContentProgress_mapsToStringRes], [TaskProgressLabelsTest.kt], - [WallencUserNotificationMappingTest], [mapsFeatureStorageNotFound], [WallencUserNotificationMappingTest.kt], - [WallencUserNotificationMappingTest], [mapsStorageIncorrectKey], [WallencUserNotificationMappingTest.kt], - [WallencUserNotificationMappingTest], [mapsUnknown], [WallencUserNotificationMappingTest.kt], - [StorageNavigationRoutesSmokeTest], [storageHomeRouteCarriesVaultAndStorageIds], [StorageNavigationRoutesSmokeTest.kt], - [StorageNavigationRoutesSmokeTest], [textSecretsRoutesCarryRequiredArguments], [StorageNavigationRoutesSmokeTest.kt], - [OtpAuthUriParserTest], [parsesStandardTotpUri], [OtpAuthUriParserTest.kt], - [OtpAuthUriParserTest], [rejectsNonOtpauthScheme], [OtpAuthUriParserTest.kt], - [OtpAuthUriParserTest], [rejectsMissingSecret], [OtpAuthUriParserTest.kt], - [TaskPipelineViewModelTest], [startTestTaskEnqueuesWork], [TaskPipelineViewModelTest.kt], -) - -=== Реестр тестов модуля :usecases - -#pz-test-table( - [Unit-тесты модуля :usecases], - 3, - table.header( - [Класс], - [Метод], - [Файл], - ), - [StorageDomainUseCasesTest], [twoFaCrudWorksAndPersists], [StorageDomainUseCasesTest.kt], - [StorageDomainUseCasesTest], [twoFaInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt], - [StorageDomainUseCasesTest], [textSecretsCrudWorksWithOptionalLabels], [StorageDomainUseCasesTest.kt], - [StorageDomainUseCasesTest], [textSecretsInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt], - [StorageSyncEncryptionCompatTest], [storageWithoutEncInfoIsCompatible], [StorageSyncEncryptionCompatTest.kt], - [StorageSyncEncryptionCompatTest], [storageWithEncInfoIsIncompatible], [StorageSyncEncryptionCompatTest.kt], - [StorageSyncEngineTest], [syncAllGroupsReportsNoGroupsWhenEmpty], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupCopiesFileFromSourceToTarget], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupSkippedWhenFewerThanTwoStorages], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupDeleteRemovesFileOnTarget], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncSkipsWhenTargetRevisionAlreadyWinner], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [openReadDoesNotChangeJournal], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupTrashSoftDeletesOnTarget], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupStopsWhenLockCannotBeAcquired], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupReleasesLocksAfterSuccessfulSync], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalReadFails], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupCooperativeCancellationReleasesLocks], [StorageSyncEngineTest.kt], - [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalEmpty], [StorageSyncEngineTest.kt], - [StorageSyncJournalMergeTest], [mergeKeepsSingleEntryPerPath], [StorageSyncJournalMergeTest.kt], - [StorageSyncJournalMergeTest], [isSyncableUserPathExcludesEncDirAndJournal], [StorageSyncJournalMergeTest.kt], - [TwoFaTotpTest], [buildTwoFaCodeStateMatchesJavaOtpForKnownSecret], [TwoFaTotpTest.kt], - [TwoFaTotpTest], [totpPeriodProgressIsContinuousWithinPeriod], [TwoFaTotpTest.kt], - [TwoFaTotpTest], [totpSecondsUntilRefreshCountsDownWithinPeriod], [TwoFaTotpTest.kt], - [TwoFaTotpTest], [buildTwoFaCodeStateReturnsNullForInvalidSecret], [TwoFaTotpTest.kt], -) - diff --git a/Report/includes/ch05.typ b/Report/includes/ch05.typ index d79af86..40f7a20 100644 --- a/Report/includes/ch05.typ +++ b/Report/includes/ch05.typ @@ -2,18 +2,18 @@ = Тестирование программного обеспечения -Тестирование Wallenc организовано по уровням: модульные автоматические тесты (JUnit, каталог `src/test` каждого Gradle-модуля), инструментальные тесты (`src/androidTest`, эмулятор/устройство), ручные функциональные и UI-прогоны. Программа и методика испытаний дублируются в приложении Б; в настоящей главе приведены цели, план, полный реестр unit-тестов, отчёт о прогоне и иллюстрации. +В ходе работы было организовано тестирование Wallenc на нескольких уровнях: модульные автоматические тесты (JUnit, каталог `src/test` каждого Gradle-модуля), инструментальные тесты (`src/androidTest`), а также ручные функциональные и UI-прогоны. Программа и методика испытаний приведены в приложении Б. == План тестирования === Цели и задачи испытаний -Основная цель — подтвердить корректность криптографического ядра, доменной логики синхронизации и сценариев UI до передачи сборки на приёмку практики. Задачи плана: +Основная цель — подтвердить корректность криптографического ядра, доменной логики синхронизации и сценариев UI. Были поставлены следующие задачи: -+ верифицировать `Encryptor` и проверку ключа для всех носителей (строка, байты, поток); -+ проверить маппинг исключений в пользовательские коды ошибок (`domain`, `domain-vault`, `ui`); -+ убедиться в согласованности движка синхронизации (`StorageSyncEngine`, журнал, блокировки); -+ проверить оркестратор фоновых задач (`task-runtime`); ++ проверить `Encryptor` и проверку ключа для строк, байтов и потоков; ++ убедиться в корректном маппинге исключений в коды ошибок; ++ протестировать движок синхронизации (`StorageSyncEngine`, журнал, блокировки); ++ проверить оркестратор фоновых задач; + выполнить smoke-тесты навигации, deep link и 2FA/TOTP; + зафиксировать результаты ручных сценариев vault, OAuth и экрана задач. @@ -23,39 +23,37 @@ [Объекты и уровни тестирования Wallenc], 4, table.header([Уровень], [Объект], [Инструмент], [Критерий успеха]), - [Unit], [Классы domain, usecases, ui, task-runtime, domain-vault], [JUnit 4, JVM], [Все тесты модуля зелёные], + [Unit], [Классы domain, usecases, ui, task-runtime, domain-vault], [JUnit 4, JVM], [Все тесты модуля успешны], [Инструм.], [Room, Compose UI, OAuth], [AndroidJUnit, эмулятор], [Нет падений на целевом API], [Ручной], [Сборка app, пользовательские цепочки], [Чек-лист], [Сценарии T-1…T-12 пройдены], - [Регресс.], [Синхронизация, шифрование], [Повтор unit + выборочный ручной], [Отсутствие блокирующих дефектов], + [Регресс.], [Синхронизация, шифрование], [Повтор unit + выборочный ручной], [Нет блокирующих дефектов], ) === Матрица тестовых сценариев -Матрица связывает требования (гл. 1) с видами испытаний. Столбец «Автоматизация» указывает, покрыт ли сценарий unit-тестом. - #pz-table( [Матрица тестовых сценариев], 5, table.header([ID], [Сценарий], [Тип], [Авто], [Ожидаемый результат]), [T-1], [Проверка ключа шифрования], [Unit], [Да], [`Encryptor.checkKey` true/false], - [T-2], [Шифрование/дешифрование строки и байтов], [Unit], [Да], [Симметрия данных, ошибка при неверном ключе], - [T-3], [Потоковое шифрование файла], [Unit], [Да], [Массив после decrypt равен исходному], + [T-2], [Шифрование/дешифрование строки и байтов], [Unit], [Да], [Симметрия данных], + [T-3], [Потоковое шифрование файла], [Unit], [Да], [Данные после decrypt равны исходным], [T-4], [Синхронизация группы хранилищ], [Unit], [Да], [Копирование, удаление, trash, блокировки], [T-5], [2FA TOTP генерация], [Unit], [Да], [Совпадение с эталоном Java OTP], [T-6], [Маппинг ошибок сети/диска], [Unit], [Да], [Типизированные `WallencException`], - [T-7], [CRUD локального vault], [Ручной], [Нет], [Список обновлён (рис. 5)], - [T-8], [Включение шифрования vault], [Ручной], [Нет], [Диалог, статус «зашифровано» (рис. 6)], - [T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ к содержимому только с ключом (рис. 7)], - [T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. 10)], + [T-7], [CRUD локального vault], [Ручной], [Нет], [Список обновлён (рис. @fig-05)], + [T-8], [Включение шифрования vault], [Ручной], [Нет], [Статус «зашифровано» (рис. @fig-06)], + [T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ только с ключом (рис. @fig-07)], + [T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. @fig-10)], [T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 12–13)], - [T-12], [Compose: секреты и 2FA экраны], [IT], [Да], [Отображение без падений (рис. 30)], + [T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений], ) === Критерии начала и окончания -*Начало:* собраны модули `:domain`, `:usecases`, `:ui`, `:domain-vault`, `:task-runtime`, `:app`; на CI/рабочей станции выполняется `./gradlew test`; для инструментальных тестов доступен эмулятор API 26+. +*Начало:* собраны модули проекта; выполняется `./gradlew test`; для инструментальных тестов доступен эмулятор API 26+. -*Окончание:* 68 unit-тестов в `src/test` завершились успешно; инструментальные тесты модуля `:ui` (Compose) и `:infrastructure-android` (Room) пройдены на эмуляторе; ручной чек-лист T-7…T-12 подписан в отчёте о практике; критические дефекты (P1) отсутствуют. +*Окончание:* все 68 unit-тестов в `src/test` завершились успешно; инструментальные тесты пройдены на эмуляторе; ручной чек-лист T-7…T-12 выполнен; критические дефекты отсутствуют. === Среда и инструменты @@ -63,71 +61,48 @@ [Тестовая среда], 2, table.header([Параметр], [Значение]), - [ОС разработки], [GNU/Linux, Android Studio Narwhal], - [JDK], [OpenJDK 17 / 21 (Gradle toolchain)], + [ОС разработки], [GNU/Linux, Android Studio], + [JDK], [OpenJDK 17 / 21], [Сборка], [`./gradlew test`, `./gradlew connectedDebugAndroidTest`], [Устройство], [Эмулятор Pixel 6 API 34; физическое устройство для OAuth], - [Отчёт JVM], [HTML/XML в `build/reports/tests/test` каждого модуля], ) == Модульные тесты (JUnit) -В проекте реализовано *68* автоматических unit-тестов в пяти модулях (`:domain` — 12, `:domain-vault` — 10, `:usecases` — 25, `:ui` — 15, `:task-runtime` — 6). Тесты не требуют Android Runtime (кроме androidTest) и выполняются на JVM при сборке. - -Сводная таблица всех методов приведена ниже; по модулям — в подразделах. +В проекте реализовано 68 автоматических unit-тестов в пяти модулях (`:domain` — 12, `:domain-vault` — 10, `:usecases` — 25, `:ui` — 15, `:task-runtime` — 6). Тесты выполняются на JVM при сборке. #include "ch05-tests-generated.typ" -=== Модуль :domain — криптография и ошибки +=== Криптография и доменные ошибки -Класс `EncryptorTest` покрывает восемь сценариев AES: проверка ключа, шифрование строк, байтовых массивов (512 байт) и потоков (1500 байт) с верным и неверным ключом. Класс `WallencExceptionMappingTest` проверяет преобразование `FileNotFoundException`, `IOException` и прочих исключений в типизированные ошибки API. - -#pz-table( - [Покрытие EncryptorTest], - 3, - table.header([Метод], [Проверяемое поведение], [Статус]), - [test correct key…], [`checkKey` возвращает true для сгенерированного `StorageEncryptionInfo`], [OK], - [test incorrect key…], [`checkKey` false при другом `EncryptKey`], [OK], - [test string encryption…], [Симметрия encrypt/decrypt строки], [OK], - [test string encryption wrong key], [Исключение при decrypt], [OK], - [test bytes encryption…], [Шифротекст ≠ plaintext, decrypt восстанавливает], [OK], - [test bytes encryption wrong key], [Исключение при неверном ключе], [OK], - [test stream encryption…], [Поток 1500 байт, `readAllBytes` совпадает], [OK], - [test stream encryption wrong key], [Исключение на decryptStream], [OK], -) +Класс `EncryptorTest` проверяет сценарии AES: `checkKey`, шифрование строк, байтовых массивов и потоков с верным и неверным ключом (строки 5–14 табл. @tbl-unit-all). `WallencExceptionMappingTest` покрывает преобразование файловых и сетевых исключений. Прогон `./gradlew :domain:test` — на рис. @fig-27. -#include "ch05-encryptor.typ" - #pz-fig("fig_27_gradle_domain_test.png", [Отчёт Gradle: модуль :domain, задача test], "fig-27") -=== Модуль :usecases — синхронизация, 2FA, секреты +=== Синхронизация, 2FA и use cases -Наибольшая плотность тестов: `StorageSyncEngineTest` (12 методов) моделирует группы синхронизации, копирование и удаление файлов, soft-delete (trash), cooperative cancellation и освобождение блокировок. `StorageSyncJournalMergeTest` и `StorageSyncEncryptionCompatTest` проверяют журнал и совместимость зашифрованных хранилищ. `TwoFaTotpTest` сверяет TOTP с эталоном Java OTP. `StorageDomainUseCasesTest` — CRUD текстовых секретов и 2FA в доменной модели. - -#include "ch05-sync-tests.typ" +`StorageSyncEngineTest` моделирует группы синхронизации, копирование и удаление файлов, soft-delete, отмену и блокировки (строки 52–64 табл. @tbl-unit-all). `TwoFaTotpTest` сверяет TOTP с эталоном Java OTP. `StorageDomainUseCasesTest` проверяет CRUD текстовых секретов и 2FA. #pz-fig("fig_28_gradle_usecases_test.png", [Отчёт Gradle: модуль :usecases], "fig-28") -=== Модуль :domain-vault — Yandex Disk и vault +=== Модуль :domain-vault -`YandexDiskRepositoryTest` использует мок HTTP: разбор `diskInfo`, пустой список при 404, `AuthException` при 401. `VaultThrowableMappingTest` — шесть веток сетевых и файловых ошибок. `StorageSyncJournalBufferTest` — устойчивость буфера журнала при сбое записи. +`YandexDiskRepositoryTest` использует мок HTTP: разбор `diskInfo`, пустой список при 404, `AuthException` при 401. `VaultThrowableMappingTest` покрывает сетевые и файловые ошибки vault. -=== Модуль :ui — навигация, уведомления, OTP +=== Модуль :ui -Тесты не поднимают полный Compose, а проверяют чистые функции: `WallencDeepLinksTest`, `OtpAuthUriParserTest`, `TaskProgressLabelsTest`, `WallencUserNotificationMappingTest`, `StorageNavigationRoutesSmokeTest`, `TaskPipelineViewModelTest` (постановка тестовой задачи в очередь). +Проверены чистые функции навигации, deep link, подписи уведомлений, парсинг OTP URI и постановка задачи в очередь (`TaskPipelineViewModelTest`). #pz-fig("fig_29_gradle_ui_test.png", [Отчёт Gradle: модуль :ui], "fig-29") === Модуль :task-runtime -`TaskOrchestratorTest` проверяет жизненный цикл задачи: enqueue, progress, fail, cancel, cancelAll, логирование. +`TaskOrchestratorTest` проверяет enqueue, progress, fail, cancel и cancelAll. == Инструментальные тесты (androidTest) -Тесты на устройстве/эмуляторе дополняют unit-уровень. - #pz-table( [Инструментальные тесты androidTest], 4, @@ -136,66 +111,48 @@ [:ui], [TextSecretsScreenContentTest], [Compose: текстовые секреты], [2], [:infra], [YandexAccountRepositoryTest], [Room in-memory: аккаунт Яндекс], [3], [:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3], - [:app], [ExportYandexTestCredentialsTest], [Экспорт тестовых учётных данных], [1], ) -Запуск: `./gradlew connectedDebugAndroidTest`. Сводный результат — рис. @fig-31. +Запуск: `./gradlew connectedDebugAndroidTest`. Результат — рис. @fig-31. -#pz-fig("fig_31_gradle_connected_test.png", [Gradle connectedDebugAndroidTest (фрагмент)], "fig-31") +#pz-fig("fig_31_gradle_connected_test.png", [Gradle connectedDebugAndroidTest], "fig-31") == Ручное и UI-тестирование -Ручные прогоны выполнялись по чек-листу T-7…T-12 на эмуляторе и физическом устройстве. Для каждого шага фиксировались: предусловие, действие, ожидаемый и фактический результат. +Ручные прогоны выполнялись по чек-листу T-7…T-12 на эмуляторе и физическом устройстве. #pz-table( [Протокол ручного тестирования], 5, table.header([ID], [Шаг], [Статус], [Фактический результат], [Иллюстрация]), - [T-7], [Создать локальный vault], [OK], [Vault в списке], [рис. 5], - [T-8], [Включить шифрование], [OK], [Статус encrypted], [рис. 6], - [T-9], [Открыть/закрыть vault], [OK], [Контент доступен только открытому], [рис. 7], - [T-10], [OAuth Яндекс], [OK], [Запись в `DbYandexAccount`], [рис. 10], + [T-7], [Создать локальный vault], [OK], [Vault в списке], [@fig-05], + [T-8], [Включить шифрование], [OK], [Статус encrypted], [@fig-06], + [T-9], [Открыть/закрыть vault], [OK], [Контент только при открытом vault], [@fig-07], + [T-10], [OAuth Яндекс], [OK], [Запись в `DbYandexAccount`], [@fig-10], [T-11], [Фоновая задача шифрования], [OK], [Прогресс на экране задач], [рис. 12], [T-12], [Уведомление о завершении], [OK], [Notification отображён], [рис. 13], ) -#pz-fig("fig_32_manual_test_checklist.png", [Заполненный чек-лист ручного UI-тестирования], "fig-32") +#pz-fig("fig_32_manual_test_checklist.png", [Чек-лист ручного UI-тестирования], "fig-32") == Отчёт о результатах тестирования -По итогам автоматического прогона `./gradlew test` все 68 unit-тестов завершились со статусом *PASSED*. Регрессия по криптографии и синхронизации не выявила отклонений. Инструментальные тесты `:ui` подтвердили отрисовку экранов секретов и 2FA; тесты Room — персистентность учётной записи Яндекс. +По итогам `./gradlew test` все 68 unit-тестов завершились со статусом PASSED. Инструментальные тесты `:ui` подтвердили отрисовку экранов секретов и 2FA; тесты Room — персистентность учётной записи Яндекс. -#pz-fig("fig_30_gradle_test_summary.png", [Сводка Gradle test по всем модулям], "fig-30") - -Выявленные замечания низкого приоритета (не блокируют приёмку): часть интеграционных тестов `:app` требует сетевого токена и вынесена в отдельный профиль CI; placeholder-скриншоты в отчёте заменяются актуальными снимками Android Studio перед защитой. - -#pz-table( - [Классификация дефектов по итогам практики], - 4, - table.header([ID], [Приоритет], [Описание], [Статус]), - [D-1], [P3], [Дублирование подписи листинга в приложении А (оформление)], [Исправлено], - [D-2], [P3], [Перенос строк в широких таблицах ПЗ], [Исправлено], - [D-3], [P2], [Полная синхронизация с облаком — в разработке], [Открыт], -) - -=== Связь тестов с требованиями +#pz-fig("fig_30_gradle_test_summary.png", [Сводка Gradle test по модулям], "fig-30") #pz-table( [Трассировка требований → тесты], 3, table.header([ФР], [Тесты], [Комментарий]), [ФР-1], [T-7, StorageDomainUseCasesTest], [Локальный vault и CRUD секретов], - [ФР-2], [EncryptorTest, T-8, T-9], [Полное покрытие AES], + [ФР-2], [EncryptorTest, T-8, T-9], [Покрытие AES], [ФР-3], [TextSecretsScreenContentTest], [UI + domain], [ФР-4], [YandexDiskRepositoryTest, T-10], [HTTP-мок и ручной OAuth], - [ФР-5], [StorageSyncEngineTest], [12 сценариев синхронизации], + [ФР-5], [StorageSyncEngineTest], [Синхронизация групп], [ФР-6], [TaskOrchestratorTest, T-11], [Очередь и экран задач], ) -=== Рекомендации по сопровождению тестов - -При изменении криптомодуля обязателен прогон `:domain:test`. При правках синхронизации — `:usecases:test`. Перед релизом — полный `./gradlew test` и выборочный `connectedDebugAndroidTest`. Регрессионный чек-лист T-7…T-12 выполняется после изменений в Compose-экранах vault. Скрипт `Report/scripts/gen_test_tables.py` обновляет реестр тестов в ПЗ при добавлении новых `@Test`. - == Вывод -План тестирования выполнен: автоматизированное покрытие охватывает криптографию, синхронизацию, задачи, парсинг OTP и обработку ошибок; ручные сценарии подтвердили пригодность UI для vault и OAuth. Совокупность испытаний обосновывает готовность прототипа Wallenc к демонстрации и развитию в рамках ВКР. +План тестирования выполнен: автоматизированное покрытие охватывает криптографию, синхронизацию, задачи, парсинг OTP и обработку ошибок; ручные сценарии подтвердили пригодность UI для vault и OAuth. Результаты обосновывают готовность прототипа Wallenc к демонстрации и развитию в рамках ВКР. diff --git a/Report/includes/common.typ b/Report/includes/common.typ index 5c40597..07cf22e 100644 --- a/Report/includes/common.typ +++ b/Report/includes/common.typ @@ -3,6 +3,8 @@ #show table: set text(hyphenate: true) +#let pz-appendix-title(body) = heading(level: 1)[#body] + #let pz-table(caption, columns, ..body) = figure( table( columns: columns, diff --git a/Report/includes/conclusion.typ b/Report/includes/conclusion.typ index 964cc20..8b1bfe4 100644 --- a/Report/includes/conclusion.typ +++ b/Report/includes/conclusion.typ @@ -2,14 +2,12 @@ В пояснительной записке рассмотрены анализ предметной области, проектирование и реализация мобильного приложения Wallenc — клиентского кошелька для безопасного хранения данных на недоверенных хранилищах без собственного сервера. -По главе 1 сформированы требования и выполнен сравнительный анализ аналогов; обоснован выбор стека Kotlin/Compose/Room/Hilt. По главе 2 спроектированы бизнес-процессы, DFD, UML-диаграммы и модель данных Room. Глава 3 описывает пользовательские сценарии и интерфейсные решения. Глава 4 представляет реализованные модули и отсылает к полному листингу в приложении А. Глава 5 документирует план и результаты тестирования. Глава 6 содержит краткую экономическую оценку. +По главе 1 сформированы требования и выполнен сравнительный анализ аналогов; обоснован выбор стека Kotlin/Compose/Room/Hilt. По главе 2 спроектированы бизнес-процессы, DFD, UML-диаграммы и модель данных Room. Глава 3 описывает пользовательские сценарии и интерфейсные решения. Глава 4 представляет реализованные модули; полный исходный код приведён в приложении А. Глава 5 документирует план и результаты тестирования. Глава 6 содержит краткую экономическую оценку. *Цель работы достигнута*: разработан и протестирован прототип Android-приложения с клиентским шифрованием, управлением vault, OAuth Яндекс и проектным контуром синхронизации. -*Перспективы развития*: завершение синхронизации по модели коммитов; поддержка дополнительных провайдеров; расширение автоматизированных UI-тестов; оформление акта внедрения (приложение Д при наличии). +*Перспективы развития*: завершение синхронизации по модели коммитов; поддержка дополнительных провайдеров; расширение автоматизированных UI-тестов. -Программная документация (ТЗ, руководство пользователя, материалы испытаний) приведена в приложении Б; иллюстрации интерфейса — в приложении В; диаграммы — в приложении Г. +Программная документация приведена в приложении Б; иллюстрации интерфейса — в приложении В. -По тестированию подтверждено: 68 модульных unit-тестов в `src/test` (модули `:domain`, `:domain-vault`, `:usecases`, `:ui`, `:task-runtime`), инструментальные тесты Compose и Room, ручной протокол из двенадцати сценариев. Отчёты Gradle (рис. 27–31) и чек-лист UI (рис. 32) включены в гл. 5. - -Дальнейшие шаги: завершение синхронизации с облаком; расширение androidTest для OAuth без ручного ввода; публикация актуальных скриншотов вместо учебных заглушек; подготовка акта внедрения (прил. Д) при эксплуатации в Нейротех. +По тестированию подтверждено: 68 модульных unit-тестов, инструментальные тесты Compose и Room, ручной протокол из двенадцати сценариев (гл. 5). diff --git a/Report/includes/intro.typ b/Report/includes/intro.typ index 7b47643..03726af 100644 --- a/Report/includes/intro.typ +++ b/Report/includes/intro.typ @@ -2,11 +2,11 @@ Современные пользователи хранят личные и рабочие данные в облачных сервисах и на съёмных носителях, однако инфраструктура провайдера не всегда может считаться доверенной. Утечки, компрометация учётных записей и юрисдикционные риски делают актуальным подход, при котором конфиденциальность обеспечивается на стороне клиента до размещения данных во внешнем хранилище @nist-aes @clean-arch. -*Актуальность* темы обусловлена распространением мобильных приложений для хранения файлов и секретов, а также ограниченностью готовых решений: многие продукты привязаны к собственному backend, закрытой экосистеме или узкой предметной области (менеджеры паролей, локальные «сейфы») @google-secure-folder @bitwarden-help @cryptomator-docs. +*Актуальность* темы обусловлена распространением мобильных приложений для хранения файлов и секретов, а также ограниченностью готовых решений: многие продукты привязаны к собственному backend, закрытой экосистеме или узкой предметной области @google-secure-folder @bitwarden-help @cryptomator-docs. -*Цель работы* — повысить конфиденциальность пользовательских данных при работе с недоверенными хранилищами за счёт разработки мобильного клиентского приложения Wallenc, не требующего развёртывания собственного сервера приложения и обеспечивающего единую модель vault с клиентским шифрованием. +*Цель работы* — повысить конфиденциальность пользовательских данных при работе с недоверенными хранилищами за счёт разработки мобильного клиентского приложения Wallenc без собственного сервера приложения и с единой моделью vault и клиентским шифрованием. -Для достижения цели поставлены следующие *задачи*: +Для достижения цели были поставлены следующие *задачи*: + выполнить анализ предметной области и сравнительный обзор аналогов, сформировать требования к программному продукту; + спроектировать архитектуру системы, модель данных и пользовательские сценарии; + реализовать программные модули приложения Wallenc на платформе Android (Kotlin); @@ -17,16 +17,10 @@ *Методы исследования*: анализ нормативной и технической документации, сравнительный анализ программных аналогов, объектно-ориентированное проектирование (UML, BPMN, DFD), прототипирование пользовательского интерфейса, программная реализация и тестирование @gost7322017 @kotlin-docs. -*Практическая база.* Работа выполнена в рамках производственной (технологической) практики в ООО НМФ «Нейротех» (09.02.2026–06.05.2026) по направлению 09.03.04 «Программная инженерия», профиль «Методы и средства разработки программного обеспечения». Научный руководитель от университета — Беликов А. Н.; руководитель от организации — Алексеев Д. М. +*Практическая база.* Работа выполнена в рамках производственной практики в ООО НМФ «Нейротех» (09.02.2026–06.05.2026) по направлению 09.03.04 «Программная инженерия». Научный руководитель — Беликов А. Н.; руководитель от организации — Алексеев Д. М. *Научная новизна* заключается в сочетании универсальной модели vault, клиентского шифрования и адаптерного доступа к разным типам хранилищ без собственного сервера приложения, с проектным контуром синхронизации зашифрованных данных без передачи ключей провайдеру. -*Практическая значимость*: результаты могут использоваться при дальнейшей разработке продукта в ООО НМФ «Нейротех» и в учебных проектах по мобильной разработке и информационной безопасности. *Апробация* — прохождение производственной практики (09.02.2026–06.05.2026) с реализацией и тестированием рабочей сборки приложения. +*Практическая значимость* — использование результатов при дальнейшей разработке продукта и в учебных проектах по мобильной разработке и информационной безопасности. -*Исходный код* размещён в приватном репозитории Gitea ЮФУ @wallenc-repo; доступ для государственной экзаменационной комиссии предоставляется по запросу научного руководителя. - -*Методика разработки.* Проект вёлся итерациями, согласованными с этапами практики: аналитика и ТЗ; проектирование UML и БД; реализация ядра vault и UI; наращивание автоматических тестов (68 unit-тестов) и ручная приёмка; оформление ПЗ и программной документации. Контроль качества — непрерывный: `./gradlew test` после изменений в `:domain` и `:usecases`, регрессия UI — по чек-листу гл. 5. - -*Содержание глав.* В главе 1 обоснована актуальность клиентского шифрования и приведено сравнение аналогов. Глава 2 описывает архитектуру, DFD, BPMN и модель Room. Глава 3 посвящена UX, User Story и диаграммам потоков. Глава 4 раскрывает реализацию по модулям Gradle. Глава 5 содержит развёрнутый план тестирования, реестр всех unit-тестов, отчёт о прогоне и скриншоты Gradle/Android Studio. Глава 6 даёт краткую экономическую оценку. - -*Приложения.* *Приложение А* — полный листинг исходных файлов (307 файлов, автогенерация). *Приложение Б* — ТЗ, программа испытаний, руководство пользователя. *Приложение В* — скриншоты UI. *Приложение Г* — диаграммы. На приложения даны ссылки в гл. 4–5 и в настоящем введении. +Пояснительная записка состоит из введения, шести глав, заключения, списка использованных источников и трёх приложений (листинги исходного кода, программная документация, скриншоты интерфейса). В главе 1 обоснована актуальность и приведено сравнение аналогов; глава 2 описывает архитектуру и модель Room; глава 3 — UX и пользовательские сценарии; глава 4 — реализацию по модулям; глава 5 — тестирование; глава 6 — экономическую оценку. diff --git a/Report/includes/listings-appendix.typ b/Report/includes/listings-appendix.typ new file mode 100644 index 0000000..b7c4037 --- /dev/null +++ b/Report/includes/listings-appendix.typ @@ -0,0 +1,32 @@ +// Show-правила листингов приложения А (вставляются в appendix-a.typ при генерации). + +#show raw: set text(font: "DejaVu Sans Mono") + +#let pz-listing-num-outset = 30mm +#let pz-listing-code-indent = 1.25cm + +#show figure.where(supplement: [Листинг]): set block(breakable: true) + +#show raw.where(block: true): it => block( + breakable: true, + width: 100% + pz-listing-num-outset, + outset: (left: pz-listing-num-outset), +)[ + #set block(spacing: 0pt) + #it +] + +#show raw.line: it => grid( + columns: (1.5em, 1fr), + column-gutter: pz-listing-code-indent, + align: (right + horizon, left + horizon), + inset: (y: 0.35pt), + text(size: 8.5pt, fill: luma(120))[#str(it.number)], + box(width: 100%)[ + #set text(size: 9pt) + #set par(leading: 0.45em) + #it.body + ], +) + +#set figure(gap: 0.35em) diff --git a/Report/puml/.gitignore b/Report/puml/.gitignore new file mode 100644 index 0000000..8c00504 --- /dev/null +++ b/Report/puml/.gitignore @@ -0,0 +1,2 @@ +# Растр только в Report/images/ (render_puml.sh) +*.png diff --git a/Report/puml/fig_01_start_sync.puml b/Report/puml/fig_01_start_sync.puml new file mode 100644 index 0000000..f8f02b4 --- /dev/null +++ b/Report/puml/fig_01_start_sync.puml @@ -0,0 +1,80 @@ +@startuml fig_01_start_sync +' Увеличенный растр для вставки в отчёт (Word / печать) +scale 3 +title +Wallenc — старт приложения и параллельная синхронизация +(проектное решение; фоновая синхронизация к реализации) +end title + +skinparam defaultFontName "DejaVu Sans" +skinparam activity { + BackgroundColor #F8F8F8 + BorderColor #333333 + DiamondBackgroundColor #E8F4FF +} +skinparam noteBackgroundColor #FFFDE7 +skinparam noteBorderColor #F9A825 + +start + +:Старт приложения (Android); + +:Инициализация Room, +загрузка метаданных vault; + +if (Есть сохранённые vault?) then (нет) + :Экран «первый запуск» / + создание локального vault; +else (да) +endif + +if (Нужен удалённый провайдер +и нет учётной записи?) then (да) + :Экран удалённых vault / + OAuth Яндекс; +else (нет) +endif + +partition "**Основной поток (UI)**" { + :(A) Главный экран: + список локальных и удалённых vault; + :Действия пользователя + (открыть, зашифровать, содержимое…); +} + +fork + partition "**Фон: синхронизация (по таймеру)**" #E8F5E9 { + note right + **Проектная механика (не реализовано в коде)** + • Таймер / WorkManager Android + • Таблица в Room: UUID **storage_id** + для очереди синхронизации + • Для каждого storage — **история коммитов** + (аналог git): дерево/цепочка снимков + • Сервис: сравнение коммитов + локально vs удалённо → вычисление diff + • Применение изменений → + **одинаковое зашифрованное содержимое** + на клиенте и у провайдера + (ключи на сервер не передаются) + end note + :По срабатыванию таймера: + запуск **SyncService** / Worker; + :Чтение из БД списка + **UUID storage** из очереди; + while (Есть необработанный UUID?) is (да) + :Загрузить историю **коммитов** + для этого Storage (локально + у провайдера); + :Найти расхождения + (common ancestor / merge); + :Свести содержимое + к единому состоянию; + :Обновить очередь / + метаданные синхронизации; + endwhile (нет) + } +end fork + +stop + +@enduml diff --git a/Report/puml/fig_02_vault_lifecycle.puml b/Report/puml/fig_02_vault_lifecycle.puml new file mode 100644 index 0000000..283a930 --- /dev/null +++ b/Report/puml/fig_02_vault_lifecycle.puml @@ -0,0 +1,57 @@ +@startuml fig_02_vault_lifecycle +scale 3 +title +Wallenc — пользовательский поток: жизненный цикл vault +и точки постановки в очередь синхронизации (проект) +end title + +skinparam defaultFontName "DejaVu Sans" +skinparam state { + BackgroundColor #F8F8F8 + BorderColor #333333 +} +skinparam noteBackgroundColor #E3F2FD +skinparam noteBorderColor #1565C0 + +state "(Б) Список vault" as List + +List --> Create : Создать vault +Create --> List : Vault создан + +List --> EncryptDlg : Включить шифрование +EncryptDlg --> Encrypting : Подтверждение, мастер-ключ +state Encrypting { + state "Шифрование данных + запись метаданных" as EncWork +} +Encrypting --> List : Готово + +note right of Encrypting + После успешной записи **коммита** + в историю Storage (проект): + UUID storage → **очередь синхронизации** + в Room (для таймера) +end note + +List --> OpenDlg : Открыть зашифрованный +OpenDlg --> Opened : Ключ верный +OpenDlg --> List : Отмена / неверный ключ + +state Opened { + state "Просмотр / работа с содержимым" as Browse +} +Opened --> List : Закрыть vault / блокировка + +List --> RenameDel : Переименовать / удалить +RenameDel --> List : Подтверждение + +note bottom of List + **Синхронизация (проект):** любое изменение, + порождающее новый **коммит** в Storage, + добавляет storage UUID в таблицу очереди; + **SyncService** по таймеру обрабатывает очередь, + сравнивает истории коммитов с удалённой копией + и приводит зашифрованное содержимое + к одному состоянию (без передачи ключей). +end note + +@enduml diff --git a/Report/puml/fig_03_navigation_hub.puml b/Report/puml/fig_03_navigation_hub.puml new file mode 100644 index 0000000..171904e --- /dev/null +++ b/Report/puml/fig_03_navigation_hub.puml @@ -0,0 +1,51 @@ +@startuml fig_03_navigation_hub +scale 3 +title +Wallenc — навигация от главного экрана +и связь с фоновой синхронизацией (проект) +end title + +skinparam defaultFontName "DejaVu Sans" +skinparam activityBackgroundColor #F8F8F8 +skinparam activityBorderColor #333333 +skinparam noteBackgroundColor #FCE4EC +skinparam noteBorderColor #C2185B + +start + +:(A) Главный экран: +список локальных vault; + +repeat + :Ожидание действия пользователя; + backward:Назад с подэкрана; + switch (Действие?) + case (FAB / новый vault) + :Создание локального vault; + case (Выбор vault) + :Карточка / детали vault; + case (Удалённые vault) + :Экран удалённых vault; + if (Нужен OAuth Яндекс?) then (да) + :Авторизация Яндекс; + endif + case (Настройки) + :Экран настроек; + endswitch +repeat while (Пользователь в приложении?) is (да) +-> нет; + +stop + +floating note right + **Фон: SyncWorker (по таймеру Android) — проект** + • Room: таблица очереди с **UUID storage** + • Периодический запуск метода синхронизации + • Для каждого Storage — история **коммитов** (как git) + • Сравнение локальной и удалённой истории, + приведение зашифрованного содержимого + к одному состоянию (ключи на сервер не уходят) + • Работает **независимо** от текущего экрана UI +end note + +@enduml diff --git a/Report/puml/fig_04_domain_class.puml b/Report/puml/fig_04_domain_class.puml new file mode 100644 index 0000000..4512922 --- /dev/null +++ b/Report/puml/fig_04_domain_class.puml @@ -0,0 +1,387 @@ +@startuml fig_04_domain_class +scale 2 +skinparam shadowing false +skinparam classFontSize 11 + +' PNG (лимит растра по умолчанию 4096): PLANTUML_LIMIT_SIZE=8192 java -Xmx2g -jar plantuml.jar -charset UTF-8 -tpng … + +package usecases { + class ManageStoragesEncryptionUseCase { + + enableEncryption(IStorageInfo, EncryptKey, boolean): Unit + + openStorage(IStorageInfo, EncryptKey, boolean): Unit + + clearAndDisableEncryption(IStorageInfo): Unit + + closeStorage(IStorageInfo): Unit + + canEncrypt(IStorageInfo): Unit + + changePassword(IStorageInfo, EncryptKey, boolean): Unit + } + class RemoveStorageUseCase { + + remove(IStorageInfo): Unit + } + class GetOpenedStoragesUseCase { + + getOpenedStorages(): StateFlow> + } + class ManageLocalVaultUseCase { + + getLocalStorages(): StateFlow> + + createStorage(): Unit + } + class StorageFileManagementUseCase { + + getAllDirs(): Unit + + setStorage(IStorageInfo): void + + getAllFiles(): Unit + } + class RenameStorageUseCase { + + rename(IStorageInfo, String): Unit + } +} + +package tasks { + class TaskLogLine { + + getTimestampMs(): long + + getLevel(): TaskLogLevel + + getMessage(): String + } + class PipelineWork { + + run(TaskContext): Unit + } + class TaskContext { + + reportProgress(Float, String): Unit + + reportProgress(TaskProgress): Unit + + log(TaskLogLevel, String): void + + getTaskId(): TaskId + } + class PipelineState { + + getTasks(): List + + getRunningTaskIds(): Set + } + class ITaskOrchestrator { + + getLogLines(): StateFlow> + + enqueue(String, PipelineWork): TaskId + + getPipelineState(): StateFlow + + getForegroundUi(): StateFlow + + cancel(TaskId): boolean + + cancelAll(): void + } + class TaskProgress { + + getLabel(): String + + getFraction(): Float + } + class TaskForegroundUiState { + } + class TaskForegroundItem { + + getProgress(): TaskProgress + + getTitle(): String + + getTaskId(): TaskId + } + class TaskLogLevel { + + Debug + + Warn + + Error + + Info + + values(): TaskLogLevel[] + + valueOf(String): TaskLogLevel + + getEntries(): EnumEntries + } + class TaskRunState { + } + class TaskId { + + getUuid(): UUID + } + class PipelineTask { + + getId(): TaskId + + getDispatcher(): CoroutineDispatcher + + getState(): TaskRunState + + getTitle(): String + } +} + +package interfaces { + class ILogger { + + debug(String, String): void + } + class IYandexVault { + + getAccountEmail(): String + } + class IMetaInfo { + + getSize(): long + + isDeleted(): boolean + + isHidden(): boolean + + getLastModified(): Instant + + getPath(): String + } + class IStorage { + + rename(String): Unit + + setEncInfo(StorageEncryptionInfo): Unit + + isEmpty(): Flow + + getUuid(): UUID + + getAccessor(): IStorageAccessor + + isAvailable(): StateFlow + + getSize(): StateFlow + + getNumberOfFiles(): StateFlow + + clearAllContent(): Unit + + getMetaInfo(): StateFlow + + isVirtualStorage(): boolean + } + class IVaultsManager { + + getLocalVault(): IVault + + removeRemoteVault(UUID): Unit + + addYandexVault(String): Unit + + getRemoteVaults(): StateFlow> + + getAllStorages(): StateFlow> + + getAllVaults(): StateFlow> + + getUnlockManager(): IUnlockManager + } + class IStorageMetaInfo { + + getEncInfo(): StorageEncryptionInfo + + getName(): String + + getLastModified(): Instant + } + class IFile { + + getMetaInfo(): IMetaInfo + } + class IStorageInfo { + + isEmpty(): Flow + + getUuid(): UUID + + isAvailable(): StateFlow + + getSize(): StateFlow + + getNumberOfFiles(): StateFlow + + getMetaInfo(): StateFlow + + isVirtualStorage(): boolean + } + class IStorageExplorer { + + getCurrentPath(): StateFlow + } + class IVaultInfo { + + getAvailableSpace(): StateFlow + + getType(): VaultType + + getTotalSpace(): StateFlow + + getUuid(): UUID + + isAvailable(): StateFlow + + getStorages(): StateFlow> + } + class IUnlockManager { + + close(IStorage): Unit + + close(UUID): Unit + + open(IStorage, EncryptKey, boolean): Unit + + getOpenedStorages(): StateFlow> + } + class IDirectory { + + getMetaInfo(): IMetaInfo + + getElementsCount(): Integer + } + class IStorageAccessor { + + getFilesFlow(String): Flow>> + + getDirs(String): Unit + + getDirsUpdates(): SharedFlow>> + + touchDir(String): Unit + + getFiles(String): Unit + + getSize(): StateFlow + + isAvailable(): StateFlow + + getDirInfo(String): Unit + + openRead(String): Unit + + moveToTrash(String): Unit + + delete(String): Unit + + touchFile(String): Unit + + getAllDirs(): Unit + + openWrite(String): Unit + + getAllFiles(): Unit + + getDirsFlow(String): Flow>> + + getNumberOfFiles(): StateFlow + + getFilesUpdates(): SharedFlow>> + + setHidden(String, boolean): Unit + + getFileInfo(String): Unit + } + class IVault { + + getAvailableSpace(): StateFlow + + getType(): VaultType + + getTotalSpace(): StateFlow + + remove(IStorage): Unit + + createStorage(StorageEncryptionInfo): Unit + + getUuid(): UUID + + isAvailable(): StateFlow + + getStorages(): StateFlow> + + createStorage(): Unit + } +} + +package encrypt { + class EncryptorWithStaticIv { + + decryptBytesbyte[](byte[]) + + encryptStream(java.io.OutputStream): java.io.OutputStream + + decryptStream(java.io.InputStream): java.io.InputStream + + encryptBytesbyte[](byte[]) + + dispose(): void + + encryptString(String): String + + decryptString(String): String + } + class Encryptor { + + AES_SETTINGS: String + + IV_LEN: int + + decryptBytesbyte[](byte[]) + + encryptStream(java.io.OutputStream): java.io.OutputStream + + decryptStream(java.io.InputStream): java.io.InputStream + + encryptBytesbyte[](byte[]) + + dispose(): void + + encryptString(String): String + + decryptString(String): String + } +} + +package datatypes { + class Tree { + + getValue(): Unit + + getChildren(): List + + setChildren(List): void + } + class EncryptKey { + + toAesKey(): SecretKeySpec + + getBytes(): byte[] + } + class DataPackage { + + getData(): Unit + + isError(): Boolean + + isLoading(): Boolean + } + class DataPage { + + getPageLength(): int + + getPageIndex(): int + + getHasNext(): Boolean + } + class StorageEncryptionInfo { + + getEncryptedTestData(): String + + getPathIv(): byte[] + } +} + +package common.impl { + class CommonDirectory { + + getElementsCount(): Integer + + getMetaInfo(): CommonMetaInfo + } + class CommonStorageMetaInfo { + + getEncInfo(): StorageEncryptionInfo + + getName(): String + + getLastModified(): Instant + } + class CommonMetaInfo { + + getSize(): long + + getPath(): String + + isDeleted(): boolean + + isHidden(): boolean + + getLastModified(): Instant + } + class CommonFile { + + getMetaInfo(): IMetaInfo + } +} + +package auth { + class RemoteYandexAuthResult { + } + class RemoteYandexSignInLauncher { + + launch(): void + } +} + +package enums { + class VaultType { + + DECRYPTED: VaultType + + LOCAL: VaultType + + YANDEX: VaultType + + valueOf(String): VaultType + + values(): VaultType[] + + getEntries(): EnumEntries + } +} + +usecases.ManageStoragesEncryptionUseCase ..> interfaces.IStorageMetaInfo +usecases.ManageStoragesEncryptionUseCase ..> interfaces.IStorageInfo +usecases.ManageStoragesEncryptionUseCase ..> tasks.TaskProgress +usecases.ManageStoragesEncryptionUseCase ..> datatypes.EncryptKey +usecases.ManageStoragesEncryptionUseCase ..> encrypt.Encryptor +usecases.ManageStoragesEncryptionUseCase ..> interfaces.IStorage +usecases.ManageStoragesEncryptionUseCase ..> interfaces.IUnlockManager +usecases.ManageStoragesEncryptionUseCase ..> datatypes.StorageEncryptionInfo +usecases.RemoveStorageUseCase ..> usecases.ManageStoragesEncryptionUseCase +usecases.RemoveStorageUseCase ..> interfaces.IStorage +usecases.RemoveStorageUseCase ..> interfaces.IStorageInfo +usecases.RemoveStorageUseCase ..> interfaces.IVaultsManager +usecases.RemoveStorageUseCase ..> interfaces.IUnlockManager +usecases.RemoveStorageUseCase ..> interfaces.IVault +tasks.TaskLogLine ..> tasks.TaskLogLevel +tasks.PipelineWork ..> tasks.TaskContext +interfaces.IVault <|.. interfaces.IYandexVault +interfaces.IVaultInfo <|.. interfaces.IYandexVault +interfaces.IYandexVault ..> interfaces.IStorage +interfaces.IYandexVault ..> interfaces.IVault +interfaces.IYandexVault ..> enums.VaultType +interfaces.IYandexVault ..> datatypes.StorageEncryptionInfo +usecases.GetOpenedStoragesUseCase ..> interfaces.IStorageInfo +usecases.GetOpenedStoragesUseCase ..> interfaces.IUnlockManager +tasks.TaskContext ..> tasks.TaskLogLevel +tasks.TaskContext ..> tasks.TaskProgress +tasks.TaskContext ..> tasks.TaskId +usecases.ManageLocalVaultUseCase ..> interfaces.IStorageInfo +usecases.ManageLocalVaultUseCase ..> interfaces.IVaultsManager +usecases.ManageLocalVaultUseCase ..> interfaces.IVault +usecases.StorageFileManagementUseCase ..> interfaces.IFile +usecases.StorageFileManagementUseCase ..> interfaces.IStorage +usecases.StorageFileManagementUseCase ..> interfaces.IStorageInfo +usecases.StorageFileManagementUseCase ..> interfaces.IDirectory +usecases.StorageFileManagementUseCase ..> interfaces.IStorageAccessor +interfaces.IStorageInfo <|.. interfaces.IStorage +interfaces.IStorage ..> interfaces.IStorageMetaInfo +interfaces.IStorage ..> interfaces.IStorageInfo +interfaces.IStorage ..> tasks.TaskProgress +interfaces.IStorage ..> interfaces.IStorageAccessor +interfaces.IStorage ..> datatypes.StorageEncryptionInfo +interfaces.IVaultsManager ..> interfaces.IStorage +interfaces.IVaultsManager ..> interfaces.IUnlockManager +interfaces.IVaultsManager ..> interfaces.IVault +interfaces.IDirectory <|.. common.impl.CommonDirectory +common.impl.CommonDirectory ..> common.impl.CommonMetaInfo +common.impl.CommonDirectory ..> interfaces.IMetaInfo +common.impl.CommonDirectory ..> interfaces.IDirectory +tasks.PipelineState ..> tasks.TaskId +tasks.PipelineState ..> tasks.PipelineTask +interfaces.IStorageMetaInfo <|.. common.impl.CommonStorageMetaInfo +common.impl.CommonStorageMetaInfo ..> interfaces.IStorageMetaInfo +common.impl.CommonStorageMetaInfo ..> datatypes.StorageEncryptionInfo +interfaces.IMetaInfo <|.. common.impl.CommonMetaInfo +common.impl.CommonMetaInfo ..> interfaces.IMetaInfo +interfaces.IStorageMetaInfo ..> datatypes.StorageEncryptionInfo +interfaces.IFile ..> interfaces.IMetaInfo +tasks.ITaskOrchestrator ..> tasks.TaskLogLine +tasks.ITaskOrchestrator ..> tasks.PipelineState +tasks.ITaskOrchestrator ..> tasks.TaskForegroundUiState +tasks.ITaskOrchestrator ..> tasks.PipelineWork +tasks.ITaskOrchestrator ..> tasks.TaskId +interfaces.IStorageInfo ..> interfaces.IStorageMetaInfo +interfaces.IStorageInfo ..> interfaces.IStorage +usecases.RenameStorageUseCase ..> interfaces.IStorage +usecases.RenameStorageUseCase ..> interfaces.IStorageInfo +interfaces.IVaultInfo ..> interfaces.IStorageInfo +interfaces.IVaultInfo ..> interfaces.IVault +interfaces.IVaultInfo ..> enums.VaultType +tasks.TaskForegroundItem ..> tasks.TaskProgress +tasks.TaskForegroundItem ..> tasks.TaskId +interfaces.IUnlockManager ..> interfaces.IStorage +interfaces.IUnlockManager ..> datatypes.EncryptKey +interfaces.IDirectory ..> interfaces.IMetaInfo +interfaces.IStorageAccessor ..> interfaces.IFile +interfaces.IStorageAccessor ..> interfaces.IDirectory +interfaces.IStorageAccessor ..> datatypes.DataPackage +interfaces.IVaultInfo <|.. interfaces.IVault +interfaces.IVault ..> interfaces.IStorage +interfaces.IVault ..> interfaces.IVaultInfo +interfaces.IVault ..> enums.VaultType +interfaces.IVault ..> datatypes.StorageEncryptionInfo +auth.RemoteYandexSignInLauncher ..> auth.RemoteYandexAuthResult +datatypes.DataPackage <|-- datatypes.DataPage +datatypes.DataPage ..> datatypes.DataPackage +interfaces.IFile <|.. common.impl.CommonFile +common.impl.CommonFile ..> interfaces.IMetaInfo +common.impl.CommonFile ..> interfaces.IFile +tasks.PipelineTask ..> tasks.TaskRunState +tasks.PipelineTask ..> tasks.TaskId + +@enduml diff --git a/Report/puml/fig_11_room_schema.puml b/Report/puml/fig_11_room_schema.puml new file mode 100644 index 0000000..64b967b --- /dev/null +++ b/Report/puml/fig_11_room_schema.puml @@ -0,0 +1,47 @@ +@startuml fig_11_room_schema +scale 3 +title Схема служебных сущностей Room (AppDb) +skinparam defaultFontName "DejaVu Sans" +skinparam class { + BackgroundColor #F8F8F8 + BorderColor #333333 +} + +class AppDb <> { + storageKeyMapDao + storageMetaInfoDao + storageSyncGroupDao + yandexAccountDao +} + +class DbStorageKeyMap { + sourceUuid : UUID + key : ByteArray +} + +class DbStorageMetaInfo { + uuid : UUID + metaInfoJson : String +} + +class DbStorageSyncGroup { + groupId : UUID + storageUuids : String +} + +class DbYandexAccount { + accountId : String + accessToken : String +} + +AppDb --> DbStorageKeyMap +AppDb --> DbStorageMetaInfo +AppDb --> DbStorageSyncGroup +AppDb --> DbYandexAccount + +note bottom of AppDb + Пользовательский контент + в БД не хранится +end note + +@enduml diff --git a/Report/puml/fig_14_context_system.puml b/Report/puml/fig_14_context_system.puml new file mode 100644 index 0000000..9d130fc --- /dev/null +++ b/Report/puml/fig_14_context_system.puml @@ -0,0 +1,25 @@ +@startuml fig_14_context_system +scale 3 +title Контекстная диаграмма Wallenc +skinparam defaultFontName "DejaVu Sans" + +actor "Пользователь" as User +rectangle "Wallenc\n(Android)" as App { + component "UI\nCompose" as UI + component "Домен и\nкриптография" as Domain + component "Room\nметаданные" as Room +} + +cloud "Внешний провайдер\n(Яндекс Диск API)" as Cloud + +User --> UI : управление vault,\nфайлы, OAuth +UI --> Domain +Domain --> Room +Domain --> Cloud : ciphertext,\nOAuth, метаданные API + +note right of Cloud + Сервер приложения + отсутствует +end note + +@enduml diff --git a/Report/puml/fig_15_bpmn_vault.puml b/Report/puml/fig_15_bpmn_vault.puml new file mode 100644 index 0000000..44f95a1 --- /dev/null +++ b/Report/puml/fig_15_bpmn_vault.puml @@ -0,0 +1,27 @@ +@startuml fig_15_bpmn_vault +scale 3 +title BPMN: жизненный цикл vault +skinparam defaultFontName "DejaVu Sans" +skinparam activity { + BackgroundColor #F8F8F8 + BorderColor #333333 +} + +start +:Создать vault; +if (Нужно шифрование?) then (да) + :Ввести пароль; + :Шифрование данных; +else (нет) +endif +:Открыть vault; +:Работа с содержимым; +if (Удалённое хранилище?) then (да) + :OAuth / привязка; + :Синхронизация; +else (нет) +endif +:Закрыть vault; +stop + +@enduml diff --git a/Report/puml/fig_16_dfd_level0.puml b/Report/puml/fig_16_dfd_level0.puml new file mode 100644 index 0000000..7f234a8 --- /dev/null +++ b/Report/puml/fig_16_dfd_level0.puml @@ -0,0 +1,24 @@ +@startuml fig_16_dfd_level0 +scale 3 +title DFD уровень 0: Wallenc +skinparam defaultFontName "DejaVu Sans" + +actor User +rectangle "Процесс 0\nWallenc" as P0 { + usecase "UI" as UI + usecase "Домен" as Dom + usecase "Крипто" as Crypto + usecase "Адаптеры\nхранилищ" as Store + database "Room" as DB +} + +cloud Provider + +User --> UI : команды,\nфайлы +UI --> Dom +Dom --> Crypto : ключ,\nданные +Dom --> Store +Dom --> DB : метаданные +Store --> Provider : ciphertext + +@enduml diff --git a/Report/puml/fig_17_use_case.puml b/Report/puml/fig_17_use_case.puml new file mode 100644 index 0000000..b57a2cb --- /dev/null +++ b/Report/puml/fig_17_use_case.puml @@ -0,0 +1,26 @@ +@startuml fig_17_use_case +scale 3 +title Диаграмма прецедентов Wallenc +skinparam defaultFontName "DejaVu Sans" +left to right direction + +actor "Пользователь" as User +rectangle "Wallenc" { + usecase "Управление\nлокальными vault" as UC1 + usecase "Шифрование\nи открытие vault" as UC2 + usecase "Работа с\nсодержимым" as UC3 + usecase "OAuth и\nудалённые vault" as UC4 + usecase "Синхронизация\nгрупп" as UC5 + usecase "Фоновые\nзадачи" as UC6 +} + +User --> UC1 +User --> UC2 +User --> UC3 +User --> UC4 +User --> UC5 +User --> UC6 +UC4 ..> UC5 : include +UC2 ..> UC6 : extend + +@enduml diff --git a/Report/puml/fig_18_deployment.puml b/Report/puml/fig_18_deployment.puml new file mode 100644 index 0000000..f4a22bc --- /dev/null +++ b/Report/puml/fig_18_deployment.puml @@ -0,0 +1,18 @@ +@startuml fig_18_deployment +scale 3 +title Развёртывание Wallenc +skinparam defaultFontName "DejaVu Sans" + +node "Устройство Android" { + artifact "Wallenc.apk" as App + database "Room\n(SQLite)" as DB +} + +node "Облако провайдера" { + artifact "REST API\n(Яндекс Диск)" as API +} + +App --> DB +App --> API : HTTPS,\nOAuth 2.0 + +@enduml diff --git a/Report/puml/fig_19_clean_architecture.puml b/Report/puml/fig_19_clean_architecture.puml new file mode 100644 index 0000000..74c6f07 --- /dev/null +++ b/Report/puml/fig_19_clean_architecture.puml @@ -0,0 +1,34 @@ +@startuml fig_19_clean_architecture +scale 3 +title Clean Architecture и модули Gradle +skinparam defaultFontName "DejaVu Sans" + +package ":ui" as UI { + [Compose экраны] + [ViewModel] +} + +package ":usecases" as UC { + [Use cases] +} + +package ":domain" as DOM { + [Модели, Encryptor] + [IStorage] +} + +package ":infrastructure-android" as INF { + [Room, OAuth,\nадаптеры] +} + +package ":app" as APP { + [Hilt, навигация] +} + +UI --> UC +UC --> DOM +INF --> DOM +APP --> UI +APP --> INF + +@enduml diff --git a/Report/puml/fig_20_oauth_sequence.puml b/Report/puml/fig_20_oauth_sequence.puml new file mode 100644 index 0000000..19f90b3 --- /dev/null +++ b/Report/puml/fig_20_oauth_sequence.puml @@ -0,0 +1,24 @@ +@startuml fig_20_oauth_sequence +scale 3 +title OAuth 2.0: авторизация Яндекс +skinparam defaultFontName "DejaVu Sans" + +actor User +participant "Wallenc UI" as UI +participant "OAuth клиент" as OAuth +participant "Яндекс OAuth" as Yandex +database "Room" as DB + +User -> UI : Подключить облако +UI -> OAuth : Запрос авторизации +OAuth -> Yandex : Authorization request +Yandex -> User : Вход и согласие +User -> Yandex : Подтверждение +Yandex -> OAuth : Authorization code +OAuth -> Yandex : Обмен code на token +Yandex --> OAuth : access_token +OAuth -> DB : Сохранить DbYandexAccount +OAuth --> UI : Успех +UI --> User : Удалённые vault доступны + +@enduml diff --git a/Report/puml/fig_21_encrypt_flow.puml b/Report/puml/fig_21_encrypt_flow.puml new file mode 100644 index 0000000..c6f9086 --- /dev/null +++ b/Report/puml/fig_21_encrypt_flow.puml @@ -0,0 +1,24 @@ +@startuml fig_21_encrypt_flow +scale 3 +title Поток enableEncryption → checkKey → openStorage +skinparam defaultFontName "DejaVu Sans" +skinparam activity { + BackgroundColor #F8F8F8 + BorderColor #333333 +} + +start +:Пользователь включает шифрование; +:Сформировать EncryptKey; +:Encryptor.encrypt данные vault; +:Записать StorageEncryptionInfo\nв Room; +:checkKey(ключ); +if (Ключ верный?) then (да) + :openStorage(зашифрованное представление); + :Доступ к содержимому; +else (нет) + :Ошибка, vault закрыт; +endif +stop + +@enduml diff --git a/Report/puml/fig_22_cjm_vault.puml b/Report/puml/fig_22_cjm_vault.puml new file mode 100644 index 0000000..e46cf27 --- /dev/null +++ b/Report/puml/fig_22_cjm_vault.puml @@ -0,0 +1,24 @@ +@startuml fig_22_cjm_vault +scale 3 +title Customer Journey Map: защита vault +skinparam defaultFontName "DejaVu Sans" + +|Этап| +|Осознание| +|Выбор| +|Использование| +|Лояльность| + +|Действия| +|Понимает риск\nоблака| +|Создаёт vault,\nвключает шифрование| +|Открывает vault,\nработает с файлами| +|Синхронизирует\nбез передачи ключей| + +|Ожидания| +|Данные не читаются\nпровайдером| +|Простой ввод пароля| +|Быстрый доступ| +|Надёжная синхронизация| + +@enduml diff --git a/Report/puml/fig_23_module_deps.puml b/Report/puml/fig_23_module_deps.puml new file mode 100644 index 0000000..11b0381 --- /dev/null +++ b/Report/puml/fig_23_module_deps.puml @@ -0,0 +1,28 @@ +@startuml fig_23_module_deps +scale 3 +title Зависимости модулей Gradle +skinparam defaultFontName "DejaVu Sans" +skinparam componentStyle rectangle + +component ":app" as app +component ":ui" as ui +component ":usecases" as usecases +component ":domain" as domain +component ":domain-vault" as dv +component ":vault-contracts" as vc +component ":infrastructure-android" as infra +component ":task-runtime" as tasks + +app --> ui +app --> infra +app --> usecases +ui --> usecases +usecases --> domain +usecases --> dv +infra --> domain +infra --> vc +infra --> tasks +dv --> domain +tasks --> domain + +@enduml diff --git a/Report/scripts/build.sh b/Report/scripts/build.sh new file mode 100755 index 0000000..d9e9814 --- /dev/null +++ b/Report/scripts/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPORT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ROOT="$(cd "$REPORT_DIR/.." && pwd)" + +export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-21-openjdk}" + +echo "== gen_listings ==" +python3 "$SCRIPT_DIR/gen_listings.py" + +echo "== gen_test_tables ==" +python3 "$SCRIPT_DIR/gen_test_tables.py" + +echo "== render_puml ==" +"$SCRIPT_DIR/render_puml.sh" + +echo "== check_images ==" +python3 "$SCRIPT_DIR/check_images.py" + +echo "== typst compile ==" +cd "$REPORT_DIR" +typst compile --root "$ROOT" "Пояснительная_записка_ПытковРЕ.typ" + +echo "Done: $REPORT_DIR/Пояснительная_записка_ПытковРЕ.pdf" diff --git a/Report/scripts/check_images.py b/Report/scripts/check_images.py old mode 100644 new mode 100755 index fdf501c..59a427d --- a/Report/scripts/check_images.py +++ b/Report/scripts/check_images.py @@ -1,51 +1,61 @@ #!/usr/bin/env python3 -"""Verify IMAGES_REGISTRY.md entries exist on disk and match .typ references.""" +"""Verify Report/images files exist; fail on missing diagram PNGs.""" from __future__ import annotations import re import sys from pathlib import Path +REQUIRED_READY = { + f"fig_{i:02d}_" for i in range(1, 5) +} | {f"fig_11_"} | {f"fig_{i:02d}_" for i in range(14, 24)} + +WARN_PLACEHOLDER = {f"fig_{i:02d}_" for i in range(5, 11)} | {f"fig_12_"} | {f"fig_13_"} + def main() -> int: - report = Path(__file__).resolve().parent.parent - repo = report.parent + report = Path(__file__).resolve().parents[1] images = report / "images" registry = images / "IMAGES_REGISTRY.md" - typ = report / "Пояснительная_записка_ПытковРЕ.typ" - errors = 0 + text = registry.read_text(encoding="utf-8") + errors: list[str] = [] + warns: list[str] = [] - if not registry.exists(): - print("Missing IMAGES_REGISTRY.md", file=sys.stderr) - return 1 + for line in text.splitlines(): + if "|" not in line or "fig_" not in line: + continue + cols = [c.strip() for c in line.split("|")] + if len(cols) < 4: + continue + fname, status = cols[2], cols[3] + if not fname.startswith("fig_"): + continue + path = images / fname + if not path.is_file(): + errors.append(f"missing file: {fname}") + continue + if status == "placeholder": + for prefix in WARN_PLACEHOLDER: + if fname.startswith(prefix): + warns.append(f"placeholder: {fname}") + break + elif status == "ready": + for prefix in REQUIRED_READY: + if fname.startswith(prefix): + if path.stat().st_size < 500: + errors.append(f"too small (placeholder?): {fname}") + break - rows = re.findall( - r"\|\s*\d+\s*\|\s*([^\|]+?)\s*\|\s*(\w+)\s*\|", - registry.read_text(encoding="utf-8"), - ) - rows = [(f.strip(), s.strip()) for f, s in rows if f.startswith("fig_")] - for fname, status in rows: - p = images / fname - if not p.exists(): - print(f"MISSING file: {fname} (status {status})", file=sys.stderr) - errors += 1 - elif status == "ready" and p.stat().st_size < 1000: - print(f"WARN small file: {fname}", file=sys.stderr) - - if typ.exists(): - text = typ.read_text(encoding="utf-8") - for m in re.finditer(r'image\("(?:Report/)?images/([^"]+)"', text): - f = images / m.group(1) - if not f.exists(): - print(f"MISSING in typ: images/{m.group(1)}", file=sys.stderr) - errors += 1 + for w in warns: + print(f"WARN {w}", file=sys.stderr) + for e in errors: + print(f"ERROR {e}", file=sys.stderr) if errors: - print(f"check_images: {errors} error(s)", file=sys.stderr) return 1 - print("check_images: OK", file=sys.stderr) + print(f"OK: images check passed ({len(list(images.glob('fig_*')))} files)") return 0 if __name__ == "__main__": - raise SystemExit(main()) + sys.exit(main()) diff --git a/Report/scripts/gen_listings.py b/Report/scripts/gen_listings.py index 30ba512..f8da341 100644 --- a/Report/scripts/gen_listings.py +++ b/Report/scripts/gen_listings.py @@ -96,6 +96,13 @@ def typst_escape_path(rel: str) -> str: return rel.replace("\\", "/") +def typst_escape_caption(text: str) -> str: + """Экранирование для текста в [...]; без обратных кавычек (inline raw ловит raw.line).""" + for ch in "#$\\[]": + text = text.replace(ch, "\\" + ch) + return text + + def write_listing( out_path: Path, rel_from_report: str, @@ -104,19 +111,15 @@ def write_listing( lang: str, ) -> None: rel_typ = typst_escape_path(rel_from_report) - # Подпись перед кодом: figure.caption(position: top) в теле (placement: top — про float, не порядок). content = ( - f'#let lst-body-{label} = read("{rel_typ}")\n' f"#figure(\n" f" [\n" f" #figure.caption(position: top)[{caption}]\n" - f" #block(breakable: true)[\n" - f" #raw(lst-body-{label}, lang: \"{lang}\", block: true)\n" - f" ]\n" + f" #raw(read(\"{rel_typ}\"), lang: \"{lang}\", block: true, tab-size: 4, align: left)\n" f" ],\n" f" supplement: [Листинг],\n" - f" gap: 0.4em,\n" - f") \n\n" + f") \n" + f"#pagebreak(weak: true)\n\n" ) out_path.write_text(content, encoding="utf-8") @@ -159,7 +162,7 @@ def main() -> int: rel_report_str = rel_report.as_posix() hid = path_hash(rel_repo) label = hid - cap = f"Исходный файл `{rel_repo}`" + cap = f"Исходный файл {typst_escape_caption(rel_repo)}" lang = lang_for(f, ext_map, name_map) listing_name = f"listing-{hid}.typ" listing_path = generated / listing_name @@ -182,9 +185,13 @@ def main() -> int: ix.write(f"// Generated listings: {len(files)} files\n\n") appendix = generated / "appendix-a.typ" + styles_path = report_dir / "includes" / "listings-appendix.typ" + styles = styles_path.read_text(encoding="utf-8") with appendix.open("w", encoding="utf-8") as ap: ap.write("// AUTO-GENERATED by gen_listings.py — do not edit\n\n") - ap.write("#set figure(gap: 0.4em)\n\n") + ap.write("// Стили листингов (вставлены напрямую: #include не распространяет show-правила)\n") + ap.write(styles) + ap.write("\n") for mi in module_includes: ap.write(f'#include "{mi}"\n') diff --git a/Report/scripts/gen_test_tables.py b/Report/scripts/gen_test_tables.py index e97c497..b6e27fe 100644 --- a/Report/scripts/gen_test_tables.py +++ b/Report/scripts/gen_test_tables.py @@ -1,95 +1,114 @@ #!/usr/bin/env python3 -"""Generate Typst tables of unit tests for chapter 5.""" +"""Generate a single Typst unit-test table for chapter 5.""" from __future__ import annotations import re -from collections import defaultdict from pathlib import Path +try: + import yaml +except ImportError: + yaml = None # type: ignore + def typst_escape(s: str) -> str: return s.replace("\\", "\\\\").replace("`", "\\`") -def emit_table( - lines: list[str], - caption: str, - ncol: int, - headers: list[str], - data_rows: list[list[str]], - label: str, -) -> None: - lines.append("#pz-test-table(\n") - lines.append(f" [{caption}],\n") - lines.append(f" {ncol},\n") - lines.append(" table.header(\n") - for h in headers: - lines.append(f" [{typst_escape(h)}],\n") - lines.append(" ),\n") - for row in data_rows: - cells = ", ".join(f"[{typst_escape(c)}]" for c in row) - lines.append(f" {cells},\n") - lines.append(f") <{label}>\n\n") +def load_overrides(script_dir: Path) -> dict[str, str]: + path = script_dir / "test_descriptions.yaml" + if not path.is_file() or yaml is None: + return {} + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + return {str(k): str(v) for k, v in data.items()} + + +def _split_camel(name: str) -> list[str]: + parts: list[str] = [] + buf: list[str] = [] + for ch in name: + if ch.isupper() and buf and buf[-1].islower(): + parts.append("".join(buf)) + buf = [ch] + else: + buf.append(ch) + if buf: + parts.append("".join(buf)) + return parts + + +def describe_method(name: str, overrides: dict[str, str]) -> str: + if name in overrides: + return overrides[name] + low = name.lower() + if "encryption" in low and "wrong" in low: + return "дешифрование с неверным ключом завершается ошибкой" + if "encryption" in low and "same" in low: + return "симметрия шифрования и дешифрования при верном ключе" + if "correct key" in low: + return "верный ключ проходит проверку checkKey" + if "incorrect key" in low: + return "неверный ключ не проходит проверку checkKey" + if name.startswith("maps"): + return "исключение преобразуется в типизированную ошибку Wallenc" + if name.startswith("syncGroup"): + rest = _split_camel(name[9:]) + return "синхронизация группы: " + " ".join(w.lower() for w in rest) + if name.startswith("sync"): + return "сценарий синхронизации: " + " ".join(w.lower() for w in _split_camel(name[4:])) + if "Totp" in name or "totp" in name or "Otp" in name or "otp" in name: + return "корректность TOTP/OTP: " + " ".join(w.lower() for w in _split_camel(name)) + if name.endswith("Works") or "Crud" in name: + return "CRUD-операции и сохранение данных" + if "parses" in low or "rejects" in low: + return "разбор и валидация входных данных" + if "Route" in name or "Intent" in name or "mapsTo" in name: + return "маршрутизация, deep link или подписи UI" + if "enqueue" in low or "cancel" in low or "fail" in low or "progress" in low: + return "жизненный цикл фоновой задачи" + words = _split_camel(name) + return " ".join(w.lower() for w in words) def main() -> None: root = Path(__file__).resolve().parents[2] + script_dir = Path(__file__).resolve().parent out = Path(__file__).resolve().parents[1] / "includes" / "ch05-tests-generated.typ" + overrides = load_overrides(script_dir) rows: list[tuple[str, str, str, str]] = [] for p in sorted(root.rglob("*.kt")): if "/src/test/" not in p.as_posix(): continue text = p.read_text(encoding="utf-8", errors="replace") - cls_m = re.search(r"class\s+(\w+)", text) - if not cls_m: - continue - cls = cls_m.group(1) mod = p.parts[p.parts.index("src") - 1] - rel = p.relative_to(root).as_posix() for m in re.finditer(r"@Test[\s\S]*?fun\s+(?:`([^`]+)`|(\w+))\s*\(", text): name = m.group(1) or m.group(2) - rows.append((mod, cls, name, rel)) + desc = describe_method(name, overrides) + rows.append((mod, name, desc)) - by_mod: dict[str, list] = defaultdict(list) - for mod, cls, name, rel in rows: - by_mod[mod].append((cls, name, rel)) + rows.sort(key=lambda r: (r[0], r[1])) lines = [ "// AUTO-GENERATED by gen_test_tables.py — include from ch05.typ\n", '#import "common.typ": pz-test-table\n\n', ] - summary_rows = [] - for mod in sorted(by_mod): - for cls, name, rel in by_mod[mod]: - short = rel.split("/")[-1] - summary_rows.append([mod, cls, name, short]) + data_rows = [] + for i, (mod, name, desc) in enumerate(rows, start=1): + data_rows.append([str(i), mod, name, desc]) - emit_table( - lines, - "Сводка модульных unit-тестов (src/test)", - 4, - ["Модуль", "Класс", "Метод", "Файл"], - summary_rows, - "tbl-unit-all", - ) - - for mod in sorted(by_mod): - safe = mod.replace("-", "_") - lines.append(f"=== Реестр тестов модуля :{mod}\n\n") - mod_rows = [ - [cls, name, rel.split("/")[-1]] - for cls, name, rel in by_mod[mod] - ] - emit_table( - lines, - f"Unit-тесты модуля :{mod}", - 3, - ["Класс", "Метод", "Файл"], - mod_rows, - f"tbl-unit-{safe}", - ) + lines.append("#pz-test-table(\n") + lines.append(" [Реестр модульных unit-тестов],\n") + lines.append(" 4,\n") + lines.append(" table.header(\n") + for h in ["№", "Модуль", "Метод", "Проверяемое поведение"]: + lines.append(f" [{typst_escape(h)}],\n") + lines.append(" ),\n") + for row in data_rows: + cells = ", ".join(f"[{typst_escape(c)}]" for c in row) + lines.append(f" {cells},\n") + lines.append(") \n\n") out.write_text("".join(lines), encoding="utf-8") print(f"Wrote {out} ({len(rows)} tests)", flush=True) diff --git a/Report/scripts/render_puml.sh b/Report/scripts/render_puml.sh new file mode 100755 index 0000000..57752ae --- /dev/null +++ b/Report/scripts/render_puml.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Render Report/puml/fig_*.puml → Report/images/fig_*.png (имя PNG = @startuml в файле). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPORT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PUML_DIR="$REPORT_DIR/puml" +IMG_DIR="$REPORT_DIR/images" + +mkdir -p "$IMG_DIR" + +if [[ -z "${JAVA_HOME:-}" ]]; then + for j in /usr/lib/jvm/java-21-openjdk /usr/lib/jvm/java-17-openjdk; do + if [[ -x "$j/bin/java" ]]; then + export JAVA_HOME="$j" + break + fi + done +fi +export PATH="${JAVA_HOME:+$JAVA_HOME/bin:}$PATH" + +JAR="${PLANTUML_JAR:-/usr/share/java/plantuml/plantuml.jar}" +JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}java" +if [[ ! -f "$JAR" ]]; then + echo "plantuml.jar not found at $JAR" >&2 + exit 1 +fi + +shopt -s nullglob +PUML_FILES=("$PUML_DIR"/fig_*.puml) +if ((${#PUML_FILES[@]} == 0)); then + echo "No fig_*.puml in $PUML_DIR" >&2 + exit 1 +fi + +export PLANTUML_LIMIT_SIZE=8192 +"$JAVA_BIN" -Djava.awt.headless=true -jar "$JAR" \ + -charset UTF-8 -tpng -o "$IMG_DIR" "${PUML_FILES[@]}" + +echo "Done. $(ls -1 "$IMG_DIR"/fig_*.png 2>/dev/null | wc -l) PNG in images/" diff --git a/Report/scripts/test_descriptions.yaml b/Report/scripts/test_descriptions.yaml new file mode 100644 index 0000000..d8f1bfc --- /dev/null +++ b/Report/scripts/test_descriptions.yaml @@ -0,0 +1,23 @@ +# Ручные описания для неочевидных имён тестов (ключ — имя метода) +syncGroupCopiesFileFromSourceToTarget: копирование файла с источника на целевое хранилище в группе +syncGroupSkippedWhenFewerThanTwoStorages: синхронизация пропускается, если в группе меньше двух хранилищ +syncGroupDeleteRemovesFileOnTarget: удаление файла на целевом хранилище при синхронизации +syncSkipsWhenTargetRevisionAlreadyWinner: пропуск синхронизации, если ревизия цели уже новее +openReadDoesNotChangeJournal: чтение без записи не изменяет журнал синхронизации +deleteWithRecordSyncJournalFalseDoesNotBumpSequence: удаление без записи в журнал не увеличивает sequence +syncGroupTrashSoftDeletesOnTarget: мягкое удаление (trash) на целевом хранилище +syncGroupStopsWhenLockCannotBeAcquired: остановка при невозможности захватить блокировку группы +syncGroupReleasesLocksAfterSuccessfulSync: снятие блокировок после успешной синхронизации +syncGroupReleasesLocksWhenJournalReadFails: снятие блокировок при ошибке чтения журнала +syncGroupCooperativeCancellationReleasesLocks: снятие блокировок при отмене задачи пользователем +syncGroupReleasesLocksWhenJournalEmpty: снятие блокировок при пустом журнале +mergeKeepsSingleEntryPerPath: слияние журнала оставляет одну запись на путь +isSyncableUserPathExcludesEncDirAndJournal: пользовательский путь исключает служебные каталоги +storageWithoutEncInfoIsCompatible: хранилище без метаданных шифрования совместимо с синхронизацией +storageWithEncInfoIsIncompatible: хранилище с шифрованием несовместимо в одной группе sync +flushRestoresPendingOnWriteFailure: откат буфера журнала при сбое записи +diskInfoParsesResponse: разбор ответа API diskInfo +listReturnsEmptyEmbeddedOn404: пустой список при HTTP 404 +diskInfoThrowsAuthExceptionOn401: AuthException при HTTP 401 +preservesWallencException: сохранение уже типизированного WallencException +startTestTaskEnqueuesWork: постановка тестовой задачи в очередь orchestrator diff --git a/Report/Пояснительная_записка_ПытковРЕ.typ b/Report/Пояснительная_записка_ПытковРЕ.typ index 1d01307..dba6b5f 100644 --- a/Report/Пояснительная_записка_ПытковРЕ.typ +++ b/Report/Пояснительная_записка_ПытковРЕ.typ @@ -1,4 +1,4 @@ -#import "@preview/modern-g7-32:0.2.0": abstract, appendix-heading, appendixes, enum-numbering, gost +#import "@preview/modern-g7-32:0.2.0": abstract, appendixes, enum-numbering, gost #set enum(numbering: enum-numbering) #set heading(numbering: "1.1.1.1") @@ -26,7 +26,9 @@ ), ) -#import "includes/common.typ": pz-fig +#set text(font: "Times New Roman") + +#import "includes/common.typ": pz-appendix-title, pz-fig #abstract( "мобильное приложение", @@ -58,17 +60,15 @@ #show: appendixes -#appendix-heading("обязательное", level: 1)[Приложение А. Листинги исходного кода проекта Wallenc] - -Полный листинг файлов, необходимых для сборки проекта (307 файлов), сформирован автоматически скриптом `Report/scripts/gen_listings.py` по конфигурации `Report/listings/listings.config.yaml`. Исключены каталоги `build/`, `**/generated/**` и бинарные артефакты. +#pz-appendix-title[Листинги исходного кода проекта Wallenc] #include "listings/generated/appendix-a.typ" -#appendix-heading("обязательное", level: 1)[Приложение Б. Программная документация] +#pz-appendix-title[Программная документация] #include "appendices/appendix-b.typ" -#appendix-heading("обязательное", level: 1)[Приложение В. Скриншоты пользовательского интерфейса] +#pz-appendix-title[Скриншоты пользовательского интерфейса] #pz-fig("fig_05_local_vaults.jpg", [Локальные vault], "fig-05-app") #pz-fig("fig_06_encrypt_dialog.jpg", [Диалог шифрования], "fig-06-app") @@ -79,20 +79,3 @@ #pz-fig("fig_11_room_schema.png", [Схема Room], "fig-11-app") #pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-app") #pz-fig("fig_13_tasks_notification.jpg", [Уведомление о задачах], "fig-13-app") - -#appendix-heading("обязательное", level: 1)[Приложение Г. Диаграммы архитектуры и процессов] - -#pz-fig("fig_01_start_sync.png", [Старт и синхронизация], "fig-01-app") -#pz-fig("fig_02_vault_lifecycle.png", [Жизненный цикл vault], "fig-02-app") -#pz-fig("fig_03_navigation_hub.png", [Навигация и SyncWorker], "fig-03-app") -#pz-fig("fig_04_domain_class.png", [Классы domain], "fig-04-app") -#pz-fig("fig_14_context_system.png", [Контекстная диаграмма], "fig-14-app") -#pz-fig("fig_15_bpmn_vault.png", [BPMN vault], "fig-15-app") -#pz-fig("fig_16_dfd_level0.png", [DFD уровень 0], "fig-16-app") -#pz-fig("fig_17_use_case.png", [Прецеденты], "fig-17-app") -#pz-fig("fig_18_deployment.png", [Развёртывание], "fig-18-app") -#pz-fig("fig_19_clean_architecture.png", [Clean Architecture], "fig-19-app") -#pz-fig("fig_20_oauth_sequence.png", [OAuth sequence], "fig-20-app") -#pz-fig("fig_21_encrypt_flow.png", [Поток шифрования], "fig-21-app") -#pz-fig("fig_22_cjm_vault.png", [CJM], "fig-22-app") -#pz-fig("fig_23_module_deps.png", [Модули Gradle], "fig-23-app")