Skip to content

Commit bf2296a

Browse files
committed
feat(lab): Lab 03-01 real docker-compose and PostgreSQL functional tests
Add working standalone lab for PostgreSQL 16: - docker/docker-compose.standalone.yml: postgres:16 + pgAdmin4, pg_isready healthcheck - docker/init/01-create-app-users.sql: test users/DBs/tables - tests/labs/test-lab-03-01.sh: 11 test sections covering auth, CRUD, transactions, encoding, isolation, and performance baseline
1 parent 9fc5095 commit bf2296a

File tree

3 files changed

+320
-27
lines changed

3 files changed

+320
-27
lines changed
Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
# Lab 01 — Standalone: Complete PostgreSQL primary database in isolation
2-
# No external dependencies required.
1+
# Lab 03-01 — Standalone: PostgreSQL 16 in complete isolation
2+
# Purpose : Verify PostgreSQL install, auth, DB creation, and basic CRUD
3+
# Duration: 30–45 minutes | Machines: 1
4+
# Usage : docker compose -f docker/docker-compose.standalone.yml up -d
35
---
46
services:
57
postgresql:
68
image: postgres:16
7-
container_name: it-stack-postgresql
9+
container_name: it-stack-postgresql-lab01
810
restart: unless-stopped
911
ports:
10-
- "5432:$firstPort"
12+
- "5432:5432"
1113
environment:
12-
- IT_STACK_ENV=lab-01-standalone
14+
POSTGRES_USER: labadmin
15+
POSTGRES_PASSWORD: Lab01Password!
16+
POSTGRES_DB: labdb
17+
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
18+
# Create additional test users/databases via init scripts
19+
POSTGRES_MULTIPLE_DATABASES: "appdb,testdb"
1320
volumes:
14-
- postgresql_data:/var/lib/postgresql
21+
- postgresql_data:/var/lib/postgresql/data
22+
- ./init:/docker-entrypoint-initdb.d:ro
1523
healthcheck:
16-
test: ["CMD-SHELL", "curl -sf http://localhost/health || exit 1"]
17-
interval: 30s
18-
timeout: 10s
19-
retries: 5
20-
start_period: 60s
24+
test: ["CMD-SHELL", "pg_isready -U labadmin -d labdb"]
25+
interval: 10s
26+
timeout: 5s
27+
retries: 10
28+
start_period: 30s
29+
networks:
30+
- it-stack-net
31+
32+
# pgAdmin — web UI for verifying connectivity (optional, comment out to save RAM)
33+
pgadmin:
34+
image: dpage/pgadmin4:latest
35+
container_name: it-stack-pgadmin-lab01
36+
restart: unless-stopped
37+
ports:
38+
- "5050:80"
39+
environment:
40+
PGADMIN_DEFAULT_EMAIL: admin@lab.local
41+
PGADMIN_DEFAULT_PASSWORD: Lab01Password!
42+
PGADMIN_CONFIG_SERVER_MODE: "False"
43+
depends_on:
44+
postgresql:
45+
condition: service_healthy
2146
networks:
2247
- it-stack-net
2348

@@ -27,3 +52,4 @@ networks:
2752

