Finalize nx9-wg production release
This commit is contained in:
1 parent
4dfe42fe68
commit
d704c1e131
30 files changed
+2502
-370
No files matched your search
+11
-10
@@ -1,12 +1,12 @@
|
||||
# nx9-db — SQLite Persistence Layer
|
||||
# nx9-wg-db — SQLite Persistence Layer
|
||||
|
||||
`nx9-db` provides the authoritative SQLite persistence layer for the `nx9-wg` native Rust WireGuard management system.
|
||||
`nx9-wg-db` provides the authoritative SQLite persistence layer for the `nx9-wg` native Rust WireGuard management system.
|
||||
|
||||
## Architectural Boundaries
|
||||
|
||||
- **Authoritative State**: SQLite is the authoritative persistent store for `nx9-wg` desired state. It stores what the system intends the network, interfaces, peers, routes, firewall rules, administrator credentials, sessions, tokens, and settings to be.
|
||||
- **Separation of Concerns**: SQLite records desired configuration only. Live kernel state (WireGuard interface status, handshake counters, packet counters, live nftables rules, live kernel routes) is queried directly from Linux kernel subsystems in later phases.
|
||||
- **SQL Encapsulation**: All SQL queries, SQLite connection lifecycle, migrations, and row conversions are strictly encapsulated inside `nx9-db`. Neither `nx9-core`, `nx9-api`, `nx9-ui`, `nx9-wireguard`, nor `nx9-network` issue SQL directly.
|
||||
- **Authoritative State**: SQLite is the authoritative persistent store for `nx9-wg` desired state. It stores what the system intends the network, interfaces, peers, routes, firewall rules, administrator credentials, sessions, tokens, client profiles, and settings to be.
|
||||
- **Separation of Concerns**: SQLite records desired configuration only. Live kernel state (WireGuard interface status, handshake counters, packet counters, live nftables rules, live kernel routes) is queried directly from Linux kernel subsystems.
|
||||
- **SQL Encapsulation**: All SQL queries, SQLite connection lifecycle, migrations, and row conversions are strictly encapsulated inside `nx9-wg-db`. Neither `nx9-wg-core`, `nx9-wg-api`, `nx9-wg-ui`, `nx9-wireguard`, nor `nx9-wg-network` issue SQL directly.
|
||||
|
||||
## SQLite Configuration
|
||||
|
||||
@@ -16,7 +16,7 @@ Every connection opened by `Store` enforces:
|
||||
- `PRAGMA busy_timeout = 5000` — 5-second busy timeout to avoid contention errors.
|
||||
- `PRAGMA synchronous = NORMAL` — Optimal reliability and performance in WAL mode.
|
||||
|
||||
## Database Schema (12 Tables)
|
||||
## Database Schema (13 Tables)
|
||||
|
||||
1. `admin` — Single administrator identity (`CHECK (id = 1)`), Argon2id password hash, TOTP secrets, and login timestamp.
|
||||
2. `sessions` — Admin web sessions (`ON DELETE CASCADE`).
|
||||
@@ -27,20 +27,21 @@ Every connection opened by `Store` enforces:
|
||||
7. `networks` — Named network CIDRs for routing and organization.
|
||||
8. `routes` — Desired kernel routing rules (`ON DELETE SET NULL`).
|
||||
9. `firewall_rules` — Desired firewall policy rules with priorities and directions (`in`, `out`, `forward`).
|
||||
10. `settings` — Key-value system settings with secret redaction support.
|
||||
10. `settings` — Key-value system settings with secret redaction support and structured WireGuard server endpoint keys.
|
||||
11. `audit_events` — Append-only operational audit log with event filtering and pagination.
|
||||
12. `backups` — Backup metadata and manifest checksum records.
|
||||
13. `client_profiles` — Device, connection, and MTU transport profile specifications with built-in protections.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
- Migrations are defined in `crates/nx9-db/migrations/` and embedded at compile time via `sqlx::migrate!("./migrations")`.
|
||||
- Migrations are defined in `crates/nx9-wg-db/migrations/` and embedded at compile time via `sqlx::migrate!("./migrations")`.
|
||||
- Migrations are executed automatically via `store.migrate().await?`.
|
||||
- Migrations are tracked in the `_sqlx_migrations` table for idempotency.
|
||||
|
||||
## Usage in Code
|
||||
|
||||
```rust
|
||||
use nx9_db::Store;
|
||||
use nx9_wg_db::Store;
|
||||
use std::path::Path;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -63,5 +64,5 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Tests use isolated in-memory or temporary file SQLite instances:
|
||||
|
||||
```bash
|
||||
cargo test -p nx9-db
|
||||
cargo test -p nx9-wg-db
|
||||
```
|
||||
@@ -3,7 +3,11 @@
|
||||
use crate::error::{DbError, Result};
|
||||
use crate::models::{format_datetime, parse_datetime};
|
||||
use chrono::Utc;
|
||||
use nx9_wg_core::types::settings::Setting;
|
||||
use nx9_wg_core::types::settings::{
|
||||
LEGACY_SETTING_PUBLIC_ENDPOINT, LEGACY_SETTING_SERVER_ENDPOINT,
|
||||
SETTING_SERVER_ENDPOINT_ENABLED, SETTING_SERVER_HOST, SETTING_SERVER_PORT,
|
||||
ServerEndpointSettings, Setting,
|
||||
};
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
/// Retrieve a setting by its key.
|
||||
@@ -99,3 +103,254 @@ pub async fn list_settings(pool: &SqlitePool) -> Result<Vec<Setting>> {
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Retrieve structured server endpoint settings from the database with legacy fallback.
|
||||
pub async fn get_server_endpoint_settings(pool: &SqlitePool) -> Result<ServerEndpointSettings> {
|
||||
let host_opt = get_setting_value(pool, SETTING_SERVER_HOST).await?;
|
||||
let port_opt = get_setting_value(pool, SETTING_SERVER_PORT).await?;
|
||||
let enabled_opt = get_setting_value(pool, SETTING_SERVER_ENDPOINT_ENABLED).await?;
|
||||
|
||||
let enabled = enabled_opt
|
||||
.as_deref()
|
||||
.map(|v| {
|
||||
let t = v.trim();
|
||||
t.parse::<bool>().unwrap_or_else(|_| t == "1")
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let port = port_opt
|
||||
.as_deref()
|
||||
.and_then(|v| v.trim().parse::<u16>().ok())
|
||||
.filter(|&p| p > 0)
|
||||
.unwrap_or(51820);
|
||||
|
||||
if let Some(host) = host_opt.filter(|h| !h.trim().is_empty()) {
|
||||
return Ok(ServerEndpointSettings {
|
||||
host: host.trim().to_string(),
|
||||
port,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy fallback: inspect server_endpoint
|
||||
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_SERVER_ENDPOINT).await? {
|
||||
let trimmed = legacy.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let (legacy_host, legacy_port) = split_host_port(trimmed, port);
|
||||
return Ok(ServerEndpointSettings {
|
||||
host: legacy_host,
|
||||
port: legacy_port,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback: inspect public_endpoint
|
||||
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await? {
|
||||
let trimmed = legacy.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let (legacy_host, legacy_port) = split_host_port(trimmed, port);
|
||||
return Ok(ServerEndpointSettings {
|
||||
host: legacy_host,
|
||||
port: legacy_port,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ServerEndpointSettings {
|
||||
host: String::new(),
|
||||
port,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
/// Persist structured server endpoint settings.
|
||||
pub async fn set_server_endpoint_settings(
|
||||
pool: &SqlitePool,
|
||||
settings: &ServerEndpointSettings,
|
||||
) -> Result<()> {
|
||||
let host_trimmed = settings.host.trim();
|
||||
if settings.enabled && !host_trimmed.is_empty() {
|
||||
nx9_wg_core::validation::validate_server_host(host_trimmed)?;
|
||||
nx9_wg_core::validation::validate_server_port(settings.port)?;
|
||||
}
|
||||
|
||||
set_setting(pool, SETTING_SERVER_HOST, host_trimmed, false).await?;
|
||||
set_setting(pool, SETTING_SERVER_PORT, &settings.port.to_string(), false).await?;
|
||||
set_setting(
|
||||
pool,
|
||||
SETTING_SERVER_ENDPOINT_ENABLED,
|
||||
&settings.enabled.to_string(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Synchronize legacy server_endpoint setting for backwards compatibility
|
||||
if settings.enabled && !host_trimmed.is_empty() {
|
||||
let formatted = nx9_wg_core::validation::format_endpoint(host_trimmed, settings.port);
|
||||
set_setting(pool, LEGACY_SETTING_SERVER_ENDPOINT, &formatted, false).await?;
|
||||
} else {
|
||||
delete_setting(pool, LEGACY_SETTING_SERVER_ENDPOINT).await?;
|
||||
delete_setting(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Authoritative server endpoint resolver.
|
||||
///
|
||||
/// Precedence:
|
||||
/// 1. Explicit endpoint override (if non-empty)
|
||||
/// 2. Persistent `wireguard.server_*` settings (when enabled and host non-empty)
|
||||
/// 3. Legacy `server_endpoint` setting (if non-empty)
|
||||
/// 4. Legacy `public_endpoint` setting (if non-empty)
|
||||
/// 5. Actionable error explaining how to configure server endpoint or provide `--endpoint`.
|
||||
pub async fn resolve_server_endpoint(
|
||||
pool: &SqlitePool,
|
||||
explicit_override: Option<&str>,
|
||||
) -> Result<String> {
|
||||
// 1. Explicit endpoint override
|
||||
if let Some(ep) = explicit_override {
|
||||
let trimmed = ep.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return parse_and_normalize_endpoint(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// Check enabled toggle
|
||||
let enabled_opt = get_setting_value(pool, SETTING_SERVER_ENDPOINT_ENABLED).await?;
|
||||
let enabled = enabled_opt
|
||||
.as_deref()
|
||||
.map(|v| {
|
||||
let t = v.trim();
|
||||
t.parse::<bool>().unwrap_or_else(|_| t == "1")
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if !enabled {
|
||||
return Err(DbError::Validation(
|
||||
"No reachable WireGuard server endpoint is configured. Configure WireGuard Server Endpoint in Settings or provide --endpoint.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 2. Persistent wireguard.server_* settings
|
||||
let host_opt = get_setting_value(pool, SETTING_SERVER_HOST).await?;
|
||||
let port_opt = get_setting_value(pool, SETTING_SERVER_PORT).await?;
|
||||
let port = port_opt
|
||||
.as_deref()
|
||||
.and_then(|v| v.trim().parse::<u16>().ok())
|
||||
.filter(|&p| p > 0)
|
||||
.unwrap_or(51820);
|
||||
|
||||
if let Some(host) = host_opt.filter(|h| !h.trim().is_empty()) {
|
||||
let validated_host = nx9_wg_core::validation::validate_server_host(&host)?;
|
||||
let validated_port = nx9_wg_core::validation::validate_server_port(port)?;
|
||||
return Ok(nx9_wg_core::validation::format_endpoint(
|
||||
&validated_host,
|
||||
validated_port,
|
||||
));
|
||||
}
|
||||
|
||||
// 3. Legacy server_endpoint fallback
|
||||
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_SERVER_ENDPOINT).await? {
|
||||
let trimmed = legacy.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return parse_and_normalize_endpoint(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Legacy public_endpoint fallback
|
||||
if let Some(legacy) = get_setting_value(pool, LEGACY_SETTING_PUBLIC_ENDPOINT).await? {
|
||||
let trimmed = legacy.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return parse_and_normalize_endpoint(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Actionable error
|
||||
Err(DbError::Validation(
|
||||
"No reachable WireGuard server endpoint is configured. Configure WireGuard Server Endpoint in Settings or provide --endpoint.".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn split_host_port(s: &str, default_port: u16) -> (String, u16) {
|
||||
let trimmed = s.trim();
|
||||
if trimmed.starts_with('[')
|
||||
&& let Some(closing) = trimmed.find(']')
|
||||
{
|
||||
let host_part = &trimmed[1..closing];
|
||||
let rest = &trimmed[closing + 1..];
|
||||
if let Some(port_str) = rest.strip_prefix(':')
|
||||
&& let Ok(port) = port_str.parse::<u16>()
|
||||
&& port > 0
|
||||
{
|
||||
return (host_part.to_string(), port);
|
||||
}
|
||||
return (host_part.to_string(), default_port);
|
||||
}
|
||||
if let Ok(ipv6) = trimmed.parse::<std::net::Ipv6Addr>() {
|
||||
return (ipv6.to_string(), default_port);
|
||||
}
|
||||
if let Some(last_colon) = trimmed.rfind(':') {
|
||||
let host_part = &trimmed[..last_colon];
|
||||
let port_part = &trimmed[last_colon + 1..];
|
||||
if let Ok(port) = port_part.parse::<u16>()
|
||||
&& port > 0
|
||||
{
|
||||
return (host_part.to_string(), port);
|
||||
}
|
||||
}
|
||||
(trimmed.to_string(), default_port)
|
||||
}
|
||||
|
||||
fn parse_and_normalize_endpoint(ep: &str) -> Result<String> {
|
||||
let trimmed = ep.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(DbError::Validation("endpoint cannot be empty".into()));
|
||||
}
|
||||
|
||||
if trimmed.starts_with('[')
|
||||
&& let Some(closing) = trimmed.find(']')
|
||||
{
|
||||
let host_part = &trimmed[1..closing];
|
||||
let ipv6 = host_part.parse::<std::net::Ipv6Addr>().map_err(|e| {
|
||||
DbError::Validation(format!("invalid IPv6 in endpoint '{trimmed}': {e}"))
|
||||
})?;
|
||||
let rest = &trimmed[closing + 1..];
|
||||
let port = if let Some(port_str) = rest.strip_prefix(':') {
|
||||
port_str
|
||||
.parse::<u16>()
|
||||
.map_err(|_| DbError::Validation(format!("invalid port in endpoint '{trimmed}'")))?
|
||||
} else if rest.is_empty() {
|
||||
51820
|
||||
} else {
|
||||
return Err(DbError::Validation(format!(
|
||||
"invalid endpoint format '{trimmed}'"
|
||||
)));
|
||||
};
|
||||
if port == 0 {
|
||||
return Err(DbError::Validation("port must be non-zero".into()));
|
||||
}
|
||||
return Ok(format!("[{}]:{}", ipv6, port));
|
||||
}
|
||||
|
||||
if let Ok(ipv6) = trimmed.parse::<std::net::Ipv6Addr>() {
|
||||
return Ok(format!("[{}]:51820", ipv6));
|
||||
}
|
||||
|
||||
if let Some(last_colon) = trimmed.rfind(':') {
|
||||
let host_part = &trimmed[..last_colon];
|
||||
let port_part = &trimmed[last_colon + 1..];
|
||||
if let Ok(port) = port_part.parse::<u16>() {
|
||||
if port == 0 {
|
||||
return Err(DbError::Validation("port must be non-zero".into()));
|
||||
}
|
||||
let host = nx9_wg_core::validation::validate_server_host(host_part)?;
|
||||
return Ok(nx9_wg_core::validation::format_endpoint(&host, port));
|
||||
}
|
||||
}
|
||||
|
||||
let host = nx9_wg_core::validation::validate_server_host(trimmed)?;
|
||||
Ok(nx9_wg_core::validation::format_endpoint(&host, 51820))
|
||||
}
|
||||
@@ -524,6 +524,23 @@ impl Store {
|
||||
crate::settings::list_settings(&self.pool).await
|
||||
}
|
||||
|
||||
pub async fn get_server_endpoint_settings(
|
||||
&self,
|
||||
) -> Result<nx9_wg_core::types::settings::ServerEndpointSettings> {
|
||||
crate::settings::get_server_endpoint_settings(&self.pool).await
|
||||
}
|
||||
|
||||
pub async fn set_server_endpoint_settings(
|
||||
&self,
|
||||
settings: &nx9_wg_core::types::settings::ServerEndpointSettings,
|
||||
) -> Result<()> {
|
||||
crate::settings::set_server_endpoint_settings(&self.pool, settings).await
|
||||
}
|
||||
|
||||
pub async fn resolve_server_endpoint(&self, explicit_override: Option<&str>) -> Result<String> {
|
||||
crate::settings::resolve_server_endpoint(&self.pool, explicit_override).await
|
||||
}
|
||||
|
||||
// Audit
|
||||
pub async fn create_audit_event(
|
||||
&self,
|
||||
|
||||
@@ -225,3 +225,171 @@ async fn test_backup_metadata_crud() {
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_endpoint_settings_crud_and_persistence() {
|
||||
let store = Store::connect_in_memory().await.expect("connect");
|
||||
store.migrate().await.expect("migrate");
|
||||
|
||||
// 1. Initial state defaults
|
||||
let initial = store
|
||||
.get_server_endpoint_settings()
|
||||
.await
|
||||
.expect("get initial");
|
||||
assert_eq!(initial.host, "");
|
||||
assert_eq!(initial.port, 51820);
|
||||
assert!(initial.enabled);
|
||||
|
||||
// Initial resolution with no settings returns actionable error
|
||||
let err = store.resolve_server_endpoint(None).await.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("No reachable WireGuard server endpoint is configured")
|
||||
);
|
||||
|
||||
// 2. Set structured server endpoint settings
|
||||
let new_settings = nx9_wg_core::types::settings::ServerEndpointSettings {
|
||||
host: "vpn.thakares.com".to_string(),
|
||||
port: 51820,
|
||||
enabled: true,
|
||||
};
|
||||
store
|
||||
.set_server_endpoint_settings(&new_settings)
|
||||
.await
|
||||
.expect("set server endpoint settings");
|
||||
|
||||
let loaded = store
|
||||
.get_server_endpoint_settings()
|
||||
.await
|
||||
.expect("get loaded settings");
|
||||
assert_eq!(loaded.host, "vpn.thakares.com");
|
||||
assert_eq!(loaded.port, 51820);
|
||||
assert!(loaded.enabled);
|
||||
|
||||
// 3. Resolve persistent setting
|
||||
let resolved = store.resolve_server_endpoint(None).await.expect("resolve");
|
||||
assert_eq!(resolved, "vpn.thakares.com:51820");
|
||||
|
||||
// 4. IPv6 persistence and formatting
|
||||
let ipv6_settings = nx9_wg_core::types::settings::ServerEndpointSettings {
|
||||
host: "2001:db8::10".to_string(),
|
||||
port: 51821,
|
||||
enabled: true,
|
||||
};
|
||||
store
|
||||
.set_server_endpoint_settings(&ipv6_settings)
|
||||
.await
|
||||
.expect("set ipv6");
|
||||
let resolved_v6 = store
|
||||
.resolve_server_endpoint(None)
|
||||
.await
|
||||
.expect("resolve v6");
|
||||
assert_eq!(resolved_v6, "[2001:db8::10]:51821");
|
||||
|
||||
// 5. Disable setting
|
||||
let disabled = nx9_wg_core::types::settings::ServerEndpointSettings {
|
||||
host: "vpn.thakares.com".to_string(),
|
||||
port: 51820,
|
||||
enabled: false,
|
||||
};
|
||||
store
|
||||
.set_server_endpoint_settings(&disabled)
|
||||
.await
|
||||
.expect("set disabled");
|
||||
let err = store.resolve_server_endpoint(None).await.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("No reachable WireGuard server endpoint is configured")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_endpoint_resolution_precedence() {
|
||||
let store = Store::connect_in_memory().await.expect("connect");
|
||||
store.migrate().await.expect("migrate");
|
||||
|
||||
// 1. Explicit override with no settings configured
|
||||
let ep = store
|
||||
.resolve_server_endpoint(Some("custom.vpn.net:51820"))
|
||||
.await
|
||||
.expect("override");
|
||||
assert_eq!(ep, "custom.vpn.net:51820");
|
||||
|
||||
// 2. Configure persistent settings
|
||||
let settings = nx9_wg_core::types::settings::ServerEndpointSettings {
|
||||
host: "persistent.vpn.io".to_string(),
|
||||
port: 51820,
|
||||
enabled: true,
|
||||
};
|
||||
store
|
||||
.set_server_endpoint_settings(&settings)
|
||||
.await
|
||||
.expect("set");
|
||||
|
||||
// Precedence test: explicit override beats persistent setting
|
||||
let overridden = store
|
||||
.resolve_server_endpoint(Some("override.vpn.io:5555"))
|
||||
.await
|
||||
.expect("override beats persistent");
|
||||
assert_eq!(overridden, "override.vpn.io:5555");
|
||||
|
||||
// Precedence test: None uses persistent setting
|
||||
let default_resolved = store.resolve_server_endpoint(None).await.expect("default");
|
||||
assert_eq!(default_resolved, "persistent.vpn.io:51820");
|
||||
|
||||
// 3. Legacy fallback test when wireguard.server_host is missing
|
||||
store
|
||||
.delete_setting(nx9_wg_core::types::settings::SETTING_SERVER_HOST)
|
||||
.await
|
||||
.expect("delete new host");
|
||||
store
|
||||
.set_setting("server_endpoint", "legacy.vpn.org:51820", false)
|
||||
.await
|
||||
.expect("set legacy");
|
||||
|
||||
let legacy_resolved = store
|
||||
.resolve_server_endpoint(None)
|
||||
.await
|
||||
.expect("legacy resolved");
|
||||
assert_eq!(legacy_resolved, "legacy.vpn.org:51820");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_endpoint_survives_reopen() {
|
||||
let temp_dir = tempfile::tempdir().expect("temp dir");
|
||||
let db_path = temp_dir.path().join("persist_endpoint.db");
|
||||
let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
|
||||
|
||||
{
|
||||
let store = Store::connect(&db_url).await.expect("connect 1");
|
||||
store.migrate().await.expect("migrate 1");
|
||||
|
||||
let settings = nx9_wg_core::types::settings::ServerEndpointSettings {
|
||||
host: "vpn.thakares.com".to_string(),
|
||||
port: 51820,
|
||||
enabled: true,
|
||||
};
|
||||
store
|
||||
.set_server_endpoint_settings(&settings)
|
||||
.await
|
||||
.expect("set");
|
||||
}
|
||||
|
||||
// Reconnect to existing store on disk
|
||||
{
|
||||
let store = Store::connect(&db_url).await.expect("connect 2");
|
||||
let loaded = store
|
||||
.get_server_endpoint_settings()
|
||||
.await
|
||||
.expect("get after reopen");
|
||||
assert_eq!(loaded.host, "vpn.thakares.com");
|
||||
assert_eq!(loaded.port, 51820);
|
||||
assert!(loaded.enabled);
|
||||
|
||||
let resolved = store
|
||||
.resolve_server_endpoint(None)
|
||||
.await
|
||||
.expect("resolve after reopen");
|
||||
assert_eq!(resolved, "vpn.thakares.com:51820");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user