From 2767edc7dbbdcabbc46fb9023406104231e6241a Mon Sep 17 00:00:00 2001 From: Azuna <36605286+azunaVT@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:36:21 +0000 Subject: [PATCH] Added session model, tests and migrations --- Makefile | 6 +- app/models/session.rb | 32 +++++++++ db/migrate/20260112020109_create_sessions.rb | 16 +++++ db/schema.rb | 16 ++++- test/fixtures/sessions.yml | 6 ++ test/models/session_test.rb | 71 ++++++++++++++++++++ test/test_helper.rb | 2 +- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 app/models/session.rb create mode 100644 db/migrate/20260112020109_create_sessions.rb create mode 100644 test/fixtures/sessions.yml create mode 100644 test/models/session_test.rb diff --git a/Makefile b/Makefile index 256861f..3c3063a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: up down logs build test run +.PHONY: up down logs build test run migrate up: @docker-compose -f docker-compose.yml up -d down: @@ -10,4 +10,6 @@ build: test: @rails test run: - @rails server \ No newline at end of file + @rails server +migrate: + @rails db:migrate \ No newline at end of file diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..7cd717e --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,32 @@ +class Session < ApplicationRecord + belongs_to :user + + scope :active, -> { where(revoked_at: nil).where("expires_at > ?", Time.current) } + + SESSION_TTL = 2.hours + + validates :token, presence: true, uniqueness: true + validates :expires_at, presence: true + + before_validation :set_expiration, on: :create + + def revoked? + revoked_at.present? + end + + def expired? + expires_at < Time.current + end + + def active? + !revoked? && !expired? + end + + def set_expiration + self.expires_at ||= SESSION_TTL.from_now + end + + def generate_token + self.token ||= SecureRandom.hex(32) + end +end diff --git a/db/migrate/20260112020109_create_sessions.rb b/db/migrate/20260112020109_create_sessions.rb new file mode 100644 index 0000000..8fdf1be --- /dev/null +++ b/db/migrate/20260112020109_create_sessions.rb @@ -0,0 +1,16 @@ +class CreateSessions < ActiveRecord::Migration[8.1] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + + t.string :token, null: false + t.datetime :expires_at, null: false + t.datetime :revoked_at + + t.timestamps + end + + add_index :sessions, :token, unique: true + add_index :sessions, :expires_at + end +end diff --git a/db/schema.rb b/db/schema.rb index 848d591..ff86380 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_08_050125) do +ActiveRecord::Schema[8.1].define(version: 2026_01_12_020109) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + create_table "sessions", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.datetime "revoked_at" + t.string "token", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.index ["expires_at"], name: "index_sessions_on_expires_at" + t.index ["token"], name: "index_sessions_on_token", unique: true + t.index ["user_id"], name: "index_sessions_on_user_id" + end + create_table "users", force: :cascade do |t| t.datetime "created_at", null: false t.string "email", null: false @@ -21,4 +33,6 @@ t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true end + + add_foreign_key "sessions", "users" end diff --git a/test/fixtures/sessions.yml b/test/fixtures/sessions.yml new file mode 100644 index 0000000..54c484b --- /dev/null +++ b/test/fixtures/sessions.yml @@ -0,0 +1,6 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +alice_session: + user: alice + token: <%= SecureRandom.hex(32) %> + expires_at: <%= 30.days.from_now %> \ No newline at end of file diff --git a/test/models/session_test.rb b/test/models/session_test.rb new file mode 100644 index 0000000..431a09e --- /dev/null +++ b/test/models/session_test.rb @@ -0,0 +1,71 @@ +require "test_helper" + +class SessionTest < ActiveSupport::TestCase + def setup + @user = users(:alice) + end + + def test_fixture_session_is_active + session = sessions(:alice_session) + + assert session.active? + assert_equal @user, session.user + end + + def test_token_is_generated_on_create + session = Session.create!(user: @user, token: SecureRandom.hex(32)) + + assert session.token.present? + end + + def test_token_is_unique + existing = Session.create!(user: @user, token: SecureRandom.hex(32)) + + duplicate = Session.new( + user: @user, + token: existing.token, + expires_at: 30.days.from_now + ) + + refute duplicate.valid? + assert_includes duplicate.errors[:token], "has already been taken" + end + + def test_expired_session + session = Session.new( + user: @user, + expires_at: 1.hour.ago + ) + + assert session.expired? + refute session.active? + end + + def test_revoked_session + session = Session.new( + user: @user, + revoked_at: Time.current + ) + + assert session.revoked? + refute session.active? + end + + def test_active_scope_excludes_revoked_and_expired_sessions + active = Session.create!(user: @user, token: SecureRandom.hex(32)) + expired = Session.create!( + user: @user, + token: SecureRandom.hex(32), + expires_at: 1.hour.ago + ) + revoked = Session.create!( + user: @user, + token: SecureRandom.hex(32), + revoked_at: Time.current + ) + + assert_includes Session.active, active + refute_includes Session.active, expired + refute_includes Session.active, revoked + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index b18c239..524d4cf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,6 @@ ENV["RAILS_ENV"] ||= "test" -ActiveRecord::Migration.maintain_test_schema! require_relative "../config/environment" +ActiveRecord::Migration.maintain_test_schema! require "rails/test_help" require "minitest/reporters" Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new