Promote dev → main: green CI + workflow/security cleanup #211

Merged
KofTwentyTwo merged 2 commits from dev into main 2026-07-29 16:16:53 +00:00
9 changed files with 105 additions and 184 deletions

No files matched your search

+16 -30
View File
@@ -2,9 +2,9 @@ name: CI Quality Gates & Build Pipeline
on:
push:
branches: [ main ]
branches: [ main, dev ]
pull_request:
branches: [ main ]
branches: [ main, dev ]
env:
CARGO_TERM_COLOR: always
@@ -19,9 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
# No toolchain-install action: rust-toolchain.toml pins 1.97.1 (+ rustfmt,
# clippy) and rustup auto-installs it on first cargo/rustc invocation, so
# CI always matches the local pinned toolchain instead of a drifting
# `stable`.
- name: Check Formatting
run: cargo fmt --all -- --check
@@ -30,13 +31,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Linux Desktop Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev build-essential curl wget file libssl-dev
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Run Clippy Linter (all targets, matches local `cargo check-all`)
@@ -47,36 +41,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Install Linux Desktop Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev build-essential curl wget file libssl-dev
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Run Workspace Tests
run: cargo test --workspace
run: cargo test-all
coverage:
name: Enforce 100% Code Coverage Threshold
name: Code Coverage (informational)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Install Linux Desktop Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev build-essential curl wget file libssl-dev
- name: Install llvm-tools component
run: rustup component add llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
# Coverage is informational for now. The former `--fail-under-lines 100`
# gate excluded all main.rs (the integration wiring) and incentivized
# stub+matching-test theater. A realistic threshold plus a required
# mock-server E2E gate lands with Phase 1.C.6 (GH #143).
# Coverage is informational only (matches the local `cargo cov` alias):
# no --fail-under-lines threshold, and the job is continue-on-error so a
# coverage regression or tooling hiccup never reds the pipeline. A
# realistic coverage gate is a separate roadmap story.
- name: Measure Code Coverage (informational)
run: cargo llvm-cov --workspace --ignore-filename-regex "main\.rs" --summary-only
run: cargo cov
build-matrix:
name: Cross-Platform Build Matrix (${{ matrix.os }})
@@ -88,7 +75,6 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Build All Workspace Crates
-93
View File
@@ -1,93 +0,0 @@
name: Nuncio Desktop GUI Release Pipeline (MSI, DMG, AppImage, Deb)
on:
# Desktop installers are built ONLY from version tags (per the release policy).
# Branch-push triggers were removed during the pre-alpha rebuild: they ran on
# every dev push and failed (the Tauri GUI is archived under _reference/).
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
build-tauri:
name: Build Desktop (${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin'
- platform: 'macos-13'
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout Code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
- name: Install Rust Toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin' || matrix.platform == 'macos-13' && 'x86_64-apple-darwin' || '' }}
- name: Install Linux Dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Frontend UI Dependencies
working-directory: crates/nuncio-gui/ui
run: npm install --legacy-peer-deps
- name: Install Tauri CLI Dependencies
working-directory: crates/nuncio-gui/src-tauri
run: npm install --legacy-peer-deps
- name: Determine Environment Channel & Tag
id: env_prep
shell: bash
run: |
if [[ "${{ github.ref }}" == refs/heads/dev ]]; then
echo "channel=dev" >> $GITHUB_OUTPUT
echo "tag_name=dev-latest" >> $GITHUB_OUTPUT
echo "release_name=Nuncio Desktop (Development Nightly)" >> $GITHUB_OUTPUT
echo "is_prerelease=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/heads/rc || "${{ github.ref }}" == refs/tags/*rc* ]]; then
echo "channel=rc" >> $GITHUB_OUTPUT
echo "tag_name=rc-latest" >> $GITHUB_OUTPUT
echo "release_name=Nuncio Desktop (Release Candidate)" >> $GITHUB_OUTPUT
echo "is_prerelease=true" >> $GITHUB_OUTPUT
else
echo "channel=prod" >> $GITHUB_OUTPUT
echo "tag_name=${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "release_name=Nuncio Desktop Release ${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Build Tauri Desktop Installers (.msi, .dmg, .AppImage, .deb)
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectPath: 'crates/nuncio-gui/src-tauri'
tagName: ${{ steps.env_prep.outputs.tag_name }}
releaseName: ${{ steps.env_prep.outputs.release_name }}
releaseBody: 'Production Tauri v2 Desktop GUI installers (.msi, .dmg, .AppImage, .deb).'
releaseDraft: false
prerelease: ${{ steps.env_prep.outputs.is_prerelease == 'true' }}
args: ${{ matrix.args }}
-4
View File
@@ -48,8 +48,6 @@ jobs:
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/nuncio-cli dist/nuncio-cli
cp target/${{ matrix.target }}/release/nuncio-tui dist/nuncio-tui
cp target/${{ matrix.target }}/release/nuncio-mcp dist/nuncio-mcp
cp target/${{ matrix.target }}/release/nunciod dist/nunciod
tar -czf ${{ matrix.artifact_name }} -C dist .
@@ -59,8 +57,6 @@ jobs:
run: |
New-Item -ItemType Directory -Path dist
Copy-Item target/${{ matrix.target }}/release/nuncio-cli.exe dist/nuncio-cli.exe
Copy-Item target/${{ matrix.target }}/release/nuncio-tui.exe dist/nuncio-tui.exe
Copy-Item target/${{ matrix.target }}/release/nuncio-mcp.exe dist/nuncio-mcp.exe
Copy-Item target/${{ matrix.target }}/release/nunciod.exe dist/nunciod.exe
Compress-Archive -Path dist/* -DestinationPath ${{ matrix.artifact_name }}
+8 -13
View File
@@ -52,8 +52,6 @@ jobs:
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/nuncio-cli dist/nuncio-cli
cp target/${{ matrix.target }}/release/nuncio-tui dist/nuncio-tui
cp target/${{ matrix.target }}/release/nuncio-mcp dist/nuncio-mcp
cp target/${{ matrix.target }}/release/nunciod dist/nunciod
tar -czf ${{ matrix.artifact_name }} -C dist .
@@ -63,8 +61,6 @@ jobs:
run: |
New-Item -ItemType Directory -Path dist
Copy-Item target/${{ matrix.target }}/release/nuncio-cli.exe dist/nuncio-cli.exe
Copy-Item target/${{ matrix.target }}/release/nuncio-tui.exe dist/nuncio-tui.exe
Copy-Item target/${{ matrix.target }}/release/nuncio-mcp.exe dist/nuncio-mcp.exe
Copy-Item target/${{ matrix.target }}/release/nunciod.exe dist/nunciod.exe
Compress-Archive -Path dist/* -DestinationPath ${{ matrix.artifact_name }}
@@ -122,17 +118,16 @@ jobs:
prerelease: ${{ steps.prep.outputs.is_prerelease == 'true' }}
files: dist-final/*
body: |
## Nuncio Sovereign Mail, Calendar & Contacts Suite
## Nuncio Engine (nunciod + nuncio-cli)
**Release Channel**: ${{ steps.prep.outputs.channel }}
### 📦 Official Installer & Package Downloads
- **Windows Installer**: `Nuncio_x64.msi` (WiX Installer) / `Nuncio_x64-setup.exe` (NSIS)
- **macOS Disk Image**: `Nuncio_x64.dmg` (Intel) / `Nuncio_aarch64.dmg` (Apple Silicon)
- **Linux Packages**: `Nuncio_amd64.AppImage` / `Nuncio_amd64.deb`
- **CLI & Daemon Archives**: `nuncio-x86_64-unknown-linux-gnu.tar.gz`, `nuncio-aarch64-apple-darwin.tar.gz`, `nuncio-x86_64-apple-darwin.tar.gz`, `nuncio-x86_64-pc-windows-msvc.zip`
### 📦 Daemon & CLI Archives
- `nuncio-x86_64-unknown-linux-gnu.tar.gz`
- `nuncio-aarch64-apple-darwin.tar.gz`
- `nuncio-x86_64-apple-darwin.tar.gz`
- `nuncio-x86_64-pc-windows-msvc.zip`
### 🚀 Package Managers
- **Homebrew Tap**: `brew install koftwentytwo/tap/nuncio`
- **Cargo Source**: `cargo install --git https://github.com/KofTwentyTwo/nuncio.git nuncio-cli nuncio-tui nuncio-mcp nunciod`
### 🚀 Cargo Source
- `cargo install --git https://github.com/KofTwentyTwo/nuncio.git nuncio-cli nunciod`
Verify payload hashes against `SHA256SUMS.txt`.
Generated
+1
View File
@@ -2478,6 +2478,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"tempfile",
"thiserror 1.0.69",
"tokio",
"tokio-stream",
+1
View File
@@ -29,6 +29,7 @@ tracing.workspace = true
rpassword.workspace = true
chrono.workspace = true
hex.workspace = true
tempfile.workspace = true
[dev-dependencies]
tokio-stream = { workspace = true, features = ["net"] }
+6 -1
View File
@@ -134,6 +134,10 @@ pub struct HeadlessRunner {
db: DatabaseEngine,
secrets: Arc<SecretManager>,
grpc_addr: String,
// Kept only to hold the ephemeral database's backing directory open for
// the runner's lifetime: dropping it would unlink the directory out from
// under `db` while the pool may still need to open new connections.
_db_dir: tempfile::TempDir,
}
impl HeadlessRunner {
@@ -159,7 +163,7 @@ impl HeadlessRunner {
secrets: Arc<SecretManager>,
grpc_addr: String,
) -> Result<Self, RunnerError> {
let (db, _dir) = DatabaseEngine::connect_ephemeral()
let (db, db_dir) = DatabaseEngine::connect_ephemeral()
.await
.map_err(|e| RunnerError::InitFailed(e.to_string()))?;
let event_bus = EventBus::new();
@@ -167,6 +171,7 @@ impl HeadlessRunner {
event_bus,
db,
secrets,
_db_dir: db_dir,
grpc_addr,
})
}
+57 -37
View File
@@ -1264,13 +1264,25 @@ mod tests {
async fn spawn_test_server(
event_bus: Arc<EventBus>,
token: &str,
) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) {
let (db, _dir) = DatabaseEngine::connect_ephemeral()
) -> (
std::net::SocketAddr,
tokio::task::JoinHandle<()>,
tempfile::TempDir,
) {
// The `TempDir` guard must outlive this function: the spawned server
// task keeps using `db` long after we return, and on Linux dropping
// the guard here unlinks the directory backing the sqlite file out
// from under it, so any connection the pool opens afterward fails
// with "unable to open database file". Return it to the caller so it
// stays alive for the test's duration.
let (db, dir) = DatabaseEngine::connect_ephemeral()
.await
.expect("connect ephemeral test db");
let secrets = Arc::new(SecretManager::mock());
let filter_engine = Arc::new(FilterEngine::new(Vec::new()).expect("empty rule set"));
spawn_test_server_with(event_bus, Arc::new(db), filter_engine, secrets, token).await
let (addr, handle) =
spawn_test_server_with(event_bus, Arc::new(db), filter_engine, secrets, token).await;
(addr, handle, dir)
}
/// Spawns a test server on an ephemeral loopback port backed by the
@@ -1356,7 +1368,7 @@ mod tests {
#[tokio::test]
async fn get_status_rejects_missing_bearer_token() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1371,7 +1383,7 @@ mod tests {
#[tokio::test]
async fn get_status_rejects_wrong_bearer_token() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1393,7 +1405,7 @@ mod tests {
#[tokio::test]
async fn get_status_rejects_malformed_authorization_header() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1418,7 +1430,7 @@ mod tests {
event_bus.process_command(CoreCommand::SyncAll);
assert_eq!(event_bus.current_state().status, EngineStatus::Syncing);
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1503,7 +1515,7 @@ mod tests {
#[tokio::test]
async fn subscribe_streams_mapped_core_events_from_the_live_event_bus() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus.clone(), "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus.clone(), "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1546,7 +1558,7 @@ mod tests {
// `GetStatus` because both are defined on the single `System`
// service (no separate, un-intercepted service).
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
@@ -1569,7 +1581,7 @@ mod tests {
// `GetStatus` calls complete correctly and without corrupting one
// another.
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus.clone(), "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus.clone(), "correct-token").await;
// Open (and await) the subscription first, on its own connection,
// so the server-side registration has deterministically happened
@@ -1663,7 +1675,7 @@ mod tests {
// Confirms `Accounts` is mounted behind its own `BearerAuthInterceptor`
// exactly like `System` (no un-intercepted service).
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = AccountsClient::connect(format!("http://{addr}"))
.await
@@ -1684,7 +1696,7 @@ mod tests {
#[tokio::test]
async fn list_accounts_rejects_missing_bearer_token() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = AccountsClient::connect(format!("http://{addr}"))
.await
@@ -1699,7 +1711,7 @@ mod tests {
#[tokio::test]
async fn add_account_rejects_empty_password() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = AccountsClient::connect(format!("http://{addr}"))
.await
@@ -1717,7 +1729,7 @@ mod tests {
#[tokio::test]
async fn add_account_rejects_missing_config() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = AccountsClient::connect(format!("http://{addr}"))
.await
@@ -1944,7 +1956,7 @@ mod tests {
// Confirms `Mail` is mounted behind its own `BearerAuthInterceptor`
// exactly like `System` and `Accounts` (no un-intercepted service).
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2013,7 +2025,7 @@ mod tests {
#[tokio::test]
async fn get_message_reports_not_found_for_unknown_message_id() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2030,7 +2042,7 @@ mod tests {
#[tokio::test]
async fn mark_read_reports_not_found_for_unknown_message_id() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2048,7 +2060,7 @@ mod tests {
#[tokio::test]
async fn list_messages_rejects_empty_folder_id() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2215,7 +2227,7 @@ mod tests {
#[tokio::test]
async fn send_message_rejects_empty_to() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2232,7 +2244,7 @@ mod tests {
#[tokio::test]
async fn send_message_rejects_empty_subject() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2253,7 +2265,7 @@ mod tests {
#[tokio::test]
async fn send_message_reports_honest_error_when_no_account_configured() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2330,7 +2342,7 @@ mod tests {
#[tokio::test]
async fn sync_without_override_and_without_accounts_reports_zero_synced() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2350,7 +2362,7 @@ mod tests {
#[tokio::test]
async fn sync_without_override_reports_error_for_unknown_account() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = MailClient::connect(format!("http://{addr}"))
.await
@@ -2632,8 +2644,14 @@ mod tests {
tokio::task::JoinHandle<()>,
Arc<DatabaseEngine>,
Arc<FilterEngine>,
tempfile::TempDir,
) {
let (db, _dir) = DatabaseEngine::connect_ephemeral()
// The `TempDir` guard must outlive this function: on Linux, dropping it
// here would unlink the directory backing the sqlite file while `db`
// is still in use, so any pool connection opened *after* this
// function returns fails with "unable to open database file". Return
// it to the caller instead so it stays alive for the test's duration.
let (db, dir) = DatabaseEngine::connect_ephemeral()
.await
.expect("connect ephemeral test db");
let db = Arc::new(db);
@@ -2647,7 +2665,7 @@ mod tests {
token,
)
.await;
(addr, handle, db, filter_engine)
(addr, handle, db, filter_engine, dir)
}
#[tokio::test]
@@ -2655,7 +2673,7 @@ mod tests {
// Confirms `Filters` is mounted behind its own `BearerAuthInterceptor`
// exactly like `System`/`Accounts`/`Mail` (no un-intercepted
// service).
let (addr, _handle, _db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2711,7 +2729,8 @@ mod tests {
/// back to empty.
#[tokio::test]
async fn create_list_and_delete_rule_round_trip_and_keep_the_live_engine_in_sync() {
let (addr, _handle, _db, filter_engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, filter_engine, _dir) =
spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2790,7 +2809,8 @@ mod tests {
#[tokio::test]
async fn create_rule_rejects_invalid_nsql_and_persists_nothing() {
let (addr, _handle, _db, filter_engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, filter_engine, _dir) =
spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2834,7 +2854,7 @@ mod tests {
#[tokio::test]
async fn delete_rule_rejects_empty_id() {
let (addr, _handle, _db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2854,7 +2874,7 @@ mod tests {
/// doc comment). Also proves validation never persists anything.
#[tokio::test]
async fn validate_rule_reports_honest_ok_and_error_verdicts_without_persisting() {
let (addr, _handle, _db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2897,7 +2917,7 @@ mod tests {
/// `Mail/GetMessage` reads) without ever persisting the rule itself.
#[tokio::test]
async fn preview_rule_dry_runs_against_a_stored_message_without_persisting() {
let (addr, _handle, db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
db.save_email(&nuncio_core::model::Email {
id: "msg-preview-1".to_string(),
@@ -2963,7 +2983,7 @@ mod tests {
/// message rather than failing the RPC.
#[tokio::test]
async fn preview_rule_falls_back_to_a_synthetic_message_when_none_is_stored() {
let (addr, _handle, _db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -2994,7 +3014,7 @@ mod tests {
#[tokio::test]
async fn preview_rule_and_validate_rule_reject_invalid_nsql_syntax() {
let (addr, _handle, _db, _engine) = spawn_filters_test_server("correct-token").await;
let (addr, _handle, _db, _engine, _dir) = spawn_filters_test_server("correct-token").await;
let mut client = FiltersClient::connect(format!("http://{addr}"))
.await
@@ -3019,7 +3039,7 @@ mod tests {
// Confirms `Export` is mounted behind its own `BearerAuthInterceptor`
// exactly like every other service (no un-intercepted service).
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = ExportClient::connect(format!("http://{addr}"))
.await
@@ -3038,7 +3058,7 @@ mod tests {
#[tokio::test]
async fn export_mailbox_rejects_empty_output_path() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = ExportClient::connect(format!("http://{addr}"))
.await
@@ -3057,7 +3077,7 @@ mod tests {
#[tokio::test]
async fn export_mailbox_rejects_unspecified_format() {
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = ExportClient::connect(format!("http://{addr}"))
.await
@@ -3200,7 +3220,7 @@ mod tests {
// Confirms `Audit` is mounted behind its own `BearerAuthInterceptor`
// exactly like every other service (no un-intercepted service).
let event_bus = Arc::new(EventBus::new());
let (addr, _handle) = spawn_test_server(event_bus, "correct-token").await;
let (addr, _handle, _dir) = spawn_test_server(event_bus, "correct-token").await;
let mut client = AuditClient::connect(format!("http://{addr}"))
.await
+16 -6
View File
@@ -32,12 +32,22 @@ use tonic::{Code, Request};
/// Backs the mounted `nuncio.v1.Accounts` service with its own fresh
/// ephemeral database + mock keyring, since this test file only exercises
/// `System` and doesn't care about account/keyring state.
async fn start_server(event_bus: Arc<EventBus>, token: String) -> std::net::SocketAddr {
///
/// Also returns the ephemeral database's backing `TempDir` guard: the
/// spawned server task keeps using `db` long after this function returns,
/// and on Linux dropping the guard here would unlink the directory out from
/// under it, so any connection the pool opens afterward fails with "unable
/// to open database file". Callers must hold the guard for as long as they
/// keep talking to the server.
async fn start_server(
event_bus: Arc<EventBus>,
token: String,
) -> (std::net::SocketAddr, tempfile::TempDir) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("listener has local addr");
let (db, _dir) = DatabaseEngine::connect_ephemeral()
let (db, dir) = DatabaseEngine::connect_ephemeral()
.await
.expect("connect ephemeral test db");
let secrets = Arc::new(SecretManager::mock());
@@ -53,7 +63,7 @@ async fn start_server(event_bus: Arc<EventBus>, token: String) -> std::net::Sock
)
.await;
});
addr
(addr, dir)
}
#[tokio::test]
@@ -66,7 +76,7 @@ async fn rejects_calls_without_a_bearer_token() {
.expect("token provisioned from mock vault"),
);
let addr = start_server(event_bus, token).await;
let (addr, _dir) = start_server(event_bus, token).await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
.expect("client connects");
@@ -88,7 +98,7 @@ async fn rejects_calls_with_an_invalid_bearer_token() {
.expect("token provisioned from mock vault"),
);
let addr = start_server(event_bus, token).await;
let (addr, _dir) = start_server(event_bus, token).await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
.expect("client connects");
@@ -123,7 +133,7 @@ async fn accepts_correct_bearer_token_and_returns_real_engine_status() {
.expect("token provisioned from mock vault"),
);
let addr = start_server(event_bus.clone(), token.clone()).await;
let (addr, _dir) = start_server(event_bus.clone(), token.clone()).await;
let mut client = SystemClient::connect(format!("http://{addr}"))
.await
.expect("client connects");