Commit Graph
10 Commits
Author SHA1 Message Date
michel 54d7d8f8a3 test(shares): two real clients search and download a shared file e2e
End-to-end proof of the whole sharing stack with no mock peer: a sharer indexes
a temp file, the leecher's search is distributed by soulfind, the sharer replies
with a FileSearchResponse, and the leecher downloads the real bytes that the
sharer serves over a peer-to-peer transfer. Asserts the search result and that
the downloaded bytes match the source. Server-gated (SOULSEEK_E2E_REQUIRED).
2026-07-19 18:48:33 +02:00
michel 51c5397d8f style: use Duration::from_secs for whole-second sleeps in e2e
Satisfies clippy::duration_suboptimal_units (a stable-toolchain lint the
local nightly did not surface), unblocking the CI Lint job.
2026-07-19 17:35:24 +02:00
michel b82b03a143 feat: firewalled peer connections via server-brokered connect
Implement the FIREWALLED connection mode: when a direct outbound connection to
a peer fails (peer unreachable), fall back to asking the server to broker it.

- Add MessageFactory::build_connect_to_peer (server code 18).
- A pre-established outbound failure now emits ClientOperation::PeerConnectFailed
  instead of a normal disconnect; the client registers a correlation token and
  sends ConnectToPeer so the peer connects back.
- The listener accepts the peer's PierceFirewall (peer code 0) connect-back,
  matches the pending token, registers it as the P control connection, and
  fires PeerConnected to flush the queued download.

Fix a latent aliasing bug this surfaced: a peer actor could emit two terminal
notifications (a connect-failure, then a disconnect when later replaced and
stopped), and the second removed the *newer* actor registered under the same
username, killing the brokered connection mid-transfer. An actor now reports a
terminal outcome at most once (disconnect_reported guard).

Verified end to end against soulfind: the mock advertises an unlistened port so
the direct connect is refused, the server brokers the connection, the mock
pierces back, and the file transfers — bytes match. A companion test
(soulfind_brokers_connect_to_peer_between_users) pins the server behaviour the
mode relies on.
2026-07-19 17:26:52 +02:00
michel e4093b1ea8 feat: direct peer connections — download() connects to unconnected peers
Implement the DIRECT connection-initiation mode so a download can target a
peer the client is not already connected to:

- Fix the outbound peer handshake. on_connection_established sent
  build_watch_user() (a server WatchUser message, wrongly framed with
  get_data()) for outbound connections; it now sends a proper length-prefixed
  PeerInit (peer code 1) with our own username. own_username is plumbed in
  via PeerRegistry -> PeerActor; inbound peers (outbound=false) are unchanged.
- download() now auto-connects: if there is no control connection to the peer
  it sends GetPeerAddress and defers the QueueUpload until the outbound stream
  handshakes, signalled by a new ClientOperation::PeerConnected. A never-
  connected client still resolves the download to Failed (unit test).
- Add Client::connect_peer(username) to open/pre-warm a control connection.

Verified end to end against soulfind: a_file_downloads_from_a_peer_via_direct_connection
logs a mock peer into the server, advertises its port via SetWaitPort, and the
client resolves the address, dials it, and downloads the file — bytes match.
The F-serve leg is shared with the passive-download mock.
2026-07-19 17:12:17 +02:00
michel 06cd638736 fix: drive inbound peer connections non-blocking so downloads don't stall
A peer actor multiplexes socket reads with its mailbox on one thread: tick()
reads the socket while outgoing messages (e.g. a queued QueueUpload that starts
a download) are delivered through the mailbox between ticks. Inbound
connections accepted by the listener were left in blocking mode, so once such a
peer went idle tick() parked inside read() and never drained the mailbox — a
download queued against that peer never sent its QueueUpload and hung. Outbound
connections already set non-blocking; do the same for inbound P connections.

Covered by a new server-optional e2e test that downloads a file end to end from
an in-process mock peer over real P (control) and F (transfer) connections,
asserting the bytes written match the source. The mock speaks the library's own
public Message wire format, so no dependencies are added.
2026-07-19 13:58:05 +02:00
michel a7372cc88e feat(lib): send and receive private messages
Wire private messaging through the client:

- MessageFactory::build_message_user (code 22) and build_message_acked
  (code 23);
- the incoming MessageUser handler now acknowledges freshly delivered
  messages and forwards them to the client instead of only logging them;
- ClientContext buffers received UserMessages;
- Client::send_private_message and Client::take_private_messages expose the
  feature, and UserMessage (with accessors) is re-exported from the crate.

Verified end to end against soulfind: a_private_message_is_delivered_between_users
logs in two clients and asserts one receives the other's message.
2026-07-19 13:31:30 +02:00
michel f61e02e05c test: broaden soulfind e2e coverage of implemented features
Cover implemented, server-verifiable paths that were previously untested:

- assert an issued search is registered in client state, not just that
  results are empty;
- a_search_is_forwarded_to_a_connected_peer: a second client exercises the
  incoming FileSearch handler (the server distributes searches to it), and we
  prove that session survives by round-tripping its own search afterwards;
- login_succeeds_with_listener_enabled: drives the post-login handshake with
  the peer listener on (SetWaitPort), which every other e2e test skips.

Peer downloads / file sharing remain out of scope here: they need a second
peer that actually serves files, which the client does not implement yet.
2026-07-19 13:27:30 +02:00
michel a127dd97bf ci: run soulfind e2e tests on GitHub Actions
Extend the existing CI workflow with an ubuntu-only "End-to-end (soulfind)"
job that builds soulfind from source (LDC + `dub build :server`), points
SOULFIND_BIN at it, and runs the e2e suite with SOULSEEK_E2E_REQUIRED=1.
Also widen the clippy job to the whole workspace.

The e2e harness gains SOULSEEK_E2E_REQUIRED: when set, a missing/unstartable
server is a hard failure instead of a silent skip, so CI genuinely exercises
the client end-to-end. Add e2e coverage for two existing behaviours a single
server can verify: wrong-password rejection and concurrent logins.
2026-07-19 13:19:46 +02:00
michel 1328736550 feat: configure download & upload (shared) directories
Add a --shared-dir / SOULSEEK_SHARED_DIR option and a SOULSEEK_DOWNLOAD_DIR
env for the download directory. The shared directory is resolved and
validated up front (tilde-expanded, must exist and be a directory); a
misconfigured value is a warning, not a fatal error.

The library gains an optional shared_directory on ClientSettings/Client
with a getter, keeping soulseek-rs-lib dependency-free. Resolution lives
in a new soulseek-rs/src/directories.rs with unit tests.
2026-07-19 13:12:44 +02:00
michel 9035ecebcb test: add server-optional e2e integration harness against soulfind
Adds soulseek-rs-lib/tests/e2e.rs, which drives the client's public API
(connect, login, search) against a real Soulseek server. The harness is
server-optional so cargo test stays green everywhere: it uses
SOULSEEK_TEST_SERVER if set, otherwise spawns a soulfind binary (SOULFIND_BIN
or a soulfind/bin/soulfind checkout in a parent dir) on an ephemeral port with
a throwaway database, and skips with a notice when no server is available.

Uses only std and the library's public API — no new dependencies. Documents how
to run it (including the macOS soulfind SQLite workaround) in the README.
2026-07-19 00:13:35 +02:00