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
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
ruby: ['3.0', '3.1', '3.2']
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0']

steps:
- name: Checkout repository
Expand All @@ -33,7 +33,7 @@ jobs:
run: cd spec/dummy_rails && bundle exec rails db:migrate

- name: Run tests
env:
RUBYOPT: '-rbundler/setup -rrbs/test/setup'
RBS_TEST_TARGET: 'TinyAdmin::*'
#env:
# RUBYOPT: '-rbundler/setup -rrbs/test/setup'
# RBS_TEST_TARGET: 'TinyAdmin::*'
run: bin/rspec --profile
5 changes: 1 addition & 4 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
inherit_from:
- https://relaxed.ruby.style/rubocop.yml

require:
plugins:
- rubocop-packaging
- rubocop-performance
- rubocop-rspec
Expand All @@ -26,9 +26,6 @@ Lint/UnusedMethodArgument:
RSpec/ExampleLength:
Max: 20

RSpec/Rails/InferredSpecType:
Enabled: false

Style/ExplicitBlockArgument:
Enabled: false

Expand Down
46 changes: 46 additions & 0 deletions spec/lib/tiny_admin/actions/basic_action_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

RSpec.describe TinyAdmin::Actions::BasicAction do
let(:action) { described_class.new }

describe "#attribute_options" do
it "returns nil for nil input" do
expect(action.attribute_options(nil)).to be_nil
end

it "handles simple string field names" do
result = action.attribute_options(["id", "name"])
expect(result).to eq(
"id" => {field: "id"},
"name" => {field: "name"}
)
end

it "handles single-entry hash with method shorthand" do
result = action.attribute_options([{title: "downcase, capitalize"}])
expect(result).to eq(
"title" => {field: "title", method: "downcase, capitalize"}
)
end

it "handles multi-entry hash with explicit field key" do
result = action.attribute_options([{field: "author_id", link_to: "authors"}])
expect(result).to eq(
"author_id" => {field: "author_id", link_to: "authors"}
)
end

it "handles mixed input types" do
result = action.attribute_options([
"id",
{title: "upcase"},
{field: "created_at", method: "strftime, %Y-%m-%d"}
])
expect(result).to eq(
"id" => {field: "id"},
"title" => {field: "title", method: "upcase"},
"created_at" => {field: "created_at", method: "strftime, %Y-%m-%d"}
)
end
end
end
98 changes: 98 additions & 0 deletions spec/lib/tiny_admin/field_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

RSpec.describe TinyAdmin::Field do
describe ".create_field" do
it "creates a field with humanized title from name", :aggregate_failures do
field = described_class.create_field(name: "author_name")
expect(field.name).to eq("author_name")
expect(field.title).to eq("Author name")
expect(field.type).to eq(:string)
expect(field.options).to eq({})
end

it "uses the provided title when given" do
field = described_class.create_field(name: "email", title: "Email Address")
expect(field.title).to eq("Email Address")
end

it "uses the provided type when given" do
field = described_class.create_field(name: "age", type: :integer)
expect(field.type).to eq(:integer)
end

it "uses the provided options when given" do
options = {method: "downcase"}
field = described_class.create_field(name: "name", options: options)
expect(field.options).to eq(options)
end

it "converts symbol names to strings" do
field = described_class.create_field(name: :user_id)
expect(field.name).to eq("user_id")
end

it "handles nil options by defaulting to empty hash" do
field = described_class.create_field(name: "test", options: nil)
expect(field.options).to eq({})
end
end

describe "#apply_call_option" do
it "chains method calls on the target" do
field = described_class.new(name: "title", title: "Title", type: :string, options: {call: "to_s, downcase"})
expect(field.apply_call_option(42)).to eq("42")
end

it "returns nil when call option is not set" do
field = described_class.new(name: "title", title: "Title", type: :string, options: {})
expect(field.apply_call_option("test")).to be_nil
end