2853
volumes:
2954
postgresql_data:
55+
name: it-stack-postgresql-lab01-data
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- docker/init/01-create-app-users.sql
2+
-- PostgreSQL Lab 01 initialization: create test users and databases
3+
-- Runs automatically on first container start via docker-entrypoint-initdb.d
4+
5+
-- ── App user ────────────────────────────────────────────────────────────────
6+
CREATE USER appuser WITH
7+
NOSUPERUSER
8+
NOCREATEDB
9+
NOCREATEROLE
10+
LOGIN
11+
ENCRYPTED PASSWORD 'AppPass123!';
12+
13+
-- ── Test user ────────────────────────────────────────────────────────────────
14+
CREATE USER testuser WITH
15+
NOSUPERUSER
16+
NOCREATEDB
17+
NOCREATEROLE
18+
LOGIN
19+
ENCRYPTED PASSWORD 'TestPass123!';
20+
21+
-- ── Application database ─────────────────────────────────────────────────────
22+
CREATE DATABASE appdb
23+
WITH OWNER = appuser
24+
ENCODING = 'UTF8'
25+
LC_COLLATE = 'C'
26+
LC_CTYPE = 'C'
27+
TEMPLATE = template0;
28+
29+
GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;
30+
31+
-- Revoke PUBLIC create on appdb to match production security posture
32+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
33+
34+
-- ── Test database ─────────────────────────────────────────────────────────────
35+
CREATE DATABASE testdb
36+
WITH OWNER = testuser
37+
ENCODING = 'UTF8'
38+
LC_COLLATE = 'C'
39+
LC_CTYPE = 'C'
40+
TEMPLATE = template0;
41+
42+
GRANT ALL PRIVILEGES ON DATABASE testdb TO testuser;
43+
44+
-- ── Sample table in labdb (owned by labadmin) ─────────────────────────────────
45+
\connect labdb
46+
47+
CREATE TABLE IF NOT EXISTS it_stack_lab (
48+
id SERIAL PRIMARY KEY,
49+
module TEXT NOT NULL,
50+
lab_number TEXT NOT NULL,
51+
status TEXT NOT NULL DEFAULT 'pending',
52+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
53+
);
54+
55+
INSERT INTO it_stack_lab (module, lab_number, status) VALUES
56+
('postgresql', '03-01', 'running'),
57+
('redis', '04-01', 'pending'),
58+
('keycloak', '02-01', 'pending');
59+
60+
GRANT SELECT, INSERT, UPDATE, DELETE ON it_stack_lab TO appuser;
61+
GRANT USAGE, SELECT ON SEQUENCE it_stack_lab_id_seq TO appuser;

tests/labs/test-lab-03-01.sh

Lines changed: 222 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,239 @@ echo -e "${CYAN} Module: ${MODULE}${NC}"
2626
echo -e "${CYAN}======================================${NC}"
2727
echo ""
2828

29+
# ── Helpers ───────────────────────────────────────────────────────────────────
30+
PG_HOST=localhost
31+
PG_PORT=5432
32+
ADMIN_USER=labadmin
33+
ADMIN_PASS=Lab01Password!
34+
APP_USER=appuser
35+
APP_PASS=AppPass123!
36+
TEST_USER=testuser
37+
TEST_PASS=TestPass123!
38+
39+
psql_admin() {
40+
PGPASSWORD="${ADMIN_PASS}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
41+
-U "${ADMIN_USER}" -d labdb -t -c "$1" 2>/dev/null
42+
}
43+
44+
psql_as() {
45+
local user="$1" pass="$2" db="$3" query="$4"
46+
PGPASSWORD="${pass}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
47+
-U "${user}" -d "${db}" -t -c "${query}" 2>/dev/null
48+
}
49+
50+
psql_check() {
51+
local user="$1" pass="$2" db="$3" query="$4" test_name="$5"
52+
if PGPASSWORD="${pass}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
53+
-U "${user}" -d "${db}" -t -c "${query}" > /dev/null 2>&1; then
54+
pass "${test_name}"
55+
else
56+
fail "${test_name}"
57+
fi
58+
}
59+
60+
wait_for_postgres() {
61+
local retries=30
62+
until PGPASSWORD="${ADMIN_PASS}" pg_isready -h "${PG_HOST}" -p "${PG_PORT}" \
63+
-U "${ADMIN_USER}" > /dev/null 2>&1; do
64+
retries=$((retries - 1))
65+
if [[ "${retries}" -le 0 ]]; then
66+
fail "PostgreSQL did not become ready within 150 seconds"
67+
return 1
68+
fi
69+
info "Waiting for PostgreSQL... (${retries} retries left)"
70+
sleep 5
71+
done
72+
pass "PostgreSQL is ready"
73+
}
74+
2975
# ── PHASE 1: Setup ────────────────────────────────────────────────────────────
30-
info "Phase 1: Setup"
31-
docker compose -f "${COMPOSE_FILE}" up -d
32-
info "Waiting 30s for ${MODULE} to initialize..."
33-
sleep 30
76+
info "Phase 1: Setup"
77+
docker compose -f "${COMPOSE_FILE}" pull --quiet 2>/dev/null || true
78+
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
79+
info "Waiting for PostgreSQL to be ready..."
80+
wait_for_postgres
3481

3582
# ── PHASE 2: Health Checks ────────────────────────────────────────────────────
3683
info "Phase 2: Health Checks"
3784

