Raft Demo / Documentation

Writing Tests

  1. Writing Tests
    1. No Fixtures
    2. The once {} Pattern
    3. The creator Helper
      1. Encryption Context
      2. Globals
    4. Tests That Delete Records
    5. Authentication in Integration Tests
    6. Test Encryption Assertions
  2. Running Tests
    1. All Tests
    2. Single Test File
    3. Single Test Method
    4. Engine Tests Only
    5. Engine Test Helpers

No Fixtures

The app uses database-level encryption (via raft_encrypts). Fixtures don’t work because encrypted columns would need pre-encrypted fixture values tied to specific test keys. Instead of fixtures, tests create records inline using the creator helper and the once {} pattern.

The once {} Pattern

Database records live for the duration of a single test file, not per test method.
This is why parallelization is disabled — each test process would independently
truncate the database, causing race conditions.

In setup, use once {} to initialize records that all tests in the file will share:

def setup
  once {
    creator.user              # creates a user with a userspace
    creator.workspace         # creates a groupspace with memberships
  }
end

The call once {} does three things:

  1. Truncates the database (via DatabaseCleaner)
  2. Resets global references (@@user, @@workspace, etc.)
  3. Yields the block — this only runs once per test class

Because once {} sets self.class.use_transactional_tests = false, there is no
transaction wrapping individual tests. Records created in once {} persist across
all test methods in the class.

The creator Helper

creator returns a TestCreator instance that sets up users, workspaces, and encryption
keys for you.

# creates a user with a userspace, profile, and memberships
creator.user

# creates an additional user with a different username
creator.user("fredrick")

# creates a Groupspace with memberships. by default the global user is the admin
creator.workspace

# creates a workspace with specific roles
creator.workspace("Team Space", admin: user1, edit: user2)

Encryption Context

creator.user and creator.workspace automatically set up vaults and encryption keys. The
first call to creator.user also sets the global user (@@user or global_user), and the first call to
creator.workspace sets the global workspace (@@workspace or global_workspace) and switches the encryption
context to that workspace.

Because of this, you only need to call encryption_context() directly if a test needs to
operate under a different encryption context than the one set up in once {}:

def test_something
  encryption_context(global_workspace) do
    # ... create or read records in the workspace
  end
  encryption_context(global_user.userspace) do
    # ... create or read records in the userspace
  end
end

encryption_context accepts a block and yields within that context, or sets the key globally
if called without a block.

Globals

The creator helper sets these globals on the test class:

Global Set by Accessor
@@user creator.user global_user
@@workspace creator.workspace global_workspace
@@vault (automatic) global_vault

These are available in any test method after setup runs.

Tests That Delete Records

Since once {} creates records that are shared across every test method, never delete the
records created in once {}
. If a test needs to destroy something, create a new record
specifically for that test and destroy it:

def test_destroy
  label = Label.create!(workspace: global_workspace, name: "test", hue: 0)
  assert_difference("LabelAssignment.count", -1) do
    label.destroy
  end
end

The same principle applies to any state mutation: create fresh records for the test rather
than modifying the ones set up in once {}.

Authentication in Integration Tests

Controller and integration tests use the demo auth system to log in:

def setup
  once {
    creator.user
  }
end

test "requires login" do
  login_as(global_user)
  get some_path
  assert_response :success
end

Test Encryption Assertions

The test helper provides assert_encrypted to verify a field is properly encrypted:

assert_encrypted record: doc, field: :title, cleartext: "Secret Title"

This checks that:

  • The field uses Raft::EncryptedAttributeType
  • The ciphertext differs from the cleartext
  • The ciphertext decrypts to the original value
  • A fresh database read returns the correct cleartext

Running Tests

All Tests

rake test

This runs the main app’s tests first, then iterates over every directory in engines/ and
gems/.

For directories that contain a test/dummy folder (a full Rails engine with its own dummy app), it runs:

cd engine/ && RAILS_ENV=test rake db:test:prepare test

For directories without a dummy app (most engines), it runs from the parent app:

bin/rails test engine/test

Single Test File

bin/rails test test/models/my_test.rb
bin/rails test engines/raft_files/test/models/files/file_tree_test.rb

Single Test Method

bin/rails test test/models/my_test.rb:15

Engine Tests Only

rake engine:test

Engine Test Helpers

Most engines don’t have their own dummy Rails app. Instead, their test_helper.rb simply
requires the main app’s test helper:

# engines/raft_files/test/test_helper.rb
require_relative "../../../test/test_helper"

module ActiveSupport
  class TestCase
    # engine-specific helpers go here
  end
end

This gives engine tests access to all the same helpers (creator, encryption_context,
once, login_as, etc.) as the main app’s tests.