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
52 changes: 51 additions & 1 deletion spec/adapter/memory_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,56 @@ Alumna::Testing::AdapterSuite.run("Alumna::MemoryAdapter") do
.int("score").float("price").int("order_index").str("category").bool("is_published")
.str("title", required: false).str("sequence", required: false)
.str("first_name", required: false).str("last_name", required: false)
.int("view_count", required: false).nullable("metadata", required: false)
.int("view_count", required: false).any("metadata", nullable: true, required: false)
)
end

describe "MemoryAdapter Unique Constraints" do
schema = Alumna::Schema.new
.str("email", unique: true)
.str("username", unique: true)
.str("bio", required: false)

it "enforces uniqueness on create" do
adapter = Alumna::MemoryAdapter.new(schema)

# 1. First insert should succeed
ctx1 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Create, data: {"email" => "alice@a.com", "username" => "alice"} of String => Alumna::AnyData)
res1 = adapter.create(ctx1)
res1.should be_a(Hash(String, Alumna::AnyData))

# 2. Duplicate email should fail
ctx2 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Create, data: {"email" => "alice@a.com", "username" => "bob"} of String => Alumna::AnyData)
res2 = adapter.create(ctx2)
res2.should be_a(Alumna::ServiceError)
res2.as(Alumna::ServiceError).status.should eq(422)
res2.as(Alumna::ServiceError).details["email"].should eq("already exists")

# 3. Duplicate username should fail
ctx3 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Create, data: {"email" => "bob@a.com", "username" => "alice"} of String => Alumna::AnyData)
res3 = adapter.create(ctx3)
res3.as(Alumna::ServiceError).details["username"].should eq("already exists")
end

it "enforces uniqueness on update and patch, safely skipping its own ID" do
adapter = Alumna::MemoryAdapter.new(schema)

# Setup
c_ctx1 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Create, data: {"email" => "alice@a.com", "username" => "alice"} of String => Alumna::AnyData)
c_ctx2 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Create, data: {"email" => "bob@a.com", "username" => "bob"} of String => Alumna::AnyData)
id1 = adapter.create(c_ctx1).as(Hash(String, Alumna::AnyData))["id"].as(String)
id2 = adapter.create(c_ctx2).as(Hash(String, Alumna::AnyData))["id"].as(String)

# 1. Patching to an existing email (owned by someone else) should fail
p_ctx1 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Patch, id: id2, data: {"email" => "alice@a.com"} of String => Alumna::AnyData)
res1 = adapter.patch(p_ctx1)
res1.should be_a(Alumna::ServiceError)
res1.as(Alumna::ServiceError).status.should eq(422)

# 2. Patching a record WITHOUT changing its unique fields (or updating to the same value) MUST succeed
p_ctx2 = Alumna::Testing.build_ctx(service: adapter, method: Alumna::ServiceMethod::Patch, id: id1, data: {"email" => "alice@a.com", "bio" => "new bio"} of String => Alumna::AnyData)
res2 = adapter.patch(p_ctx2)
res2.should be_a(Hash(String, Alumna::AnyData))
res2.as(Hash(String, Alumna::AnyData))["bio"].should eq("new bio")
end
end
58 changes: 52 additions & 6 deletions spec/schema/base_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe Alumna::Schema do
s = Alumna::Schema.new
s.fields.should be_empty
s.strict.should be_true
s.schema_indexes.should be_empty
end

it "accepts strict: false" do
Expand Down Expand Up @@ -36,9 +37,54 @@ describe Alumna::Schema do
fd.max_length.should eq(10)
end

it "stores read_only flag" do
fd = Alumna::Schema.new.field("x", :str, read_only: true).fields.first
it "stores traits (read_only, nullable, unique, indexed)" do
fd = Alumna::Schema.new.field("x", :str, read_only: true, nullable: true, unique: true, indexed: true).fields.first
fd.read_only.should be_true
fd.nullable.should be_true
fd.unique.should be_true
fd.indexed.should be_true
end
end

describe "default values" do
it "identifies when a default is not provided" do
fd = Alumna::Schema.new.field("x", :str).fields.first
fd.has_default.should be_false
fd.default_value.should be_nil
end

it "identifies when a default is explicitly provided as nil" do
fd = Alumna::Schema.new.field("x", :str, default: nil).fields.first
fd.has_default.should be_true
fd.default_value.should be_nil
end

it "stores and returns static default values" do
fd = Alumna::Schema.new.field("x", :int, default: 42_i64).fields.first
fd.has_default.should be_true
fd.default_value.should eq(42_i64)
end

it "stores and evaluates dynamic default Procs at runtime" do
fd = Alumna::Schema.new.field("x", :str, default: -> { "dynamic".as(Alumna::AnyData) }).fields.first
fd.has_default.should be_true
fd.default_value.should eq("dynamic")
end
end

describe "schema-level indexes" do
it "stores single field indexes" do
s = Alumna::Schema.new.index("email", unique: true)
idx = s.schema_indexes.first
idx.fields.should eq(["email"])
idx.unique.should be_true
end

it "stores compound field indexes" do
s = Alumna::Schema.new.index(["user_id", "status"])
idx = s.schema_indexes.first
idx.fields.should eq(["user_id", "status"])
idx.unique.should be_false
end
end

Expand Down Expand Up @@ -114,7 +160,7 @@ describe Alumna::Schema do
.int("b", required: false)
.float("c")
.bool("d")
.nullable("e", required_on: [:patch])
.any("e", nullable: true)
.time("f")
.bytes("g")

Expand All @@ -123,12 +169,12 @@ describe Alumna::Schema do
Alumna::FieldType::Int,
Alumna::FieldType::Float,
Alumna::FieldType::Bool,
Alumna::FieldType::Nullable,
Alumna::FieldType::Any,
Alumna::FieldType::Time,
Alumna::FieldType::Bytes,
])
s.fields[1].required.should be_false
s.fields[4].required_on.should eq([Alumna::ServiceMethod::Patch])
s.fields[4].nullable.should be_true
end
end

Expand Down Expand Up @@ -162,7 +208,7 @@ describe Alumna::Schema do
end
end

describe "validator integration (kept from your original)" do
describe "validator integration" do
it "validates format when given as symbol" do
schema = Alumna::Schema.new.str("email", format: :email)
errors = schema.validate({"email" => "not-an-email"} of String => Alumna::AnyData)
Expand Down
Loading
Loading