38-
if docker compose -f "${COMPOSE_FILE}" ps | grep -q "running\|Up"; then
39-
pass "Container is running"
85+
# Container running
86+
if docker compose -f "${COMPOSE_FILE}" ps postgresql | grep -qE "running|Up|healthy"; then
87+
pass "Container is running"
88+
else
89+
fail "Container is not running"
90+
fi
91+
92+
# Docker healthcheck status
93+
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' it-stack-postgresql-lab01 2>/dev/null)
94+
if [[ "${HEALTH}" == "healthy" ]]; then
95+
pass "Docker healthcheck reports healthy"
96+
else
97+
warn "Docker healthcheck status: ${HEALTH} (may still be starting)"
98+
fi
99+
100+
# Port is open
101+
if nc -z -w3 "${PG_HOST}" "${PG_PORT}" 2>/dev/null; then
102+
pass "Port ${PG_PORT} is open"
40103
else
41-
fail "Container is not running"
104+
fail "Port ${PG_PORT} is not reachable"
105+
fi
106+
107+
# pg_isready
108+
if PGPASSWORD="${ADMIN_PASS}" pg_isready -h "${PG_HOST}" -p "${PG_PORT}" \
109+
-U "${ADMIN_USER}" > /dev/null 2>&1; then
110+
pass "pg_isready confirms server accepting connections"
111+
else
112+
fail "pg_isready returned non-zero"
42113
fi
43114

44115
# ── PHASE 3: Functional Tests ─────────────────────────────────────────────────
45-
info "Phase 3: Functional Tests (Lab 01 — Standalone)"
116+
info "Phase 3: Functional Tests"
117+
118+
# 3.1 Admin connection
119+
info "3.1 — Admin connection"
120+
psql_check "${ADMIN_USER}" "${ADMIN_PASS}" labdb "SELECT version();" \
121+
"Admin user can connect to labdb"
122+
123+
# 3.2 PostgreSQL version
124+
PG_VER=$(psql_admin "SELECT split_part(version(), ' ', 2);" | tr -d '[:space:]')
125+
if [[ "${PG_VER}" == 16* ]]; then
126+
pass "PostgreSQL version is 16.x (got: ${PG_VER})"
127+
else
128+
fail "Expected PostgreSQL 16.x, got: ${PG_VER}"
129+
fi
130+
131+
# 3.3 Databases exist
132+
info "3.3 — Database existence"
133+
for db in labdb appdb testdb; do
134+
if psql_admin "SELECT 1 FROM pg_database WHERE datname='${db}';" \
135+
| grep -q "1"; then
136+
pass "Database '${db}' exists"
137+
else
138+
fail "Database '${db}' not found"
139+
fi
140+
done
141+
142+
# 3.4 Users exist
143+
info "3.4 — User existence"
144+
for u in appuser testuser; do
145+
if psql_admin "SELECT 1 FROM pg_roles WHERE rolname='${u}';" \
146+
| grep -q "1"; then
147+
pass "User '${u}' exists"
148+
else
149+
fail "User '${u}' not found"
150+
fi
151+
done
152+
153+
# 3.5 User authentication
154+
info "3.5 — User authentication"
155+
psql_check "${APP_USER}" "${APP_PASS}" appdb "SELECT current_user;" \
156+
"appuser authenticates to appdb"
157+
psql_check "${TEST_USER}" "${TEST_PASS}" testdb "SELECT current_user;" \
158+
"testuser authenticates to testdb"
159+
160+
# 3.6 Sample table and data from init script
161+
info "3.6 — Init script data"
162+
if psql_admin "SELECT count(*) FROM it_stack_lab;" \
163+
| grep -qE "[0-9]+"; then
164+
pass "it_stack_lab table exists and has rows"
165+
else
166+
fail "it_stack_lab table missing or empty (init script may have failed)"
167+
fi
168+
169+
# 3.7 CRUD operations
170+
info "3.7 — CRUD operations"
171+
# INSERT
172+
psql_check "${ADMIN_USER}" "${ADMIN_PASS}" labdb \
173+
"INSERT INTO it_stack_lab (module, lab_number, status) VALUES ('test','00-00','created');" \
174+
"INSERT row into it_stack_lab"
175+
176+
# SELECT with filter
177+
if psql_admin "SELECT id FROM it_stack_lab WHERE status='created';" \
178+
| grep -qE "[0-9]+"; then
179+
pass "SELECT with WHERE filter works"
180+
else
181+
fail "SELECT with WHERE filter returned no results"
182+
fi
183+
184+
# UPDATE
185+
psql_check "${ADMIN_USER}" "${ADMIN_PASS}" labdb \
186+
"UPDATE it_stack_lab SET status='updated' WHERE lab_number='00-00';" \
187+
"UPDATE row in it_stack_lab"
46188

