Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions .junie/guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# ChainForge — Development Guidelines

This document captures project-specific practices for building, testing, and extending ChainForge. It assumes an advanced Ruby developer familiar with Bundler, RSpec, Docker, and MongoDB.

## Build and Configuration

- Ruby: 3.2.2 (see `Gemfile` and `Dockerfile`).
- App stack: Sinatra 4.x, Mongoid 7.0.x, MongoDB.
- Environment configuration is consumed via `dotenv` and `Mongoid`:
- `Mongoid.load!('./config/mongoid.yml', ENV['ENVIRONMENT'] || :development)` in `main.rb`
- `Mongoid.load!('./config/mongoid.yml', ENV['ENVIRONMENT'] || :test)` in `spec/spec_helper.rb`
- Required ENV (both development and test environments):
- `ENVIRONMENT` — one of `development` or `test` (symbol allowed by Mongoid). Defaults to `development` in app, `test` in specs when unset.
- `MONGO_DB_NAME` — database name (e.g., `chain_forge_dev` or `chain_forge_test`).
- `MONGO_DB_HOST` — MongoDB host (e.g., `127.0.0.1`).
- `MONGO_DB_PORT` — MongoDB port (e.g., `27017`).
- Mongoid config (`config/mongoid.yml`) uses only these ENV variables (no auth block). Ensure the vars are set or use Docker.

### Installing dependencies

```bash
# Ruby 3.2.2 assumed
gem install bundler -v 2.4.13
bundle install
```

### Running via Docker (recommended for a clean Mongo)

```bash
docker-compose up -d
# Exposes app on :1910 and mongo on :27017 by default
```

### Running app locally

```bash
# Ensure MongoDB is reachable at MONGO_DB_HOST:MONGO_DB_PORT and DB exists/auto-creates
ENVIRONMENT=development \
MONGO_DB_NAME=chain_forge_dev \
MONGO_DB_HOST=127.0.0.1 \
MONGO_DB_PORT=27017 \
bundle exec ruby main.rb -p 1910
```

## Testing

RSpec is configured in `spec/spec_helper.rb` and loads Mongoid in `:test` unless `ENVIRONMENT` overrides.

### Baseline run (verified)

The following has been verified to pass for unit-level specs on a fresh run with MongoDB available (e.g., via `docker-compose up -d`):

```bash
ENVIRONMENT=test \
MONGO_DB_NAME=chain_forge_test \
MONGO_DB_HOST=127.0.0.1 \
MONGO_DB_PORT=27017 \
bundle exec rspec spec/block_spec.rb spec/blockchain_spec.rb
```

Notes:
- The full suite currently includes API specs that may fail depending on content-type behavior and rack/test expectations. If you want green runs while iterating on models, scope to the unit specs above or filter via `-e/--example` or `--pattern`.
- Full-suite run example (may include failing API specs today):

```bash
ENVIRONMENT=test MONGO_DB_NAME=chain_forge_test MONGO_DB_HOST=127.0.0.1 MONGO_DB_PORT=27017 \
bundle exec rspec
```

### Focused runs and filters

- Single file: `bundle exec rspec spec/block_spec.rb`
- Single example: `bundle exec rspec spec/block_spec.rb:37`
- By example name: `bundle exec rspec -e 'validates the blockchain'`

### Coverage

SimpleCov is available (loaded on demand). To enable coverage, set an env var before running:

```bash
COVERAGE=true ENVIRONMENT=test MONGO_DB_NAME=chain_forge_test MONGO_DB_HOST=127.0.0.1 MONGO_DB_PORT=27017 \
bundle exec rspec
# Coverage report will be in coverage/index.html
```

### Adding a new spec (demonstrated workflow)

The following process was executed and verified end-to-end; replicate as needed:

