Skip to content

Commit d4aa90c

Browse files
authored
Merge pull request #51 from blocknotes/specs/improve-tests
test: Improve specs
2 parents 1f3fe98 + da7c83b commit d4aa90c

13 files changed

Lines changed: 799 additions & 8 deletions

.github/workflows/tests.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
ruby: ['3.0', '3.1', '3.2']
17+
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
1818

1919
steps:
2020
- name: Checkout repository
@@ -33,7 +33,7 @@ jobs:
3333
run: cd spec/dummy_rails && bundle exec rails db:migrate
3434

3535
- name: Run tests
36-
env:
37-
RUBYOPT: '-rbundler/setup -rrbs/test/setup'
38-
RBS_TEST_TARGET: 'TinyAdmin::*'
36+
#env:
37+
# RUBYOPT: '-rbundler/setup -rrbs/test/setup'
38+
# RBS_TEST_TARGET: 'TinyAdmin::*'
3939
run: bin/rspec --profile

.rubocop.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
inherit_from:
33
- https://relaxed.ruby.style/rubocop.yml
44

5-
require:
5+
plugins:
66
- rubocop-packaging
77
- rubocop-performance
88
- rubocop-rspec
@@ -26,9 +26,6 @@ Lint/UnusedMethodArgument:
2626
RSpec/ExampleLength:
2727
Max: 20
2828

29-
RSpec/Rails/InferredSpecType:
30-
Enabled: false
31-
3229
Style/ExplicitBlockArgument:
3330
Enabled: false
3431

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe TinyAdmin::Actions::BasicAction do
4+
let(:action) { described_class.new }
5+
6+
describe "#attribute_options" do
7+
it "returns nil for nil input" do
8+
expect(action.attribute_options(nil)).to be_nil
9+
end
10+
11+
it "handles simple string field names" do
12+
result = action.attribute_options(["id", "name"])
13+
expect(result).to eq(
14+
"id" => {field: "id"},
15+
"name" => {field: "name"}
16+
)
17+
end
18+
19+
it "handles single-entry hash with method shorthand" do
20+
result = action.attribute_options([{title: "downcase, capitalize"}])
21+
expect(result).to eq(
22+
"title" => {field: "title", method: "downcase, capitalize"}
23+
)
24+
end
25+
26+
it "handles multi-entry hash with explicit field key" do
27+
result = action.attribute_options([{field: "author_id", link_to: "authors"}])
28+
expect(result).to eq(
29+
"author_id" => {field: "author_id", link_to: "authors"}
30+
)
31+
end
32+
33+
it "handles mixed input types" do
34+
result = action.attribute_options([
35+
"id",
36+
{title: "upcase"},
37+
{field: "created_at", method: "strftime, %Y-%m-%d"}
38+
])
39+
expect(result).to eq(
40+
"id" => {field: "id"},
41+
"title" => {field: "title", method: "upcase"},
42+
"created_at" => {field: "created_at", method: "strftime, %Y-%m-%d"}
43+
)
44+
end
45+
end
46+
end

