A store record is a name and a catalog
Both extras go. `config` was added this morning on the idea that the registry should say how each store behaves; that was wrong. The configuration is fixed per installed client — the client carries its own store engine and knows its own machine — so a copy here was a second authority over decisions the client had already made correctly, including which directories it may delete from. Keeping two stores on one machine apart is a subfolder, and the client derives that itself. `store_repository_url` goes with it. The store engines it pointed at no longer exist, and a URL nobody follows is a URL that goes stale. The public stores page loses its desktop card for the same reason: it advertised a `curl … | sh` for a repository that is gone, and an ordinary computer is served by the app in the section above it. `/desktop` now lands on that app rather than on a device tab, so the old URL still means what someone typing it wants. Unrelated but in the way: the dead `engines` list in that page has been failing `vue-tsc` on master, so the frontend could not be built to check any of this. Removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,36 +19,10 @@ RSpec.describe Api::StoresController, type: :request do
|
||||
get "/api/stores"
|
||||
|
||||
store = JSON.parse(response.body).first
|
||||
expect(store).to include("name", "catalogUrl", "storeRepositoryUrl", "config")
|
||||
# Exactly two fields: a name and a catalog are the whole record, and a client
|
||||
# that starts reading a third would be reading something this site no longer says.
|
||||
expect(store.keys).to contain_exactly("name", "catalogUrl")
|
||||
expect(store["catalogUrl"]).to eq("https://teletypegames.org")
|
||||
expect(store["storeRepositoryUrl"]).to end_with("/stores/ttg-desktop-store")
|
||||
end
|
||||
|
||||
# The config is what the client applies instead of fetching a repository's
|
||||
# config.json, so it has to come back as an object rather than as JSON text.
|
||||
it "carries the store's own config as an object" do
|
||||
create(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
|
||||
|
||||
get "/api/stores"
|
||||
|
||||
config = JSON.parse(response.body).first["config"]
|
||||
expect(config).to eq("paths" => { "subfolder" => "teletypegames" })
|
||||
end
|
||||
|
||||
it "answers null for a store that configures nothing" do
|
||||
create(:store, config: nil)
|
||||
|
||||
get "/api/stores"
|
||||
|
||||
expect(JSON.parse(response.body).first["config"]).to be_nil
|
||||
end
|
||||
|
||||
it "answers null for a store with no repository" do
|
||||
create(:store, store_repository_url: nil)
|
||||
|
||||
get "/api/stores"
|
||||
|
||||
expect(JSON.parse(response.body).first["storeRepositoryUrl"]).to be_nil
|
||||
end
|
||||
|
||||
it "leaves out soft-deleted stores" do
|
||||
|
||||
@@ -2,6 +2,5 @@ FactoryBot.define do
|
||||
factory :store do
|
||||
name { "Teletype Games" }
|
||||
catalog_url { "https://teletypegames.org" }
|
||||
store_repository_url { "https://git.teletypegames.org/stores/ttg-desktop-store" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,63 +10,6 @@ RSpec.describe Store, type: :model do
|
||||
expect(store.errors[:catalog_url]).to include("must be an http(s) URL")
|
||||
end
|
||||
|
||||
it "rejects a repository url that is not http(s)" do
|
||||
expect(build(:store, store_repository_url: "not a url")).not_to be_valid
|
||||
end
|
||||
|
||||
# A store without a repository is a complete store: the client takes identity
|
||||
# from the record and everything else from the engine's defaults.
|
||||
it "accepts a store with no repository at all" do
|
||||
expect(build(:store, store_repository_url: nil)).to be_valid
|
||||
expect(build(:store, store_repository_url: "")).to be_valid
|
||||
end
|
||||
|
||||
describe "config" do
|
||||
it "accepts a store that configures nothing" do
|
||||
expect(build(:store, config: nil)).to be_valid
|
||||
end
|
||||
|
||||
it "rejects a config that is not a JSON object" do
|
||||
store = build(:store, config: ["not", "an", "object"])
|
||||
expect(store).not_to be_valid
|
||||
expect(store.errors[:config]).to include("must be a JSON object")
|
||||
end
|
||||
|
||||
# The admin form edits JSON text; the column holds real JSON. These two accessors
|
||||
# are the only bridge between them, so both directions are worth pinning.
|
||||
it "parses config_json into the column" do
|
||||
store = build(:store, config_json: '{"paths": {"subfolder": "teletypegames"}}')
|
||||
|
||||
expect(store).to be_valid
|
||||
expect(store.config).to eq("paths" => { "subfolder" => "teletypegames" })
|
||||
end
|
||||
|
||||
it "renders the column back as pretty JSON text" do
|
||||
store = build(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
|
||||
|
||||
expect(JSON.parse(store.config_json)).to eq(store.config)
|
||||
expect(store.config_json).to include("\n")
|
||||
end
|
||||
|
||||
it "clears the config when the text is emptied" do
|
||||
store = build(:store, config: { "paths" => {} })
|
||||
store.config_json = ""
|
||||
|
||||
expect(store).to be_valid
|
||||
expect(store.config).to be_nil
|
||||
end
|
||||
|
||||
# Refused with the text kept, rather than raising: the form has to come back with
|
||||
# what the person typed and a message they can act on.
|
||||
it "refuses invalid JSON and keeps the text" do
|
||||
store = build(:store, config_json: '{"paths": ')
|
||||
|
||||
expect(store).not_to be_valid
|
||||
expect(store.errors[:config].first).to start_with("is not valid JSON")
|
||||
expect(store.config_json).to eq('{"paths": ')
|
||||
end
|
||||
end
|
||||
|
||||
describe ".ordered" do
|
||||
it "lists stores by name" do
|
||||
later = create(:store, name: "Zed Games")
|
||||
|
||||
Reference in New Issue
Block a user