1. Create a new spec file under `spec/`, e.g. `spec/demo_math_spec.rb`:
```ruby
# frozen_string_literal: true
require 'rspec'

RSpec.describe 'Math demo' do
it 'adds numbers' do
expect(1 + 1).to eq(2)
end
end
```
2. Run just that spec:
```bash
ENVIRONMENT=test MONGO_DB_NAME=chain_forge_test MONGO_DB_HOST=127.0.0.1 MONGO_DB_PORT=27017 \
bundle exec rspec spec/demo_math_spec.rb
```
Expected: 1 example, 0 failures.
3. Remove the file once done (to keep repo clean):
```bash
rm spec/demo_math_spec.rb
```

### Test data and MongoDB

- Specs create and persist documents via Mongoid (e.g., `Block`, `Blockchain`). Use a dedicated `MONGO_DB_NAME` for tests to avoid polluting dev data.
- No cleaning strategy is configured. If isolation becomes necessary, consider adding `database_cleaner-mongoid` or dropping the test DB between runs.

## Additional Development Notes

### API behavior

- `main.rb` sets `content_type :json` for POST requests in a `before` block. If API specs expect more strict headers (e.g., `application/json` vs `text/html`), ensure routes are executed in the same Rack env as tests (Sinatra + rack-test). If content-type mismatches persist, review middleware or how tests mount the app.

### Code style and linting

- RuboCop with `rubocop-rspec` is configured in `.rubocop.yml`.
- Target Ruby: 3.2
- Selected Metrics/Style cops are tuned; specs and config directories are largely excluded from method/ block length checks.
- Run lint:
```bash
bundle exec rubocop
```

### Project structure

- Core domain:
- `src/block.rb` — `Block` model with SHA256 hashing and `valid_data?` verification.
- `src/blockchain.rb` — `Blockchain` aggregate with `add_genesis_block`, `add_block`, `integrity_valid?`.
- App entrypoint: `main.rb` (Sinatra routes for creating chains, adding blocks, validating block data).
- Tests: `spec/` directory with unit specs for `Block` and `Blockchain`, and API specs.

### Debugging tips

- Use focused RSpec runs (`-e`, `:line_number`).
- Inspect Mongo documents in a console by starting `irb` with the same environment:
```bash
ENVIRONMENT=development MONGO_DB_NAME=chain_forge_dev MONGO_DB_HOST=127.0.0.1 MONGO_DB_PORT=27017 \
bundle exec irb -r ./src/blockchain -r ./src/block
```
- When debugging hashing/time-sensitive assertions, compare against `created_at.to_i` (used in hash calculation).

### Docker notes

- The provided `docker-compose.yml` spins up `app` and `db`. The `app` container runs `ruby main.rb -p 1910` per `Dockerfile` CMD. For test runs, it’s simpler to run RSpec on the host and use the `db` service for Mongo.
- If you want to run tests inside the container, `docker-compose exec app bash` then run the same `ENVIRONMENT=test` commands with `MONGO_DB_HOST=db`.

---

This document reflects commands verified on 2025-11-08 23:58 local time. Keep it updated as the test suite and APIs evolve.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Metrics/BlockLength:
Exclude:
- 'spec/**/*'
- 'config/**/*'
AllowedMethods:
- namespace

# Style
Style/Documentation:
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ source 'https://rubygems.org'
ruby '3.2.2'

gem 'dotenv', '~> 2.7'
gem 'dry-validation', '~> 1.10'
gem 'mongoid', '~> 7.0.5'
gem 'rack-attack', '~> 6.7'
gem 'rackup', '~> 2.1.0'
gem 'rspec', '~> 3.10'
gem 'sinatra', '~> 4.0.0'
Expand Down
41 changes: 41 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,53 @@ GEM
zeitwerk (~> 2.3)
ast (2.4.3)
base64 (0.2.0)
bigdecimal (3.3.1)
bson (4.15.0)
concurrent-ruby (1.2.3)
diff-lcs (1.5.1)
docile (1.4.1)
dotenv (2.8.1)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-initializer (3.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-schema (1.14.1)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.1)
dry-initializer (~> 3.2)
dry-logic (~> 1.5)
dry-types (~> 1.8)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
dry-validation (1.11.1)
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
dry-initializer (~> 3.2)
dry-schema (~> 1.14)
zeitwerk (~> 2.6)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.16.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.22.0)
mongo (2.19.3)
bson (>= 4.14.1, < 5.0.0)
Expand All @@ -37,6 +74,8 @@ GEM
prism (1.6.0)
racc (1.8.1)
rack (3.0.8)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-protection (4.0.0)
base64 (>= 0.1.0)
rack (>= 3.0.0, < 4)
Expand Down Expand Up @@ -124,7 +163,9 @@ PLATFORMS

