Отличное форматирование

This commit is contained in:
2026-05-26 14:59:25 +03:00
parent 2b139a18b3
commit e3a615cb50
49 changed files with 1294 additions and 501 deletions

View File

@@ -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`. Рис. 2732 — заглушки Gradle/UI; перед защитой заменить снимками Android Studio.
Диаграммы fig_0104, fig_11, fig_1423: исходники `Report/puml/fig_*.puml` (`@startuml` = имя PNG), растр — `Report/scripts/render_puml.sh` сразу в `Report/images/` (без копирования).
Скриншоты UI (fig_05fig_10, fig_1213) и Gradle (fig_2732) — заменить placeholder на реальные снимки перед защитой.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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.

View File

@@ -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"

View File

@@ -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], [+],
) <tbl-encryptor-detail>
Методика: для потоков используется `ByteArrayOutputStream` с запасом ёмкости `dataLen*3`, чтобы учесть расширение ciphertext.
=== Детальное описание тестов StorageSyncEngineTest
Движок синхронизации тестируется на in-memory двойниках хранилищ. Полный реестр методов в таблице модуля `:usecases` выше.

View File

@@ -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], [Пустой журнал], [+],
) <tbl-sync-engine>
Тесты `StorageSyncJournalMergeTest` и `StorageSyncEncryptionCompatTest` дополняют движок проверкой слияния журнала и совместимости шифрования в группе.

View File

@@ -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],
) <tbl-unit-all>
=== Реестр тестов модуля :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],
) <tbl-unit-domain>
=== Реестр тестов модуля :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],
) <tbl-unit-domain_vault>
=== Реестр тестов модуля :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],
) <tbl-unit-task_runtime>
=== Реестр тестов модуля :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],
) <tbl-unit-ui>
=== Реестр тестов модуля :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],
) <tbl-unit-usecases>

View File

@@ -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 + выборочный ручной], [Нет блокирующих дефектов],
) <tbl-test-levels>
=== Матрица тестовых сценариев
Матрица связывает требования (гл. 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], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 1213)],
[T-12], [Compose: секреты и 2FA экраны], [IT], [Да], [Отображение без падений (рис. 30)],
[T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений],
) <tbl-testplan>
=== Критерии начала и окончания
*Начало:* собраны модули `: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` каждого модуля],
) <tbl-test-env>
== Модульные тесты (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],
) <tbl-encryptor>
Класс `EncryptorTest` проверяет сценарии AES: `checkKey`, шифрование строк, байтовых массивов и потоков с верным и неверным ключом (строки 514 табл. @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, отмену и блокировки (строки 5264 табл. @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],
) <tbl-androidtest>
Запуск: `./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],
) <tbl-testres>
#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], [Полная синхронизация с облаком в разработке], [Открыт],
) <tbl-defects>
=== Связь тестов с требованиями
#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], [Очередь и экран задач],
) <tbl-trace>
=== Рекомендации по сопровождению тестов
При изменении криптомодуля обязателен прогон `: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 к демонстрации и развитию в рамках ВКР.

View File

@@ -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,

View File

@@ -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 (рис. 2731) и чек-лист UI (рис. 32) включены в гл. 5.
Дальнейшие шаги: завершение синхронизации с облаком; расширение androidTest для OAuth без ручного ввода; публикация актуальных скриншотов вместо учебных заглушек; подготовка акта внедрения (прил. Д) при эксплуатации в Нейротех.
По тестированию подтверждено: 68 модульных unit-тестов, инструментальные тесты Compose и Room, ручной протокол из двенадцати сценариев (гл. 5).

View File

@@ -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.202606.05.2026) по направлению 09.03.04 «Программная инженерия», профиль «Методы и средства разработки программного обеспечения». Научный руководитель от университета Беликов А. Н.; руководитель от организации Алексеев Д. М.
*Практическая база.* Работа выполнена в рамках производственной практики в ООО НМФ «Нейротех» (09.02.202606.05.2026) по направлению 09.03.04 «Программная инженерия». Научный руководитель Беликов А. Н.; руководитель от организации Алексеев Д. М.
*Научная новизна* заключается в сочетании универсальной модели vault, клиентского шифрования и адаптерного доступа к разным типам хранилищ без собственного сервера приложения, с проектным контуром синхронизации зашифрованных данных без передачи ключей провайдеру.
*Практическая значимость*: результаты могут использоваться при дальнейшей разработке продукта в ООО НМФ «Нейротех» и в учебных проектах по мобильной разработке и информационной безопасности. *Апробация* прохождение производственной практики (09.02.202606.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. *Приложение Г* диаграммы. На приложения даны ссылки в гл. 45 и в настоящем введении.
Пояснительная записка состоит из введения, шести глав, заключения, списка использованных источников и трёх приложений (листинги исходного кода, программная документация, скриншоты интерфейса). В главе 1 обоснована актуальность и приведено сравнение аналогов; глава 2 описывает архитектуру и модель Room; глава 3 UX и пользовательские сценарии; глава 4 реализацию по модулям; глава 5 тестирование; глава 6 экономическую оценку.

View File

@@ -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)

2
Report/puml/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Растр только в Report/images/ (render_puml.sh)
*.png

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<Map<UUID, IStorageInfo>>
}
class ManageLocalVaultUseCase {
+ getLocalStorages(): StateFlow<List<IStorageInfo>>
+ 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<List<TaskLogLine>>
+ enqueue(String, PipelineWork): TaskId
+ getPipelineState(): StateFlow<PipelineState>
+ getForegroundUi(): StateFlow<TaskForegroundUiState>
+ 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<TaskLogLevel>
}
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<Boolean>
+ getUuid(): UUID
+ getAccessor(): IStorageAccessor
+ isAvailable(): StateFlow<Boolean>
+ getSize(): StateFlow<Long>
+ getNumberOfFiles(): StateFlow<Integer>
+ clearAllContent(): Unit
+ getMetaInfo(): StateFlow<IStorageMetaInfo>
+ isVirtualStorage(): boolean
}
class IVaultsManager {
+ getLocalVault(): IVault
+ removeRemoteVault(UUID): Unit
+ addYandexVault(String): Unit
+ getRemoteVaults(): StateFlow<List<IVault>>
+ getAllStorages(): StateFlow<List<IStorage>>
+ getAllVaults(): StateFlow<List<IVault>>
+ getUnlockManager(): IUnlockManager
}
class IStorageMetaInfo {
+ getEncInfo(): StorageEncryptionInfo
+ getName(): String
+ getLastModified(): Instant
}
class IFile {
+ getMetaInfo(): IMetaInfo
}
class IStorageInfo {
+ isEmpty(): Flow<Boolean>
+ getUuid(): UUID
+ isAvailable(): StateFlow<Boolean>
+ getSize(): StateFlow<Long>
+ getNumberOfFiles(): StateFlow<Integer>
+ getMetaInfo(): StateFlow<IStorageMetaInfo>
+ isVirtualStorage(): boolean
}
class IStorageExplorer {
+ getCurrentPath(): StateFlow<String>
}
class IVaultInfo {
+ getAvailableSpace(): StateFlow<Integer>
+ getType(): VaultType
+ getTotalSpace(): StateFlow<Integer>
+ getUuid(): UUID
+ isAvailable(): StateFlow<Boolean>
+ getStorages(): StateFlow<List<IStorageInfo>>
}
class IUnlockManager {
+ close(IStorage): Unit
+ close(UUID): Unit
+ open(IStorage, EncryptKey, boolean): Unit
+ getOpenedStorages(): StateFlow<Map<UUID, IStorage>>
}
class IDirectory {
+ getMetaInfo(): IMetaInfo
+ getElementsCount(): Integer
}
class IStorageAccessor {
+ getFilesFlow(String): Flow<DataPackage<List<IFile>>>
+ getDirs(String): Unit
+ getDirsUpdates(): SharedFlow<DataPackage<List<IDirectory>>>
+ touchDir(String): Unit
+ getFiles(String): Unit
+ getSize(): StateFlow<Long>
+ isAvailable(): StateFlow<Boolean>
+ 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<DataPackage<List<IDirectory>>>
+ getNumberOfFiles(): StateFlow<Integer>
+ getFilesUpdates(): SharedFlow<DataPackage<List<IFile>>>
+ setHidden(String, boolean): Unit
+ getFileInfo(String): Unit
}
class IVault {
+ getAvailableSpace(): StateFlow<Integer>
+ getType(): VaultType
+ getTotalSpace(): StateFlow<Integer>
+ remove(IStorage): Unit
+ createStorage(StorageEncryptionInfo): Unit
+ getUuid(): UUID
+ isAvailable(): StateFlow<Boolean>
+ getStorages(): StateFlow<List<IStorage>>
+ 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<VaultType>
}
}
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

View File

@@ -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 <<Database>> {
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

25
Report/scripts/build.sh Executable file
View File

@@ -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"

72
Report/scripts/check_images.py Normal file → Executable file
View File

@@ -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())

View File

@@ -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") <lst-{label}>\n\n"
f") <lst-{label}>\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')

View File

@@ -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(") <tbl-unit-all>\n\n")
out.write_text("".join(lines), encoding="utf-8")
print(f"Wrote {out} ({len(rows)} tests)", flush=True)

40
Report/scripts/render_puml.sh Executable file
View File

@@ -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/"

View File

@@ -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

View File

@@ -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")