Skip to content

Instantly share code, notes, and snippets.

@drhenner
Last active February 11, 2026 17:01
Show Gist options
  • Select an option

  • Save drhenner/13161c86fbedb9bd591485cfdb949305 to your computer and use it in GitHub Desktop.

Select an option

Save drhenner/13161c86fbedb9bd591485cfdb949305 to your computer and use it in GitHub Desktop.
Cursor rules for testing & security
---
description: Manage CODEOWNERS when creating new files
alwaysApply: true
---
# CODEOWNERS Management
When creating a new file, **always** add an entry to `.github/CODEOWNERS`.
## Workflow
1. **Ask about ownership**: "Should this file be owned by @zendesk/fang?"
2. **If not fang, suggest logical teams** based on the file path/type:
- **Rules/automation**: @zendesk/fang, @zendesk/libretto
- **Authentication/SSO**: @zendesk/authentication
- **Users/roles**: @zendesk/users-platform
- **Tickets**: @zendesk/ticket-platform, @zendesk/ticket-product
- **Platform objects/custom fields**: @zendesk/vinyl, @zendesk/libretto
- **Operations/infra**: @zendesk/classic-operations, @zendesk/squonk
- **Search**: @zendesk/support-search
- **Views**: @zendesk/support-views
- **GraphQL**: @zendesk/skvader, @zendesk/one-graph
- **Rails/Ruby core**: @zendesk/ruby-core
- **Testing/CI**: @zendesk/classic-operations
3. **Add entry in alphabetical order**: The file must be sorted alphabetically
4. **Format**: `path/to/file @zendesk/team-name`
## Example
```
# User creates: app/jobs/ticket_automation_job.rb
Ask: "Should this be owned by @zendesk/fang?"
If yes: Add `app/jobs/ticket_automation_job.rb @zendesk/fang`
If no: "This looks like it could be: @zendesk/fang (automation), @zendesk/ticket-platform (tickets), or @zendesk/classic-operations (general). Which team should own it?"
```
## Important Notes
- CODEOWNERS must be kept in **alphabetical order** (see file header comment)
- Use full paths, not globs (unless following existing patterns in the file)
- Multiple teams can own the same file (space-separated)
---
description: Guidelines for testing controller params and strong parameters
globs: "{test/**/controllers/**/*_test.rb,components/**/test/**/controllers/**/*_test.rb}"
alwaysApply: false
---
# Controller Test Params Guidelines
## Don't Stub Params
Never stub or mock `params` in controller tests. Pass real params through the request:
```ruby
# ❌ BAD - stubbing params
controller.stubs(:params).returns(ActionController::Parameters.new(name: "test"))
# ✅ GOOD - pass params through the request
post :create, params: { name: "test" }
```
## Verify Strong Parameters Indirectly
Don't write separate tests just to verify `allow_parameters`/`permit`. Instead, verify params are used correctly through integration:
```ruby
# ❌ UNNECESSARY - testing permit directly
test "permits name parameter" do
params = ActionController::Parameters.new(name: "test")
assert controller.send(:allowed_params).permitted?
end
# ✅ GOOD - verify indirectly through actual behavior
test "creates resource with provided name" do
post :create, params: { name: "test" }
assert_equal "test", Resource.last.name
end
```
## Key Principles
1. **Test behavior, not implementation** - verify the endpoint does what it should
2. **Pass real params** - let Rails handle parameter filtering naturally
3. **Assert on outcomes** - check that permitted params affect the result
4. **Unpermitted params should be ignored** - test that extra params don't cause errors
---
description: Do not push code to GitHub unless explicitly requested
alwaysApply: true
---
# Git Push Policy
**Never push code to GitHub unless the user explicitly asks you to push.**
## Allowed Git Operations (without asking)
- `git status`
- `git diff`
- `git log`
- `git add`
- `git commit`
- `git branch`
- `git checkout`
- `git stash`
## Requires Explicit User Request
- `git push` (any variant)
- `git push origin`
- `git push --force`
- `git push -u origin`
## Example
```
❌ BAD: Automatically pushing after a commit
"Let me commit and push this change..."
git commit -m "Fix bug" && git push
✅ GOOD: Commit only, inform user
"I've committed the change. Let me know when you'd like me to push it."
git commit -m "Fix bug"
```
When you've made commits that are ready to push, inform the user and wait for their explicit instruction to push.
---
description: Ask clarifying questions when starting work on a new Jira ticket
alwaysApply: true
---
# Jira Ticket Clarification Questions
When the user mentions starting work on a new Jira ticket (e.g., "working on CW-1234", "starting ticket XYZ"), ask clarifying questions before diving into implementation.
## Acceptance Criteria Questions
- What is the expected behavior when this is complete?
- Are there specific edge cases to handle?
- What should happen in error scenarios?
- Are there any performance requirements?
- What testing approach is expected (unit tests, integration tests, manual testing)?
## Context Gathering Questions
- Are there related tickets or prior work I should reference?
- Are there existing code patterns in this area I should follow?
- Which components/files are likely affected?
- Are there any dependencies on other teams or services?
- Is this behind a feature flag (Arturo)?
## When to Ask
Ask these questions when:
- User explicitly mentions a Jira ticket number
- User describes a new feature or bug fix without providing context
- The scope seems ambiguous
## When to Skip
Skip clarifying questions when:
- User has already provided detailed context
- User says "just do it" or indicates urgency
- It's a follow-up to an existing conversation with established context
---
description: Default formatting guidelines for writing to Logseq via MCP tools
alwaysApply: true
---
# Logseq MCP Default Formatting
**Always follow these rules when using Logseq MCP tools.**
## Core Rules
1. **One item per block** - Each bullet point, sentence, or piece of content must be its own separate block
2. **Bold for section headers** - Use `**Section Title**` instead of markdown `##` or `###`
3. **Italics for sub-headers** - Use `*Sub-section*` for secondary headings
4. **Horizontal rules for separation** - Use `---` blocks between major sections
5. **No newlines in content** - Never include `\n` in block content strings
## Formatting Reference
| Element | Use This | NOT This |
|---------|----------|----------|
| Section header | `**Summary**` | `## Summary` |
| Sub-header | `*Details:*` | `### Details` |
| Section separator | `---` | (nothing) |
| Multiple items | Separate blocks | `item1\nitem2` |
## Standard Page Structure
```
Block 1: "**Summary**"
Block 2: "Key point one"
Block 3: "Key point two"
Block 4: "---"
Block 5: "**Next Section**"
Block 6: "Content for this section"
Block 7: "*Sub-header:*"
Block 8: "Sub-content here"
```
## Workflow
1. Create page with `logseq_create_page` including properties (tags, status, date, etc.)
2. Add section headers as bold text blocks
3. Add content items as separate blocks (one per block)
4. Use `---` blocks to visually separate major sections
5. Use `is_page_block: true` with page name as `parent_block`
## Why These Rules
- Logseq shows "full content not displayed" warning for multi-line content
- Markdown `##` headers don't render as headers in Logseq blocks
- Bold text renders correctly and looks good as section headers
- One item per block follows Logseq's outliner philosophy
---
description: Append highlights to personal changelog on every commit
alwaysApply: true
---
# Personal Changelog
Every time the user asks to commit code, append a summary to `.cursor/CHANGELOG.md` BEFORE creating the commit.
## Changelog Location
`.cursor/CHANGELOG.md` in the current repo root (gitignored, personal only).
## Format
Append a new entry at the end of the file (before any trailing whitespace), using this format:
```markdown
## YYYY-MM-DD | branch-name
**Commit:** Short commit message here
- Highlight of what changed and why
- Another highlight if multiple things changed
- Reference ticket numbers (e.g., CW-1234) when applicable
---
```
## Rules
1. **Always append, never overwrite** existing entries
2. **Focus on the "why" and impact**, not just file names
3. **Keep it concise** - 2-5 bullet points per commit
4. **Include ticket/PR references** when available
5. **Group related changes** if committing multiple files
6. Do NOT include the changelog update in the git commit itself (it's gitignored anyway)
7. Write in first person ("Added...", "Fixed...", "Refactored...")
## Example
```markdown
## 2025-02-05 | drhenner/SLA_policy_confusion_CW-4129
**Commit:** Bump protobuf clients to 9.1583.0
- Updated zendesk_protobuf_clients and zendesk_grpc_clients for new SlaPolicyChanged schema
- Required google-protobuf bump from 3.x to 4.x for compatibility
- Supports CW-4129: SLA policy assignment tracking improvements
---
```
---
description: Add accomplishments to quarterly notes with guided questions
alwaysApply: true
---
# Quarterly Accomplishments Notes
When the user asks to "add to notes", "log this accomplishment", "record this for quarterly review", or similar:
## Notes File Location
`/Users/dhenner/Code/drhenner/quarterly_accomplishments/config/notes.yml`
## Workflow
1. **Determine category** by asking:
```
Which category best fits this accomplishment?
1. Slack/helping others
2. Mentoring
3. Presentation/knowledge sharing
4. Process improvement
5. Other (cross-team, incident, hiring, etc.)
```
2. **Ask category-specific follow-ups:**
### For Slack/Helping Others:
- What channel or context? (e.g., #classic-dev, DM, meeting)
- Who did you help? (person, team, or "multiple engineers")
- What was the impact? (time saved, unblocked work, etc.)
### For Mentoring:
- Who did you mentor? (new hire, intern, team member)
- What did you help with? (onboarding, code review, architecture)
### For Presentations:
- What was the title/topic?
- Who was the audience? (team, org, external)
- When was it? (approximate date)
### For Process Improvements:
- What did you improve? (runbook, tooling, documentation)
- What was the impact? (time saved, errors prevented)
### For Other:
- What category? (cross-team, incident response, hiring, etc.)
- Brief description of contribution
3. **Format and add** to the appropriate section in notes.yml
## Notes Format
```yaml
slack_highlights:
- channel: "#channel-name"
description: "What you helped with"
impact: "Result/outcome"
mentoring:
- who: "Person or group"
description: "What you did"
presentations:
- title: "Title"
audience: "Who attended"
date: "YYYY-MM-DD"
process_improvements:
- description: "What you improved"
impact: "How it helped"
other:
- category: "Category name"
description: "What you did"
impact: "Optional impact"
```
## Example Interaction
User: "Add to notes - just helped Sarah debug the SLA calculation issue"
Assistant response:
1. Ask: "Which channel/context was this in?"
2. Ask: "What was the impact - did it unblock her work?"
3. Add entry to slack_highlights section
---
description: Security considerations when reviewing code changes and PRs
globs: ["**/*.rb", "**/*.erb", "**/*.js", "**/*.ts"]
---
# Security Review Checklist
When reviewing code changes or PRs, **always consider these security concerns**:
## 🔐 Authorization & Access Control
### Account Scoping (CRITICAL for Multi-tenancy)
Every database query must be scoped to the current account to prevent data leakage between tenants:
```ruby
# ✅ Good - scoped to account
account.tickets.find(params[:id])
Ticket.where(account_id: current_account.id).find(params[:id])
# ❌ Bad - unscoped query can access other accounts' data
Ticket.find(params[:id])
Ticket.where(id: params[:id]).first
```
### Permission Checks
Verify the user has permission to perform the action:
```ruby
# ✅ Good - explicit authorization check
def update
@ticket = current_account.tickets.find(params[:id])
authorize! :update, @ticket # or use access policies
# ...
end
# ❌ Bad - no authorization check
def update
@ticket = Ticket.find(params[:id])
@ticket.update(ticket_params)
end
```
## 💉 Injection Vulnerabilities
### SQL Injection
Never interpolate user input directly into SQL:
```ruby
# ✅ Good - parameterized query
User.where("email = ?", params[:email])
User.where(email: params[:email])
# ❌ Bad - SQL injection vulnerability
User.where("email = '#{params[:email]}'")
User.where("email = " + params[:email])
```
### Command Injection
Never pass user input to shell commands:
```ruby
# ✅ Good - use arrays to avoid shell interpretation
system("convert", input_file, output_file)
# ❌ Bad - command injection vulnerability
system("convert #{params[:filename]}")
`convert #{params[:filename]}`
```
## 🌐 Cross-Site Scripting (XSS)
### Output Encoding
All user-controlled data must be escaped in views:
```erb
<%# ✅ Good - auto-escaped %>
<%= user.name %>
<%# ❌ Bad - raw output, XSS vulnerability %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
<%== user.comment %>
```
### Be Careful With:
- `raw()`, `html_safe`, `<%==` in ERB
- `dangerouslySetInnerHTML` in React
- Rendering user content in JavaScript contexts
## 🔑 Sensitive Data Handling
### Don't Log Sensitive Data
```ruby
# ❌ Bad - logging sensitive information
Rails.logger.info("User login: #{user.email}, password: #{password}")
Rails.logger.info("API key: #{api_key}")
# ✅ Good - filter sensitive params
# Ensure config/initializers/filter_parameter_logging.rb includes sensitive fields
```
### Don't Expose Internal IDs/Data in Errors
```ruby
# ❌ Bad - exposes internal details
render json: { error: e.message, backtrace: e.backtrace }
# ✅ Good - generic error message
render json: { error: "An error occurred" }, status: :internal_server_error
# Log details internally via Sentry/zendesk_exceptions
```
## 🛡️ Mass Assignment
### Strong Parameters
Always whitelist permitted attributes:
```ruby
# ✅ Good - explicit permit
def ticket_params
params.require(:ticket).permit(:subject, :description, :priority)
end
# ❌ Bad - permits everything
params.require(:ticket).permit!
params[:ticket].to_unsafe_h
```
### Watch for Privilege Escalation
```ruby
# ❌ Dangerous - user could set admin: true
params.require(:user).permit(:name, :email, :admin, :role)
# ✅ Good - sensitive fields handled separately
params.require(:user).permit(:name, :email)
# Admin/role changes require separate authorization
```
## 🔄 CSRF & Request Forgery
### State-Changing Actions
All state-changing actions should:
- Use POST/PUT/PATCH/DELETE (not GET)
- Verify CSRF tokens (Rails does this by default)
```ruby
# ❌ Bad - GET request changes state
get '/users/:id/delete'
# ✅ Good - proper HTTP verb
delete '/users/:id'
```
## 🔗 Insecure Direct Object References (IDOR)
### Always Verify Ownership
```ruby
# ❌ Bad - IDOR vulnerability
@attachment = Attachment.find(params[:attachment_id])
# ✅ Good - scoped through association
@attachment = @ticket.attachments.find(params[:attachment_id])
```
## 📦 Dependency Security
### When Adding/Updating Gems
- Check for known vulnerabilities: `bundle audit`
- Review gem's security history
- Prefer well-maintained, widely-used gems
## 🚨 Red Flags to Watch For
| Pattern | Risk |
|---------|------|
| `Ticket.find(params[:id])` | Missing account scope |
| `raw()`, `html_safe` | Potential XSS |
| String interpolation in SQL | SQL injection |
| `permit!` or `to_unsafe_h` | Mass assignment |
| `system()` with string arg | Command injection |
| Logging request params | Sensitive data exposure |
| `skip_before_action :verify_authenticity_token` | CSRF bypass |
| Hardcoded secrets/API keys | Credential exposure |
## 📋 PR Security Checklist
Before approving any PR, verify:
- [ ] All queries are scoped to account
- [ ] Authorization checks are in place
- [ ] User input is validated and sanitized
- [ ] No raw SQL with string interpolation
- [ ] No `html_safe`/`raw` on user content
- [ ] Strong parameters properly configured
- [ ] Sensitive data not logged or exposed
- [ ] New gems have been security-reviewed
---
description: Testing conventions for Zendesk Classic - prefer real objects, use Arturo test helpers
globs: ["test/**/*.rb", "components/**/test/**/*.rb"]
---
# Testing Conventions
## Prefer Real Objects Over Stubs/Mocks
When writing tests, **prefer using real objects and fixtures over stubs and mocks**:
### ✅ Do: Use Real Objects
```ruby
# Good - use fixtures and real objects
test "user can update ticket" do
account = accounts(:acme)
user = users(:admin)
ticket = tickets(:problem_ticket)
ticket.update!(subject: "New subject")
assert_equal "New subject", ticket.reload.subject
end
# Good - create objects with FactoryBot when needed
test "creates a new ticket" do
account = accounts(:acme)
ticket = create(:ticket, account: account, subject: "Test")
assert ticket.persisted?
end
```
### ❌ Avoid: Excessive Stubbing
```ruby
# Avoid - stubbing everything makes tests brittle and hides real behavior
test "user can update ticket" do
ticket = stub(id: 1, subject: "Old")
ticket.stubs(:update!).returns(true)
ticket.stubs(:reload).returns(ticket)
ticket.stubs(:subject).returns("New subject")
# This test doesn't actually verify anything real
end
```
### When Stubs ARE Appropriate
- External API calls (use WebMock for HTTP)
- Time-sensitive operations (`travel_to`)
- Expensive operations that are not the focus of the test
- Third-party services
## Testing Arturo Feature Flags
When testing code behind an Arturo feature flag, **always test both enabled and disabled states** using the helpers from `ArturoTestHelper`.
### Required Pattern: Test Both States
```ruby
class MyFeatureTest < ActiveSupport::TestCase
# Test with Arturo DISABLED (old behavior)
describe_with_arturo_disabled :my_feature_flag do
test "behaves the old way when feature is off" do
# Assert old behavior
end
end
# Test with Arturo ENABLED (new behavior)
describe_with_arturo_enabled :my_feature_flag do
test "behaves the new way when feature is on" do
# Assert new behavior
end
end
end
```
### Or Use the Combined Helper
```ruby
class MyFeatureTest < ActiveSupport::TestCase
# Tests both states automatically
describe_with_and_without_arturo_enabled :my_feature_flag do
test "works correctly" do
if @arturo_enabled
# Assert new behavior
else
# Assert old behavior
end
end
end
end
```
### Available Arturo Test Helpers
#### Option 1: Describe Blocks (Preferred for grouping tests)
| Helper | Use Case |
|--------|----------|
| `describe_with_arturo_enabled :flag` | Test with feature ON |
| `describe_with_arturo_disabled :flag` | Test with feature OFF |
| `describe_with_and_without_arturo_enabled :flag` | Test both states |
| `describe_with_arturo_enabled_for_pod :flag` | Test pod-specific rollout |
| `describe_with_arturos [{arturo: :a, enabled: true}, ...]` | Multiple flags |
#### Option 2: Enable/Disable Within Tests
For more granular control within individual tests, use `Arturo.enable_feature!` and `Arturo.disable_feature!`:
```ruby
test "behaves differently based on feature flag" do
# Test with feature disabled
Arturo.disable_feature!(:my_feature_flag)
assert_equal "old behavior", subject.do_something
# Test with feature enabled
Arturo.enable_feature!(:my_feature_flag)
assert_equal "new behavior", subject.do_something
end
```
You can also scope to specific subdomains or pods:
```ruby
test "feature enabled for specific subdomain" do
Arturo.enable_feature_for!(:my_feature_flag, subdomains: ['acme'], pods: [1])
# ...
end
```
**Pros of enable/disable approach:**
- More accurate tests, less error prone
- Can be nested
- State automatically resets after test completes
- Works well for testing transitions within a single test
**Con:** Requires database writes
### ⚠️ Important Warning
The Arturo **must be registered in `app/models/account/capabilities.rb`** before using these test helpers. Failing to do so causes false positives, incidents, and failed deploys.
```ruby
# In app/models/account/capabilities.rb
feature :my_feature_flag, arturo: !Rails.env.test? # enabled by default in tests
```
### Accessing Arturo State in Tests
Inside `describe_with_arturo_*` blocks, you can check the current state:
```ruby
describe_with_and_without_arturo_enabled :my_feature do
test "conditional behavior" do
result = some_method_that_checks_arturo
if @arturo_enabled
assert_equal "new result", result
else
assert_equal "old result", result
end
end
end
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment