5.7 KiB
Protocol E2E Tests
FTPS, WebDAV, and SFTP protocol end-to-end tests for RustFS.
Prerequisites
No external SSH tooling is required. The test framework generates ed25519 host keys in-process via russh::keys under the per-test temp directory before each SFTP server spawn, and russh-sftp drives the protocol from the test process directly.
Running Tests
Use the canonical CI-equivalent protocol command in the parent
e2e_test README.
For targeted debugging of the core suite only:
python3 scripts/e2e_binary.py build --features ftps,webdav,sftp
python3 scripts/e2e_binary.py run --features ftps,webdav,sftp -- cargo test --package e2e_test test_protocol_core_suite -- --test-threads=1 --nocapture
This targeted command does not cover the full e2e-protocols profile.
e2e_binary.py supplies RUSTFS_BUILD_FEATURES from the verified server's resolved Cargo features. The protocol runner schedules only entries present in that feature list; helpers check that their required features are available without rebuilding the server.
--test-threads=1 is required because every entry spawns a rustfs server
on fixed bind ports.
Test Coverage
FTPS Tests
- mkdir bucket
- cd to bucket
- put file
- ls list objects
- cd . (stay in current directory)
- cd / (return to root)
- cd nonexistent bucket (should fail)
- delete object
- cdup
- rmdir delete bucket
WebDAV Tests
- PROPFIND at root (list buckets)
- MKCOL (create bucket)
- PUT (upload file)
- GET (download file)
- PROPFIND on bucket (list objects)
- DELETE file
- MOVE file (rename object)
- DELETE bucket
- Authentication failure test
SFTP Tests
The SFTP suite lives in three entries plus a standalone idle-timeout case.
Every assertion runs against a freshly spawned rustfs binary with
RUSTFS_SFTP_ENABLE=true; the test framework also pins
RUSTFS_SFTP_PART_SIZE=5242880 so the multipart boundary is deterministic.
sftp_core (test_sftp_core_operations)
Bind ports 9022 (SFTP) and 9200 (S3). 22 in-suite assertions covering the core protocol surface plus cross-protocol consistency:
- Subsystem canary: SFTPv3 version exchange completes after password auth
- Bucket lifecycle: mkdir, root listing, rmdir, post-delete listing
- Small-file round-trip with SHA256 compare
- Stat on a file (size + file type) and on a bucket (directory)
- SETSTAT on a path returns ok
- Rename within bucket, listing reflects the rename
- Multipart-sized round-trip (just over 2 × part_size) with SHA256 compare
- Negative cases: symlink rejected, open of nonexistent file rejected, read_dir of nonexistent bucket rejected, path traversal rejected
- Spec-letter assertions: APPEND open returns an error, CREATE+EXCLUDE on an existing path returns an error, bad-password authentication is rejected
- Cross-protocol via aws-sdk-s3: SFTP write then S3 read with SHA256 match, S3 write then SFTP read with SHA256 match
- Cross-API directory visibility: SFTP-created sub-directory visible via S3
ListObjectsV2, S3-created
__XLDIR__marker visible via SFTP readdir as a directory entry
sftp_compliance (test_sftp_compliance_suite)
Bind ports 9024 (SFTP) and 9300 (S3). 14 compliance regression cases against one shared server spawn. Each case carries a stable CMPTST-NN identifier:
- CMPTST-01: medium-binary upload then download with SHA256 compare (single-shot PutObject path below the multipart boundary)
- CMPTST-02: zero-byte upload, download, and stat-size match
- CMPTST-03: rm against a bucket path is rejected; the bucket is preserved
- CMPTST-04: rmdir against a non-empty bucket is rejected; the contained object survives
- CMPTST-05: rmdir against a non-empty sub-directory is rejected; the inner object survives
- CMPTST-06: open with a path-traversal pattern cannot leak a host file via SFTP read
- CMPTST-07: read_dir of
/..either errors or returns a listing that contains no host system entries - CMPTST-08: rename across buckets preserves payload and removes the source object
- CMPTST-09: paths with embedded spaces round-trip through the russh-sftp client
- CMPTST-10: read_link is rejected (S3 storage has no symlinks)
- CMPTST-11: SETSTAT on a path and FSETSTAT on a separate open handle both return ok (rsync, WinSCP transfer-success contract)
- CMPTST-12: rename to the same path is a no-op; the file persists with the original payload
- CMPTST-13: implicit-directory round-trip; uploading to a nested key creates the parent directory implicitly and three listing forms surface the inner file
- CMPTST-14: OPEN, WRITE, FSETSTAT, CLOSE on the same write handle all return ok (WinSCP wire shape)
sftp_compliance_readonly (test_sftp_compliance_readonly)
Bind ports 9025 (SFTP) and 9301 (S3). Spawns a second rustfs binary with
RUSTFS_SFTP_READ_ONLY=true; the S3 endpoint stays writable so the suite
can seed a bucket and a fixture object via aws-sdk-s3 before opening the
SFTP session. 7 compliance cases:
- CMPTST-15: put through SFTP is rejected
- CMPTST-16: rm through SFTP is rejected
- CMPTST-17: mkdir through SFTP is rejected
- CMPTST-18: rmdir through SFTP is rejected
- CMPTST-19: rename through SFTP is rejected
- CMPTST-20: ls through SFTP is allowed and lists the seeded bucket
- CMPTST-21: get through SFTP is allowed and returns the seeded payload byte-for-byte
The full case index lives at the top of sftp_compliance.rs; each helper's
log lines name its CMPTST-NN code so a failure in CI points at one named
property without consulting any external doc.
sftp_idle_timeout (test_sftp_idle_timeout_disconnects)
Bind ports 9023 (SFTP) and 9100 (S3). Spawns rustfs with
RUSTFS_SFTP_IDLE_TIMEOUT=5, sleeps 10 s past the timeout, then issues an
SFTP request and asserts the server has closed the session.