Move the example config to configs/, delete scripts/ and CONVENTIONS.md (closes #97)
check / check (push) Failing after 2s

config.example.yml moves unchanged to configs/config.example.yml, the
directory REPO_POLICIES.md names for configuration examples. README.md,
the comments in internal/config/config.go and the startup error for the
placeholder signing key name the new path. scripts/manual-test.sh and
its directory are deleted. The handler tests in internal/handlers cover
every check it made except two: fetching a real image from the internet,
and a URL made on the generator page with a ttl answering 410 once the
ttl has passed (#199).
CONVENTIONS.md, a reformatted copy of the Go HTTP server conventions, is
deleted, as REPO_POLICIES.md links the canonical document.

Model: opus-5-5
This commit was merged in pull request #194.
This commit is contained in:
2026-10-04 21:07:52 +02:00
parent 625fd42ace
commit 8568c17d1b
6 changed files with 16 additions and 1412 deletions
-1259
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -18,7 +18,7 @@ make build
# run with a config file: copy the example and set a real signing key # run with a config file: copy the example and set a real signing key
# (the example placeholder is refused at startup), e.g. with # (the example placeholder is refused at startup), e.g. with
# openssl rand -base64 32 # openssl rand -base64 32
cp config.example.yml config.yml cp configs/config.example.yml config.yml
$EDITOR config.yml # replace the signing_key placeholder $EDITOR config.yml # replace the signing_key placeholder
./bin/pixad --config config.yml ./bin/pixad --config config.yml
@@ -520,7 +520,7 @@ Key settings in more detail:
the container unhealthy, and upaas marks a deploy failed when its container the container unhealthy, and upaas marks a deploy failed when its container
is unhealthy. The login and URL generator pages and `/metrics` keep working is unhealthy. The login and URL generator pages and `/metrics` keep working
See `config.example.yml` for all options with defaults. See `configs/config.example.yml` for all options with defaults.
### Architecture ### Architecture
+10
View File
@@ -31,6 +31,16 @@ P2: security: referer blacklist
# Completed Steps # Completed Steps
- 2026-10-04 fewer files in the repository root (closes #97):
`config.example.yml` moved unchanged to `configs/config.example.yml`, and
`README.md`, the comments in `internal/config/config.go` and the startup error
for the placeholder signing key name the new path; `scripts/manual-test.sh`
and its directory are deleted, as the handler tests in `internal/handlers`
cover every check it made except two: fetching a real image from the
internet, and a URL made on the generator page with a `ttl` answering 410 once
the `ttl` has passed (https://git.eeqj.de/sneak/pixa/issues/199);
`CONVENTIONS.md` is deleted, as `REPO_POLICIES.md` links the canonical Go HTTP
server conventions.
- 2026-10-04 SQLite writes no longer fail with "database is locked" (closes - 2026-10-04 SQLite writes no longer fail with "database is locked" (closes
#198): pixa adds `_pragma=busy_timeout(5000)` to every `db_url`, so a write #198): pixa adds `_pragma=busy_timeout(5000)` to every `db_url`, so a write
that finds another in progress on another connection waits up to five seconds that finds another in progress on another connection waits up to five seconds
+4 -4
View File
@@ -62,7 +62,7 @@ const (
) )
// placeholderSigningKey is the dummy signing_key shipped in // placeholderSigningKey is the dummy signing_key shipped in
// config.example.yml. It is 45 characters, so it passes the length // configs/config.example.yml. It is 45 characters, so it passes the length
// check, but it is public in this repository and must be rejected at // check, but it is public in this repository and must be rejected at
// startup so no deployment ever signs URLs with it. // startup so no deployment ever signs URLs with it.
const placeholderSigningKey = "CHANGE_ME_generate_with_openssl_rand_base64_32" const placeholderSigningKey = "CHANGE_ME_generate_with_openssl_rand_base64_32"
@@ -88,7 +88,7 @@ var (
errMustBeAtLeastOne = errors.New("must be at least 1") errMustBeAtLeastOne = errors.New("must be at least 1")
errValueTooShort = errors.New("value too short") errValueTooShort = errors.New("value too short")
errPlaceholderKey = errors.New( errPlaceholderKey = errors.New(
"is the placeholder from config.example.yml; " + "is the placeholder from configs/config.example.yml; " +
"generate a real key with: openssl rand -base64 32") "generate a real key with: openssl rand -base64 32")
errMustBeSetTogether = errors.New("must be set together") errMustBeSetTogether = errors.New("must be set together")
errMustNotBeNegative = errors.New("must not be negative") errMustNotBeNegative = errors.New("must not be negative")
@@ -554,8 +554,8 @@ func (c *Config) ensureStateDirWritable() error {
} }
// validateSigningKey checks that the signing key is present, long // validateSigningKey checks that the signing key is present, long
// enough, and not the public placeholder from config.example.yml. The // enough, and not the public placeholder from configs/config.example.yml.
// key value itself is never echoed in error messages. // The key value itself is never echoed in error messages.
func (c *Config) validateSigningKey() error { func (c *Config) validateSigningKey() error {
if c.SigningKey == "" { if c.SigningKey == "" {
return fmt.Errorf("%s: %w", settingName(keySigningKey), errValueRequired) return fmt.Errorf("%s: %w", settingName(keySigningKey), errValueRequired)
-147
View File
@@ -1,147 +0,0 @@
#!/bin/bash
#
# Manual test script for pixa server
# Requires: server running on localhost:8080
#
set -e
BASE_URL="${BASE_URL:-http://localhost:8080}"
SIGNING_KEY="${SIGNING_KEY:-test-signing-key-for-development-only}"
TEST_IMAGE_URL="https://s3.sneak.cloud/sneak-public/2021/2021-04-18.untitled.a7r4.07723.jpg"
COOKIE_JAR=$(mktemp)
cleanup() {
rm -f "$COOKIE_JAR"
}
trap cleanup EXIT
pass() {
echo "✓ PASS: $1"
}
fail() {
echo "✗ FAIL: $1"
exit 1
}
echo "=== Pixa Manual Test Suite ==="
echo "Base URL: $BASE_URL"
echo ""
# Test 1: Healthcheck
echo "--- Test 1: Healthcheck endpoint ---"
HEALTH=$(curl -sf "$BASE_URL/.well-known/healthcheck.json")
if echo "$HEALTH" | grep -q '"status"'; then
pass "Healthcheck returns status"
else
fail "Healthcheck did not return expected response"
fi
# Test 2: Login page displays
echo "--- Test 2: Login page (GET /) ---"
LOGIN_PAGE=$(curl -sf "$BASE_URL/")
if echo "$LOGIN_PAGE" | grep -qi "password\|login\|sign"; then
pass "Login page displays password form"
else
fail "Login page did not display expected content"
fi
# Test 3: Wrong password shows error
echo "--- Test 3: Login with wrong password ---"
WRONG_LOGIN=$(curl -sf -X POST "$BASE_URL/" -d "key=wrong-key" -c "$COOKIE_JAR")
if echo "$WRONG_LOGIN" | grep -qi "invalid\|error\|incorrect\|wrong"; then
pass "Wrong password shows error message"
else
fail "Wrong password did not show error"
fi
# Test 4: Correct password redirects to generator
echo "--- Test 4: Login with correct signing key ---"
curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
GENERATOR_PAGE=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR")
if echo "$GENERATOR_PAGE" | grep -qi "generate\|url\|source\|logout"; then
pass "Correct password shows generator page"
else
fail "Generator page not displayed after login"
fi
# Test 5: Generate encrypted URL
echo "--- Test 5: Generate encrypted URL ---"
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "url=$TEST_IMAGE_URL" \
-d "width=800" \
-d "height=600" \
-d "format=jpeg" \
-d "quality=85" \
-d "fit=cover" \
-d "ttl=3600")
if echo "$GEN_RESULT" | grep -q "/v1/e/"; then
pass "Encrypted URL generated"
# Extract the encrypted URL
ENC_URL=$(echo "$GEN_RESULT" | grep -o '/v1/e/[^"<]*' | head -1)
echo " Generated URL: $ENC_URL"
else
fail "Failed to generate encrypted URL"
fi
# Test 6: Fetch image via encrypted URL
echo "--- Test 6: Fetch image via encrypted URL ---"
if [ -n "$ENC_URL" ]; then
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL$ENC_URL")
if [ "$HTTP_CODE" = "200" ]; then
pass "Encrypted URL returns image (HTTP 200)"
else
fail "Encrypted URL returned HTTP $HTTP_CODE"
fi
else
fail "No encrypted URL to test"
fi
# Test 7: Fetch image via allowlisted host (direct proxy)
echo "--- Test 7: Fetch image via direct proxy (allowlisted host) ---"
# URL format: /v1/image/<host>/<path>/<WxH>.<format>
PROXY_PATH="/v1/image/s3.sneak.cloud/sneak-public/2021/2021-04-18.untitled.a7r4.07723.jpg/400x300.jpeg"
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL$PROXY_PATH")
if [ "$HTTP_CODE" = "200" ]; then
pass "Direct proxy returns image (HTTP 200)"
else
fail "Direct proxy returned HTTP $HTTP_CODE"
fi
# Test 8: Logout
echo "--- Test 8: Logout ---"
curl -sf "$BASE_URL/logout" -b "$COOKIE_JAR" -c "$COOKIE_JAR" -L -o /dev/null
AFTER_LOGOUT=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR")
if echo "$AFTER_LOGOUT" | grep -qi "password\|login"; then
pass "Logout redirects to login page"
else
fail "Logout did not redirect to login"
fi
# Test 9: Generate short-TTL URL and verify expiration
echo "--- Test 9: Expired URL returns 410 ---"
# Login again
curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
# Generate URL with 1 second TTL
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "url=$TEST_IMAGE_URL" \
-d "width=100" \
-d "height=100" \
-d "format=jpeg" \
-d "ttl=1")
SHORT_URL=$(echo "$GEN_RESULT" | grep -o '/v1/e/[^"<]*' | head -1)
if [ -n "$SHORT_URL" ]; then
echo " Waiting 2 seconds for URL to expire..."
sleep 2
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$SHORT_URL")
if [ "$HTTP_CODE" = "410" ]; then
pass "Expired URL returns 410 Gone"
else
fail "Expired URL returned HTTP $HTTP_CODE (expected 410)"
fi
else
fail "Could not generate short-TTL URL"
fi
echo ""
echo "=== All tests passed! ==="