DEPENDENCIES
dotenv (~> 2.7)
dry-validation (~> 1.10)
mongoid (~> 7.0.5)
rack-attack (~> 6.7)
rack-test (~> 2.1)
rackup (~> 2.1.0)
rspec (~> 3.10)
Expand Down
28 changes: 28 additions & 0 deletions config/rack_attack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# Configure Rack::Attack for rate limiting
module Rack
class Attack
# Throttle all requests by IP (60 requests per minute)
throttle('req/ip', limit: 60, period: 60, &:ip)

# Throttle POST requests to /chain by IP (10 per minute)
throttle('chain/ip', limit: 10, period: 60) do |req|
req.ip if req.path == '/chain' && req.post?
end

# Throttle POST requests to block creation (30 per minute)
throttle('block/ip', limit: 30, period: 60) do |req|
req.ip if req.path.match?(%r{^/chain/.+/block$}) && req.post?
end

# Custom response for throttled requests
self.throttled_responder = lambda do |_env|
[
429,
{ 'Content-Type' => 'application/json' },
[{ error: 'Rate limit exceeded. Please try again later.' }.to_json]
]
end
end
end
27 changes: 17 additions & 10 deletions main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
require 'json'
require 'mongoid'
require 'dotenv/load'
require 'rack/attack'
require_relative 'src/blockchain'
require_relative 'src/validators'
require_relative 'config/rack_attack'

Mongoid.load!('./config/mongoid.yml', ENV['ENVIRONMENT'] || :development)

# Enable Rack::Attack middleware (disabled in test environment)
use Rack::Attack unless ENV['ENVIRONMENT'] == 'test'

# Set default content type for JSON responses
before do
content_type :json if request.post?
Expand All @@ -33,10 +39,14 @@

post '/chain/:id/block' do
block_data = parse_json_body
validation = BlockDataContract.new.call(block_data)

halt 400, { errors: validation.errors.to_h }.to_json if validation.failure?

chain_id = params[:id]
blockchain = find_block_chain(chain_id)
difficulty = validate_difficulty(block_data['difficulty'])
block = blockchain.add_block(block_data['data'], difficulty: difficulty)
difficulty = validation[:difficulty] || 2
block = blockchain.add_block(validation[:data], difficulty: difficulty)

{
chain_id: chain_id,
Expand All @@ -49,13 +59,17 @@

post '/chain/:id/block/:block_id/valid' do
block_data = parse_json_body
validation = BlockDataContract.new.call(block_data)

halt 400, { errors: validation.errors.to_h }.to_json if validation.failure?

chain_id = params[:id]
block_id = params[:block_id]
blockchain = find_block_chain(chain_id)
block = blockchain.blocks.find(block_id)
raise 'Block not found' unless block

valid = block.valid_data?(block_data['data'])
valid = block.valid_data?(validation[:data])

{
chain_id: chain_id,
Expand Down Expand Up @@ -99,11 +113,4 @@ def find_block_chain(chain_id)

blockchain
end

def validate_difficulty(difficulty)
difficulty = difficulty.nil? ? 2 : difficulty.to_i
halt 422, { error: 'Difficulty must be a positive integer' }.to_json if difficulty <= 0
halt 422, { error: 'Difficulty must be between 1 and 10' }.to_json if difficulty > 10
difficulty
end
end
18 changes: 18 additions & 0 deletions src/validators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require 'dry-validation'

# Validator for block data
class BlockDataContract < Dry::Validation::Contract
params do
required(:data).filled(:string)
optional(:difficulty).maybe(:integer)
end

rule(:difficulty) do
if value
key.failure('must be a positive integer') if value <= 0
key.failure('must be between 1 and 10') if value > 10
end
end
end