Отличное форматирование
@@ -4,29 +4,29 @@
|
|||||||
|
|
||||||
| № | Имя файла | Статус | Где используется | Typst label |
|
| № | Имя файла | Статус | Где используется | Typst label |
|
||||||
|---|-----------|--------|------------------|-------------|
|
|---|-----------|--------|------------------|-------------|
|
||||||
| 1 | fig_01_start_sync.png | ready | 3.3.3, 4.1.4, прил. Г | fig-01 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 12 | fig_12_tasks_screen.jpg | placeholder | 5.2.3, прил. В | fig-12 |
|
||||||
| 13 | fig_13_tasks_notification.jpg | placeholder | 5.2.3, прил. В | fig-13 |
|
| 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 |
|
| 14 | fig_14_context_system.png | ready | 1.2.1, 2.1.4 | fig-14 |
|
||||||
| 15 | fig_15_bpmn_vault.png | placeholder | 2.1.3 | fig-15 |
|
| 15 | fig_15_bpmn_vault.png | ready | 2.1.3 | fig-15 |
|
||||||
| 16 | fig_16_dfd_level0.png | placeholder | 2.2 | fig-16 |
|
| 16 | fig_16_dfd_level0.png | ready | 2.2 | fig-16 |
|
||||||
| 17 | fig_17_use_case.png | placeholder | 2.3.1 | fig-17 |
|
| 17 | fig_17_use_case.png | ready | 2.3.1 | fig-17 |
|
||||||
| 18 | fig_18_deployment.png | placeholder | 2.3.3 | fig-18 |
|
| 18 | fig_18_deployment.png | ready | 2.3.3 | fig-18 |
|
||||||
| 19 | fig_19_clean_architecture.png | placeholder | 2.3, 4.3 | fig-19 |
|
| 19 | fig_19_clean_architecture.png | ready | 2.3, 4.3 | fig-19 |
|
||||||
| 20 | fig_20_oauth_sequence.png | placeholder | 1.5.2, 4.2.3 | fig-20 |
|
| 20 | fig_20_oauth_sequence.png | ready | 1.5.2, 4.2.3 | fig-20 |
|
||||||
| 21 | fig_21_encrypt_flow.png | placeholder | 4.1.1, 5.2.1 | fig-21 |
|
| 21 | fig_21_encrypt_flow.png | ready | 4.1.1, 5.2.1 | fig-21 |
|
||||||
| 22 | fig_22_cjm_vault.png | placeholder | 3.3.2 | fig-22 |
|
| 22 | fig_22_cjm_vault.png | ready | 3.3.2 | fig-22 |
|
||||||
| 23 | fig_23_module_deps.png | placeholder | 4.3 | fig-23 |
|
| 23 | fig_23_module_deps.png | ready | 4.3 | fig-23 |
|
||||||
| 27 | fig_27_gradle_domain_test.png | ready | 5.2.1 | fig-27 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 31 | fig_31_gradle_connected_test.png | ready | 5.3 | fig-31 |
|
||||||
| 32 | fig_32_manual_test_checklist.png | ready | 5.3 | fig-32 |
|
| 32 | fig_32_manual_test_checklist.png | ready | 5.3 | fig-32 |
|
||||||
|
|
||||||
Замените файлы со статусом `placeholder` на реальные скриншоты/диаграммы, затем смените статус на `ready`. Рис. 27–32 — заглушки Gradle/UI; перед защитой заменить снимками Android Studio.
|
Диаграммы fig_01–04, fig_11, fig_14–23: исходники `Report/puml/fig_*.puml` (`@startuml` = имя PNG), растр — `Report/scripts/render_puml.sh` сразу в `Report/images/` (без копирования).
|
||||||
|
|
||||||
|
Скриншоты UI (fig_05–fig_10, fig_12–13) и Gradle (fig_27–32) — заменить placeholder на реальные снимки перед защитой.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 933 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 44 KiB |
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#v(0.5em)
|
#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.
|
||||||
|
|||||||
@@ -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")
|
#pz-fig("fig_23_module_deps.png", [Зависимости модулей Gradle], "fig-23")
|
||||||
|
|
||||||
В основном тексте приведены показательные фрагменты; листинги по модулям `:domain`, `:infrastructure-android`, `:app` — в приложении А, разделы «Модуль :domain» и др.
|
В основном тексте приведены показательные фрагменты; полные листинги — в приложении А.
|
||||||
|
|
||||||
#include "ch04-expand.typ"
|
#include "ch04-expand.typ"
|
||||||
#include "ch04-modules.typ"
|
#include "ch04-modules.typ"
|
||||||
|
|||||||
@@ -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` выше.
|
|
||||||
@@ -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` дополняют движок проверкой слияния журнала и совместимости шифрования в группе.
|
|
||||||
@@ -2,209 +2,81 @@
|
|||||||
#import "common.typ": pz-test-table
|
#import "common.typ": pz-test-table
|
||||||
|
|
||||||
#pz-test-table(
|
#pz-test-table(
|
||||||
[Сводка модульных unit-тестов (src/test)],
|
[Реестр модульных unit-тестов],
|
||||||
4,
|
4,
|
||||||
table.header(
|
table.header(
|
||||||
|
[№],
|
||||||
[Модуль],
|
[Модуль],
|
||||||
[Класс],
|
|
||||||
[Метод],
|
[Метод],
|
||||||
[Файл],
|
[Проверяемое поведение],
|
||||||
),
|
),
|
||||||
[domain], [EncryptorTest], [test correct key for StorageEncryptionInfo], [EncryptorTest.kt],
|
[1], [domain], [mapsFileNotFoundException], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain], [EncryptorTest], [test incorrect key for StorageEncryptionInfo], [EncryptorTest.kt],
|
[2], [domain], [mapsGenericExceptionToUnknown], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain], [EncryptorTest], [test string encryption with the same key], [EncryptorTest.kt],
|
[3], [domain], [mapsIOExceptionToIoFailed], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain], [EncryptorTest], [test string encryption with the wrong key], [EncryptorTest.kt],
|
[4], [domain], [preservesWallencException], [сохранение уже типизированного WallencException],
|
||||||
[domain], [EncryptorTest], [test bytes encryption with the same key], [EncryptorTest.kt],
|
[5], [domain], [test bytes encryption with the same key], [симметрия шифрования и дешифрования при верном ключе],
|
||||||
[domain], [EncryptorTest], [test bytes encryption with the wrong key], [EncryptorTest.kt],
|
[6], [domain], [test bytes encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой],
|
||||||
[domain], [EncryptorTest], [test stream encryption with the same key], [EncryptorTest.kt],
|
[7], [domain], [test correct key for StorageEncryptionInfo], [верный ключ проходит проверку checkKey],
|
||||||
[domain], [EncryptorTest], [test stream encryption with the wrong key], [EncryptorTest.kt],
|
[8], [domain], [test incorrect key for StorageEncryptionInfo], [верный ключ проходит проверку checkKey],
|
||||||
[domain], [WallencExceptionMappingTest], [preservesWallencException], [WallencExceptionMappingTest.kt],
|
[9], [domain], [test stream encryption with the same key], [симметрия шифрования и дешифрования при верном ключе],
|
||||||
[domain], [WallencExceptionMappingTest], [mapsFileNotFoundException], [WallencExceptionMappingTest.kt],
|
[10], [domain], [test stream encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой],
|
||||||
[domain], [WallencExceptionMappingTest], [mapsIOExceptionToIoFailed], [WallencExceptionMappingTest.kt],
|
[11], [domain], [test string encryption with the same key], [симметрия шифрования и дешифрования при верном ключе],
|
||||||
[domain], [WallencExceptionMappingTest], [mapsGenericExceptionToUnknown], [WallencExceptionMappingTest.kt],
|
[12], [domain], [test string encryption with the wrong key], [дешифрование с неверным ключом завершается ошибкой],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsYandexDiskAuthToAuthFailed], [VaultThrowableMappingTest.kt],
|
[13], [domain-vault], [diskInfoParsesResponse], [разбор ответа API diskInfo],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsHttpExceptionToNetworkHttpFailed], [VaultThrowableMappingTest.kt],
|
[14], [domain-vault], [diskInfoThrowsAuthExceptionOn401], [AuthException при HTTP 401],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsMissingOAuthTokenIoToTokenMissing], [VaultThrowableMappingTest.kt],
|
[15], [domain-vault], [flushRestoresPendingOnWriteFailure], [откат буфера журнала при сбое записи],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsSocketTimeoutToOperationTimedOut], [VaultThrowableMappingTest.kt],
|
[16], [domain-vault], [listReturnsEmptyEmbeddedOn404], [пустой список при HTTP 404],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsFileNotFoundToStorageFileNotFound], [VaultThrowableMappingTest.kt],
|
[17], [domain-vault], [mapsFileNotFoundToStorageFileNotFound], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain-vault], [VaultThrowableMappingTest], [mapsIllegalStateNotAFile], [VaultThrowableMappingTest.kt],
|
[18], [domain-vault], [mapsHttpExceptionToNetworkHttpFailed], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain-vault], [YandexDiskRepositoryTest], [diskInfoParsesResponse], [YandexDiskRepositoryTest.kt],
|
[19], [domain-vault], [mapsIllegalStateNotAFile], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain-vault], [YandexDiskRepositoryTest], [listReturnsEmptyEmbeddedOn404], [YandexDiskRepositoryTest.kt],
|
[20], [domain-vault], [mapsMissingOAuthTokenIoToTokenMissing], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain-vault], [YandexDiskRepositoryTest], [diskInfoThrowsAuthExceptionOn401], [YandexDiskRepositoryTest.kt],
|
[21], [domain-vault], [mapsSocketTimeoutToOperationTimedOut], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[domain-vault], [StorageSyncJournalBufferTest], [flushRestoresPendingOnWriteFailure], [StorageSyncJournalBufferTest.kt],
|
[22], [domain-vault], [mapsYandexDiskAuthToAuthFailed], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[task-runtime], [TaskOrchestratorTest], [enqueueCompletesTask], [TaskOrchestratorTest.kt],
|
[23], [task-runtime], [cancelAllMarksRunningTaskCancelled], [жизненный цикл фоновой задачи],
|
||||||
[task-runtime], [TaskOrchestratorTest], [cancelAllMarksRunningTaskCancelled], [TaskOrchestratorTest.kt],
|
[24], [task-runtime], [cancelMarksTaskCancelled], [жизненный цикл фоновой задачи],
|
||||||
[task-runtime], [TaskOrchestratorTest], [cancelMarksTaskCancelled], [TaskOrchestratorTest.kt],
|
[25], [task-runtime], [enqueueCompletesTask], [жизненный цикл фоновой задачи],
|
||||||
[task-runtime], [TaskOrchestratorTest], [failRecordsFailedState], [TaskOrchestratorTest.kt],
|
[26], [task-runtime], [failRecordsFailedState], [жизненный цикл фоновой задачи],
|
||||||
[task-runtime], [TaskOrchestratorTest], [progressUpdatesRunningState], [TaskOrchestratorTest.kt],
|
[27], [task-runtime], [logAppendsLine], [log appends line],
|
||||||
[task-runtime], [TaskOrchestratorTest], [logAppendsLine], [TaskOrchestratorTest.kt],
|
[28], [task-runtime], [progressUpdatesRunningState], [жизненный цикл фоновой задачи],
|
||||||
[ui], [WallencDeepLinksTest], [matchesWallencViewIntent], [WallencDeepLinksTest.kt],
|
[29], [ui], [clearContentProgress_mapsToStringRes], [маршрутизация, deep link или подписи UI],
|
||||||
[ui], [WallencDeepLinksTest], [rejectsUnrelatedIntent], [WallencDeepLinksTest.kt],
|
[30], [ui], [mapsFeatureStorageNotFound], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[ui], [WallencDeepLinksTest], [matchesTasksAndSettingsHosts], [WallencDeepLinksTest.kt],
|
[31], [ui], [mapsStorageIncorrectKey], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[ui], [TaskProgressLabelsTest], [syncNoGroups_mapsToStringRes], [TaskProgressLabelsTest.kt],
|
[32], [ui], [mapsUnknown], [исключение преобразуется в типизированную ошибку Wallenc],
|
||||||
[ui], [TaskProgressLabelsTest], [vaultTask_mapsToStringRes], [TaskProgressLabelsTest.kt],
|
[33], [ui], [matchesTasksAndSettingsHosts], [matches tasks and settings hosts],
|
||||||
[ui], [TaskProgressLabelsTest], [clearContentProgress_mapsToStringRes], [TaskProgressLabelsTest.kt],
|
[34], [ui], [matchesWallencViewIntent], [маршрутизация, deep link или подписи UI],
|
||||||
[ui], [WallencUserNotificationMappingTest], [mapsFeatureStorageNotFound], [WallencUserNotificationMappingTest.kt],
|
[35], [ui], [parsesStandardTotpUri], [корректность TOTP/OTP: parses standard totp uri],
|
||||||
[ui], [WallencUserNotificationMappingTest], [mapsStorageIncorrectKey], [WallencUserNotificationMappingTest.kt],
|
[36], [ui], [rejectsMissingSecret], [разбор и валидация входных данных],
|
||||||
[ui], [WallencUserNotificationMappingTest], [mapsUnknown], [WallencUserNotificationMappingTest.kt],
|
[37], [ui], [rejectsNonOtpauthScheme], [корректность TOTP/OTP: rejects non otpauth scheme],
|
||||||
[ui], [StorageNavigationRoutesSmokeTest], [storageHomeRouteCarriesVaultAndStorageIds], [StorageNavigationRoutesSmokeTest.kt],
|
[38], [ui], [rejectsUnrelatedIntent], [разбор и валидация входных данных],
|
||||||
[ui], [StorageNavigationRoutesSmokeTest], [textSecretsRoutesCarryRequiredArguments], [StorageNavigationRoutesSmokeTest.kt],
|
[39], [ui], [startTestTaskEnqueuesWork], [постановка тестовой задачи в очередь orchestrator],
|
||||||
[ui], [OtpAuthUriParserTest], [parsesStandardTotpUri], [OtpAuthUriParserTest.kt],
|
[40], [ui], [storageHomeRouteCarriesVaultAndStorageIds], [маршрутизация, deep link или подписи UI],
|
||||||
[ui], [OtpAuthUriParserTest], [rejectsNonOtpauthScheme], [OtpAuthUriParserTest.kt],
|
[41], [ui], [syncNoGroups_mapsToStringRes], [сценарий синхронизации: no groups_maps to string res],
|
||||||
[ui], [OtpAuthUriParserTest], [rejectsMissingSecret], [OtpAuthUriParserTest.kt],
|
[42], [ui], [textSecretsRoutesCarryRequiredArguments], [маршрутизация, deep link или подписи UI],
|
||||||
[ui], [TaskPipelineViewModelTest], [startTestTaskEnqueuesWork], [TaskPipelineViewModelTest.kt],
|
[43], [ui], [vaultTask_mapsToStringRes], [маршрутизация, deep link или подписи UI],
|
||||||
[usecases], [StorageDomainUseCasesTest], [twoFaCrudWorksAndPersists], [StorageDomainUseCasesTest.kt],
|
[44], [usecases], [buildTwoFaCodeStateMatchesJavaOtpForKnownSecret], [корректность TOTP/OTP: build two fa code state matches java otp for known secret],
|
||||||
[usecases], [StorageDomainUseCasesTest], [twoFaInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt],
|
[45], [usecases], [buildTwoFaCodeStateReturnsNullForInvalidSecret], [build two fa code state returns null for invalid secret],
|
||||||
[usecases], [StorageDomainUseCasesTest], [textSecretsCrudWorksWithOptionalLabels], [StorageDomainUseCasesTest.kt],
|
[46], [usecases], [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [удаление без записи в журнал не увеличивает sequence],
|
||||||
[usecases], [StorageDomainUseCasesTest], [textSecretsInvalidJsonFallsBackToEmptyList], [StorageDomainUseCasesTest.kt],
|
[47], [usecases], [isSyncableUserPathExcludesEncDirAndJournal], [пользовательский путь исключает служебные каталоги],
|
||||||
[usecases], [StorageSyncEncryptionCompatTest], [storageWithoutEncInfoIsCompatible], [StorageSyncEncryptionCompatTest.kt],
|
[48], [usecases], [mergeKeepsSingleEntryPerPath], [слияние журнала оставляет одну запись на путь],
|
||||||
[usecases], [StorageSyncEncryptionCompatTest], [storageWithEncInfoIsIncompatible], [StorageSyncEncryptionCompatTest.kt],
|
[49], [usecases], [openReadDoesNotChangeJournal], [чтение без записи не изменяет журнал синхронизации],
|
||||||
[usecases], [StorageSyncEngineTest], [syncAllGroupsReportsNoGroupsWhenEmpty], [StorageSyncEngineTest.kt],
|
[50], [usecases], [storageWithEncInfoIsIncompatible], [хранилище с шифрованием несовместимо в одной группе sync],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupCopiesFileFromSourceToTarget], [StorageSyncEngineTest.kt],
|
[51], [usecases], [storageWithoutEncInfoIsCompatible], [хранилище без метаданных шифрования совместимо с синхронизацией],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupSkippedWhenFewerThanTwoStorages], [StorageSyncEngineTest.kt],
|
[52], [usecases], [syncAllGroupsReportsNoGroupsWhenEmpty], [сценарий синхронизации: all groups reports no groups when empty],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupDeleteRemovesFileOnTarget], [StorageSyncEngineTest.kt],
|
[53], [usecases], [syncGroupCooperativeCancellationReleasesLocks], [снятие блокировок при отмене задачи пользователем],
|
||||||
[usecases], [StorageSyncEngineTest], [syncSkipsWhenTargetRevisionAlreadyWinner], [StorageSyncEngineTest.kt],
|
[54], [usecases], [syncGroupCopiesFileFromSourceToTarget], [копирование файла с источника на целевое хранилище в группе],
|
||||||
[usecases], [StorageSyncEngineTest], [openReadDoesNotChangeJournal], [StorageSyncEngineTest.kt],
|
[55], [usecases], [syncGroupDeleteRemovesFileOnTarget], [удаление файла на целевом хранилище при синхронизации],
|
||||||
[usecases], [StorageSyncEngineTest], [deleteWithRecordSyncJournalFalseDoesNotBumpSequence], [StorageSyncEngineTest.kt],
|
[56], [usecases], [syncGroupReleasesLocksAfterSuccessfulSync], [снятие блокировок после успешной синхронизации],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupTrashSoftDeletesOnTarget], [StorageSyncEngineTest.kt],
|
[57], [usecases], [syncGroupReleasesLocksWhenJournalEmpty], [снятие блокировок при пустом журнале],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupStopsWhenLockCannotBeAcquired], [StorageSyncEngineTest.kt],
|
[58], [usecases], [syncGroupReleasesLocksWhenJournalReadFails], [снятие блокировок при ошибке чтения журнала],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksAfterSuccessfulSync], [StorageSyncEngineTest.kt],
|
[59], [usecases], [syncGroupSkippedWhenFewerThanTwoStorages], [синхронизация пропускается, если в группе меньше двух хранилищ],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalReadFails], [StorageSyncEngineTest.kt],
|
[60], [usecases], [syncGroupStopsWhenLockCannotBeAcquired], [остановка при невозможности захватить блокировку группы],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupCooperativeCancellationReleasesLocks], [StorageSyncEngineTest.kt],
|
[61], [usecases], [syncGroupTrashSoftDeletesOnTarget], [мягкое удаление (trash) на целевом хранилище],
|
||||||
[usecases], [StorageSyncEngineTest], [syncGroupReleasesLocksWhenJournalEmpty], [StorageSyncEngineTest.kt],
|
[62], [usecases], [syncSkipsWhenTargetRevisionAlreadyWinner], [пропуск синхронизации, если ревизия цели уже новее],
|
||||||
[usecases], [StorageSyncJournalMergeTest], [mergeKeepsSingleEntryPerPath], [StorageSyncJournalMergeTest.kt],
|
[63], [usecases], [textSecretsCrudWorksWithOptionalLabels], [CRUD-операции и сохранение данных],
|
||||||
[usecases], [StorageSyncJournalMergeTest], [isSyncableUserPathExcludesEncDirAndJournal], [StorageSyncJournalMergeTest.kt],
|
[64], [usecases], [textSecretsInvalidJsonFallsBackToEmptyList], [text secrets invalid json falls back to empty list],
|
||||||
[usecases], [TwoFaTotpTest], [buildTwoFaCodeStateMatchesJavaOtpForKnownSecret], [TwoFaTotpTest.kt],
|
[65], [usecases], [totpPeriodProgressIsContinuousWithinPeriod], [корректность TOTP/OTP: totp period progress is continuous within period],
|
||||||
[usecases], [TwoFaTotpTest], [totpPeriodProgressIsContinuousWithinPeriod], [TwoFaTotpTest.kt],
|
[66], [usecases], [totpSecondsUntilRefreshCountsDownWithinPeriod], [корректность TOTP/OTP: totp seconds until refresh counts down within period],
|
||||||
[usecases], [TwoFaTotpTest], [totpSecondsUntilRefreshCountsDownWithinPeriod], [TwoFaTotpTest.kt],
|
[67], [usecases], [twoFaCrudWorksAndPersists], [CRUD-операции и сохранение данных],
|
||||||
[usecases], [TwoFaTotpTest], [buildTwoFaCodeStateReturnsNullForInvalidSecret], [TwoFaTotpTest.kt],
|
[68], [usecases], [twoFaInvalidJsonFallsBackToEmptyList], [two fa invalid json falls back to empty list],
|
||||||
) <tbl-unit-all>
|
) <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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
= Тестирование программного обеспечения
|
= Тестирование программного обеспечения
|
||||||
|
|
||||||
Тестирование Wallenc организовано по уровням: модульные автоматические тесты (JUnit, каталог `src/test` каждого Gradle-модуля), инструментальные тесты (`src/androidTest`, эмулятор/устройство), ручные функциональные и UI-прогоны. Программа и методика испытаний дублируются в приложении Б; в настоящей главе приведены цели, план, полный реестр unit-тестов, отчёт о прогоне и иллюстрации.
|
В ходе работы было организовано тестирование Wallenc на нескольких уровнях: модульные автоматические тесты (JUnit, каталог `src/test` каждого Gradle-модуля), инструментальные тесты (`src/androidTest`), а также ручные функциональные и UI-прогоны. Программа и методика испытаний приведены в приложении Б.
|
||||||
|
|
||||||
== План тестирования
|
== План тестирования
|
||||||
|
|
||||||
=== Цели и задачи испытаний
|
=== Цели и задачи испытаний
|
||||||
|
|
||||||
Основная цель — подтвердить корректность криптографического ядра, доменной логики синхронизации и сценариев UI до передачи сборки на приёмку практики. Задачи плана:
|
Основная цель — подтвердить корректность криптографического ядра, доменной логики синхронизации и сценариев UI. Были поставлены следующие задачи:
|
||||||
|
|
||||||
+ верифицировать `Encryptor` и проверку ключа для всех носителей (строка, байты, поток);
|
+ проверить `Encryptor` и проверку ключа для строк, байтов и потоков;
|
||||||
+ проверить маппинг исключений в пользовательские коды ошибок (`domain`, `domain-vault`, `ui`);
|
+ убедиться в корректном маппинге исключений в коды ошибок;
|
||||||
+ убедиться в согласованности движка синхронизации (`StorageSyncEngine`, журнал, блокировки);
|
+ протестировать движок синхронизации (`StorageSyncEngine`, журнал, блокировки);
|
||||||
+ проверить оркестратор фоновых задач (`task-runtime`);
|
+ проверить оркестратор фоновых задач;
|
||||||
+ выполнить smoke-тесты навигации, deep link и 2FA/TOTP;
|
+ выполнить smoke-тесты навигации, deep link и 2FA/TOTP;
|
||||||
+ зафиксировать результаты ручных сценариев vault, OAuth и экрана задач.
|
+ зафиксировать результаты ручных сценариев vault, OAuth и экрана задач.
|
||||||
|
|
||||||
@@ -23,39 +23,37 @@
|
|||||||
[Объекты и уровни тестирования Wallenc],
|
[Объекты и уровни тестирования Wallenc],
|
||||||
4,
|
4,
|
||||||
table.header([Уровень], [Объект], [Инструмент], [Критерий успеха]),
|
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],
|
[Инструм.], [Room, Compose UI, OAuth], [AndroidJUnit, эмулятор], [Нет падений на целевом API],
|
||||||
[Ручной], [Сборка app, пользовательские цепочки], [Чек-лист], [Сценарии T-1…T-12 пройдены],
|
[Ручной], [Сборка app, пользовательские цепочки], [Чек-лист], [Сценарии T-1…T-12 пройдены],
|
||||||
[Регресс.], [Синхронизация, шифрование], [Повтор unit + выборочный ручной], [Отсутствие блокирующих дефектов],
|
[Регресс.], [Синхронизация, шифрование], [Повтор unit + выборочный ручной], [Нет блокирующих дефектов],
|
||||||
) <tbl-test-levels>
|
) <tbl-test-levels>
|
||||||
|
|
||||||
=== Матрица тестовых сценариев
|
=== Матрица тестовых сценариев
|
||||||
|
|
||||||
Матрица связывает требования (гл. 1) с видами испытаний. Столбец «Автоматизация» указывает, покрыт ли сценарий unit-тестом.
|
|
||||||
|
|
||||||
#pz-table(
|
#pz-table(
|
||||||
[Матрица тестовых сценариев],
|
[Матрица тестовых сценариев],
|
||||||
5,
|
5,
|
||||||
table.header([ID], [Сценарий], [Тип], [Авто], [Ожидаемый результат]),
|
table.header([ID], [Сценарий], [Тип], [Авто], [Ожидаемый результат]),
|
||||||
[T-1], [Проверка ключа шифрования], [Unit], [Да], [`Encryptor.checkKey` true/false],
|
[T-1], [Проверка ключа шифрования], [Unit], [Да], [`Encryptor.checkKey` true/false],
|
||||||
[T-2], [Шифрование/дешифрование строки и байтов], [Unit], [Да], [Симметрия данных, ошибка при неверном ключе],
|
[T-2], [Шифрование/дешифрование строки и байтов], [Unit], [Да], [Симметрия данных],
|
||||||
[T-3], [Потоковое шифрование файла], [Unit], [Да], [Массив после decrypt равен исходному],
|
[T-3], [Потоковое шифрование файла], [Unit], [Да], [Данные после decrypt равны исходным],
|
||||||
[T-4], [Синхронизация группы хранилищ], [Unit], [Да], [Копирование, удаление, trash, блокировки],
|
[T-4], [Синхронизация группы хранилищ], [Unit], [Да], [Копирование, удаление, trash, блокировки],
|
||||||
[T-5], [2FA TOTP генерация], [Unit], [Да], [Совпадение с эталоном Java OTP],
|
[T-5], [2FA TOTP генерация], [Unit], [Да], [Совпадение с эталоном Java OTP],
|
||||||
[T-6], [Маппинг ошибок сети/диска], [Unit], [Да], [Типизированные `WallencException`],
|
[T-6], [Маппинг ошибок сети/диска], [Unit], [Да], [Типизированные `WallencException`],
|
||||||
[T-7], [CRUD локального vault], [Ручной], [Нет], [Список обновлён (рис. 5)],
|
[T-7], [CRUD локального vault], [Ручной], [Нет], [Список обновлён (рис. @fig-05)],
|
||||||
[T-8], [Включение шифрования vault], [Ручной], [Нет], [Диалог, статус «зашифровано» (рис. 6)],
|
[T-8], [Включение шифрования vault], [Ручной], [Нет], [Статус «зашифровано» (рис. @fig-06)],
|
||||||
[T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ к содержимому только с ключом (рис. 7)],
|
[T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ только с ключом (рис. @fig-07)],
|
||||||
[T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. 10)],
|
[T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. @fig-10)],
|
||||||
[T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 12–13)],
|
[T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 12–13)],
|
||||||
[T-12], [Compose: секреты и 2FA экраны], [IT], [Да], [Отображение без падений (рис. 30)],
|
[T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений],
|
||||||
) <tbl-testplan>
|
) <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,
|
2,
|
||||||
table.header([Параметр], [Значение]),
|
table.header([Параметр], [Значение]),
|
||||||
[ОС разработки], [GNU/Linux, Android Studio Narwhal],
|
[ОС разработки], [GNU/Linux, Android Studio],
|
||||||
[JDK], [OpenJDK 17 / 21 (Gradle toolchain)],
|
[JDK], [OpenJDK 17 / 21],
|
||||||
[Сборка], [`./gradlew test`, `./gradlew connectedDebugAndroidTest`],
|
[Сборка], [`./gradlew test`, `./gradlew connectedDebugAndroidTest`],
|
||||||
[Устройство], [Эмулятор Pixel 6 API 34; физическое устройство для OAuth],
|
[Устройство], [Эмулятор Pixel 6 API 34; физическое устройство для OAuth],
|
||||||
[Отчёт JVM], [HTML/XML в `build/reports/tests/test` каждого модуля],
|
|
||||||
) <tbl-test-env>
|
) <tbl-test-env>
|
||||||
|
|
||||||
== Модульные тесты (JUnit)
|
== Модульные тесты (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"
|
#include "ch05-tests-generated.typ"
|
||||||
|
|
||||||
=== Модуль :domain — криптография и ошибки
|
=== Криптография и доменные ошибки
|
||||||
|
|
||||||
Класс `EncryptorTest` покрывает восемь сценариев AES: проверка ключа, шифрование строк, байтовых массивов (512 байт) и потоков (1500 байт) с верным и неверным ключом. Класс `WallencExceptionMappingTest` проверяет преобразование `FileNotFoundException`, `IOException` и прочих исключений в типизированные ошибки API.
|
Класс `EncryptorTest` проверяет сценарии AES: `checkKey`, шифрование строк, байтовых массивов и потоков с верным и неверным ключом (строки 5–14 табл. @tbl-unit-all). `WallencExceptionMappingTest` покрывает преобразование файловых и сетевых исключений.
|
||||||
|
|
||||||
#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>
|
|
||||||
|
|
||||||
Прогон `./gradlew :domain:test` — на рис. @fig-27.
|
Прогон `./gradlew :domain:test` — на рис. @fig-27.
|
||||||
|
|
||||||
#include "ch05-encryptor.typ"
|
|
||||||
|
|
||||||
#pz-fig("fig_27_gradle_domain_test.png", [Отчёт Gradle: модуль :domain, задача test], "fig-27")
|
#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 в доменной модели.
|
`StorageSyncEngineTest` моделирует группы синхронизации, копирование и удаление файлов, soft-delete, отмену и блокировки (строки 52–64 табл. @tbl-unit-all). `TwoFaTotpTest` сверяет TOTP с эталоном Java OTP. `StorageDomainUseCasesTest` проверяет CRUD текстовых секретов и 2FA.
|
||||||
|
|
||||||
#include "ch05-sync-tests.typ"
|
|
||||||
|
|
||||||
#pz-fig("fig_28_gradle_usecases_test.png", [Отчёт Gradle: модуль :usecases], "fig-28")
|
#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")
|
#pz-fig("fig_29_gradle_ui_test.png", [Отчёт Gradle: модуль :ui], "fig-29")
|
||||||
|
|
||||||
=== Модуль :task-runtime
|
=== Модуль :task-runtime
|
||||||
|
|
||||||
`TaskOrchestratorTest` проверяет жизненный цикл задачи: enqueue, progress, fail, cancel, cancelAll, логирование.
|
`TaskOrchestratorTest` проверяет enqueue, progress, fail, cancel и cancelAll.
|
||||||
|
|
||||||
== Инструментальные тесты (androidTest)
|
== Инструментальные тесты (androidTest)
|
||||||
|
|
||||||
Тесты на устройстве/эмуляторе дополняют unit-уровень.
|
|
||||||
|
|
||||||
#pz-table(
|
#pz-table(
|
||||||
[Инструментальные тесты androidTest],
|
[Инструментальные тесты androidTest],
|
||||||
4,
|
4,
|
||||||
@@ -136,66 +111,48 @@
|
|||||||
[:ui], [TextSecretsScreenContentTest], [Compose: текстовые секреты], [2],
|
[:ui], [TextSecretsScreenContentTest], [Compose: текстовые секреты], [2],
|
||||||
[:infra], [YandexAccountRepositoryTest], [Room in-memory: аккаунт Яндекс], [3],
|
[:infra], [YandexAccountRepositoryTest], [Room in-memory: аккаунт Яндекс], [3],
|
||||||
[:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3],
|
[:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3],
|
||||||
[:app], [ExportYandexTestCredentialsTest], [Экспорт тестовых учётных данных], [1],
|
|
||||||
) <tbl-androidtest>
|
) <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-тестирование
|
== Ручное и UI-тестирование
|
||||||
|
|
||||||
Ручные прогоны выполнялись по чек-листу T-7…T-12 на эмуляторе и физическом устройстве. Для каждого шага фиксировались: предусловие, действие, ожидаемый и фактический результат.
|
Ручные прогоны выполнялись по чек-листу T-7…T-12 на эмуляторе и физическом устройстве.
|
||||||
|
|
||||||
#pz-table(
|
#pz-table(
|
||||||
[Протокол ручного тестирования],
|
[Протокол ручного тестирования],
|
||||||
5,
|
5,
|
||||||
table.header([ID], [Шаг], [Статус], [Фактический результат], [Иллюстрация]),
|
table.header([ID], [Шаг], [Статус], [Фактический результат], [Иллюстрация]),
|
||||||
[T-7], [Создать локальный vault], [OK], [Vault в списке], [рис. 5],
|
[T-7], [Создать локальный vault], [OK], [Vault в списке], [@fig-05],
|
||||||
[T-8], [Включить шифрование], [OK], [Статус encrypted], [рис. 6],
|
[T-8], [Включить шифрование], [OK], [Статус encrypted], [@fig-06],
|
||||||
[T-9], [Открыть/закрыть vault], [OK], [Контент доступен только открытому], [рис. 7],
|
[T-9], [Открыть/закрыть vault], [OK], [Контент только при открытом vault], [@fig-07],
|
||||||
[T-10], [OAuth Яндекс], [OK], [Запись в `DbYandexAccount`], [рис. 10],
|
[T-10], [OAuth Яндекс], [OK], [Запись в `DbYandexAccount`], [@fig-10],
|
||||||
[T-11], [Фоновая задача шифрования], [OK], [Прогресс на экране задач], [рис. 12],
|
[T-11], [Фоновая задача шифрования], [OK], [Прогресс на экране задач], [рис. 12],
|
||||||
[T-12], [Уведомление о завершении], [OK], [Notification отображён], [рис. 13],
|
[T-12], [Уведомление о завершении], [OK], [Notification отображён], [рис. 13],
|
||||||
) <tbl-testres>
|
) <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")
|
#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-table(
|
#pz-table(
|
||||||
[Трассировка требований → тесты],
|
[Трассировка требований → тесты],
|
||||||
3,
|
3,
|
||||||
table.header([ФР], [Тесты], [Комментарий]),
|
table.header([ФР], [Тесты], [Комментарий]),
|
||||||
[ФР-1], [T-7, StorageDomainUseCasesTest], [Локальный vault и CRUD секретов],
|
[ФР-1], [T-7, StorageDomainUseCasesTest], [Локальный vault и CRUD секретов],
|
||||||
[ФР-2], [EncryptorTest, T-8, T-9], [Полное покрытие AES],
|
[ФР-2], [EncryptorTest, T-8, T-9], [Покрытие AES],
|
||||||
[ФР-3], [TextSecretsScreenContentTest], [UI + domain],
|
[ФР-3], [TextSecretsScreenContentTest], [UI + domain],
|
||||||
[ФР-4], [YandexDiskRepositoryTest, T-10], [HTTP-мок и ручной OAuth],
|
[ФР-4], [YandexDiskRepositoryTest, T-10], [HTTP-мок и ручной OAuth],
|
||||||
[ФР-5], [StorageSyncEngineTest], [12 сценариев синхронизации],
|
[ФР-5], [StorageSyncEngineTest], [Синхронизация групп],
|
||||||
[ФР-6], [TaskOrchestratorTest, T-11], [Очередь и экран задач],
|
[ФР-6], [TaskOrchestratorTest, T-11], [Очередь и экран задач],
|
||||||
) <tbl-trace>
|
) <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 к демонстрации и развитию в рамках ВКР.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#show table: set text(hyphenate: true)
|
#show table: set text(hyphenate: true)
|
||||||
|
|
||||||
|
#let pz-appendix-title(body) = heading(level: 1)[#body]
|
||||||
|
|
||||||
#let pz-table(caption, columns, ..body) = figure(
|
#let pz-table(caption, columns, ..body) = figure(
|
||||||
table(
|
table(
|
||||||
columns: columns,
|
columns: columns,
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
В пояснительной записке рассмотрены анализ предметной области, проектирование и реализация мобильного приложения Wallenc — клиентского кошелька для безопасного хранения данных на недоверенных хранилищах без собственного сервера.
|
В пояснительной записке рассмотрены анализ предметной области, проектирование и реализация мобильного приложения 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 Яндекс и проектным контуром синхронизации.
|
*Цель работы достигнута*: разработан и протестирован прототип Android-приложения с клиентским шифрованием, управлением vault, OAuth Яндекс и проектным контуром синхронизации.
|
||||||
|
|
||||||
*Перспективы развития*: завершение синхронизации по модели коммитов; поддержка дополнительных провайдеров; расширение автоматизированных UI-тестов; оформление акта внедрения (приложение Д при наличии).
|
*Перспективы развития*: завершение синхронизации по модели коммитов; поддержка дополнительных провайдеров; расширение автоматизированных UI-тестов.
|
||||||
|
|
||||||
Программная документация (ТЗ, руководство пользователя, материалы испытаний) приведена в приложении Б; иллюстрации интерфейса — в приложении В; диаграммы — в приложении Г.
|
Программная документация приведена в приложении Б; иллюстрации интерфейса — в приложении В.
|
||||||
|
|
||||||
По тестированию подтверждено: 68 модульных unit-тестов в `src/test` (модули `:domain`, `:domain-vault`, `:usecases`, `:ui`, `:task-runtime`), инструментальные тесты Compose и Room, ручной протокол из двенадцати сценариев. Отчёты Gradle (рис. 27–31) и чек-лист UI (рис. 32) включены в гл. 5.
|
По тестированию подтверждено: 68 модульных unit-тестов, инструментальные тесты Compose и Room, ручной протокол из двенадцати сценариев (гл. 5).
|
||||||
|
|
||||||
Дальнейшие шаги: завершение синхронизации с облаком; расширение androidTest для OAuth без ручного ввода; публикация актуальных скриншотов вместо учебных заглушек; подготовка акта внедрения (прил. Д) при эксплуатации в Нейротех.
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Современные пользователи хранят личные и рабочие данные в облачных сервисах и на съёмных носителях, однако инфраструктура провайдера не всегда может считаться доверенной. Утечки, компрометация учётных записей и юрисдикционные риски делают актуальным подход, при котором конфиденциальность обеспечивается на стороне клиента до размещения данных во внешнем хранилище @nist-aes @clean-arch.
|
Современные пользователи хранят личные и рабочие данные в облачных сервисах и на съёмных носителях, однако инфраструктура провайдера не всегда может считаться доверенной. Утечки, компрометация учётных записей и юрисдикционные риски делают актуальным подход, при котором конфиденциальность обеспечивается на стороне клиента до размещения данных во внешнем хранилище @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);
|
+ реализовать программные модули приложения Wallenc на платформе Android (Kotlin);
|
||||||
@@ -17,16 +17,10 @@
|
|||||||
|
|
||||||
*Методы исследования*: анализ нормативной и технической документации, сравнительный анализ программных аналогов, объектно-ориентированное проектирование (UML, BPMN, DFD), прототипирование пользовательского интерфейса, программная реализация и тестирование @gost7322017 @kotlin-docs.
|
*Методы исследования*: анализ нормативной и технической документации, сравнительный анализ программных аналогов, объектно-ориентированное проектирование (UML, BPMN, DFD), прототипирование пользовательского интерфейса, программная реализация и тестирование @gost7322017 @kotlin-docs.
|
||||||
|
|
||||||
*Практическая база.* Работа выполнена в рамках производственной (технологической) практики в ООО НМФ «Нейротех» (09.02.2026–06.05.2026) по направлению 09.03.04 «Программная инженерия», профиль «Методы и средства разработки программного обеспечения». Научный руководитель от университета — Беликов А. Н.; руководитель от организации — Алексеев Д. М.
|
*Практическая база.* Работа выполнена в рамках производственной практики в ООО НМФ «Нейротех» (09.02.2026–06.05.2026) по направлению 09.03.04 «Программная инженерия». Научный руководитель — Беликов А. Н.; руководитель от организации — Алексеев Д. М.
|
||||||
|
|
||||||
*Научная новизна* заключается в сочетании универсальной модели vault, клиентского шифрования и адаптерного доступа к разным типам хранилищ без собственного сервера приложения, с проектным контуром синхронизации зашифрованных данных без передачи ключей провайдеру.
|
*Научная новизна* заключается в сочетании универсальной модели vault, клиентского шифрования и адаптерного доступа к разным типам хранилищ без собственного сервера приложения, с проектным контуром синхронизации зашифрованных данных без передачи ключей провайдеру.
|
||||||
|
|
||||||
*Практическая значимость*: результаты могут использоваться при дальнейшей разработке продукта в ООО НМФ «Нейротех» и в учебных проектах по мобильной разработке и информационной безопасности. *Апробация* — прохождение производственной практики (09.02.2026–06.05.2026) с реализацией и тестированием рабочей сборки приложения.
|
*Практическая значимость* — использование результатов при дальнейшей разработке продукта и в учебных проектах по мобильной разработке и информационной безопасности.
|
||||||
|
|
||||||
*Исходный код* размещён в приватном репозитории Gitea ЮФУ @wallenc-repo; доступ для государственной экзаменационной комиссии предоставляется по запросу научного руководителя.
|
Пояснительная записка состоит из введения, шести глав, заключения, списка использованных источников и трёх приложений (листинги исходного кода, программная документация, скриншоты интерфейса). В главе 1 обоснована актуальность и приведено сравнение аналогов; глава 2 описывает архитектуру и модель Room; глава 3 — UX и пользовательские сценарии; глава 4 — реализацию по модулям; глава 5 — тестирование; глава 6 — экономическую оценку.
|
||||||
|
|
||||||
*Методика разработки.* Проект вёлся итерациями, согласованными с этапами практики: аналитика и ТЗ; проектирование UML и БД; реализация ядра vault и UI; наращивание автоматических тестов (68 unit-тестов) и ручная приёмка; оформление ПЗ и программной документации. Контроль качества — непрерывный: `./gradlew test` после изменений в `:domain` и `:usecases`, регрессия UI — по чек-листу гл. 5.
|
|
||||||
|
|
||||||
*Содержание глав.* В главе 1 обоснована актуальность клиентского шифрования и приведено сравнение аналогов. Глава 2 описывает архитектуру, DFD, BPMN и модель Room. Глава 3 посвящена UX, User Story и диаграммам потоков. Глава 4 раскрывает реализацию по модулям Gradle. Глава 5 содержит развёрнутый план тестирования, реестр всех unit-тестов, отчёт о прогоне и скриншоты Gradle/Android Studio. Глава 6 даёт краткую экономическую оценку.
|
|
||||||
|
|
||||||
*Приложения.* *Приложение А* — полный листинг исходных файлов (307 файлов, автогенерация). *Приложение Б* — ТЗ, программа испытаний, руководство пользователя. *Приложение В* — скриншоты UI. *Приложение Г* — диаграммы. На приложения даны ссылки в гл. 4–5 и в настоящем введении.
|
|
||||||
|
|||||||
32
Report/includes/listings-appendix.typ
Normal 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
@@ -0,0 +1,2 @@
|
|||||||
|
# Растр только в Report/images/ (render_puml.sh)
|
||||||
|
*.png
|
||||||
80
Report/puml/fig_01_start_sync.puml
Normal 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
|
||||||
57
Report/puml/fig_02_vault_lifecycle.puml
Normal 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
|
||||||
51
Report/puml/fig_03_navigation_hub.puml
Normal 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
|
||||||
387
Report/puml/fig_04_domain_class.puml
Normal 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
|
||||||
47
Report/puml/fig_11_room_schema.puml
Normal 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
|
||||||
25
Report/puml/fig_14_context_system.puml
Normal 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
|
||||||
27
Report/puml/fig_15_bpmn_vault.puml
Normal 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
|
||||||
24
Report/puml/fig_16_dfd_level0.puml
Normal 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
|
||||||
26
Report/puml/fig_17_use_case.puml
Normal 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
|
||||||
18
Report/puml/fig_18_deployment.puml
Normal 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
|
||||||
34
Report/puml/fig_19_clean_architecture.puml
Normal 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
|
||||||
24
Report/puml/fig_20_oauth_sequence.puml
Normal 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
|
||||||
24
Report/puml/fig_21_encrypt_flow.puml
Normal 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
|
||||||
24
Report/puml/fig_22_cjm_vault.puml
Normal 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
|
||||||
28
Report/puml/fig_23_module_deps.puml
Normal 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
@@ -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
@@ -1,51 +1,61 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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:
|
def main() -> int:
|
||||||
report = Path(__file__).resolve().parent.parent
|
report = Path(__file__).resolve().parents[1]
|
||||||
repo = report.parent
|
|
||||||
images = report / "images"
|
images = report / "images"
|
||||||
registry = images / "IMAGES_REGISTRY.md"
|
registry = images / "IMAGES_REGISTRY.md"
|
||||||
typ = report / "Пояснительная_записка_ПытковРЕ.typ"
|
text = registry.read_text(encoding="utf-8")
|
||||||
errors = 0
|
errors: list[str] = []
|
||||||
|
warns: list[str] = []
|
||||||
|
|
||||||
if not registry.exists():
|
for line in text.splitlines():
|
||||||
print("Missing IMAGES_REGISTRY.md", file=sys.stderr)
|
if "|" not in line or "fig_" not in line:
|
||||||
return 1
|
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(
|
for w in warns:
|
||||||
r"\|\s*\d+\s*\|\s*([^\|]+?)\s*\|\s*(\w+)\s*\|",
|
print(f"WARN {w}", file=sys.stderr)
|
||||||
registry.read_text(encoding="utf-8"),
|
for e in errors:
|
||||||
)
|
print(f"ERROR {e}", file=sys.stderr)
|
||||||
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
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
print(f"check_images: {errors} error(s)", file=sys.stderr)
|
|
||||||
return 1
|
return 1
|
||||||
print("check_images: OK", file=sys.stderr)
|
print(f"OK: images check passed ({len(list(images.glob('fig_*')))} files)")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ def typst_escape_path(rel: str) -> str:
|
|||||||
return rel.replace("\\", "/")
|
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(
|
def write_listing(
|
||||||
out_path: Path,
|
out_path: Path,
|
||||||
rel_from_report: str,
|
rel_from_report: str,
|
||||||
@@ -104,19 +111,15 @@ def write_listing(
|
|||||||
lang: str,
|
lang: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
rel_typ = typst_escape_path(rel_from_report)
|
rel_typ = typst_escape_path(rel_from_report)
|
||||||
# Подпись перед кодом: figure.caption(position: top) в теле (placement: top — про float, не порядок).
|
|
||||||
content = (
|
content = (
|
||||||
f'#let lst-body-{label} = read("{rel_typ}")\n'
|
|
||||||
f"#figure(\n"
|
f"#figure(\n"
|
||||||
f" [\n"
|
f" [\n"
|
||||||
f" #figure.caption(position: top)[{caption}]\n"
|
f" #figure.caption(position: top)[{caption}]\n"
|
||||||
f" #block(breakable: true)[\n"
|
f" #raw(read(\"{rel_typ}\"), lang: \"{lang}\", block: true, tab-size: 4, align: left)\n"
|
||||||
f" #raw(lst-body-{label}, lang: \"{lang}\", block: true)\n"
|
|
||||||
f" ]\n"
|
|
||||||
f" ],\n"
|
f" ],\n"
|
||||||
f" supplement: [Листинг],\n"
|
f" supplement: [Листинг],\n"
|
||||||
f" gap: 0.4em,\n"
|
f") <lst-{label}>\n"
|
||||||
f") <lst-{label}>\n\n"
|
f"#pagebreak(weak: true)\n\n"
|
||||||
)
|
)
|
||||||
out_path.write_text(content, encoding="utf-8")
|
out_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
@@ -159,7 +162,7 @@ def main() -> int:
|
|||||||
rel_report_str = rel_report.as_posix()
|
rel_report_str = rel_report.as_posix()
|
||||||
hid = path_hash(rel_repo)
|
hid = path_hash(rel_repo)
|
||||||
label = hid
|
label = hid
|
||||||
cap = f"Исходный файл `{rel_repo}`"
|
cap = f"Исходный файл {typst_escape_caption(rel_repo)}"
|
||||||
lang = lang_for(f, ext_map, name_map)
|
lang = lang_for(f, ext_map, name_map)
|
||||||
listing_name = f"listing-{hid}.typ"
|
listing_name = f"listing-{hid}.typ"
|
||||||
listing_path = generated / listing_name
|
listing_path = generated / listing_name
|
||||||
@@ -182,9 +185,13 @@ def main() -> int:
|
|||||||
ix.write(f"// Generated listings: {len(files)} files\n\n")
|
ix.write(f"// Generated listings: {len(files)} files\n\n")
|
||||||
|
|
||||||
appendix = generated / "appendix-a.typ"
|
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:
|
with appendix.open("w", encoding="utf-8") as ap:
|
||||||
ap.write("// AUTO-GENERATED by gen_listings.py — do not edit\n\n")
|
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:
|
for mi in module_includes:
|
||||||
ap.write(f'#include "{mi}"\n')
|
ap.write(f'#include "{mi}"\n')
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +1,114 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def typst_escape(s: str) -> str:
|
def typst_escape(s: str) -> str:
|
||||||
return s.replace("\\", "\\\\").replace("`", "\\`")
|
return s.replace("\\", "\\\\").replace("`", "\\`")
|
||||||
|
|
||||||
|
|
||||||
def emit_table(
|
def load_overrides(script_dir: Path) -> dict[str, str]:
|
||||||
lines: list[str],
|
path = script_dir / "test_descriptions.yaml"
|
||||||
caption: str,
|
if not path.is_file() or yaml is None:
|
||||||
ncol: int,
|
return {}
|
||||||
headers: list[str],
|
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
data_rows: list[list[str]],
|
return {str(k): str(v) for k, v in data.items()}
|
||||||
label: str,
|
|
||||||
) -> None:
|
|
||||||
lines.append("#pz-test-table(\n")
|
def _split_camel(name: str) -> list[str]:
|
||||||
lines.append(f" [{caption}],\n")
|
parts: list[str] = []
|
||||||
lines.append(f" {ncol},\n")
|
buf: list[str] = []
|
||||||
lines.append(" table.header(\n")
|
for ch in name:
|
||||||
for h in headers:
|
if ch.isupper() and buf and buf[-1].islower():
|
||||||
lines.append(f" [{typst_escape(h)}],\n")
|
parts.append("".join(buf))
|
||||||
lines.append(" ),\n")
|
buf = [ch]
|
||||||
for row in data_rows:
|
else:
|
||||||
cells = ", ".join(f"[{typst_escape(c)}]" for c in row)
|
buf.append(ch)
|
||||||
lines.append(f" {cells},\n")
|
if buf:
|
||||||
lines.append(f") <{label}>\n\n")
|
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:
|
def main() -> None:
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
out = Path(__file__).resolve().parents[1] / "includes" / "ch05-tests-generated.typ"
|
out = Path(__file__).resolve().parents[1] / "includes" / "ch05-tests-generated.typ"
|
||||||
|
overrides = load_overrides(script_dir)
|
||||||
rows: list[tuple[str, str, str, str]] = []
|
rows: list[tuple[str, str, str, str]] = []
|
||||||
|
|
||||||
for p in sorted(root.rglob("*.kt")):
|
for p in sorted(root.rglob("*.kt")):
|
||||||
if "/src/test/" not in p.as_posix():
|
if "/src/test/" not in p.as_posix():
|
||||||
continue
|
continue
|
||||||
text = p.read_text(encoding="utf-8", errors="replace")
|
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]
|
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):
|
for m in re.finditer(r"@Test[\s\S]*?fun\s+(?:`([^`]+)`|(\w+))\s*\(", text):
|
||||||
name = m.group(1) or m.group(2)
|
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)
|
rows.sort(key=lambda r: (r[0], r[1]))
|
||||||
for mod, cls, name, rel in rows:
|
|
||||||
by_mod[mod].append((cls, name, rel))
|
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"// AUTO-GENERATED by gen_test_tables.py — include from ch05.typ\n",
|
"// AUTO-GENERATED by gen_test_tables.py — include from ch05.typ\n",
|
||||||
'#import "common.typ": pz-test-table\n\n',
|
'#import "common.typ": pz-test-table\n\n',
|
||||||
]
|
]
|
||||||
|
|
||||||
summary_rows = []
|
data_rows = []
|
||||||
for mod in sorted(by_mod):
|
for i, (mod, name, desc) in enumerate(rows, start=1):
|
||||||
for cls, name, rel in by_mod[mod]:
|
data_rows.append([str(i), mod, name, desc])
|
||||||
short = rel.split("/")[-1]
|
|
||||||
summary_rows.append([mod, cls, name, short])
|
|
||||||
|
|
||||||
emit_table(
|
lines.append("#pz-test-table(\n")
|
||||||
lines,
|
lines.append(" [Реестр модульных unit-тестов],\n")
|
||||||
"Сводка модульных unit-тестов (src/test)",
|
lines.append(" 4,\n")
|
||||||
4,
|
lines.append(" table.header(\n")
|
||||||
["Модуль", "Класс", "Метод", "Файл"],
|
for h in ["№", "Модуль", "Метод", "Проверяемое поведение"]:
|
||||||
summary_rows,
|
lines.append(f" [{typst_escape(h)}],\n")
|
||||||
"tbl-unit-all",
|
lines.append(" ),\n")
|
||||||
)
|
for row in data_rows:
|
||||||
|
cells = ", ".join(f"[{typst_escape(c)}]" for c in row)
|
||||||
for mod in sorted(by_mod):
|
lines.append(f" {cells},\n")
|
||||||
safe = mod.replace("-", "_")
|
lines.append(") <tbl-unit-all>\n\n")
|
||||||
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}",
|
|
||||||
)
|
|
||||||
|
|
||||||
out.write_text("".join(lines), encoding="utf-8")
|
out.write_text("".join(lines), encoding="utf-8")
|
||||||
print(f"Wrote {out} ({len(rows)} tests)", flush=True)
|
print(f"Wrote {out} ({len(rows)} tests)", flush=True)
|
||||||
|
|||||||
40
Report/scripts/render_puml.sh
Executable 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/"
|
||||||
23
Report/scripts/test_descriptions.yaml
Normal 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
|
||||||
@@ -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 enum(numbering: enum-numbering)
|
||||||
#set heading(numbering: "1.1.1.1")
|
#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(
|
#abstract(
|
||||||
"мобильное приложение",
|
"мобильное приложение",
|
||||||
@@ -58,17 +60,15 @@
|
|||||||
|
|
||||||
#show: appendixes
|
#show: appendixes
|
||||||
|
|
||||||
#appendix-heading("обязательное", level: 1)[Приложение А. Листинги исходного кода проекта Wallenc]
|
#pz-appendix-title[Листинги исходного кода проекта Wallenc]
|
||||||
|
|
||||||
Полный листинг файлов, необходимых для сборки проекта (307 файлов), сформирован автоматически скриптом `Report/scripts/gen_listings.py` по конфигурации `Report/listings/listings.config.yaml`. Исключены каталоги `build/`, `**/generated/**` и бинарные артефакты.
|
|
||||||
|
|
||||||
#include "listings/generated/appendix-a.typ"
|
#include "listings/generated/appendix-a.typ"
|
||||||
|
|
||||||
#appendix-heading("обязательное", level: 1)[Приложение Б. Программная документация]
|
#pz-appendix-title[Программная документация]
|
||||||
|
|
||||||
#include "appendices/appendix-b.typ"
|
#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_05_local_vaults.jpg", [Локальные vault], "fig-05-app")
|
||||||
#pz-fig("fig_06_encrypt_dialog.jpg", [Диалог шифрования], "fig-06-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_11_room_schema.png", [Схема Room], "fig-11-app")
|
||||||
#pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-app")
|
#pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-app")
|
||||||
#pz-fig("fig_13_tasks_notification.jpg", [Уведомление о задачах], "fig-13-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")
|
|
||||||
|
|||||||