it "handles nil target safely via safe navigation" do
field = described_class.new(name: "title", title: "Title", type: :string, options: {call: "nonexistent"})
expect(field.apply_call_option(nil)).to be_nil
end
end

describe "#translate_value" do
it "returns value as string when no method option" do
field = described_class.new(name: "name", title: "Name", type: :string, options: {})
expect(field.translate_value(42)).to eq("42")
end

it "returns nil when value is nil and no method option" do
field = described_class.new(name: "name", title: "Name", type: :string, options: {})
expect(field.translate_value(nil)).to be_nil
end

it "applies the helper method from options" do
field = described_class.new(name: "name", title: "Name", type: :string, options: {method: "downcase"})
allow(TinyAdmin.settings).to receive(:helper_class).and_return(TinyAdmin::Support)
expect(field.translate_value("HELLO")).to eq("hello")
end

it "uses the converter class when specified" do
converter = Class.new do
def self.upcase(value, options: [])
value.upcase
end
end
stub_const("TestConverter", converter)

field = described_class.new(
name: "name", title: "Name", type: :string,
options: {method: "upcase", converter: "TestConverter"}
)
expect(field.translate_value("hello")).to eq("HELLO")
end

it "passes additional args to the method" do
field = described_class.new(
name: "value", title: "Value", type: :float,
options: {method: "round, 1"}
)
allow(TinyAdmin.settings).to receive(:helper_class).and_return(TinyAdmin::Support)
expect(field.translate_value(3.456)).to eq(3.5)
end
end
end
138 changes: 138 additions & 0 deletions spec/lib/tiny_admin/plugins/active_record_repository_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# frozen_string_literal: true

require "dummy_rails_app"
require "rails_helper"

RSpec.describe TinyAdmin::Plugins::ActiveRecordRepository do
let(:repository) { described_class.new(Author) }

before { setup_data(posts_count: 12) }

describe "#index_title" do
it "returns the pluralized model name" do
expect(repository.index_title).to eq("Authors")
end
end

describe "#show_title" do
it "returns the model name with the record id" do
author = Author.first
expect(repository.show_title(author)).to eq("Author ##{author.id}")
end
end

describe "#fields" do
it "returns all model columns as Field objects when no options given", :aggregate_failures do
fields = repository.fields
expect(fields).to be_a(Hash)
expect(fields.keys).to include("id", "name", "age", "email")
expect(fields["name"]).to be_a(TinyAdmin::Field)
expect(fields["name"].type).to eq(:string)
end

it "returns only specified fields when options given" do
options = {"name" => {}, "email" => {}}
fields = repository.fields(options: options)
expect(fields.keys).to eq(["name", "email"])
end

it "maps column types correctly", :aggregate_failures do
fields = repository.fields
expect(fields["id"].type).to eq(:integer)
expect(fields["name"].type).to eq(:string)
expect(fields["age"].type).to eq(:integer)
end
end

describe "#find" do
it "returns the record for a valid id" do
author = Author.first
expect(repository.find(author.id)).to eq(author)
end

it "raises RecordNotFound for an invalid id" do
expect { repository.find(999_999) }
.to raise_error(TinyAdmin::Plugins::BaseRepository::RecordNotFound)
end
end

describe "#collection" do
it "returns all records" do
expect(repository.collection.count).to eq(Author.count)
end
end

describe "#index_record_attrs" do
let(:author) { Author.first }

it "returns all attributes as strings when no fields given", :aggregate_failures do
attrs = repository.index_record_attrs(author)
expect(attrs["name"]).to eq(author.name)
expect(attrs["id"]).to eq(author.id.to_s)
end

it "returns only specified fields when fields given", :aggregate_failures do
attrs = repository.index_record_attrs(author, fields: {"name" => nil, "email" => nil})
expect(attrs.keys).to eq(["name", "email"])
expect(attrs["name"]).to eq(author.name)
end
end

describe "#list" do
it "returns records and total count", :aggregate_failures do
records, count = repository.list(page: 1, limit: 2)
expect(records.size).to eq(2)
expect(count).to eq(3)
end

