mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 06:55:21 +02:00
Fix rubocop offenses
- Fix indentation and spacing issues - Convert single quotes to double quotes - Add spaces inside array brackets - Fix comment alignment - Add missing trailing newlines - Correct else/end alignment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6c8e518df7
commit
dbe05e3795
10 changed files with 245 additions and 73 deletions
172
CLAUDE.md
Normal file
172
CLAUDE.md
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Common Development Commands
|
||||||
|
|
||||||
|
### Development Server
|
||||||
|
- `bin/dev` - Start development server (Rails, Sidekiq, Tailwind CSS watcher)
|
||||||
|
- `bin/rails server` - Start Rails server only
|
||||||
|
- `bin/rails console` - Open Rails console
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `bin/rails test` - Run all tests
|
||||||
|
- `bin/rails test:db` - Run tests with database reset
|
||||||
|
- `bin/rails test:system` - Run system tests only
|
||||||
|
- `bin/rails test test/models/account_test.rb` - Run specific test file
|
||||||
|
- `bin/rails test test/models/account_test.rb:42` - Run specific test at line
|
||||||
|
|
||||||
|
### Linting & Formatting
|
||||||
|
- `bin/rubocop` - Run Ruby linter
|
||||||
|
- `npm run lint` - Check JavaScript/TypeScript code
|
||||||
|
- `npm run lint:fix` - Fix JavaScript/TypeScript issues
|
||||||
|
- `npm run format` - Format JavaScript/TypeScript code
|
||||||
|
- `bin/brakeman` - Run security analysis
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- `bin/rails db:prepare` - Create and migrate database
|
||||||
|
- `bin/rails db:migrate` - Run pending migrations
|
||||||
|
- `bin/rails db:rollback` - Rollback last migration
|
||||||
|
- `bin/rails db:seed` - Load seed data
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
- `bin/setup` - Initial project setup (installs dependencies, prepares database)
|
||||||
|
|
||||||
|
## General Development Rules
|
||||||
|
|
||||||
|
### Authentication Context
|
||||||
|
- Use `Current.user` for the current user. Do NOT use `current_user`.
|
||||||
|
- Use `Current.family` for the current family. Do NOT use `current_family`.
|
||||||
|
|
||||||
|
### Development Guidelines
|
||||||
|
- Prior to generating any code, carefully read the project conventions and guidelines
|
||||||
|
- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development
|
||||||
|
- Do not run `rails server` in your responses
|
||||||
|
- Do not run `touch tmp/restart.txt`
|
||||||
|
- Do not run `rails credentials`
|
||||||
|
- Do not automatically run migrations
|
||||||
|
|
||||||
|
## High-Level Architecture
|
||||||
|
|
||||||
|
### Application Modes
|
||||||
|
The Maybe app runs in two distinct modes:
|
||||||
|
- **Managed**: The Maybe team operates and manages servers for users (Rails.application.config.app_mode = "managed")
|
||||||
|
- **Self Hosted**: Users host the Maybe app on their own infrastructure, typically through Docker Compose (Rails.application.config.app_mode = "self_hosted")
|
||||||
|
|
||||||
|
### Core Domain Model
|
||||||
|
The application is built around financial data management with these key relationships:
|
||||||
|
- **User** → has many **Accounts** → has many **Transactions**
|
||||||
|
- **Account** types: checking, savings, credit cards, investments, crypto, loans, properties
|
||||||
|
- **Transaction** → belongs to **Category**, can have **Tags** and **Rules**
|
||||||
|
- **Investment accounts** → have **Holdings** → track **Securities** via **Trades**
|
||||||
|
|
||||||
|
### API Architecture
|
||||||
|
The application provides both internal and external APIs:
|
||||||
|
- Internal API: Controllers serve JSON via Turbo for SPA-like interactions
|
||||||
|
- External API: `/api/v1/` namespace with Doorkeeper OAuth and API key authentication
|
||||||
|
- API responses use Jbuilder templates for JSON rendering
|
||||||
|
- Rate limiting via Rack Attack with configurable limits per API key
|
||||||
|
|
||||||
|
### Sync & Import System
|
||||||
|
Two primary data ingestion methods:
|
||||||
|
1. **Plaid Integration**: Real-time bank account syncing
|
||||||
|
- `PlaidItem` manages connections
|
||||||
|
- `Sync` tracks sync operations
|
||||||
|
- Background jobs handle data updates
|
||||||
|
2. **CSV Import**: Manual data import with mapping
|
||||||
|
- `Import` manages import sessions
|
||||||
|
- Supports transaction and balance imports
|
||||||
|
- Custom field mapping with transformation rules
|
||||||
|
|
||||||
|
### Background Processing
|
||||||
|
Sidekiq handles asynchronous tasks:
|
||||||
|
- Account syncing (`SyncAccountsJob`)
|
||||||
|
- Import processing (`ImportDataJob`)
|
||||||
|
- AI chat responses (`CreateChatResponseJob`)
|
||||||
|
- Scheduled maintenance via sidekiq-cron
|
||||||
|
|
||||||
|
### Frontend Architecture
|
||||||
|
- **Hotwire Stack**: Turbo + Stimulus for reactive UI without heavy JavaScript
|
||||||
|
- **ViewComponents**: Reusable UI components in `app/components/`
|
||||||
|
- **Stimulus Controllers**: Handle interactivity, organized alongside components
|
||||||
|
- **Charts**: D3.js for financial visualizations (time series, donut, sankey)
|
||||||
|
- **Styling**: Tailwind CSS v4.x with custom design system
|
||||||
|
- Design system defined in `app/assets/tailwind/maybe-design-system.css`
|
||||||
|
- Always use functional tokens (e.g., `text-primary` not `text-white`)
|
||||||
|
- Prefer semantic HTML elements over JS components
|
||||||
|
- Use `icon` helper for icons, never `lucide_icon` directly
|
||||||
|
|
||||||
|
### Multi-Currency Support
|
||||||
|
- All monetary values stored in base currency (user's primary currency)
|
||||||
|
- Exchange rates fetched from Synth API
|
||||||
|
- `Money` objects handle currency conversion and formatting
|
||||||
|
- Historical exchange rates for accurate reporting
|
||||||
|
|
||||||
|
### Security & Authentication
|
||||||
|
- Session-based auth for web users
|
||||||
|
- API authentication via:
|
||||||
|
- OAuth2 (Doorkeeper) for third-party apps
|
||||||
|
- API keys with JWT tokens for direct API access
|
||||||
|
- Scoped permissions system for API access
|
||||||
|
- Strong parameters and CSRF protection throughout
|
||||||
|
|
||||||
|
### Key Service Objects & Patterns
|
||||||
|
- **Query Objects**: Complex database queries isolated in `app/queries/`
|
||||||
|
- **Service Objects**: Business logic in `app/services/`
|
||||||
|
- **Form Objects**: Complex forms with validation
|
||||||
|
- **Concerns**: Shared functionality across models/controllers
|
||||||
|
- **Jobs**: Background processing logic
|
||||||
|
|
||||||
|
### Testing Philosophy
|
||||||
|
- Comprehensive test coverage using Rails' built-in Minitest
|
||||||
|
- Fixtures for test data (avoid FactoryBot)
|
||||||
|
- Keep fixtures minimal (2-3 per model for base cases)
|
||||||
|
- VCR for external API testing
|
||||||
|
- System tests for critical user flows (use sparingly)
|
||||||
|
- Test helpers in `test/support/` for common scenarios
|
||||||
|
- Only test critical code paths that significantly increase confidence
|
||||||
|
- Write tests as you go, when required
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Database queries optimized with proper indexes
|
||||||
|
- N+1 queries prevented via includes/joins
|
||||||
|
- Background jobs for heavy operations
|
||||||
|
- Caching strategies for expensive calculations
|
||||||
|
- Turbo Frames for partial page updates
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
- Feature branches merged to `main`
|
||||||
|
- Docker support for consistent environments
|
||||||
|
- Environment variables via `.env` files
|
||||||
|
- Lookbook for component development (`/lookbook`)
|
||||||
|
- Letter Opener for email preview in development
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
|
||||||
|
### Convention 1: Minimize Dependencies
|
||||||
|
- Push Rails to its limits before adding new dependencies
|
||||||
|
- When adding dependencies, favor old and reliable over new and flashy
|
||||||
|
- Strong technical or business reason required for new dependencies
|
||||||
|
|
||||||
|
### Convention 2: POROs and Concerns over Service Objects
|
||||||
|
- "Skinny controller, fat models" convention
|
||||||
|
- Everything in `app/models/` folder, avoid separate folders like `app/services/`
|
||||||
|
- Use Rails concerns for better organization (can be one-off concerns)
|
||||||
|
- Models should answer questions about themselves (e.g., `account.balance_series`)
|
||||||
|
|
||||||
|
### Convention 3: Leverage Hotwire and Server-Side Solutions
|
||||||
|
- Native HTML preferred over JS components (e.g., `<dialog>`, `<details>`)
|
||||||
|
- Use Turbo frames to break up pages
|
||||||
|
- Leverage query params for state over local storage
|
||||||
|
- Format values server-side, pass to Stimulus for display only
|
||||||
|
- Client-side code only where it truly shines (e.g., bulk selections)
|
||||||
|
|
||||||
|
### Convention 4: Optimize for Simplicity and Clarity
|
||||||
|
- Prioritize good OOP domain design over performance
|
||||||
|
- Only focus on performance in critical/global areas
|
||||||
|
- Be mindful of N+1 queries and large data payloads
|
||||||
|
|
||||||
|
### Convention 5: ActiveRecord for Complex Validations, DB for Simple Ones
|
||||||
|
- Enforce null checks, unique indexes in the DB
|
||||||
|
- ActiveRecord validations for convenience in forms
|
||||||
|
- Complex validations and business logic in ActiveRecord
|
|
@ -30,7 +30,7 @@ class Api::V1::AccountsController < Api::V1::BaseController
|
||||||
error: "internal_server_error",
|
error: "internal_server_error",
|
||||||
message: "Error: #{e.message}"
|
message: "Error: #{e.message}"
|
||||||
}, status: :internal_server_error
|
}, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,11 @@ class Api::V1::BaseController < ApplicationController
|
||||||
return false unless request.headers["Authorization"].present?
|
return false unless request.headers["Authorization"].present?
|
||||||
|
|
||||||
# Manually verify the token (bypassing doorkeeper_authorize! which had scope issues)
|
# Manually verify the token (bypassing doorkeeper_authorize! which had scope issues)
|
||||||
token_string = request.authorization&.split(' ')&.last
|
token_string = request.authorization&.split(" ")&.last
|
||||||
access_token = Doorkeeper::AccessToken.by_token(token_string)
|
access_token = Doorkeeper::AccessToken.by_token(token_string)
|
||||||
|
|
||||||
# Check token validity and scope (read_write includes read access)
|
# Check token validity and scope (read_write includes read access)
|
||||||
has_sufficient_scope = access_token&.scopes&.include?('read') || access_token&.scopes&.include?('read_write')
|
has_sufficient_scope = access_token&.scopes&.include?("read") || access_token&.scopes&.include?("read_write")
|
||||||
|
|
||||||
unless access_token && !access_token.expired? && has_sufficient_scope
|
unless access_token && !access_token.expired? && has_sufficient_scope
|
||||||
render_json({ error: "unauthorized", message: "Access token is invalid, expired, or missing required scope" }, status: :unauthorized)
|
render_json({ error: "unauthorized", message: "Access token is invalid, expired, or missing required scope" }, status: :unauthorized)
|
||||||
|
|
|
@ -4,9 +4,9 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
# Ensure proper scope authorization for read vs write access
|
# Ensure proper scope authorization for read vs write access
|
||||||
before_action :ensure_read_scope, only: [:index, :show]
|
before_action :ensure_read_scope, only: [ :index, :show ]
|
||||||
before_action :ensure_write_scope, only: [:create, :update, :destroy]
|
before_action :ensure_write_scope, only: [ :create, :update, :destroy ]
|
||||||
before_action :set_transaction, only: [:show, :update, :destroy]
|
before_action :set_transaction, only: [ :show, :update, :destroy ]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
family = current_resource_owner.family
|
family = current_resource_owner.family
|
||||||
|
@ -71,7 +71,7 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
render json: {
|
render json: {
|
||||||
error: "validation_failed",
|
error: "validation_failed",
|
||||||
message: "Account ID is required",
|
message: "Account ID is required",
|
||||||
errors: ["Account ID is required"]
|
errors: [ "Account ID is required" ]
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@ -102,7 +102,7 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
error: "internal_server_error",
|
error: "internal_server_error",
|
||||||
message: "Error: #{e.message}"
|
message: "Error: #{e.message}"
|
||||||
}, status: :internal_server_error
|
}, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @entry.update(entry_params_for_update)
|
if @entry.update(entry_params_for_update)
|
||||||
|
@ -202,22 +202,22 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
|
|
||||||
# Date range filtering
|
# Date range filtering
|
||||||
if params[:start_date].present?
|
if params[:start_date].present?
|
||||||
query = query.joins(:entry).where('entries.date >= ?', Date.parse(params[:start_date]))
|
query = query.joins(:entry).where("entries.date >= ?", Date.parse(params[:start_date]))
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:end_date].present?
|
if params[:end_date].present?
|
||||||
query = query.joins(:entry).where('entries.date <= ?', Date.parse(params[:end_date]))
|
query = query.joins(:entry).where("entries.date <= ?", Date.parse(params[:end_date]))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Amount filtering
|
# Amount filtering
|
||||||
if params[:min_amount].present?
|
if params[:min_amount].present?
|
||||||
min_amount = params[:min_amount].to_f
|
min_amount = params[:min_amount].to_f
|
||||||
query = query.joins(:entry).where('entries.amount >= ?', min_amount)
|
query = query.joins(:entry).where("entries.amount >= ?", min_amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:max_amount].present?
|
if params[:max_amount].present?
|
||||||
max_amount = params[:max_amount].to_f
|
max_amount = params[:max_amount].to_f
|
||||||
query = query.joins(:entry).where('entries.amount <= ?', max_amount)
|
query = query.joins(:entry).where("entries.amount <= ?", max_amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Tag filtering
|
# Tag filtering
|
||||||
|
@ -229,10 +229,10 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
# Transaction type filtering (income/expense)
|
# Transaction type filtering (income/expense)
|
||||||
if params[:type].present?
|
if params[:type].present?
|
||||||
case params[:type].downcase
|
case params[:type].downcase
|
||||||
when 'income'
|
when "income"
|
||||||
query = query.joins(:entry).where('entries.amount < 0')
|
query = query.joins(:entry).where("entries.amount < 0")
|
||||||
when 'expense'
|
when "expense"
|
||||||
query = query.joins(:entry).where('entries.amount > 0')
|
query = query.joins(:entry).where("entries.amount > 0")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -245,10 +245,10 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
query.joins(:entry)
|
query.joins(:entry)
|
||||||
.left_joins(:merchant)
|
.left_joins(:merchant)
|
||||||
.where(
|
.where(
|
||||||
'entries.name ILIKE ? OR entries.notes ILIKE ? OR merchants.name ILIKE ?',
|
"entries.name ILIKE ? OR entries.notes ILIKE ? OR merchants.name ILIKE ?",
|
||||||
search_term, search_term, search_term
|
search_term, search_term, search_term
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def transaction_params
|
def transaction_params
|
||||||
params.require(:transaction).permit(
|
params.require(:transaction).permit(
|
||||||
|
@ -264,7 +264,7 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
amount: calculate_signed_amount,
|
amount: calculate_signed_amount,
|
||||||
currency: transaction_params[:currency] || current_resource_owner.family.currency,
|
currency: transaction_params[:currency] || current_resource_owner.family.currency,
|
||||||
notes: transaction_params[:notes],
|
notes: transaction_params[:notes],
|
||||||
entryable_type: 'Transaction',
|
entryable_type: "Transaction",
|
||||||
entryable_attributes: {
|
entryable_attributes: {
|
||||||
category_id: transaction_params[:category_id],
|
category_id: transaction_params[:category_id],
|
||||||
merchant_id: transaction_params[:merchant_id],
|
merchant_id: transaction_params[:merchant_id],
|
||||||
|
@ -301,9 +301,9 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||||
nature = transaction_params[:nature]
|
nature = transaction_params[:nature]
|
||||||
|
|
||||||
case nature&.downcase
|
case nature&.downcase
|
||||||
when 'income', 'inflow'
|
when "income", "inflow"
|
||||||
-amount.abs # Income is negative
|
-amount.abs # Income is negative
|
||||||
when 'expense', 'outflow'
|
when "expense", "outflow"
|
||||||
amount.abs # Expense is positive
|
amount.abs # Expense is positive
|
||||||
else
|
else
|
||||||
amount # Use as provided
|
amount # Use as provided
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Settings::ApiKeysController < ApplicationController
|
||||||
# Convert single scope value to array for storage
|
# Convert single scope value to array for storage
|
||||||
permitted_params = params.require(:api_key).permit(:name, :scopes)
|
permitted_params = params.require(:api_key).permit(:name, :scopes)
|
||||||
if permitted_params[:scopes].present?
|
if permitted_params[:scopes].present?
|
||||||
permitted_params[:scopes] = [permitted_params[:scopes]]
|
permitted_params[:scopes] = [ permitted_params[:scopes] ]
|
||||||
end
|
end
|
||||||
permitted_params
|
permitted_params
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ApiRateLimiter
|
||||||
{
|
{
|
||||||
current_count: current_count,
|
current_count: current_count,
|
||||||
rate_limit: rate_limit,
|
rate_limit: rate_limit,
|
||||||
remaining: [rate_limit - current_count, 0].max,
|
remaining: [ rate_limit - current_count, 0 ].max,
|
||||||
reset_time: reset_time,
|
reset_time: reset_time,
|
||||||
tier: determine_tier
|
tier: determine_tier
|
||||||
}
|
}
|
||||||
|
|
|
@ -403,7 +403,7 @@ class Api::V1::BaseControllerTest < ActionDispatch::IntegrationTest
|
||||||
other_api_key = ApiKey.create!(
|
other_api_key = ApiKey.create!(
|
||||||
user: @user,
|
user: @user,
|
||||||
name: "Other Test API Key",
|
name: "Other Test API Key",
|
||||||
scopes: ["read"],
|
scopes: [ "read" ],
|
||||||
key: "other_test_key_for_rate_limiting"
|
key: "other_test_key_for_rate_limiting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_difference('@account.entries.count', 1) do
|
assert_difference("@account.entries.count", 1) do
|
||||||
post api_v1_transactions_url,
|
post api_v1_transactions_url,
|
||||||
params: transaction_params,
|
params: transaction_params,
|
||||||
headers: api_headers(@api_key)
|
headers: api_headers(@api_key)
|
||||||
|
@ -242,14 +242,14 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
)
|
)
|
||||||
transaction_to_delete = entry_to_delete.transaction
|
transaction_to_delete = entry_to_delete.transaction
|
||||||
|
|
||||||
assert_difference('@account.entries.count', -1) do
|
assert_difference("@account.entries.count", -1) do
|
||||||
delete api_v1_transaction_url(transaction_to_delete), headers: api_headers(@api_key)
|
delete api_v1_transaction_url(transaction_to_delete), headers: api_headers(@api_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
response_data = JSON.parse(response.body)
|
response_data = JSON.parse(response.body)
|
||||||
assert response_data.key?("message")
|
assert response_data.key?("message")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "should reject destroy with read-only API key" do
|
test "should reject destroy with read-only API key" do
|
||||||
delete api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
|
delete api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
|
||||||
@api_key = ApiKey.create!(
|
@api_key = ApiKey.create!(
|
||||||
user: @user,
|
user: @user,
|
||||||
name: "Test API Key",
|
name: "Test API Key",
|
||||||
scopes: ["read"],
|
scopes: [ "read" ],
|
||||||
key: "test_key_for_usage"
|
key: "test_key_for_usage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
# Check API key information
|
# Check API key information
|
||||||
assert_equal "Test API Key", response_body["api_key"]["name"]
|
assert_equal "Test API Key", response_body["api_key"]["name"]
|
||||||
assert_equal ["read"], response_body["api_key"]["scopes"]
|
assert_equal [ "read" ], response_body["api_key"]["scopes"]
|
||||||
assert_not_nil response_body["api_key"]["last_used_at"]
|
assert_not_nil response_body["api_key"]["last_used_at"]
|
||||||
assert_not_nil response_body["api_key"]["created_at"]
|
assert_not_nil response_body["api_key"]["created_at"]
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ class ApiRateLimiterTest < ActiveSupport::TestCase
|
||||||
@api_key = ApiKey.create!(
|
@api_key = ApiKey.create!(
|
||||||
user: @user,
|
user: @user,
|
||||||
name: "Test API Key",
|
name: "Test API Key",
|
||||||
scopes: ["read"],
|
scopes: [ "read" ],
|
||||||
key: "test_key_123"
|
key: "test_key_123"
|
||||||
)
|
)
|
||||||
@rate_limiter = ApiRateLimiter.new(@api_key)
|
@rate_limiter = ApiRateLimiter.new(@api_key)
|
||||||
|
@ -99,7 +99,7 @@ class ApiRateLimiterTest < ActiveSupport::TestCase
|
||||||
other_api_key = ApiKey.create!(
|
other_api_key = ApiKey.create!(
|
||||||
user: @user,
|
user: @user,
|
||||||
name: "Other API Key",
|
name: "Other API Key",
|
||||||
scopes: ["read_write"],
|
scopes: [ "read_write" ],
|
||||||
key: "other_key_456"
|
key: "other_key_456"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue