Promote dev → main: green CI + workflow/security cleanup #211
No files matched your search
+16
-30
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
@@ -2478,6 +2478,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user