47-
# TODO: Add module-specific functional tests here
48-
# Example:
49-
# if curl -sf http://localhost:5432/health > /dev/null 2>&1; then
50-
# pass "Health endpoint responds"
51-
# else
52-
# fail "Health endpoint not reachable"
53-
# fi
189+
if psql_admin "SELECT 1 FROM it_stack_lab WHERE status='updated';" \
190+
| grep -q "1"; then
191+
pass "UPDATE confirmed via SELECT"
192+
else
193+
fail "UPDATE not reflected in SELECT"
194+
fi
195+
196+
# DELETE
197+
psql_check "${ADMIN_USER}" "${ADMIN_PASS}" labdb \
198+
"DELETE FROM it_stack_lab WHERE lab_number='00-00';" \
199+
"DELETE row from it_stack_lab"
200+
201+
# 3.8 Transaction test
202+
info "3.8 — Transaction support"
203+
if PGPASSWORD="${ADMIN_PASS}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
204+
-U "${ADMIN_USER}" -d labdb > /dev/null 2>&1 <<'SQL'
205+
BEGIN;
206+
INSERT INTO it_stack_lab (module, lab_number, status) VALUES ('txtest','00-99','txpending');
207+
UPDATE it_stack_lab SET status='txcommitted' WHERE lab_number='00-99';
208+
COMMIT;
209+
SQL
210+
pass "Transaction BEGIN/COMMIT succeeds"
211+
else
212+
fail "Transaction failed"
213+
fi
214+
psql_admin "DELETE FROM it_stack_lab WHERE lab_number='00-99';" > /dev/null 2>&1 || true
54215

55-
warn "Functional tests for Lab 03-01 pending implementation"
216+
# Transaction ROLLBACK
217+
if PGPASSWORD="${ADMIN_PASS}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
218+
-U "${ADMIN_USER}" -d labdb > /dev/null 2>&1 <<'SQL'
219+
BEGIN;
220+
INSERT INTO it_stack_lab (module, lab_number, status) VALUES ('rollbacktest','00-98','shouldnotexist');
221+
ROLLBACK;
222+
SQL
223+
if ! psql_admin "SELECT 1 FROM it_stack_lab WHERE lab_number='00-98';" \
224+
| grep -q "1"; then
225+
pass "ROLLBACK correctly discards changes"
226+
else
227+
fail "ROLLBACK failed — row persists after rollback"
228+
fi
229+
else
230+
fail "ROLLBACK test could not run"
231+
fi
232+
233+
# 3.9 Encoding and collation
234+
info "3.9 — Encoding and collation"
235+
if psql_admin "SELECT encoding, datcollate FROM pg_database WHERE datname='labdb';" \
236+
| grep -q "UTF8"; then
237+
pass "labdb uses UTF8 encoding"
238+
else
239+
fail "labdb encoding is not UTF8"
240+
fi
241+
242+
# 3.10 appuser privilege isolation (should NOT access testdb)
243+
info "3.10 — Privilege isolation"
244+
if PGPASSWORD="${APP_PASS}" psql -h "${PG_HOST}" -p "${PG_PORT}" \
245+
-U "${APP_USER}" -d testdb -c "SELECT 1;" > /dev/null 2>&1; then
246+
fail "appuser can connect to testdb (expected isolation failure)"
247+
else
248+
pass "appuser cannot connect to testdb (isolation correct)"
249+
fi
250+
251+
# 3.11 Performance baseline
252+
info "3.11 — Performance baseline"
253+
START_MS=$(date +%s%3N)
254+
psql_admin "SELECT count(*) FROM generate_series(1, 100000);" > /dev/null 2>&1
255+
END_MS=$(date +%s%3N)
256+
ELAPSED=$((END_MS - START_MS))
257+
if [[ "${ELAPSED}" -lt 5000 ]]; then
258+
pass "generate_series(100000) completed in ${ELAPSED}ms (<5000ms threshold)"
259+
else
260+
warn "generate_series(100000) took ${ELAPSED}ms (may indicate slow I/O)"
261+
fi
56262

57263
# ── PHASE 4: Cleanup ──────────────────────────────────────────────────────────
58264
info "Phase 4: Cleanup"

0 commit comments

Comments
 (0)