mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-04-25 12:02:18 +00:00
* 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
191 lines
8.6 KiB
Haskell
191 lines
8.6 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
|
|
import AgentTests (agentCoreTests, agentTests)
|
|
import CLITests
|
|
import Control.Concurrent (threadDelay)
|
|
import qualified Control.Exception as E
|
|
import Control.Logger.Simple
|
|
import CoreTests.BatchingTests
|
|
import CoreTests.CryptoFileTests
|
|
import CoreTests.CryptoTests
|
|
import CoreTests.EncodingTests
|
|
import CoreTests.MsgStoreTests
|
|
import CoreTests.RetryIntervalTests
|
|
import CoreTests.SOCKSSettings
|
|
import CoreTests.StoreLogTests
|
|
import CoreTests.TSessionSubs
|
|
import CoreTests.UtilTests
|
|
import CoreTests.VersionRangeTests
|
|
import FileDescriptionTests (fileDescriptionTests)
|
|
import GHC.IO.Exception (IOException (..))
|
|
import qualified GHC.IO.Exception as IOException
|
|
import RemoteControl (remoteControlTests)
|
|
import SMPProxyTests (smpProxyTests)
|
|
import ServerTests
|
|
import Simplex.Messaging.Server.Env.STM (AStoreType (..))
|
|
import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..))
|
|
import Simplex.Messaging.Transport (TLS, Transport (..))
|
|
-- import Simplex.Messaging.Transport.WebSockets (WS)
|
|
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
|
|
import System.Environment (setEnv)
|
|
import Test.Hspec hiding (fit, it)
|
|
import Util
|
|
import XFTPAgent
|
|
import XFTPCLI (xftpCLIFileTests)
|
|
import Simplex.FileTransfer.Server.Env (AFStoreType (..))
|
|
import Simplex.FileTransfer.Server.Store (SFSType (..))
|
|
import XFTPServerTests (xftpServerTests)
|
|
import WebTests (webTests)
|
|
import XFTPWebTests (xftpWebTests)
|
|
|
|
#if defined(dbPostgres)
|
|
import Fixtures
|
|
import Simplex.Messaging.Agent.Store.Postgres.Util (dropAllSchemasExceptSystem)
|
|
#else
|
|
import AgentTests.SchemaDump (schemaDumpTest)
|
|
#endif
|
|
|
|
#if defined(dbServerPostgres)
|
|
import CoreTests.XFTPStoreTests (xftpStoreTests, xftpMigrationTests)
|
|
import NtfServerTests (ntfServerTests)
|
|
import NtfClient (ntfTestServerDBConnectInfo, ntfTestStoreDBOpts)
|
|
import PostgresSchemaDump (postgresSchemaDumpTest)
|
|
import SMPClient (testServerDBConnectInfo, testStoreDBOpts)
|
|
import Simplex.Messaging.Notifications.Server.Store.Migrations (ntfServerMigrations)
|
|
import Simplex.Messaging.Server.QueueStore.Postgres.Migrations (serverMigrations)
|
|
import XFTPClient (testXFTPDBConnectInfo)
|
|
#endif
|
|
|
|
#if defined(dbPostgres) || defined(dbServerPostgres)
|
|
import SMPClient (postgressBracket)
|
|
#endif
|
|
|
|
logCfg :: LogConfig
|
|
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
|
|
|
main :: IO ()
|
|
main = do
|
|
setLogLevel testLogLevel
|
|
withGlobalLogging logCfg $ do
|
|
setEnv "APNS_KEY_ID" "H82WD9K9AQ"
|
|
setEnv "APNS_KEY_FILE" "./tests/fixtures/AuthKey_H82WD9K9AQ.p8"
|
|
hspec
|
|
#if defined(dbPostgres)
|
|
. aroundAll_ (postgressBracket testDBConnectInfo)
|
|
#endif
|
|
. before_ (createDirectoryIfMissing False "tests/tmp")
|
|
. after_ (eventuallyRemove "tests/tmp" 3)
|
|
$ do
|
|
-- TODO [postgres] schema dump for postgres
|
|
#if !defined(dbPostgres)
|
|
describe "Agent SQLite schema dump" schemaDumpTest
|
|
#endif
|
|
describe "Core tests" $ do
|
|
describe "Batching tests" batchingTests
|
|
describe "Encoding tests" encodingTests
|
|
describe "Version range" versionRangeTests
|
|
describe "Encryption tests" cryptoTests
|
|
describe "Encrypted files tests" cryptoFileTests
|
|
describe "Message store tests" msgStoreTests
|
|
describe "Retry interval tests" retryIntervalTests
|
|
describe "SOCKS settings tests" socksSettingsTests
|
|
#if defined(dbServerPostgres)
|
|
around_ (postgressBracket testServerDBConnectInfo) $
|
|
describe "Store log tests" storeLogTests
|
|
#else
|
|
describe "Store log tests" storeLogTests
|
|
#endif
|
|
describe "TSessionSubs tests" tSessionSubsTests
|
|
describe "Util tests" utilTests
|
|
describe "Agent core tests" agentCoreTests
|
|
#if defined(dbServerPostgres)
|
|
around_ (postgressBracket testServerDBConnectInfo) $
|
|
describe "SMP server schema dump" $
|
|
postgresSchemaDumpTest
|
|
serverMigrations
|
|
[ "20250320_short_links" -- snd_secure moves to the bottom on down migration
|
|
] -- skipComparisonForDownMigrations
|
|
testStoreDBOpts
|
|
"src/Simplex/Messaging/Server/QueueStore/Postgres/server_schema.sql"
|
|
around_ (postgressBracket testServerDBConnectInfo) $ do
|
|
-- xdescribe "SMP server via TLS, postgres+jornal message store" $
|
|
-- before (pure (transport @TLS, ASType SQSPostgres SMSJournal)) serverTests
|
|
describe "SMP server via TLS, postgres-only message store" $
|
|
before (pure (transport @TLS, ASType SQSPostgres SMSPostgres)) serverTests
|
|
#endif
|
|
describe "SMP server via TLS, jornal message store" $ do
|
|
describe "SMP syntax" $ serverSyntaxTests (transport @TLS)
|
|
before (pure (transport @TLS, ASType SQSMemory SMSJournal)) serverTests
|
|
describe "SMP server via TLS, memory message store" $
|
|
before (pure (transport @TLS, ASType SQSMemory SMSMemory)) serverTests
|
|
-- xdescribe "SMP server via WebSockets" $ do
|
|
-- describe "SMP syntax" $ serverSyntaxTests (transport @WS)
|
|
-- before (pure (transport @WS, ASType SQSMemory SMSJournal)) serverTests
|
|
#if defined(dbServerPostgres)
|
|
around_ (postgressBracket ntfTestServerDBConnectInfo) $
|
|
describe "Ntf server schema dump" $
|
|
postgresSchemaDumpTest
|
|
ntfServerMigrations
|
|
[] -- skipComparisonForDownMigrations
|
|
ntfTestStoreDBOpts
|
|
"src/Simplex/Messaging/Notifications/Server/Store/ntf_server_schema.sql"
|
|
around_ (postgressBracket ntfTestServerDBConnectInfo) $ do
|
|
describe "Notifications server (SMP server: memory store)" $
|
|
ntfServerTests (transport @TLS, ASType SQSMemory SMSMemory)
|
|
around_ (postgressBracket testServerDBConnectInfo) $ do
|
|
-- xdescribe "Notifications server (SMP server: postgres+jornal store)" $
|
|
-- ntfServerTests (transport @TLS, ASType SQSPostgres SMSJournal)
|
|
describe "Notifications server (SMP server: postgres-only store)" $
|
|
ntfServerTests (transport @TLS, ASType SQSPostgres SMSPostgres)
|
|
around_ (postgressBracket testServerDBConnectInfo) $ do
|
|
-- xdescribe "SMP client agent, postgres+jornal message store" $ agentTests (transport @TLS, ASType SQSPostgres SMSJournal)
|
|
describe "SMP client agent, server postgres-only message store" $ agentTests (transport @TLS, ASType SQSPostgres SMSPostgres)
|
|
-- xdescribe "SMP proxy, postgres+jornal message store" $
|
|
-- before (pure $ ASType SQSPostgres SMSJournal) smpProxyTests
|
|
describe "SMP proxy, postgres-only message store" $
|
|
before (pure $ ASType SQSPostgres SMSPostgres) smpProxyTests
|
|
#endif
|
|
-- xdescribe "SMP client agent, server jornal message store" $ agentTests (transport @TLS, ASType SQSMemory SMSJournal)
|
|
describe "SMP client agent, server memory message store" $ agentTests (transport @TLS, ASType SQSMemory SMSMemory)
|
|
describe "SMP proxy, jornal message store" $
|
|
before (pure $ ASType SQSMemory SMSJournal) smpProxyTests
|
|
describe "XFTP" $ do
|
|
describe "XFTP server" $
|
|
before (pure $ AFSType SFSMemory) xftpServerTests
|
|
describe "XFTP file description" fileDescriptionTests
|
|
describe "XFTP CLI (memory)" $
|
|
before (pure $ AFSType SFSMemory) xftpCLIFileTests
|
|
describe "XFTP agent" $
|
|
before (pure $ AFSType SFSMemory) xftpAgentTests
|
|
#if defined(dbServerPostgres)
|
|
around_ (postgressBracket testXFTPDBConnectInfo) $ do
|
|
describe "XFTP Postgres store operations" xftpStoreTests
|
|
describe "XFTP migration round-trip" xftpMigrationTests
|
|
describe "XFTP server (PostgreSQL)" $
|
|
before (pure $ AFSType SFSPostgres) xftpServerTests
|
|
describe "XFTP agent (PostgreSQL)" $
|
|
before (pure $ AFSType SFSPostgres) xftpAgentTests
|
|
describe "XFTP CLI (PostgreSQL)" $
|
|
before (pure $ AFSType SFSPostgres) xftpCLIFileTests
|
|
#endif
|
|
#if defined(dbPostgres)
|
|
describe "XFTP Web Client" $ xftpWebTests (dropAllSchemasExceptSystem testDBConnectInfo)
|
|
#else
|
|
describe "XFTP Web Client" $ xftpWebTests (pure ())
|
|
#endif
|
|
describe "XRCP" remoteControlTests
|
|
describe "Web" webTests
|
|
describe "Server CLIs" cliTests
|
|
|
|
eventuallyRemove :: FilePath -> Int -> IO ()
|
|
eventuallyRemove path retries = case retries of
|
|
0 -> action
|
|
n ->
|
|
action `E.catch` \ioe@IOError {ioe_type, ioe_filename} -> case ioe_type of
|
|
IOException.UnsatisfiedConstraints | ioe_filename == Just path -> threadDelay 1000000 >> eventuallyRemove path (n - 1)
|
|
_ -> E.throwIO ioe
|
|
where
|
|
action = removeDirectoryRecursive path
|