it "paginates correctly", :aggregate_failures do
records_page1, = repository.list(page: 1, limit: 2)
records_page2, = repository.list(page: 2, limit: 2)
expect(records_page1).not_to eq(records_page2)
expect(records_page2.size).to eq(1)
end

it "sorts when sort option given" do
records, = repository.list(page: 1, limit: 10, sort: {name: :desc})
names = records.map(&:name)
expect(names).to eq(names.sort.reverse)
end
end

describe "#apply_filters" do
let(:post_repository) { described_class.new(Post) }

it "filters string fields with LIKE" do
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
filters = {title_field => {value: "post 1"}}
results = post_repository.apply_filters(Post.all, filters)
results.each do |post|
expect(post.title.downcase).to include("post 1")
end
end

it "filters non-string fields with equality" do
author = Author.first
author_field = TinyAdmin::Field.new(name: "author_id", title: "Author", type: :integer)
filters = {author_field => {value: author.id}}
results = post_repository.apply_filters(Post.all, filters)
results.each do |post|
expect(post.author_id).to eq(author.id)
end
end

it "skips filters with nil or empty values" do
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
filters = {title_field => {value: nil}}
results = post_repository.apply_filters(Post.all, filters)
expect(results.count).to eq(Post.count)
end

it "sanitizes SQL LIKE input" do
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
filters = {title_field => {value: "100%"}}
# Should not raise or cause SQL injection
expect { post_repository.apply_filters(Post.all, filters).to_a }.not_to raise_error
end
end
end
11 changes: 11 additions & 0 deletions spec/lib/tiny_admin/plugins/authorization_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

RSpec.describe TinyAdmin::Plugins::Authorization do
describe ".allowed?" do
it "returns true for any user, action, and param", :aggregate_failures do
expect(described_class.allowed?("admin", :root)).to be true
expect(described_class.allowed?(nil, :page, "some_slug")).to be true
expect(described_class.allowed?("user", :resource_index, "posts")).to be true
end
end
end
21 changes: 21 additions & 0 deletions spec/lib/tiny_admin/plugins/base_repository_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

RSpec.describe TinyAdmin::Plugins::BaseRepository do
describe "#initialize" do
it "stores the model" do
repo = described_class.new(String)
expect(repo.model).to eq(String)
end
end

describe "RecordNotFound" do
it "is a StandardError" do
expect(described_class::RecordNotFound.new).to be_a(StandardError)
end

it "can be raised with a message" do
expect { raise described_class::RecordNotFound, "not found" }
.to raise_error(described_class::RecordNotFound, "not found")
end
end
end
37 changes: 37 additions & 0 deletions spec/lib/tiny_admin/route_for_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

RSpec.describe "TinyAdmin.route_for" do
before do
allow(TinyAdmin.settings).to receive(:root_path).and_return("/admin")
end

it "builds a route for a section" do
expect(TinyAdmin.route_for("authors")).to eq("/admin/authors")
end

it "builds a route with a reference" do
expect(TinyAdmin.route_for("authors", reference: "42")).to eq("/admin/authors/42")
end

it "builds a route with an action" do
expect(TinyAdmin.route_for("authors", action: "edit")).to eq("/admin/authors/edit")
end

it "builds a route with a reference and action" do
expect(TinyAdmin.route_for("authors", reference: "42", action: "edit")).to eq("/admin/authors/42/edit")
end

it "appends query string when given" do
expect(TinyAdmin.route_for("authors", query: "page=2")).to eq("/admin/authors?page=2")
end

it "handles root_path of /" do
allow(TinyAdmin.settings).to receive(:root_path).and_return("/")
expect(TinyAdmin.route_for("authors")).to eq("/authors")
end

it "prepends / when missing" do
allow(TinyAdmin.settings).to receive(:root_path).and_return("")
expect(TinyAdmin.route_for("authors")).to eq("/authors")
end
end
Loading
Loading