223 lines
21 KiB
Markdown
223 lines
21 KiB
Markdown
# Port coverage matrix — 0.0.1
|
||
|
||
## Scope and source identity
|
||
|
||
The supplied source code, rather than the interface concept alone, defines functional
|
||
coverage. The archive contains no `.git` metadata, so the supplied archive SHA-256
|
||
values identify the reviewed inputs:
|
||
|
||
| Archive | SHA-256 |
|
||
| --- | --- |
|
||
| `mikrotik_backup_system-master.zip` | `3be24e72b1816070a78ec7ee7336ae0a7f884db070dc019a8afdd9c229c05f47` |
|
||
| `mikrotik_backup_system_rust_migration.zip` | `65e4ee36cd60e81e4ff25a6d12d5fd7913b9360b61c96b7301b3de550c00a104` |
|
||
|
||
The source has **36 active API method/path combinations**, all represented in the
|
||
Axum router. Paths below use the default `/api` prefix; `API_PREFIX` remains
|
||
configurable.
|
||
|
||
The test column identifies source tests and their intended coverage. A test's
|
||
presence is not proof of a successful run. The revision-specific
|
||
[validation report](validation.md) records actual test, interface, build, and process
|
||
checks. Controlled fixture servers exercise protocols without contacting production
|
||
devices. No physical MikroTik or live Pushover account was supplied.
|
||
|
||
## Contract test key
|
||
|
||
These test names are in `tests/api_contract.rs`.
|
||
|
||
| Key | Full test name |
|
||
| --- | --- |
|
||
| A | `auth_json_form_preferences_password_and_expiry` |
|
||
| R | `registration_and_login_rate_limit` |
|
||
| D | `devices_defaults_validation_secrets_and_disabled_operations` |
|
||
| O | `ownership_applies_to_every_device_and_backup_operation` |
|
||
| F | `files_filters_preview_diff_download_zip_and_bulk_deletion` |
|
||
| S | `settings_secrets_validation_reveal_scheduler_and_notification_errors` |
|
||
| L | `logs_retention_and_embedded_web_do_not_expose_storage` |
|
||
|
||
## API operations
|
||
|
||
| Method and path | Rust file / handler | Test and coverage |
|
||
| --- | --- | --- |
|
||
| `POST /api/auth/register` | `src/api/auth.rs` / `register` | R: registration disabled/enabled, conflicts, validation |
|
||
| `POST /api/auth/login` | `src/api/auth.rs` / `login` | A: JSON and form; R: failed-login throttling |
|
||
| `GET /api/auth/me` | `src/api/auth.rs` / `me` | A: account, missing/invalid token, no password hash in JSON |
|
||
| `PUT /api/auth/preferences` | `src/api/auth.rs` / `preferences` | A: four languages, font, unsupported values |
|
||
| `POST /api/auth/change-password` | `src/api/auth.rs` / `change_password` | A: current password, new login, rejection of old password |
|
||
| `GET /api/dashboard` | `src/api/system.rs` / `dashboard` | O: owner-scoped counts; F: counts updated after deletion |
|
||
| `GET /api/routers` | `src/api/devices.rs` / `list` | O: user list isolation |
|
||
| `GET /api/routers/ping-statuses` | `src/api/devices.rs` / `ping` | D: disabled flag; ICMP tests in `routeros.rs` |
|
||
| `POST /api/routers` | `src/api/devices.rs` / `create` | D: RouterOS/SwOS, default ports, names, secrets |
|
||
| `GET /api/routers/{id}` | `src/api/devices.rs` / `get_one` | D: metadata and flags; O: another user's record |
|
||
| `PUT /api/routers/{id}` | `src/api/devices.rs` / `update` | D: validation, flags, secret preservation/clearing; O |
|
||
| `DELETE /api/routers/{id}` | `src/api/devices.rs` / `remove` | D: deletion and 404; O: ownership protection |
|
||
| `GET /api/routers/{id}/test-connection` | `src/api/devices.rs` / `test_connection` | O: authorization; local protocol fixtures in `routeros.rs` |
|
||
| `GET /api/backups` | `src/api/backups.rs` / `list` | F: type, device, day, search, sorting; O |
|
||
| `GET /api/backups/router/{id}` | `src/api/backups.rs` / `for_router` | O: device authorization |
|
||
| `POST /api/backups/routers/export-all` | `src/api/backups.rs` / `export_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
|
||
| `POST /api/backups/routers/binary-all` | `src/api/backups.rs` / `binary_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
|
||
| `POST /api/backups/router/{id}/export` | `src/api/backups.rs` / `export` | D: policies and unsupported SwOS export; O: ownership; SSH adapter results in validation report |
|
||
| `POST /api/backups/router/{id}/binary` | `src/api/backups.rs` / `binary` | D: policies; O: ownership; SwOS fixture and SSH adapter checks |
|
||
| `POST /api/backups/router/{id}/upload/{backup_id}` | `src/api/backups.rs` / `upload` | O: both resources owned; SSH/SFTP adapter check; upload does not restore |
|
||
| `DELETE /api/backups/{id}` | `src/api/backups.rs` / `remove` | F: file and metadata deletion; O: ownership |
|
||
| `GET /api/backups/{id}/download` | `src/api/backups.rs` / `download` | F: exact binary bytes and Content-Type; O |
|
||
| `GET /api/backups/{id}/view` | `src/api/backups.rs` / `view` | F: export text and rejection of binary content; O |
|
||
| `POST /api/backups/{id}/email` | `src/api/backups.rs` / `email` | O: ownership; `smtp_delivery_with_attachment_against_local_fixture` for SMTP |
|
||
| `GET /api/backups/{left}/diff/{right}` | `src/api/backups.rs` / `diff_json` | F: statistics/text and rejection of binary content; O: both resources |
|
||
| `GET /api/backups/{left}/diff/{right}/html` | `src/api/backups.rs` / `diff_html` | F: HTML; O; `escapes_configuration_and_file_names` |
|
||
| `POST /api/backups/bulk` | `src/api/backups.rs` / `bulk` | F: ZIP, no partial mutation for an invalid ID, deduplication; O |
|
||
| `GET /api/settings` | `src/api/system.rs` / `settings` | `settings_secrets_round_trip_without_public_exposure`; S: saved response |
|
||
| `GET /api/settings/scheduler-status` | `src/api/system.rs` / `scheduler_status` | S: timezone and five jobs; cron/DST tests in `scheduler.rs` |
|
||
| `PUT /api/settings` | `src/api/system.rs` / `update_settings` | S: cron, preserve/clear secrets, invalid input rejected before save |
|
||
| `POST /api/settings/reveal-ssh-key` | `src/api/system.rs` / `reveal_ssh_key` | S: incorrect and correct account password |
|
||
| `POST /api/settings/test-email` | `src/api/system.rs` / `test_email` | S: configuration errors; local SMTP provider fixture |
|
||
| `POST /api/settings/test-pushover` | `src/api/system.rs` / `test_pushover` | S and `missing_pushover_settings_fail_before_network`; no live account delivery |
|
||
| `GET /api/logs` | `src/api/system.rs` / `logs` | L: limit; original id/message/timestamp model |
|
||
| `DELETE /api/logs/older-than/{days}` | `src/api/system.rs` / `delete_logs` | L: validation and actual removal of old records |
|
||
| `GET /api/health` | `src/api/system.rs` / `health` | L: public health check, version, configurable prefix |
|
||
|
||
Backup list parameters are `search`, `backup_type=export|binary`, `router_id`,
|
||
`created_on=YYYY-MM-DD`, `sort_by=created_at|file_name|backup_type|router_name`, and
|
||
`order=asc|desc`. Logs retain `limit` in the range 1–500. Bulk operations accept
|
||
`action=delete|download` and `backup_ids`. Login accepts `username`/`password`;
|
||
password change uses `current_password`/`new_password`; SSH key reveal uses
|
||
`password`; preferences use `preferred_language`/`preferred_font`. A login response
|
||
contains `access_token`, `token_type=bearer`, and `user`.
|
||
|
||
## Models and source fields
|
||
|
||
Models are explicitly typed in `src/models.rs`. `src/db.rs` implements persistence,
|
||
current-schema validation, and explicit import. `src/schema.sql` is the complete
|
||
fresh-install schema. Revision 4 does not run incremental schema migrations at startup.
|
||
|
||
| Model / group | Preserved or added fields | Notes |
|
||
| --- | --- | --- |
|
||
| User | `id`, `username`, `password_hash`, `created_at`, `preferred_language`, `preferred_font` | Password hashes are never serialized. The user DTO has the same four public fields as the source. |
|
||
| Router identity | `id`, `owner_id`, `name`, `device_type`, `host`, `port`, `ssh_user`, `created_at` | Every device/backup access checks ownership. |
|
||
| Router credentials | `ssh_key`, `ssh_password` | Public responses contain null and presence flags; values remain stored in the database. |
|
||
| Router SwitchOS source | `use_global_switchos_credentials` | Nullable addition: true selects the global pair, false selects the local pair, null preserves legacy fallback. Changing mode preserves local secrets. |
|
||
| Router connection result | `last_connection_status`, `last_connection_tested_at`, `last_connection_error`, `last_connection_hostname`, `last_connection_model`, `last_connection_version`, `last_connection_uptime`, `last_connection_transport`, `last_connection_server`, `last_connection_auth_mode`, `last_connection_http_status`, `last_connection_backup_available` | Unknown values remain null; errors are not presented as success. |
|
||
| Router policies | `disable_all_backups`, `disable_export_backups`, `disable_binary_backups`, `disable_ping` | Enforced server-side, including scheduled and batch operations. |
|
||
| Router DTO | `effective_username`, `uses_global_ssh_key`, `has_effective_ssh_key`, `uses_global_switchos_credentials`, `has_effective_password`, `has_own_password`, `supports_export`, `supports_restore_upload` | Derived from the selected credential source and device type without disclosing secrets. |
|
||
| RouterTestConnection | `success`, `tested_at`, `model`, `uptime`, `hostname`, `version`, `error`, `transport`, `server`, `auth_mode`, `http_status`, `backup_available` | The same result is persisted in the device's last_connection fields. |
|
||
| RouterPingStatus | `router_id`, `reachable`, `latency_ms`, `disabled`; optional `error` | Batch response contains `items`. Diagnostic errors distinguish missing ICMP permissions from an ordinary timeout. |
|
||
| Backup | `id`, `router_id`, `file_path`, `file_name`, `backup_type`, `checksum`, `created_at` | Additional DTO fields: `router_name`, `device_type`, `file_size`. Import copies existing bytes without converting their format. |
|
||
| OperationLog | `id`, `message`, `timestamp` | No invented level/component/router_id fields. |
|
||
| Dashboard | `routers_count`, `export_count`, `binary_count`, `total_backups`, `storage`, `recent_logs` | Device/backup counts are owner-scoped; logs remain global as in the source. |
|
||
| StorageStats | `total`, `used`, `free`, `folder_used`, `usage_percent` | `usage_percent` measures the data directory against total disk capacity, matching the source. |
|
||
| Settings retention | `backup_retention_days`, `log_retention_days` | 0 means an age threshold of zero days, matching the original retention function; it does not disable cleanup. |
|
||
| Settings scheduling | `export_cron`, `binary_cron`, `retention_cron`, `enable_auto_export`, `connection_test_interval_minutes` | Five-field cron, configured timezone, Monday=0 as in APScheduler. |
|
||
| Settings device defaults | `global_ssh_key`, `default_switchos_username`, `default_switchos_password` | Revealing the global key requires password verification. |
|
||
| Settings notifications | `pushover_token`, `pushover_userkey`, `notify_failures_only`, `smtp_host`, `smtp_port`, `smtp_login`, `smtp_password`, `smtp_notifications_enabled`, `recipient_email` | SMTP, attachments, Pushover, provider tests, and failures-only mode. |
|
||
| Settings DTO | `id`, `has_global_ssh_key`, `has_default_switchos_credentials` | Additional `has_smtp_password`, `has_pushover_token`, `has_pushover_userkey`, and `has_default_switchos_password` flags support editing masked secrets. |
|
||
| Diff JSON | `left_backup_id`, `right_backup_id`, `left_file_name`, `right_file_name`, `diff_text`, `stats`, `lines` | Statistics: added/removed/modified/context. Lines: type, left_number/right_number, left_text/right_text. Standalone `diff_html` is generated only by the dedicated HTML endpoint to avoid duplicating large payloads in the browser response. |
|
||
| SchedulerStatus | `timezone`, `running`, `jobs` | Job fields: key, label, enabled, cron, description, description_params, valid, next_runs, error. |
|
||
|
||
## Client views and behavior
|
||
|
||
| Source view / behavior | Implementation |
|
||
| --- | --- |
|
||
| `/login`, `/register`, session restoration, auth/guest guards, Bearer token | `web/js/core.js`, `web/js/views.js`; actual auth API |
|
||
| `/` dashboard | API counts, storage, recent backups/logs, and batch actions |
|
||
| `/devices`, alias `/routers` | List, search, device state, create, and edit |
|
||
| `/devices/:id`, alias `/routers/:id` | Connection test, metadata, and backups; no RSC exports or diff for SwitchOS; comparison only for RouterOS text exports |
|
||
| `/files` | Filters, sorting, selection, ZIP, deletion, preview, email, and diff |
|
||
| `/diff-configs` | Two exports, counts, line numbers, side-by-side/unified views, and search |
|
||
| `/logs` | Original messages and timestamps, cleanup by age in days |
|
||
| `/settings` | Retention, cron, global credentials, SMTP/Pushover, tests, and next runs |
|
||
| `/settings?tab=automation`, alias `/schedule` | Flat schedule controls; no separate Schedule navigation item |
|
||
| `/settings?tab=account`, alias `/change-password` | Password change in account settings; old route redirects to that tab |
|
||
| `/docs`, `/openapi.json`, alias `/redoc` | Local Swagger UI, full contract, authentication, and real API requests |
|
||
| Language and font | `pl`, `en`, `es`, `no`; `default`, `adwaita_mono`, `hack`; persisted user preferences |
|
||
| Theme, sidebar, responsive layout, toasts, confirmations | `web/css/app.css`, `web/js/core.js`, `web/js/views.js`; no sample devices inserted into the database |
|
||
| CSS/JavaScript delivery | Modular `web/` sources bundled into one `app.css` and one `app.js`, embedded in the executable |
|
||
|
||
Default font families preserve the source stacks: Inter/system UI for content and
|
||
Roboto Mono/IBM Plex Mono/SFMono-Regular/Consolas for headings. Adwaita Mono and Hack
|
||
preferences select their respective stacks. Specific fonts depend on what is
|
||
installed on the client device.
|
||
|
||
## Services, configuration, and data
|
||
|
||
| Area | Implementation | Tests / evidence |
|
||
| --- | --- | --- |
|
||
| Argon2id and Python password compatibility | `security.rs` | `new_passwords_use_argon2id`, `verifies_python_passlib_fixture_without_reset`, `verifies_legacy_werkzeug_fixture`, `malformed_hashes_fail_closed` |
|
||
| JWT HS256, username subject, expiration, and nbf | `security.rs` | `jwt_has_original_username_subject`, `expired_and_wrong_algorithm_tokens_are_rejected`, `signed_malformed_or_missing_expiration_is_rejected`, `not_before_is_optional_but_must_be_valid_and_reached`, `verifies_independent_python_hmac_jwt_fixture` |
|
||
| Environment, SQLite URL, timezone, initial secret | `config.rs` | `generated_secret_survives_restart`, `accepts_legacy_sqlite_url_and_custom_prefix`, `rejects_unsafe_configuration` |
|
||
| Fresh schema and existing-schema validation | `db.rs`, `src/schema.sql` | Fresh initialization and incompatible-schema rejection regressions in `db.rs`; `generated_bootstrap_password_is_saved_and_valid` |
|
||
| Explicit database/file import | `db.rs` | `import_copies_files_and_preserves_source`, `failed_import_does_not_publish_partial_database`, `orphaned_legacy_backups_are_not_silently_skipped`, `import_rejects_corrupted_backup_before_publication`, `import_rejects_backup_symlink_outside_selected_root` |
|
||
| Import exclusivity and atomic publication | `db.rs` | `import_obeys_storage_lock_and_keeps_source_untouched`, `atomic_copy_publication_refuses_existing_destination` |
|
||
| Import wrapper | `import-python.sh`, `tests/import_launcher.rs` | `help_and_invalid_sources_do_not_build_or_import`, `resolves_source_and_preserves_arguments_and_destination_settings`, `forwards_import_failure_without_falling_back_to_serve` |
|
||
| Secret redaction and preservation | `models.rs`, `db.rs` | `settings_keep_secrets_and_clear_explicitly`, `invalid_patch_is_atomic`, `generic_serialization_does_not_leak_secrets`, `settings_secrets_round_trip_without_public_exposure` |
|
||
| RouterOS SSH export/binary, SFTP/SCP, host keys, timeout | `routeros.rs`, `backup.rs` | Controlled SSH/SFTP server checks in the validation report; no physical MikroTik supplied |
|
||
| Native SSH library fixes | `vendor/libssh2-sys` | Patch reproduction and `tests/run-security-regressions.sh`; sources, hashes, and limits in [dependency-security.md](dependency-security.md) |
|
||
| SwOS through the existing device model | `routeros.rs` | `switchos_digest_probe_and_binary_transfer_against_local_fixture`, `switchos_basic_fallback_and_html_rejection` |
|
||
| ICMP ping | `routeros.rs` | `native_icmp_loopback_and_disabled_flag`, `icmp_checksum_verifies_packet`; non-root Linux requires configured `net.ipv4.ping_group_range` |
|
||
| Atomic files, checksums, directory boundaries, device locks | `backup.rs` | `checksum_matches_sha256`, `file_names_cannot_escape`, `deletion_allows_missing_file_but_rejects_escape`, `symlink_cannot_escape_data_directory`, `device_lock_releases_on_drop` |
|
||
| Python/Rust timestamp retention | `backup.rs` | `supports_python_and_rust_timestamps`, `retention_uses_age_and_zero_is_not_disabled` |
|
||
| Diff and escaped HTML | `diff.rs` | `reports_all_line_types`, `escapes_configuration_and_file_names`, `final_newline_and_crlf_are_normalized`, `tracks_blank_lines_in_replacements`, `unified_output_matches_python_difflib_golden_fixtures` against 32 Python difflib fixtures |
|
||
| Cron and DST | `scheduler.rs` | `validates_cron_and_ranges`, `monday_zero_matches_apscheduler`, `timezone_and_dst_preview`, `named_weekdays_and_last_day`, `rare_calendar_schedule_is_not_lost` |
|
||
| SMTP/Pushover/error redaction | `notifications.rs` | `smtp_delivery_with_attachment_against_local_fixture`, `missing_pushover_settings_fail_before_network`, `error_redaction_covers_global_secret_fields` |
|
||
|
||
All source environment variables are mapped: `APP_NAME`, `APP_ENV`, `SECRET_KEY`,
|
||
`JWT_ALGORITHM` (HS256 only), `ACCESS_TOKEN_EXPIRE_MINUTES`, `DATABASE_URL`, `DATA_DIR`,
|
||
`ALLOW_REGISTRATION`, `API_PREFIX`, `TIMEZONE`, `DEFAULT_ADMIN_USERNAME`,
|
||
`DEFAULT_ADMIN_PASSWORD`, `SMTP_STARTTLS`, `SMTP_TIMEOUT_SECONDS`, and `CORS_ORIGINS`.
|
||
`APP_PORT` controls the default port. `LISTEN_ADDR`, `DATABASE_PATH`,
|
||
`SSH_TIMEOUT_SECONDS`, `SSH_HOST_KEY_POLICY`, and `MAX_PARALLEL_BACKUPS` are explicit
|
||
deployment extensions.
|
||
|
||
## Intentional security and reliability changes
|
||
|
||
- No shared `admin/admin` password. A fresh installation generates an initial
|
||
password in a private file unless one is configured. Registration is disabled
|
||
by default and can be explicitly enabled with `ALLOW_REGISTRATION=true`.
|
||
- Public responses redact secrets. Blank/null secret input preserves the value;
|
||
explicit `clear_*` removes it. Upload and diff check ownership of both resources.
|
||
- SSH host keys are verified. Default `accept-new` permits the first connection,
|
||
records the key, and rejects changes. Explicit `strict` requires a trusted
|
||
known_hosts entry. A host key is distinct from a user's authentication private key.
|
||
- Directory and symlink checks protect backups. ZIP responses stream a temporary
|
||
file rather than buffering the complete archive in memory. Bulk downloads are
|
||
limited to 2 GiB, and email attachments to 25 MiB; exceeding a limit is an error.
|
||
- New backup filenames include a finer-grained timestamp and random suffix so
|
||
concurrent operations cannot overwrite each other. Imported names and contents
|
||
remain supported.
|
||
- External service failures return controlled HTTP 502 responses with secret values
|
||
redacted. A notification failure does not remove or invalidate a successful backup.
|
||
- Naive SQLite timestamps are presented as UTC ISO 8601, preserving the source's
|
||
actual time without relying on the browser timezone to interpret it.
|
||
- Structured diff corrects old blank-line numbering in replacement blocks: an empty
|
||
existing line is not treated as a missing line. Unified output is checked against
|
||
32 file-based fixtures generated by Python difflib.
|
||
- Empty cron expressions disable their schedules. Invalid expressions, invalid
|
||
ports, negative retention, and excessive request sizes are rejected. Jobs are
|
||
prevented from overlapping themselves.
|
||
- Revision 4 initializes only a fresh schema and validates an existing database
|
||
before use. There are no startup migrations or backup-layout conversions. Python
|
||
import is explicit, reads the source without modifying it, and requires a new
|
||
destination. See [data-import.md](data-import.md).
|
||
- Runtime is one Rust process with SQLite, scheduling, and an embedded interface.
|
||
Python, Node, Angular, and nginx are not required application processes.
|
||
|
||
## Concept image and inactive source code
|
||
|
||
| Item | Decision |
|
||
| --- | --- |
|
||
| Git integration / commits / synchronization | No source model, endpoint, or service exists; the concept screen is not represented as a working feature. |
|
||
| Arbitrary job CRUD | The source has system jobs controlled through settings; that behavior is preserved. |
|
||
| `/swos-beta/probe`, `/swos-beta/download` | The router was not included in `backend/app/api/__init__.py`; no new public beta API is activated. |
|
||
| Existing `switchos` type | Preserved: ordinary connection tests and binary backups invoke SwOS code through the original RouterService despite the inactive separate beta router. |
|
||
| Concept's restore action | The source only transfers a binary file to RouterOS. Upload is preserved without automatic restore or restart. |
|
||
| Invented network traffic or log levels | Views use actual API data rather than demo measurements or fields absent from the model. |
|
||
|
||
## Evidence limits
|
||
|
||
No production database, physical routers, device host keys, or provider accounts were
|
||
supplied. Local fixture checks do not establish compatibility with every firmware
|
||
version. For a deployment transition, follow the explicit
|
||
[import procedure](data-import.md), verify a connection to one real device, and
|
||
confirm that its backup can be downloaded. Starting the new server activates
|
||
schedules from the imported settings, so stop the old processes first. Release tests
|
||
do not automatically restore any device configuration.
|