Files
simplexmq/tests/manual/README.md
T
sh 8833e5c1b5 xftp-server: support postgresql backend (#1755)
* xftp: add PostgreSQL backend design spec

* update doc

* adjust styling

* add implementation plan

* refactor: move usedStorage from FileStore to XFTPEnv

* refactor: add getUsedStorage, getFileCount, expiredFiles store functions

* refactor: change file store operations from STM to IO

* refactor: extract FileStoreClass typeclass, move STM impl to Store.STM

* refactor: make XFTPEnv and server polymorphic over FileStoreClass

* feat: add PostgreSQL store skeleton with schema migration

* feat: implement PostgresFileStore operations

* feat: add PostgreSQL INI config, store dispatch, startup validation

* feat: add database import/export CLI commands

* test: add PostgreSQL backend tests

* fix: map ForeignKeyViolation to AUTH in addRecipient

When a file is concurrently deleted while addRecipient runs, the FK
constraint on recipients.sender_id raises ForeignKeyViolation. Previously
this propagated as INTERNAL; now it returns AUTH (file not found).

* fix: only decrement usedStorage for uploaded files on expiration

expireServerFiles unconditionally subtracted file_size from usedStorage
for every expired file, including files that were never uploaded (no
file_path). Since reserve only increments usedStorage during upload,
expiring never-uploaded files caused usedStorage to drift negative.

* fix: handle setFilePath error in receiveServerFile

setFilePath result was discarded with void. If it failed (file deleted
concurrently, or double-upload where file_path IS NULL guard rejected
the second write), the server still reported FROk, incremented stats,
and left usedStorage permanently inflated. Now the error is checked:
on failure, reserved storage is released and AUTH is returned.

* fix: escape double quotes in COPY CSV status field

The status field (e.g. "blocked,reason=spam,notice={...}") is quoted in
CSV for COPY protocol, but embedded double quotes from BlockingInfo
notice (JSON) were not escaped. This could break CSV parsing during
import. Now double quotes are escaped as "" per CSV spec.

* fix: reject upload to blocked file in Postgres setFilePath

In Postgres mode, getFile returns a snapshot TVar for fileStatus. If a
file is blocked between getFile and setFilePath, the stale status check
passes but the upload should be rejected. Added status = 'active' to
the UPDATE WHERE clause so blocked files cannot receive uploads.

* fix: add CHECK constraint on file_size > 0

Prevents negative or zero file_size values at the database level.
Without this, corrupted data from import or direct DB access could
cause incorrect storage accounting (getUsedStorage sums file_size,
and expiredFiles casts to Word32 which wraps negative values).

* fix: check for existing data before database import

importFileStore now checks if the target database already contains
files and aborts with an error. Previously, importing into a non-empty
database would fail mid-COPY on duplicate primary keys, leaving the
database in a partially imported state.

* fix: clean up disk file when setFilePath fails in receiveServerFile

When setFilePath fails (file deleted or blocked concurrently, or
duplicate upload), the uploaded file was left orphaned on disk with
no DB record pointing to it. Now the file is removed on failure,
matching the cleanup in the receiveChunk error path.

* fix: check storeAction result in deleteOrBlockServerFile_

The store action result (deleteFile/blockFile) was discarded with void.
If the DB row was already deleted by a concurrent operation, the
function still decremented usedStorage, causing drift. Now the error
propagates via ExceptT, skipping the usedStorage adjustment.

* fix: check deleteFile result in expireServerFiles

deleteFile result was discarded with void. If a concurrent delete
already removed the file, deleteFile returned AUTH but usedStorage
was still decremented — causing double-decrement drift. Now the
usedStorage adjustment and filesExpired stat only run on success.

* refactor: merge STM store into Store.hs, parameterize server tests

- Move STMFileStore and its FileStoreClass instance from Store/STM.hs
  back into Store.hs — the separate file was unnecessary indirection
  for the always-present default implementation.

- Parameterize xftpFileTests over store backend using HSpec SpecWith
  pattern (following SMP's serverTests approach). The same 11 tests
  now run against both memory and PostgreSQL backends via a bracket
  parameter, eliminating all *Pg test duplicates.

- Extract shared run* functions (runTestFileChunkDeliveryAddRecipients,
  runTestWrongChunkSize, runTestFileChunkExpiration, runTestFileStorageQuota)
  from inlined test bodies.

* refactor: clean up per good-code review

- Remove internal helpers from Postgres.hs export list (withDB, withDB',
  handleDuplicate, assertUpdated, withLog are not imported externally)
- Replace local isNothing_ with Data.Maybe.isNothing in Env.hs
- Consolidate duplicate/unused imports in XFTPStoreTests.hs
- Add file_path IS NULL and status guards to STM setFilePath, matching
  the Postgres implementation semantics

* test: parameterize XFTP server, agent and CLI tests over store backend

- xftpTest/xftpTest2/xftpTest4/xftpTestN now take XFTPTestBracket as
  first argument, enabling the same test to run against both memory
  and PostgreSQL backends.

- xftpFileTests (server tests), xftpAgentFileTests (agent tests), and
  xftpCLIFileTests (CLI tests) are SpecWith-parameterized suites that
  receive the bracket from HSpec's before combinator.

- Test.hs runs each parameterized suite twice: once with
  xftpMemoryBracket, once with xftpPostgresBracket (CPP-guarded).

- STM-specific tests (store log restore/replay) stay in memory-only
  xftpAgentTests. SNI/CORS tests stay in memory-only xftpServerTests.

* refactor: remove dead test wrappers after parameterization

Remove old non-parameterized test wrapper functions that were
superseded by the store-backend-parameterized test suites.
All test bodies (run* and _ functions) are preserved and called
from the parameterized specs. Clean up unused imports.

* feat: add manual tests and guide

* refactor: merge file_size CHECK into initial migration

* refactor: extract rowToFileRec shared by getFile sender/recipient paths

* refactor: parameterize XFTPServerConfig over store type

Embed XFTPStoreConfig s as serverStoreCfg field, matching SMP's
ServerConfig. runXFTPServer and newXFTPServerEnv now take a single
XFTPServerConfig s. Restore verifyCmd local helper structure.

* refactor: minimize diff in tests

Restore xftpServerTests and xftpAgentTests bodies to match master
byte-for-byte (only type signatures change for XFTPTestBracket
parameterization); inline the runTestXXX helpers that were split
on this branch.

* refactor: restore getFile position to match master

* refactor: rename withSTMFile back to withFile

* refactor: close store log inside closeFileStore for STM backend

Move STM store log close responsibility into closeFileStore to
match PostgresFileStore, removing the asymmetry where only PG's
close was self-contained.

STMFileStore holds the log in a TVar populated by newXFTPServerEnv
after readWriteFileStore; stopServer no longer needs the explicit
withFileLog closeStoreLog call. Writes still go through XFTPEnv.storeLog
via withFileLog (unchanged).

* refactor: rename XFTPTestBracket to XFTPTestServer

* fix: move file_size check from PG schema to store log import

* refactor: use SQL-standard type names in XFTP schema

* perf: batch expired file deletions with deleteFiles

* refactor: stream export instead of loading recipients into memory

* refactor: parameterize XFTP store with FSType singleton dispatch

* refactor: minimize diff per review feedback

* refactor: use types over strings, deduplicate parser

* refactor: always parse database store type, fail at startup

* fix compilation without postgresql

* refactor: always parse database store type, fail at startup
2026-04-16 09:06:04 +01:00

5.3 KiB

XFTP Server Manual Test Suite

Automated integration tests for the XFTP server covering memory and PostgreSQL backends, migration, persistence, blocking, and edge cases.

  • xftp-test.py — automated test script (143 checks)
  • xftp-server-testing.md — manual step-by-step guide covering the same scenarios

Prerequisites

  • Linux (tested)
  • Python 3
  • Haskell toolchain (cabal, ghc)
  • PostgreSQL 16+ (postgresql-16 package or equivalent)

Setup

1. Build the XFTP binaries

cabal build -fserver_postgres exe:xftp-server exe:xftp

2. Set up a local PostgreSQL instance

The test script connects to PostgreSQL via PGHOST (Unix socket path). Set up a local instance that you own (no root required):

# Pick a data directory and socket directory
export PGDATA=/tmp/pgdata
export PGHOST=/tmp/pgsocket

# Clean up any previous instance
rm -rf $PGDATA $PGHOST
mkdir -p $PGDATA $PGHOST

# Initialize the cluster
/usr/lib/postgresql/16/bin/initdb -D $PGDATA --auth=trust --no-locale --encoding=UTF8

# Configure to listen on our socket directory and localhost TCP
echo "unix_socket_directories = '$PGHOST'" >> $PGDATA/postgresql.conf
echo "listen_addresses = '127.0.0.1'" >> $PGDATA/postgresql.conf

# Start the server
/usr/lib/postgresql/16/bin/pg_ctl -D $PGDATA -l /tmp/pg.log start

# Verify it's running
pg_isready -h $PGHOST
# Expected: /tmp/pgsocket:5432 - accepting connections

3. Create the required PostgreSQL roles

The test script expects three roles to exist:

  • postgres — admin role used by the test bracket to create/drop databases
  • xftp — test user for the XFTP server database
# Create the postgres admin role (if initdb created the cluster as your user)
psql -h $PGHOST -d postgres -c "CREATE USER postgres WITH SUPERUSER;"

# Create the xftp test user
psql -h $PGHOST -U postgres -d postgres -c "CREATE USER xftp WITH SUPERUSER;"

Verify both roles exist:

psql -h $PGHOST -U postgres -d postgres -c "\du"

Run the test suite

PGHOST=/tmp/pgsocket python3 tests/manual/xftp-test.py

Expected output (abbreviated):

XFTP server: /project/git/simplexmq-4/dist-newstyle/.../xftp-server
XFTP client: /project/git/simplexmq-4/dist-newstyle/.../xftp
Test dir:    /project/git/simplexmq-4/xftp-test
PGHOST:      /tmp/pgsocket

=== 1. Basic send/receive (memory) ===
  [PASS] 1.1 rcv1.xftp created
  ...
=== 12. Recipient cascade and storage accounting ===
  ...
  [PASS] 12.2e DB files after delete (0)

==========================================
Results: 143 passed, 0 failed
==========================================

Total runtime: ~3 minutes. Exit code 0 on success, 1 on any failure.

What the suite tests

# Section Checks Scope
1 Basic memory 9 Send/recv/delete on STM backend
2 Basic PostgreSQL 7 Send/recv/delete on PG backend, DB row verification
3 Migration memory → PG 12 Send on memory, partial recv, import, recv remaining
4 Migration PG → memory 5 Export, switch to memory, delete exported files
4b Send PG, recv memory 7 Reverse direction — send on PG, export, recv on memory
5 Restart persistence 6 memory+log / memory no log / PostgreSQL
6 Config edge cases 15 store log conflicts, missing schema, dual-write, import/export guards
7 File blocking 13 Control port block, block state survives migration both directions
8 Migration edge cases 23 Acked recipients preserved, deleted files absent, 20MB multi-chunk, double round-trip
9 Auth & access control 9 allowNewFiles, basic auth (none/wrong/correct/server-no-auth), quota
10 Control port ops 8 No auth, wrong auth, stats, delete, invalid block
11 Blocked sender delete 3 Sender can't delete blocked file
12 Cascade & storage 8 Recipient cascade, disk/DB accounting

Troubleshooting

Server binary not found

Binary not found: .../xftp-server
Run: cabal build -fserver_postgres exe:xftp-server

Run the cabal build command from step 1.

Cannot connect to PostgreSQL

Cannot connect to PostgreSQL as postgres. Is it running?

Check:

  1. pg_isready -h $PGHOST returns "accepting connections"
  2. PGHOST environment variable is exported in the shell running the test
  3. The postgres role exists: psql -h $PGHOST -U postgres -d postgres -c "SELECT 1;"

PostgreSQL user 'xftp' does not exist

PostgreSQL user 'xftp' does not exist.
Run: psql -U postgres -c "CREATE USER xftp WITH SUPERUSER;"

Run the create-user command from step 3.

Port 7921 or 15230 already in use

The test uses port 7921 for XFTP and 15230 for the control port. If these are occupied, stop whatever is using them or edit PORT / CONTROL_PORT constants at the top of xftp-test.py.

Server fails to start mid-test

Check xftp-test/server.log in the project directory for the server's stdout/stderr. The test framework prints the last 5 lines of the log on startup failure.

Stopping the test PostgreSQL instance

/usr/lib/postgresql/16/bin/pg_ctl -D /tmp/pgdata stop

Cleanup

The test script cleans up its own test directory (./xftp-test/) and drops the test database (xftp_server_store) on completion. To also remove the PostgreSQL instance:

/usr/lib/postgresql/16/bin/pg_ctl -D /tmp/pgdata stop
rm -rf /tmp/pgdata /tmp/pgsocket /tmp/pg.log