Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ reqwest = { version = "0.12.1", default-features = false, features = [
"json",
"rustls-tls",
] }
mostro-core = { version = "0.8.3", features = ["sqlx"] }
mostro-core = { version = "0.8.4", features = ["sqlx"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
clap = { version = "4.5.45", features = ["derive"] }
Expand Down
5 changes: 3 additions & 2 deletions docs/ADMIN_RPC_AND_DISPUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Admin capabilities and dispute resolution paths.

## Dispute Lifecycle
- Open: `src/app/dispute.rs` (Action=Dispute) → mark order as `Dispute` and notify.
- Admin Take: `src/app/admin_take_dispute.rs` assigns solver.
- Admin Settle: `src/app/admin_settle.rs` settles/cancels hold or pays out as needed.
- Admin Take: `src/app/admin_take_dispute.rs` assigns solver. Both `read` and `read-write` solvers may take disputes.
- Admin Settle: `src/app/admin_settle.rs` settles/cancels hold or pays out as needed. Requires a `read-write` solver.

## Admin Cancel
- File: `src/app/admin_cancel.rs`.
Expand Down Expand Up @@ -42,5 +42,6 @@ sequenceDiagram

## Audit and Safety
- Require admin authentication/authorization at message level.
- Enforce solver permission levels in the daemon: `read` solvers can assist but cannot execute `admin-settle` or `admin-cancel`.
- Record solver, timestamps, and decisions in DB for traceability.
- Avoid leaking sensitive data in logs; scrub invoices and keys.
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ flowchart LR
- `app/orders.rs` – queries and returns order listings/history.
- `app/restore_session.rs` – rehydrates context for a client after reconnect.
- `app/trade_pubkey.rs` – exchanges/updates trade pubkeys for secure comms.
- Admin modules – force cancel/settle, take disputes, add solvers; guarded and auditable.
- Admin modules – force cancel/settle, take disputes, add solvers; guarded, auditable, and permission-gated for solver capabilities.

## Configuration Constants (src/config/constants.rs)

Expand Down
6 changes: 3 additions & 3 deletions docs/ORDERS_AND_ACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ Summary of order lifecycle and key handlers.
| `orders` | https://github.com/MostroP2P/mostro/blob/main/src/app/orders.rs | Return order lists/history to requester |
| `trade-pubkey` | https://github.com/MostroP2P/mostro/blob/main/src/app/trade_pubkey.rs | Exchange or update per-trade pubkeys |
| `restore-session` | https://github.com/MostroP2P/mostro/blob/main/src/app/restore_session.rs | Rehydrate client session and state |
| `admin-cancel` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_cancel.rs | Admin cancel; optionally cancel hold invoice |
| `admin-settle` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_settle.rs | Admin settlement; settle/cancel holds, finalize |
| `admin-add-solver` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_add_solver.rs | Register dispute solver key |
| `admin-cancel` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_cancel.rs | Admin cancel; optionally cancel hold invoice. Assigned solver must have `read-write` permission |
| `admin-settle` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_settle.rs | Admin settlement; settle/cancel holds, finalize. Assigned solver must have `read-write` permission |
| `admin-add-solver` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_add_solver.rs | Register dispute solver key. `admin-add-solver` accepts a bare pubkey (defaults to read-write), `pubkey:read`, `pubkey:write` (alias for read-write), or `pubkey:read-write`; see the parser in `admin_add_solver.rs` for the accepted forms |
| `admin-take-dispute` | https://github.com/MostroP2P/mostro/blob/main/src/app/admin_take_dispute.rs | Assign or take ownership of dispute |
| `last-trade-index` | https://github.com/MostroP2P/mostro/blob/main/src/app/last_trade_index.rs | Retrieve user's last trade index from database |

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ Quick links to architecture and feature guides.
Tips
- Run tests and lints before pushing: `cargo test`, `cargo fmt`, `cargo clippy --all-targets --all-features`.
- Update SQLx offline data after query/schema changes: `cargo sqlx prepare -- --bin mostrod`.

- [Solver Permission Levels](./SOLVER_PERMISSION_LEVELS.md)
86 changes: 86 additions & 0 deletions docs/SOLVER_PERMISSION_LEVELS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Solver Permission Levels

This document specifies solver permission levels for dispute resolution.

## Summary

Mostro supports two solver permission levels:

- `read`: solver can take a dispute, receive dispute context, and communicate with users
- `read-write`: solver can do everything above and can also execute `admin-settle` and `admin-cancel`

This split is intended to support automated dispute assistants, including AI-based agents, without giving them authority to move funds.

## Goals

- allow non-human dispute assistants to participate safely
- enforce authorization at the daemon level, not in prompts or UI
- preserve backward compatibility for existing solver registration flows

## Data Model

The `users.category` field is used to represent solver permissions:

- `0`: regular user / no solver permissions
- `1`: solver with `read` permission only
- `2`: solver with `read-write` permission

The legacy `is_solver` flag still indicates whether the user is a solver at all.

## Authorization Rules

### `admin-take-dispute`
Allowed for:
- Mostro daemon admin key while dispute status is `initiated` or `in-progress`
- solvers with `is_solver = true` while dispute status is `initiated`

Both `read` and `read-write` solvers may take a dispute.

### `admin-settle`
Allowed only when:
- the caller is the solver assigned to the dispute
- and the assigned solver has `category = 2`

If the caller is assigned but only has `read` permission, Mostro returns:
- `CantDoReason::NotAuthorized`

### `admin-cancel`
Allowed only when:
- the caller is the solver assigned to the dispute
- and the assigned solver has `category = 2`

If the caller is assigned but only has `read` permission, Mostro returns:
- `CantDoReason::NotAuthorized`

## AdminAddSolver payload

`admin-add-solver` continues using `Payload::TextMessage`, but now supports an optional permission suffix.

Formats:

- `npub1...` → defaults to `read-write`
- `npub1...:read` → registers solver as read-only
- `npub1...:read-write` → registers solver as read-write
- `npub1...:write` → alias for read-write

Invalid suffixes must be rejected with `CantDoReason::InvalidParameters`.

## RPC impact

The current RPC `AddSolverRequest` still only exposes `solver_pubkey`.

That means RPC registration remains backward compatible and defaults to `read-write` until the protobuf/API is extended.

## Dependency

This feature requires `mostro-core >= 0.8.4` because it uses `CantDoReason::NotAuthorized`.

## Security rationale

The key security property is that read-only solvers can never execute dispute-closing actions, even if:

- a UI exposes the wrong button
- an operator misconfigures an agent prompt
- a remote tool attempts to call `admin-settle` or `admin-cancel` directly

The daemon enforces the permission boundary.
12 changes: 12 additions & 0 deletions migrations/20260417145500_solver_category_backfill.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Backfill legacy solver records so pre-existing solvers keep their historical
-- settle/cancel authority after solver permission categories are enforced.
--
-- Before PR #708, solver capability was represented only by `is_solver = 1`.
-- After PR #708, settle/cancel paths require `category = 2` (read-write).
--
-- Operators should not need to patch this manually, so migrate any legacy solver
-- rows that still have the default/legacy category value to read-write.
UPDATE users
SET category = 2
WHERE is_solver = 1
AND category = 0;
132 changes: 110 additions & 22 deletions src/app/admin_add_solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,141 @@ use nostr::nips::nip59::UnwrappedGift;
use nostr_sdk::prelude::*;
use tracing::{error, info};

pub const SOLVER_CATEGORY_READ_ONLY: i64 = 1;
pub const SOLVER_CATEGORY_READ_WRITE: i64 = 2;

fn parse_solver_payload(payload: &Payload) -> Result<(String, i64), MostroError> {
let raw = match payload {
Payload::TextMessage(p) => p.trim(),
_ => return Err(MostroCantDo(CantDoReason::InvalidTextMessage)),
};

if raw.is_empty() {
return Err(MostroCantDo(CantDoReason::InvalidTextMessage));
}

let mut parts = raw.split(':');
let npub = parts
.next()
.ok_or(MostroCantDo(CantDoReason::InvalidTextMessage))?
.trim();

if npub.is_empty() {
return Err(MostroCantDo(CantDoReason::InvalidTextMessage));
}

let category = match parts.next().map(str::trim) {
None => SOLVER_CATEGORY_READ_WRITE,
Some("read") => SOLVER_CATEGORY_READ_ONLY,
Some("read-write") | Some("write") => SOLVER_CATEGORY_READ_WRITE,
Some("") | Some(_) => return Err(MostroCantDo(CantDoReason::InvalidParameters)),
};
Comment thread
grunch marked this conversation as resolved.

if parts.next().is_some() {
return Err(MostroCantDo(CantDoReason::InvalidParameters));
}

Ok((npub.to_string(), category))
}

pub async fn admin_add_solver_action(
ctx: &AppContext,
msg: Message,
event: &UnwrappedGift,
my_keys: &Keys,
) -> Result<(), MostroError> {
let pool = ctx.pool();
// Get request id
let request_id = msg.get_inner_message_kind().request_id;

let inner_message = msg.get_inner_message_kind();
let payload = if let Some(payload) = &inner_message.payload {
payload
} else {
error!("No pubkey found!");
return Ok(());
};
let npubkey = if let Payload::TextMessage(p) = payload {
p
} else {
error!("No pubkey found!");
return Ok(());
};
let payload = inner_message
.payload
.as_ref()
.ok_or(MostroCantDo(CantDoReason::InvalidTextMessage))?;

// Check if the pubkey is Mostro
if event.sender.to_string() != my_keys.public_key().to_string() {
// We create a Message
return Err(MostroInternalErr(ServiceError::InvalidPubkey));
}

let trade_index = inner_message.trade_index.unwrap_or(0);
let public_key = PublicKey::from_bech32(npubkey)
let (npubkey, category) = parse_solver_payload(payload)?;
let public_key = PublicKey::from_bech32(&npubkey)
.map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?;
let user = User::new(public_key.to_string(), 0, 1, 0, 0, trade_index);
// Use CRUD to create user

let user = User::new(public_key.to_string(), 0, 1, 0, category, trade_index);

match add_new_user(pool, user).await {
Ok(r) => info!("Solver added: {:#?}", r),
Err(ee) => error!("Error creating solver: {:#?}", ee),
Ok(r) => info!("Solver added: {} with category {}", r, category),
Err(ee) => {
error!("Error creating solver: {:#?}", ee);
return Err(MostroInternalErr(ServiceError::DbAccessError(
ee.to_string(),
)));
}
}
// We create a Message for admin

let message = Message::new_dispute(None, request_id, None, Action::AdminAddSolver, None);
let message = message
.as_json()
.map_err(|_| MostroInternalErr(ServiceError::MessageSerializationError))?;
// Send the message

send_dm(event.rumor.pubkey, my_keys, &message, None)
.await
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
Comment thread
grunch marked this conversation as resolved.

Ok(())
}

#[cfg(test)]
mod tests {
use super::{parse_solver_payload, SOLVER_CATEGORY_READ_ONLY, SOLVER_CATEGORY_READ_WRITE};
use mostro_core::error::CantDoReason;
use mostro_core::message::Payload;

#[test]
fn parse_solver_payload_defaults_to_read_write() {
let (npub, category) =
parse_solver_payload(&Payload::TextMessage("npub1test".to_string())).unwrap();
assert_eq!(npub, "npub1test");
assert_eq!(category, SOLVER_CATEGORY_READ_WRITE);
}

#[test]
fn parse_solver_payload_accepts_read_only() {
let (_, category) =
parse_solver_payload(&Payload::TextMessage("npub1test:read".to_string())).unwrap();
assert_eq!(category, SOLVER_CATEGORY_READ_ONLY);
}

#[test]
fn parse_solver_payload_accepts_read_write_aliases() {
let (_, category) =
parse_solver_payload(&Payload::TextMessage("npub1test:read-write".to_string()))
.unwrap();
assert_eq!(category, SOLVER_CATEGORY_READ_WRITE);

let (_, category) =
parse_solver_payload(&Payload::TextMessage("npub1test:write".to_string())).unwrap();
assert_eq!(category, SOLVER_CATEGORY_READ_WRITE);
}

#[test]
fn parse_solver_payload_rejects_invalid_permission() {
let err =
parse_solver_payload(&Payload::TextMessage("npub1test:admin".to_string())).unwrap_err();
assert_eq!(
err,
mostro_core::error::MostroError::MostroCantDo(CantDoReason::InvalidParameters)
);
}

#[test]
fn parse_solver_payload_rejects_empty_permission_token() {
let err =
parse_solver_payload(&Payload::TextMessage("npub1test:".to_string())).unwrap_err();
assert_eq!(
err,
mostro_core::error::MostroError::MostroCantDo(CantDoReason::InvalidParameters)
);
}
}
9 changes: 8 additions & 1 deletion src/app/admin_cancel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::borrow::Cow;
use std::str::FromStr;

use crate::app::context::AppContext;
use crate::db::{find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin};
use crate::db::{
find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin,
solver_has_write_permission,
};
use crate::lightning::LndConnector;
use crate::nip33::{create_platform_tag_values, new_dispute_event};
use crate::util::{enqueue_order_msg, get_order, send_dm, update_order_event};
Expand Down Expand Up @@ -70,6 +73,10 @@ pub async fn admin_cancel_action(
_ => {}
}

if !solver_has_write_permission(pool, &event.sender.to_string(), order.id).await? {
return Err(MostroCantDo(CantDoReason::NotAuthorized));
}

// Was order cooperatively cancelled?
if order.check_status(Status::CooperativelyCanceled).is_ok() {
enqueue_order_msg(
Expand Down
14 changes: 13 additions & 1 deletion src/app/admin_settle.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::app::context::AppContext;
use crate::db::{find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin};
use crate::db::{
find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin,
solver_has_write_permission,
};
use crate::lightning::LndConnector;
use crate::nip33::{create_platform_tag_values, new_dispute_event};
use crate::util::{enqueue_order_msg, get_order, settle_seller_hold_invoice, update_order_event};
Expand Down Expand Up @@ -47,6 +50,10 @@ pub async fn admin_settle_action(
_ => {}
}

if !solver_has_write_permission(pool, &event.sender.to_string(), order.id).await? {
return Err(MostroCantDo(CantDoReason::NotAuthorized));
}

// Was order cooperatively cancelled?
if order.check_status(Status::CooperativelyCanceled).is_ok() {
enqueue_order_msg(
Expand Down Expand Up @@ -206,7 +213,12 @@ mod tests {
let admin_error = CantDoReason::DisputeTakenByAdmin;
assert_eq!(format!("{:?}", admin_error), "DisputeTakenByAdmin");

// New error for authenticated callers lacking enough permissions
let unauthorized_error = CantDoReason::NotAuthorized;
assert_eq!(format!("{:?}", unauthorized_error), "NotAuthorized");

// Verify they are different error types
assert_ne!(regular_error, admin_error);
assert_ne!(admin_error, unauthorized_error);
}
Comment on lines +216 to 223
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type rust -C3 '\b(admin_settle_action|admin_cancel_action|solver_has_write_permission|NotAuthorized)\b'

Repository: MostroP2P/mostro

Length of output: 6405


🏁 Script executed:

#!/bin/bash
# Get the full test module from admin_settle.rs
fd "admin_settle.rs" -type f | head -1 | xargs wc -l

Repository: MostroP2P/mostro

Length of output: 249


🏁 Script executed:

# Read the test module section from admin_settle.rs
fd "admin_settle.rs" -type f | head -1 | xargs tail -100

Repository: MostroP2P/mostro

Length of output: 230


🏁 Script executed:

# Also check admin_cancel.rs tests
fd "admin_cancel.rs" -type f | head -1 | xargs tail -100

Repository: MostroP2P/mostro

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Find and display the test module from admin_settle.rs
find . -name "admin_settle.rs" | head -1 | xargs tail -150

Repository: MostroP2P/mostro

Length of output: 5708


🏁 Script executed:

# Find and display the test module from admin_cancel.rs
find . -name "admin_cancel.rs" | head -1 | xargs tail -150

Repository: MostroP2P/mostro

Length of output: 5759


🏁 Script executed:

#!/bin/bash
# Check full length and tail of admin_cancel.rs to see if there's a test module
find . -name "admin_cancel.rs" | head -1 | xargs wc -l

Repository: MostroP2P/mostro

Length of output: 89


🏁 Script executed:

# Search for test functions that might exercise the handlers with different permissions
rg -n --type rust '(#\[test\]|fn test_.*admin_(settle|cancel))' src/app/

Repository: MostroP2P/mostro

Length of output: 1168


🏁 Script executed:

# Look for any integration tests or tests that call admin_settle_action or admin_cancel_action
rg -n --type rust 'admin_(settle|cancel)_action' src/ | grep -i test

Repository: MostroP2P/mostro

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check the end of admin_cancel.rs to confirm whether it has a test module
find . -name "admin_cancel.rs" | head -1 | xargs tail -50

Repository: MostroP2P/mostro

Length of output: 2086


🏁 Script executed:

# Search for any tests or fixtures that might use solver permissions
rg -n --type rust 'solver.*permission|read.*only.*solver|write.*permission' src/ -i

Repository: MostroP2P/mostro

Length of output: 679


🏁 Script executed:

#!/bin/bash
# Final confirmation: check if solver_has_write_permission is actually tested anywhere
rg -n --type rust 'solver_has_write_permission' src/ -A 5 | grep -A 5 'test\|#\[test\]'

Repository: MostroP2P/mostro

Length of output: 42


Add behavioral tests for the new solver permission gate.

The current test at lines 216-223 only verifies that CantDoReason::NotAuthorized exists as an enum variant via debug formatting. It does not exercise the authorization check in admin_settle_action or admin_cancel_action.

Since this PR introduces write-permission enforcement (lines 53-54 in admin_settle.rs and lines 76-77 in admin_cancel.rs), add at least one test that:

  • Calls admin_settle_action with a read-only solver and asserts the handler returns MostroCantDo(NotAuthorized)
  • Calls admin_settle_action with a read-write solver and asserts successful execution

Repeat the same for admin_cancel_action to confirm the authorization boundary works end-to-end.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/admin_settle.rs` around lines 216 - 223, Add behavioral tests that
exercise the solver permission gate by invoking admin_settle_action and
admin_cancel_action with two solver fixtures: one configured as read-only and
one with read-write permissions. For each action, call admin_settle_action and
admin_cancel_action using the read-only solver and assert the handler returns
MostroCantDo(CantDoReason::NotAuthorized); then call the same handlers with the
read-write solver and assert successful execution (no CantDo error). Use the
existing test helpers/fixtures to construct solvers with the appropriate
permission flags and reference the functions admin_settle_action,
admin_cancel_action, the error enum CantDoReason::NotAuthorized, and the wrapper
variant MostroCantDo when asserting results.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not implement the suggested admin_settle_action / admin_cancel_action success-path behavioral tests in this pass, because after verifying the current code they are not a direct bug fix and would require building non-trivial DB/LND fixtures to make the tests meaningful rather than synthetic. I’d prefer to handle that as a separate focused test-coverage improvement if needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grunch, understood — that's a reasonable call. Wiring up admin_settle_action / admin_cancel_action end-to-end in tests requires a live SQLite pool, mock LND connector, and valid Nostr key fixtures, which is a non-trivial undertaking that goes well beyond the scope of this change. The structural check you've added confirms the NotAuthorized variant is present and distinct, which keeps the CI signal intact.

Would you like me to open a follow-up GitHub issue to track adding proper behavioral coverage for the solver write-permission gate in admin_settle_action and admin_cancel_action? I can link back to this PR and the relevant lines as context.


🧠 Learnings used
Learnt from: arkanoider
Repo: MostroP2P/mostro PR: 530
File: src/app/order.rs:278-279
Timestamp: 2025-10-09T14:06:50.067Z
Learning: In the Mostro project, tests in files like `src/app/order.rs` may use structural checks to ensure functions like `order_action` do not panic during message construction, rather than validating specific error variants.

Learnt from: CR
Repo: MostroP2P/mostro PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T20:58:01.066Z
Learning: PRs must link the motivating issue, include `cargo test` output, and call out schema or config changes to ease verification

Learnt from: grunch
Repo: MostroP2P/mostro PR: 566
File: src/nip33.rs:1-3
Timestamp: 2026-01-16T12:05:35.401Z
Learning: Do not redefine NOSTR_REPLACEABLE_EVENT_KIND in files like src/config/constants.rs or import it explicitly. If mostro_core::prelude::* is imported, the constant is available automatically. Rely on the prelude wildcard import across Rust files; if a file compiles without a local definition, no import or definition is needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

gh issue create \
  --repo MostroP2P/mostro \
  --title "Add behavioral tests for solver write-permission gate in admin_settle_action and admin_cancel_action" \
  --body "## Context

This issue was raised during review of PR `#708` (Add read and read-write dispute solver permissions) in comment https://github.com/MostroP2P/mostro/pull/708#discussion_r3094389866, requested by `@grunch`.

## Problem

PR `#708` introduced write-permission enforcement in \`admin_settle_action\` (\`src/app/admin_settle.rs\`, lines 53–54) and \`admin_cancel_action\` (\`src/app/admin_cancel.rs\`, lines 76–77) via \`solver_has_write_permission\`. The only test currently covering this path is a structural check that verifies \`CantDoReason::NotAuthorized\` exists as an enum variant; it does **not** exercise the authorization boundary end-to-end.

## Goal

Add behavioral tests that:

1. Call \`admin_settle_action\` with a **read-only** solver fixture and assert the handler returns \`MostroCantDo(CantDoReason::NotAuthorized)\`.
2. Call \`admin_settle_action\` with a **read-write** solver fixture and assert successful execution (no \`CantDo\` error).
3. Repeat steps 1–2 for \`admin_cancel_action\`.

These tests require non-trivial fixtures (in-memory SQLite pool seeded with dispute + solver rows, mock LND connector, valid Nostr key pairs), which is why they were deferred from PR `#708`.

## Acceptance Criteria

- [ ] At least one test per action (\`admin_settle_action\`, \`admin_cancel_action\`) that covers the read-only rejection path.
- [ ] At least one test per action that covers the read-write success path (or reaches the next meaningful gate without a permission error).
- [ ] Tests use the existing test helpers/fixtures in the Mostro codebase where possible.
- [ ] \`cargo test\` passes with all new tests included.

## References

- PR `#708`: https://github.com/MostroP2P/mostro/pull/708
- Closes issue `#707` (parent feature)
- Relevant files: \`src/app/admin_settle.rs\`, \`src/app/admin_cancel.rs\`, \`src/db.rs\` (\`solver_has_write_permission\`)
" \
  --assignee grunch

Length of output: 46


@grunch, done! Follow-up issue created and assigned to you: #709

It captures the full context — the deferred behavioral tests for the read-only rejection path and read-write success path in both admin_settle_action and admin_cancel_action, along with links back to this PR and the relevant source files.


🧠 Learnings used
Learnt from: arkanoider
Repo: MostroP2P/mostro PR: 530
File: src/app/order.rs:278-279
Timestamp: 2025-10-09T14:06:50.067Z
Learning: In the Mostro project, tests in files like `src/app/order.rs` may use structural checks to ensure functions like `order_action` do not panic during message construction, rather than validating specific error variants.

Learnt from: CR
Repo: MostroP2P/mostro PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T20:58:01.066Z
Learning: PRs must link the motivating issue, include `cargo test` output, and call out schema or config changes to ease verification

Learnt from: grunch
Repo: MostroP2P/mostro PR: 566
File: src/nip33.rs:1-3
Timestamp: 2026-01-16T12:05:35.401Z
Learning: Do not redefine NOSTR_REPLACEABLE_EVENT_KIND in files like src/config/constants.rs or import it explicitly. If mostro_core::prelude::* is imported, the constant is available automatically. Rely on the prelude wildcard import across Rust files; if a file compiles without a local definition, no import or definition is needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I implemented the behavior change so a read-write solver can take over a dispute that is already in-progress when the current assignee is a read-only solver, while keeping the existing restrictions for other reassignment cases.

Fix commit: 08fe6aa (Allow write solvers to take over read-only disputes).

Note: I pushed it to branch solver-permissions while preparing the update locally, so it still needs to be moved onto this PR branch before the thread can be marked fully resolved here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 08fe6aa and now pushed to the actual PR branch (feat/solver-read-write-permissions). This allows a read-write solver to take over a dispute already taken by a read-only solver, while preserving the existing restrictions for other reassignment cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

}
Loading