From drupal-workflow
Provides Drupal testing patterns with curl smoke tests, drush eval, and bash scripts to verify modules enabled, services exist, pages render, and config correct.
npx claudepluginhub gkastanis/drupal-workflow --plugin drupal-workflowThis skill uses the workspace's default tool permissions.
Practical verification patterns for Drupal implementations.
Enforces core Drupal 10+ rules for services, dependency injection, security including sanitization and access control, code quality, and testing verification. Always use when writing Drupal code.
Runs code quality audits, security scans, test coverage, SOLID/DRY checks, and lints for Drupal (PHPStan, PHPMD, Psalm, Semgrep, Trivy, Gitleaks) and Next.js (ESLint, Jest, Semgrep, Trivy, Gitleaks) projects.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Practical verification patterns for Drupal implementations.
The testing patterns default to DDEV but support other environments. Detect and adapt:
# Auto-detect environment and set the execution prefix.
if command -v ddev >/dev/null 2>&1 && ddev describe >/dev/null 2>&1; then
DRUSH="ddev drush"
EXEC="ddev exec"
elif command -v lando >/dev/null 2>&1; then
DRUSH="lando drush"
EXEC="lando ssh -c"
else
DRUSH="drush"
EXEC="bash -c"
fi
All patterns below use ddev drush and ddev exec. Substitute with $DRUSH and $EXEC for non-DDEV environments.
The login URL and curl MUST run in the same shell so the session cookie is valid. Write a script file and run it inside ddev.
#!/bin/bash
# scripts/tests/verify-page-access.sh
# Run with: ddev exec bash scripts/tests/verify-page-access.sh
LOGIN_URL=$(drush uli --uid=1 --no-browser --uri=http://localhost 2>/dev/null)
COOKIE=$(curl -s -D - -o /dev/null -L "$LOGIN_URL" 2>/dev/null \
| grep -i 'set-cookie' | head -1 \
| sed 's/.*set-cookie: *//i' | cut -d';' -f1)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -b "$COOKIE" "http://localhost/TARGET_PATH")
echo "TARGET_PATH: $STATUS"
Why --uri=http://localhost: Without it, drush uli may generate a URL with a different domain (e.g., https://mysite.ddev.site), causing a cookie domain mismatch when curling http://localhost.
ddev exec curl -s -b "$COOKIE_FILE" "http://localhost/TARGET_PATH" \
> /tmp/claude/page-output.html
# Then read /tmp/claude/page-output.html to inspect content
# Check for specific text/element on page
ddev exec curl -s -b "$COOKIE_FILE" "http://localhost/TARGET_PATH" \
| grep -o '<title>[^<]*</title>'
#!/bin/bash
# scripts/tests/verify-PROJECT_NAME.sh
# Verify: DESCRIPTION
set -e
COOKIE_FILE="/tmp/cookies-$$.txt"
LOGIN_URL=$(ddev drush uli --uid=1 --no-browser 2>/dev/null)
ddev exec curl -s -c "$COOKIE_FILE" -L "$LOGIN_URL" -o /dev/null
STATUS=$(ddev exec curl -s -o /dev/null -w "%{http_code}" \
-b "$COOKIE_FILE" "http://localhost/TARGET_PATH")
if [ "$STATUS" = "200" ]; then
echo "PASS: TARGET_PATH returns 200"
else
echo "FAIL: TARGET_PATH returns $STATUS (expected 200)"
exit 1
fi
rm -f "$COOKIE_FILE"
ddev exec passes commands through multiple shell layers, destroying pipes, variable expansion, nested quotes, and grep -P.
# BAD — variables get expanded/mangled by ddev exec shell layers
ddev exec "drush eval 'foreach ($groups as $g) { echo $g->id(); }'"
# GOOD — write a script file, run the file
cat > scripts/tests/my-check.php << 'EOF'
<?php
$groups = \Drupal::entityTypeManager()->getStorage('group')->loadMultiple();
foreach ($groups as $g) { echo $g->id() . "\n"; }
EOF
ddev exec drush scr scripts/tests/my-check.php
Rule: If the command has pipes (|), variables ($var), nested quotes, or multi-line PHP, write it to a file first.
CRITICAL escaping rules:
Drupal:: not \Drupal:: (backslash gets eaten by shell)Exception not \Exceptionuse statements (not supported in eval context)drush scr script.php instead2>/dev/nullddev drush eval 'print json_encode(["exists" => Drupal::hasService("my_module.my_service")]);' 2>/dev/null
ddev drush eval 'print json_encode(["enabled" => Drupal::moduleHandler()->moduleExists("my_module")]);' 2>/dev/null
ddev drush eval '$defs = Drupal::service("entity_field.manager")->getFieldDefinitions("node", "article"); print json_encode(["has_field" => isset($defs["field_custom"])]);' 2>/dev/null
ddev drush eval 'print json_encode(Drupal::config("my_module.settings")->get());' 2>/dev/null
ddev drush eval '$perms = Drupal::service("user.permissions")->getPermissions(); print json_encode(["exists" => isset($perms["administer my_module"])]);' 2>/dev/null
ddev drush eval 'try { $url = Drupal::service("url_generator")->generateFromRoute("my_module.page"); print json_encode(["route" => "exists", "url" => $url]); } catch (Exception $e) { print json_encode(["route" => "missing"]); }' 2>/dev/null
scripts/tests/
index.md # Index of all test scripts
verify-*.sh # Feature verification scripts
check-*.sh # Quick check scripts
smoke-*.sh # Page smoke test scripts
scripts/tests/verify-<feature>.shchmod +x scripts/tests/verify-<feature>.shscripts/tests/index.md with entry#!/bin/bash
# scripts/tests/verify-<feature>.sh
# Verifies: <what this tests>
# Created: <date>
set -e
echo "=== Verifying <feature> ==="
# Test 1: Check module enabled
RESULT=$(ddev drush eval 'print Drupal::moduleHandler()->moduleExists("my_module") ? "yes" : "no";' 2>/dev/null)
if [ "$RESULT" = "yes" ]; then
echo "PASS: Module enabled"
else
echo "FAIL: Module not enabled"
exit 1
fi
# Test 2: Check service exists
RESULT=$(ddev drush eval 'print Drupal::hasService("my_module.service") ? "yes" : "no";' 2>/dev/null)
if [ "$RESULT" = "yes" ]; then
echo "PASS: Service registered"
else
echo "FAIL: Service not found"
exit 1
fi
echo "=== All checks passed ==="
| What to Verify | Method | Command |
|---|---|---|
| Module enabled | drush eval | Drupal::moduleHandler()->moduleExists("x") |
| Service exists | drush eval | Drupal::hasService("x.y") |
| Field on bundle | drush eval | getFieldDefinitions("node", "article") |
| Route accessible | curl | Status code 200 check |
| Config value | drush eval | Drupal::config("x.settings")->get("key") |
| Permission defined | drush eval | getPermissions() check |
| Cache clear works | drush | ddev drush cr exits 0 |
| Config imports | drush | ddev drush cim -y exits 0 |
Patched modules may live in both vendor/drupal/ (original) and web/modules/contrib/ (patched). Editing the wrong one has no effect.
// Find which file is actually loaded at runtime
$ref = new \ReflectionMethod($service, 'methodName');
echo $ref->getFileName();
Rule: When a patched module exists in both locations, use ReflectionMethod to find which file PHP actually loads. Edit that file, not the one you assume.
Create dedicated test users rather than relying on existing users with unknown role combinations.
# Create test users
ddev drush user:create testauth --password=testauth123
ddev drush user:create testadmin --password=testadmin123
ddev drush user:role:add my_admin_role testadmin
Test matrix -- verify each user type against each resource state:
| User type | Published resource | Unpublished resource |
|---|---|---|
| Anonymous | view only | 403 |
| Authenticated | view only | 403 |
| Role-based admin | view + edit | view + edit |
| uid 1 | view + edit | view + edit (bypass) |
Check content, not just status codes: Download the HTML and grep for Drupal form IDs (edit-*) to verify what users actually see -- presence of edit forms, workflow fields, and local task tabs.