1module Test.Postgres.Main where
5import Data.Either (Either(..))
6import Data.Maybe (Maybe(..))
7import Data.Time.Duration (Milliseconds(..))
9import Effect.Aff (Aff, bracket, launchAff_, throwError, try)
10import Effect.Class (liftEffect)
11import Effect.Console (log)
12import Effect.Exception (error)
13import Test.Spec (Spec, around, describe, it)
14import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
15import Test.Spec.Reporter.Console (consoleReporter)
16import Test.Spec.Runner (runSpec)
17import Yoga.Test.Docker as Docker
18import Test.Postgres.Schema as Schema
19import Yoga.Postgres as PG
20import Yoga.Postgres.TypedQuery as TypedQuery
21import Yoga.SQL.PostgresTypes as TypedSQL
24testHost :: PG.PostgresHost
25testHost = PG.PostgresHost "localhost"
27testPort :: PG.PostgresPort
28testPort = PG.PostgresPort 5433 -- Test port from docker-compose.test.yml
30testDatabase :: PG.PostgresDatabase
31testDatabase = PG.PostgresDatabase "test_playground"
33testUsername :: PG.PostgresUsername
34testUsername = PG.PostgresUsername "postgres"
36testPassword :: PG.PostgresPassword
37testPassword = PG.PostgresPassword "postgres"
39-- Helper to create and manage Postgres connection
40withPostgres :: (PG.Connection -> Aff Unit) -> Aff Unit
42 conn <- liftEffect $ PG.postgres
45 , database: testDatabase
46 , username: testUsername
47 , password: testPassword
49 -- Clean up test tables before each test
50 _ <- try $ PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_users CASCADE") conn
51 _ <- try $ PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_posts CASCADE") conn
56-- Helper to check if value is Left (for use with shouldSatisfy)
57isLeft :: forall a b. Either a b -> Boolean
61-- Helper to set up test table
62setupTestTable :: PG.Connection -> Aff Unit
63setupTestTable conn = do
67 CREATE TABLE test_users (
68 id SERIAL PRIMARY KEY,
72 active BOOLEAN DEFAULT true,
73 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
80-- Helper to set up table with constraints
81setupTableWithConstraints :: PG.Connection -> Aff Unit
82setupTableWithConstraints conn = do
86 CREATE TABLE test_users (
87 id SERIAL PRIMARY KEY,
88 email TEXT UNIQUE NOT NULL,
100 describe "Yoga.Postgres Integration Tests" do
102 -- Connection Management Tests
103 around withPostgres do
104 describe "Connection Management" do
105 it "connects to Postgres successfully" \conn -> do
106 healthy <- PG.ping conn
107 healthy `shouldEqual` true
109 it "retrieves connection options" \conn -> do
110 opts <- liftEffect $ PG.options conn
111 opts.host `shouldSatisfy` (_ /= "")
112 opts.port `shouldEqual` 5433
113 opts.database `shouldEqual` "test_playground"
115 describe "Connection Errors" do
116 it "handles connection failures" do
118 conn <- liftEffect $ PG.postgres
119 { host: PG.PostgresHost "invalid-host-that-does-not-exist"
120 , port: PG.PostgresPort 9999
121 , database: testDatabase
122 , username: testUsername
123 , password: testPassword
128 Right false -> pure unit
129 Right true -> shouldEqual "unhealthy" "healthy"
132 around withPostgres do
133 describe "Basic Queries" do
134 it "creates and drops tables" \conn -> do
135 _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_temp") conn
136 count <- PG.executeSimple (PG.SQL "CREATE TABLE test_temp (id SERIAL PRIMARY KEY, name TEXT)") conn
137 count `shouldEqual` 0
138 _ <- PG.executeSimple (PG.SQL "DROP TABLE test_temp") conn
141 it "inserts and queries data" \conn -> do
144 count <- PG.execute (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)")
145 [ PG.toPGValue "Alice", PG.toPGValue "alice@example.com" ]
147 count `shouldEqual` 1
149 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
150 result.count `shouldEqual` 1
152 it "queries with WHERE clause" \conn -> do
155 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email, age) VALUES ($1, $2, $3)")
156 [ PG.toPGValue "Bob", PG.toPGValue "bob@example.com", PG.toPGValue 30 ]
159 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email, age) VALUES ($1, $2, $3)")
160 [ PG.toPGValue "Charlie", PG.toPGValue "charlie@example.com", PG.toPGValue 25 ]
163 result <- PG.query (PG.SQL "SELECT * FROM test_users WHERE age > $1")
166 result.count `shouldEqual` 1
168 it "updates data" \conn -> do
171 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)")
172 [ PG.toPGValue "Diana", PG.toPGValue "diana@example.com" ]
175 updateCount <- PG.execute (PG.SQL "UPDATE test_users SET email = $1 WHERE name = $2")
176 [ PG.toPGValue "diana.new@example.com", PG.toPGValue "Diana" ]
178 updateCount `shouldEqual` 1
180 it "deletes data" \conn -> do
183 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)")
184 [ PG.toPGValue "Eve", PG.toPGValue "eve@example.com" ]
187 deleteCount <- PG.execute (PG.SQL "DELETE FROM test_users WHERE name = $1")
188 [ PG.toPGValue "Eve" ]
190 deleteCount `shouldEqual` 1
192 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
193 result.count `shouldEqual` 0
195 -- Parameterized Query Tests
196 around withPostgres do
197 describe "Parameterized Queries" do
198 it "prevents SQL injection" \conn -> do
201 let maliciousInput = "'; DROP TABLE test_users; --"
202 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
203 [ PG.toPGValue maliciousInput ]
206 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
207 result.count `shouldEqual` 1
209 it "handles multiple parameters" \conn -> do
212 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, age, email) VALUES ($1, $2, $3)")
213 [ PG.toPGValue "Frank", PG.toPGValue 35, PG.toPGValue "frank@example.com" ]
216 result <- PG.query (PG.SQL "SELECT * FROM test_users WHERE age > $1")
219 result.count `shouldEqual` 1
221 it "handles array parameters" \conn -> do
224 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)")
225 [ PG.toPGValue "Grace", PG.toPGValue "grace@example.com" ]
228 result <- PG.query (PG.SQL "SELECT * FROM test_users WHERE name = ANY($1)")
229 [ PG.toPGValue [ "Grace", "Unknown" ] ]
231 result.count `shouldEqual` 1
234 around withPostgres do
235 describe "Transactions" do
236 it "commits transactions successfully" \conn -> do
241 _ <- PG.txExecute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
242 [ PG.toPGValue "Henry" ]
244 _ <- PG.txExecute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
245 [ PG.toPGValue "Iris" ]
251 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
252 result.count `shouldEqual` 2
254 it "rolls back on errors" \conn -> do
257 result <- try $ PG.transaction
259 _ <- PG.txExecute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
260 [ PG.toPGValue "Jack" ]
262 throwError (error "Intentional error")
266 result `shouldSatisfy` isLeft
268 count <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
269 count.count `shouldEqual` 0
271 it "queries within transaction" \conn -> do
276 _ <- PG.txExecute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
277 [ PG.toPGValue "Leo" ]
280 result <- PG.txQuerySimple (PG.SQL "SELECT * FROM test_users") txn
281 liftEffect $ log $ "Rows in transaction: " <> show result.count
286 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
287 result.count `shouldEqual` 1
290 around withPostgres do
291 describe "Data Types" do
292 it "handles NULL values" \conn -> do
295 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)")
296 [ PG.toPGValue "Mia", PG.toPGValue "" ]
299 result <- PG.query (PG.SQL "SELECT * FROM test_users WHERE email IS NULL OR email = ''")
302 result.count `shouldSatisfy` (_ >= 0)
304 it "handles various data types" \conn -> do
307 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, age, active) VALUES ($1, $2, $3)")
308 [ PG.toPGValue "Nina", PG.toPGValue 28, PG.toPGValue true ]
311 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
312 result.count `shouldEqual` 1
314 it "handles boolean values" \conn -> do
317 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name, active) VALUES ($1, $2)")
318 [ PG.toPGValue "Oscar", PG.toPGValue false ]
321 result <- PG.query (PG.SQL "SELECT * FROM test_users WHERE active = $1")
322 [ PG.toPGValue false ]
324 result.count `shouldEqual` 1
326 -- Error Handling Tests
327 around withPostgres do
328 describe "Error Handling" do
329 it "handles syntax errors" \conn -> do
330 result <- try $ PG.executeSimple (PG.SQL "INVALID SQL SYNTAX HERE") conn
331 result `shouldSatisfy` isLeft
333 it "handles constraint violations" \conn -> do
334 setupTableWithConstraints conn
336 _ <- PG.execute (PG.SQL "INSERT INTO test_users (email, name) VALUES ($1, $2)")
337 [ PG.toPGValue "unique@example.com", PG.toPGValue "User1" ]
340 result <- try $ PG.execute (PG.SQL "INSERT INTO test_users (email, name) VALUES ($1, $2)")
341 [ PG.toPGValue "unique@example.com", PG.toPGValue "User2" ]
344 result `shouldSatisfy` isLeft
346 it "handles missing table errors" \conn -> do
347 result <- try $ PG.querySimple (PG.SQL "SELECT * FROM non_existent_table") conn
349 Left _ -> pure unit -- Expected error
350 Right _ -> throwError (error "Expected error for missing table")
352 it "handles missing column errors" \conn -> do
355 result <- try $ PG.querySimple (PG.SQL "SELECT non_existent_column FROM test_users") conn
357 Left _ -> pure unit -- Expected error
358 Right _ -> throwError (error "Expected error for missing column")
360 -- Query Helper Tests
361 around withPostgres do
362 describe "Query Helpers" do
363 it "queryOne returns Maybe for single result" \conn -> do
366 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
367 [ PG.toPGValue "Paul" ]
370 result <- PG.queryOne (PG.SQL "SELECT * FROM test_users WHERE name = $1")
371 [ PG.toPGValue "Paul" ]
375 Nothing -> throwError (error "Expected to find user Paul")
377 it "queryOne returns Nothing when no results" \conn -> do
380 result <- PG.queryOne (PG.SQL "SELECT * FROM test_users WHERE name = $1")
381 [ PG.toPGValue "NonExistent" ]
385 Just _ -> throwError (error "Expected Nothing for non-existent user")
387 it "queryOneSimple works without parameters" \conn -> do
390 _ <- PG.execute (PG.SQL "INSERT INTO test_users (name) VALUES ($1)")
391 [ PG.toPGValue "Quinn" ]
394 result <- PG.queryOneSimple (PG.SQL "SELECT * FROM test_users LIMIT 1") conn
397 Nothing -> throwError (error "Expected to find at least one user")
399 it "executeSqlOne rejects multiple rows" \conn -> do
401 _ <- PG.executeSimple
402 (PG.SQL "INSERT INTO test_users (name) VALUES ('Alice'), ('Bob')")
404 result <- TypedQuery.executeSqlOne @() @{ id :: Int }
405 (TypedSQL.sql (TypedSQL.nonArg "SELECT id FROM test_users ORDER BY id"))
408 result `shouldSatisfy` isLeft
410 -- Prepared Statement Tests
411 around withPostgres do
412 describe "Prepared Statements" do
413 it "prepares and executes statements" \conn -> do
416 let stmtName = PG.StatementName "insert_user"
417 _ <- PG.prepare stmtName (PG.SQL "INSERT INTO test_users (name, email) VALUES ($1, $2)") conn
419 result <- PG.executePrepared stmtName
420 [ PG.toPGValue "Rachel", PG.toPGValue "rachel@example.com" ]
422 result.count `shouldEqual` 1
424 _ <- PG.deallocate stmtName conn
427 it "reuses prepared statements" \conn -> do
430 let stmtName = PG.StatementName "insert_user_batch"
431 _ <- PG.prepare stmtName (PG.SQL "INSERT INTO test_users (name) VALUES ($1)") conn
433 _ <- PG.executePrepared stmtName [ PG.toPGValue "Sam" ] conn
434 _ <- PG.executePrepared stmtName [ PG.toPGValue "Tina" ] conn
435 _ <- PG.executePrepared stmtName [ PG.toPGValue "Uma" ] conn
437 result <- PG.querySimple (PG.SQL "SELECT * FROM test_users") conn
438 result.count `shouldEqual` 3
440 _ <- PG.deallocate stmtName conn
445 liftEffect $ log "\nStarting Postgres Integration Tests (with Docker)\n"
449 liftEffect $ log "Starting Postgres and waiting for it to be ready..."
450 Docker.startService (Docker.ComposeFile "docker-compose.test.yml") (Docker.Timeout (Milliseconds 30000.0))
451 liftEffect $ log "Postgres is ready!\n"
454 Docker.stopService (Docker.ComposeFile "docker-compose.test.yml")
455 liftEffect $ log "Cleanup complete\n"
457 (\_ -> runSpec [ consoleReporter ] spec)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list