Move the example config to configs/, delete scripts/ and CONVENTIONS.md (closes #97)
check / check (push) Failing after 2s
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:
-1259
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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! ==="
|
|
||||||
Reference in New Issue
Block a user