From fdfe5362e38029a46059b708601dd3513fc4d8d9 Mon Sep 17 00:00:00 2001 From: AgentKush Date: Sun, 5 Apr 2026 10:42:59 +0100 Subject: [PATCH 1/3] Fix duplicate mods on index and missing downloads for exmod files Two issues fixed: 1. Duplicate mods: Added deduplication by name+author in fetch_all so duplicate Firestore documents don't produce repeated entries on the mods listing page. 2. Missing downloads: Added :exmod support to preferred_type and download_types. Mods with only an exmod file (not exmodz) were showing no download button because exmod wasn't in the allowed file type list. Also updated the show page EXMOD notice to trigger for both exmod and exmodz files. Co-Authored-By: Claude Opus 4.6 --- app/models/mod.rb | 11 ++++++--- app/views/mods/show.html.erb | 2 +- spec/models/mod_spec.rb | 46 ++++++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/models/mod.rb b/app/models/mod.rb index c46e1f2..09e4153 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -34,7 +34,7 @@ def self.fetch_all # :nodoc: created_at: mod.create_time, updated_at: mod.update_time ) - end.sort_by(&:name) + end.uniq { |mod| [mod.name.downcase, mod.author_slug] }.sort_by(&:name) end private_class_method :fetch_all @@ -54,23 +54,28 @@ def zip? files.key?(:zip) end + def exmod? + files.key?(:exmod) + end + def exmodz? files.key?(:exmodz) end # Determines which file types can be downloaded from the index page - # Priority: pak > zip > exmodz (most common/compatible format first) + # Priority: pak > zip > exmodz > exmod (most common/compatible format first) def preferred_type return :pak if pak? return :zip if zip? return :exmodz if exmodz? + return :exmod if exmod? nil end # Determines which file types can be downloaded from the show page def download_types - file_types.map(&:to_sym) & %i[pak zip exmodz] + file_types.map(&:to_sym) & %i[pak zip exmodz exmod] end def file_types diff --git a/app/views/mods/show.html.erb b/app/views/mods/show.html.erb index dc53185..683b292 100644 --- a/app/views/mods/show.html.erb +++ b/app/views/mods/show.html.erb @@ -23,7 +23,7 @@
- <% if @mod.exmodz? %> + <% if @mod.exmodz? || @mod.exmod? %> This Mod includes an EXMOD formatted file, which doesn't need updates each week.
We recommend you use a Mod Manager to install this mod <% end %> diff --git a/spec/models/mod_spec.rb b/spec/models/mod_spec.rb index 29e080f..4a2d4b0 100644 --- a/spec/models/mod_spec.rb +++ b/spec/models/mod_spec.rb @@ -11,8 +11,24 @@ create_time: Time.now.utc, update_time: Time.now.utc, data: { - name: Faker::App.name, - author: Faker::App.author, + name: "Alpha Mod", + author: "Author One", + description: Faker::Lorem.sentence, + version: Faker::App.version, + compatibility: "w#{Random.rand(1..5)}", + files: { zip: Faker::Internet.url }, + imageURL: Faker::Internet.url, + readmeURL: Faker::Internet.url + }) + end + let(:mod_firestore_obj2) do + instance_double(Google::Cloud::Firestore::DocumentSnapshot, + document_id: SecureRandom.uuid, + create_time: Time.now.utc, + update_time: Time.now.utc, + data: { + name: "Beta Mod", + author: "Author Two", description: Faker::Lorem.sentence, version: Faker::App.version, compatibility: "w#{Random.rand(1..5)}", @@ -25,7 +41,7 @@ before do allow(Google::Cloud::Firestore).to receive(:new).and_return(firestore_client) - allow(firestore_collection).to receive(:get).and_return(Array.new(2, mod_firestore_obj)) + allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, mod_firestore_obj2]) allow(firestore_client).to receive(:col).with("mods").and_return(firestore_collection) end @@ -51,6 +67,26 @@ it "returns all mods" do expect(described_class.all.count).to eq(2) end + + it "deduplicates mods with the same name and author" do + duplicate = instance_double(Google::Cloud::Firestore::DocumentSnapshot, + document_id: SecureRandom.uuid, + create_time: Time.now.utc, + update_time: Time.now.utc, + data: { + name: "Alpha Mod", + author: "Author One", + description: Faker::Lorem.sentence, + version: "2.0.0", + compatibility: "w3", + files: { zip: Faker::Internet.url }, + imageURL: Faker::Internet.url, + readmeURL: Faker::Internet.url + }) + allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, duplicate, mod_firestore_obj2]) + + expect(described_class.all.count).to eq(2) + end end context "when the readme_url is present" do @@ -174,7 +210,7 @@ end end - %i[pak zip exmodz].each do |file_type| + %i[pak zip exmod exmodz].each do |file_type| describe "##{file_type}?" do context "when given a #{file_type} object" do before { mod.files = { file_type.to_sym => Faker::Internet.url } } @@ -227,7 +263,7 @@ before { mod.files = { exmod: Faker::Internet.url } } it "returns the preferred type" do - expect(mod.preferred_type).to be_nil + expect(mod.preferred_type).to eq(:exmod) end end From 3916f55813e70a95714f9f2568a0b8accd90d48d Mon Sep 17 00:00:00 2001 From: AgentKush Date: Sun, 5 Apr 2026 10:57:11 +0100 Subject: [PATCH 2/3] Fix RuboCop: MultilineBlockChain, ExampleLength, MultipleMemoizedHelpers - Split filter_map chain into variable + separate uniq/sort call - Extract firestore_doc helper method to reduce let count and test length - Remove mod_firestore_obj2 let in favor of inline helper calls Co-Authored-By: Claude Opus 4.6 --- app/models/mod.rb | 6 ++++-- spec/models/mod_spec.rb | 36 +++++++++++------------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/app/models/mod.rb b/app/models/mod.rb index 09e4153..e742a00 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -19,7 +19,7 @@ def self.all end def self.fetch_all # :nodoc: - firestore.col("mods").get.filter_map do |mod| + mods = firestore.col("mods").get.filter_map do |mod| new( author: mod.data[:author], compatibility: mod.data[:compatibility], @@ -34,7 +34,9 @@ def self.fetch_all # :nodoc: created_at: mod.create_time, updated_at: mod.update_time ) - end.uniq { |mod| [mod.name.downcase, mod.author_slug] }.sort_by(&:name) + end + + mods.uniq { |mod| [mod.name.downcase, mod.author_slug] }.sort_by(&:name) end private_class_method :fetch_all diff --git a/spec/models/mod_spec.rb b/spec/models/mod_spec.rb index 4a2d4b0..85181f1 100644 --- a/spec/models/mod_spec.rb +++ b/spec/models/mod_spec.rb @@ -21,27 +21,25 @@ readmeURL: Faker::Internet.url }) end - let(:mod_firestore_obj2) do + let(:mod) { build :mod } + + # Builds a Firestore document snapshot double with the given name and author + def firestore_doc(name: Faker::App.name, author: Faker::App.author) instance_double(Google::Cloud::Firestore::DocumentSnapshot, document_id: SecureRandom.uuid, create_time: Time.now.utc, update_time: Time.now.utc, data: { - name: "Beta Mod", - author: "Author Two", - description: Faker::Lorem.sentence, - version: Faker::App.version, - compatibility: "w#{Random.rand(1..5)}", + name: name, author: author, description: Faker::Lorem.sentence, + version: Faker::App.version, compatibility: "w#{Random.rand(1..5)}", files: { zip: Faker::Internet.url }, - imageURL: Faker::Internet.url, - readmeURL: Faker::Internet.url + imageURL: Faker::Internet.url, readmeURL: Faker::Internet.url }) end - let(:mod) { build :mod } before do allow(Google::Cloud::Firestore).to receive(:new).and_return(firestore_client) - allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, mod_firestore_obj2]) + allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, firestore_doc(name: "Beta Mod", author: "Author Two")]) allow(firestore_client).to receive(:col).with("mods").and_return(firestore_collection) end @@ -69,21 +67,9 @@ end it "deduplicates mods with the same name and author" do - duplicate = instance_double(Google::Cloud::Firestore::DocumentSnapshot, - document_id: SecureRandom.uuid, - create_time: Time.now.utc, - update_time: Time.now.utc, - data: { - name: "Alpha Mod", - author: "Author One", - description: Faker::Lorem.sentence, - version: "2.0.0", - compatibility: "w3", - files: { zip: Faker::Internet.url }, - imageURL: Faker::Internet.url, - readmeURL: Faker::Internet.url - }) - allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, duplicate, mod_firestore_obj2]) + duplicate = firestore_doc(name: "Alpha Mod", author: "Author One") + unique = firestore_doc(name: "Beta Mod", author: "Author Two") + allow(firestore_collection).to receive(:get).and_return([mod_firestore_obj, duplicate, unique]) expect(described_class.all.count).to eq(2) end From 65d7b706154693dfb05e5bcf56e65c0841029be5 Mon Sep 17 00:00:00 2001 From: AgentKush Date: Sun, 5 Apr 2026 11:48:20 +0100 Subject: [PATCH 3/3] Include ActiveSupport::Testing::TimeHelpers in RSpec config Makes freeze_time, travel_to, and travel available globally in specs for deterministic time-dependent tests. Co-Authored-By: Claude Opus 4.6 --- spec/rails_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 51a1614..6d61b76 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,6 +13,8 @@ require "rspec/rails" RSpec.configure do |config| + config.include ActiveSupport::Testing::TimeHelpers + config.before(:each, type: :request) do host! "localhost" end