spec/lib/tiny_admin/field_spec.rb

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe TinyAdmin::Field do
4+
describe ".create_field" do
5+
it "creates a field with humanized title from name", :aggregate_failures do
6+
field = described_class.create_field(name: "author_name")
7+
expect(field.name).to eq("author_name")
8+
expect(field.title).to eq("Author name")
9+
expect(field.type).to eq(:string)
10+
expect(field.options).to eq({})
11+
end
12+
13+
it "uses the provided title when given" do
14+
field = described_class.create_field(name: "email", title: "Email Address")
15+
expect(field.title).to eq("Email Address")
16+
end
17+
18+
it "uses the provided type when given" do
19+
field = described_class.create_field(name: "age", type: :integer)
20+
expect(field.type).to eq(:integer)
21+
end
22+
23+
it "uses the provided options when given" do
24+
options = {method: "downcase"}
25+
field = described_class.create_field(name: "name", options: options)
26+
expect(field.options).to eq(options)
27+
end
28+
29+
it "converts symbol names to strings" do
30+
field = described_class.create_field(name: :user_id)
31+
expect(field.name).to eq("user_id")
32+
end
33+
34+
it "handles nil options by defaulting to empty hash" do
35+
field = described_class.create_field(name: "test", options: nil)
36+
expect(field.options).to eq({})
37+
end
38+
end
39+
40+
describe "#apply_call_option" do
41+
it "chains method calls on the target" do
42+
field = described_class.new(name: "title", title: "Title", type: :string, options: {call: "to_s, downcase"})
43+
expect(field.apply_call_option(42)).to eq("42")
44+
end
45+
46+
it "returns nil when call option is not set" do
47+
field = described_class.new(name: "title", title: "Title", type: :string, options: {})
48+
expect(field.apply_call_option("test")).to be_nil
49+
end
50+
51+
it "handles nil target safely via safe navigation" do
52+
field = described_class.new(name: "title", title: "Title", type: :string, options: {call: "nonexistent"})
53+
expect(field.apply_call_option(nil)).to be_nil
54+
end
55+
end
56+
57+
describe "#translate_value" do
58+
it "returns value as string when no method option" do
59+
field = described_class.new(name: "name", title: "Name", type: :string, options: {})
60+
expect(field.translate_value(42)).to eq("42")
61+
end
62+
63+
it "returns nil when value is nil and no method option" do
64+
field = described_class.new(name: "name", title: "Name", type: :string, options: {})
65+
expect(field.translate_value(nil)).to be_nil
66+
end
67+
68+
it "applies the helper method from options" do
69+
field = described_class.new(name: "name", title: "Name", type: :string, options: {method: "downcase"})
70+
allow(TinyAdmin.settings).to receive(:helper_class).and_return(TinyAdmin::Support)
71+
expect(field.translate_value("HELLO")).to eq("hello")
72+
end
73+
74+
it "uses the converter class when specified" do
75+
converter = Class.new do
76+
def self.upcase(value, options: [])
77+
value.upcase
78+
end
79+
end
80+
stub_const("TestConverter", converter)
81+
82+
field = described_class.new(
83+
name: "name", title: "Name", type: :string,
84+
options: {method: "upcase", converter: "TestConverter"}
85+
)
86+
expect(field.translate_value("hello")).to eq("HELLO")
87+
end
88+
89+
it "passes additional args to the method" do
90+
field = described_class.new(
91+
name: "value", title: "Value", type: :float,
92+
options: {method: "round, 1"}
93+
)
94+
allow(TinyAdmin.settings).to receive(:helper_class).and_return(TinyAdmin::Support)
95+
expect(field.translate_value(3.456)).to eq(3.5)
96+
end
97+
end
98+
end
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# frozen_string_literal: true
2+
3+
require "dummy_rails_app"
4+
require "rails_helper"
5+
6+
RSpec.describe TinyAdmin::Plugins::ActiveRecordRepository do
7+
let(:repository) { described_class.new(Author) }
8+
9+
before { setup_data(posts_count: 12) }
10+
11+
describe "#index_title" do
12+
it "returns the pluralized model name" do
13+
expect(repository.index_title).to eq("Authors")
14+
end
15+
end
16+
17+
describe "#show_title" do
18+
it "returns the model name with the record id" do
19+
author = Author.first
20+
expect(repository.show_title(author)).to eq("Author ##{author.id}")
21+
end
22+
end
23+
24+
describe "#fields" do
25+
it "returns all model columns as Field objects when no options given", :aggregate_failures do
26+
fields = repository.fields
27+
expect(fields).to be_a(Hash)
28+
expect(fields.keys).to include("id", "name", "age", "email")
29+
expect(fields["name"]).to be_a(TinyAdmin::Field)
30+
expect(fields["name"].type).to eq(:string)
31+
end
32+
33+
it "returns only specified fields when options given" do
34+
options = {"name" => {}, "email" => {}}
35+
fields = repository.fields(options: options)
36+
expect(fields.keys).to eq(["name", "email"])
37+
end
38+
39+
it "maps column types correctly", :aggregate_failures do
40+
fields = repository.fields
41+
expect(fields["id"].type).to eq(:integer)
42+
expect(fields["name"].type).to eq(:string)
43+
expect(fields["age"].type).to eq(:integer)
44+
end
45+
end
46+
47+
describe "#find" do
48+
it "returns the record for a valid id" do
49+
author = Author.first
50+
expect(repository.find(author.id)).to eq(author)
51+
end
52+
53+
it "raises RecordNotFound for an invalid id" do
54+
expect { repository.find(999_999) }
55+
.to raise_error(TinyAdmin::Plugins::BaseRepository::RecordNotFound)
56+
end
57+
end
58+
59+
describe "#collection" do
60+
it "returns all records" do
61+
expect(repository.collection.count).to eq(Author.count)
62+
end
63+
end
64+
65+
describe "#index_record_attrs" do
66+
let(:author) { Author.first }
67+
68+
it "returns all attributes as strings when no fields given", :aggregate_failures do
69+
attrs = repository.index_record_attrs(author)
70+
expect(attrs["name"]).to eq(author.name)
71+
expect(attrs["id"]).to eq(author.id.to_s)
72+
end
73+
74+
it "returns only specified fields when fields given", :aggregate_failures do
75+
attrs = repository.index_record_attrs(author, fields: {"name" => nil, "email" => nil})
76+
expect(attrs.keys).to eq(["name", "email"])
77+
expect(attrs["name"]).to eq(author.name)
78+
end
79+
end
80+
81+
describe "#list" do
82+
it "returns records and total count", :aggregate_failures do
83+
records, count = repository.list(page: 1, limit: 2)
84+
expect(records.size).to eq(2)
85+
expect(count).to eq(3)
86+
end
87+
88+
it "paginates correctly", :aggregate_failures do
89+
records_page1, = repository.list(page: 1, limit: 2)
90+
records_page2, = repository.list(page: 2, limit: 2)
91+
expect(records_page1).not_to eq(records_page2)
92+
expect(records_page2.size).to eq(1)
93+
end
94+
95+
it "sorts when sort option given" do
96+
records, = repository.list(page: 1, limit: 10, sort: {name: :desc})
97+
names = records.map(&:name)
98+
expect(names).to eq(names.sort.reverse)
99+
end
100+
end
101+
102+
describe "#apply_filters" do
103+
let(:post_repository) { described_class.new(Post) }
104+
105+
it "filters string fields with LIKE" do
106+
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
107+
filters = {title_field => {value: "post 1"}}
108+
results = post_repository.apply_filters(Post.all, filters)
109+
results.each do |post|
110+
expect(post.title.downcase).to include("post 1")
111+
end
112+
end
113+
114+
it "filters non-string fields with equality" do
115+
author = Author.first
116+
author_field = TinyAdmin::Field.new(name: "author_id", title: "Author", type: :integer)
117+
filters = {author_field => {value: author.id}}
118+
results = post_repository.apply_filters(Post.all, filters)
119+
results.each do |post|
120+
expect(post.author_id).to eq(author.id)
121+
end
122+
end
123+
124+
it "skips filters with nil or empty values" do
125+
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
126+
filters = {title_field => {value: nil}}
127+
results = post_repository.apply_filters(Post.all, filters)
128+
expect(results.count).to eq(Post.count)
129+
end
130+
131+
it "sanitizes SQL LIKE input" do
132+
title_field = TinyAdmin::Field.new(name: "title", title: "Title", type: :string)
133+
filters = {title_field => {value: "100%"}}
134+
# Should not raise or cause SQL injection
135+
expect { post_repository.apply_filters(Post.all, filters).to_a }.not_to raise_error
136+
end
137+
end
138+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe TinyAdmin::Plugins::Authorization do
4+
describe ".allowed?" do
5+
it "returns true for any user, action, and param", :aggregate_failures do
6+
expect(described_class.allowed?("admin", :root)).to be true
7+
expect(described_class.allowed?(nil, :page, "some_slug")).to be true
8+
expect(described_class.allowed?("user", :resource_index, "posts")).to be true
9+
end
10+
end
11+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe TinyAdmin::Plugins::BaseRepository do
4+
describe "#initialize" do
5+
it "stores the model" do
6+
repo = described_class.new(String)
7+
expect(repo.model).to eq(String)
8+
end
9+
end
10+
11+
describe "RecordNotFound" do
12+
it "is a StandardError" do
13+
expect(described_class::RecordNotFound.new).to be_a(StandardError)
14+
end
15+
16+
it "can be raised with a message" do
17+
expect { raise described_class::RecordNotFound, "not found" }
18+
.to raise_error(described_class::RecordNotFound, "not found")
19+
end
20+
end
21+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe "TinyAdmin.route_for" do
4+
before do
5+
allow(TinyAdmin.settings).to receive(:root_path).and_return("/admin")
6+
end
7+
8+
it "builds a route for a section" do
9+
expect(TinyAdmin.route_for("authors")).to eq("/admin/authors")
10+
end
11+
12+
it "builds a route with a reference" do
13+
expect(TinyAdmin.route_for("authors", reference: "42")).to eq("/admin/authors/42")
14+
end
15+
16+
it "builds a route with an action" do
17+
expect(TinyAdmin.route_for("authors", action: "edit")).to eq("/admin/authors/edit")
18+
end
19+
20+
it "builds a route with a reference and action" do
21+
expect(TinyAdmin.route_for("authors", reference: "42", action: "edit")).to eq("/admin/authors/42/edit")
22+
end
23+
24+
it "appends query string when given" do
25+
expect(TinyAdmin.route_for("authors", query: "page=2")).to eq("/admin/authors?page=2")
26+
end
27+
28+
it "handles root_path of /" do
29+
allow(TinyAdmin.settings).to receive(:root_path).and_return("/")
30+
expect(TinyAdmin.route_for("authors")).to eq("/authors")
31+
end
32+
33+
it "prepends / when missing" do
34+
allow(TinyAdmin.settings).to receive(:root_path).and_return("")
35+
expect(TinyAdmin.route_for("authors")).to eq("/authors")
36+
end
37+
end

0 commit comments

Comments
 (0)