diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml
index d23208f..3d2e5dd 100644
--- a/.idea/jsLibraryMappings.xml
+++ b/.idea/jsLibraryMappings.xml
@@ -1,6 +1,942 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.rubocop.yml b/.rubocop.yml
index e816d99..e2b6db3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,4 +1,7 @@
-plugins: rubocop-rails
+plugins:
+ - rubocop-rails
+ - rubocop-rspec
+ - rubocop-rspec_rails
AllCops:
UseCache: false
@@ -294,6 +297,8 @@ Rails/NegateInclude: # new in 2.7
Enabled: true
Rails/Pluck: # new in 2.7
Enabled: true
+ Exclude:
+ - 'spec/**/*'
Rails/PluckInWhere: # new in 2.7
Enabled: true
Rails/RenderInline: # new in 2.7
@@ -594,3 +599,152 @@ Rails/WhereNotWithMultipleConditions: # new in 2.17
Enabled: true
Rails/WhereRange: # new in 2.25
Enabled: true
+RSpec/IncludeExamples: # new in 3.6
+ Enabled: true
+RSpec/LeakyLocalVariable: # new in 3.8
+ Enabled: true
+RSpecRails/HaveHttpStatus: # new in 2.12
+ Enabled: true
+RSpecRails/MinitestAssertions: # new in 2.17
+ Enabled: true
+RSpecRails/NegationBeValid: # new in 2.23
+ Enabled: true
+RSpecRails/TravelAround: # new in 2.19
+ Enabled: true
+
+############################################################
+# rubocop-rspec
+
+# believe me, it wasn't by choice
+RSpec/AnyInstance:
+ Enabled: false
+
+RSpec/VerifiedDoubleReference:
+ Enabled: false
+
+# we meant to do that
+RSpec/BeforeAfterAll:
+ Enabled: false
+
+RSpec/ContainExactly:
+ Enabled: false
+
+# more words != more readable
+RSpec/ContextWording:
+ Enabled: false
+
+# not everything we test is a class
+RSpec/DescribeClass:
+ Enabled: false
+
+# explicit >>> implicit
+RSpec/DescribedClass:
+ Enabled: false
+
+# more punctuation != more readable
+RSpec/DescribeSymbol:
+ Enabled: false
+
+# setup cost / time >>> failure granularity
+RSpec/ExampleLength:
+ Max: 25
+ CountAsOne:
+ - array
+ - hash
+ - heredoc
+
+# we meant to do that
+RSpec/ExpectInHook:
+ Enabled: false
+
+RSpec/SpecFilePathFormat:
+ Enabled: false
+
+RSpec/SpecFilePathSuffix:
+ Enabled: false
+
+# Requests are not inferred properly without explicit typing.
+# Similarly, system tests don't pick up Selenium without explicit typing.
+RSpecRails/InferredSpecType:
+ Enabled: true
+ Exclude:
+ - 'spec/requests/*.rb'
+ - 'spec/system/*.rb'
+
+# explicit >>> implicit
+RSpec/InstanceVariable:
+ Enabled: false
+
+# maybe when 'all' has a corresponding 'none' matcher
+RSpec/IteratedExpectation:
+ Enabled: false
+
+# we meant to do that
+RSpec/MessageSpies:
+ Enabled: false
+
+# too late now
+RSpec/MultipleMemoizedHelpers:
+ Enabled: false
+
+# setup cost / time >>> failure granularity
+RSpec/MultipleExpectations:
+ Enabled: false
+
+# cure is worse than the disease
+RSpec/NestedGroups:
+ Enabled: false
+
+# more quotation marks != more readable
+RSpec/SharedExamples:
+ Enabled: false
+
+# we meant to do that
+RSpec/StubbedMock:
+ Enabled: false
+
+# we meant to do that
+RSpec/VerifiedDoubles:
+ Enabled: false
+
+############################################################
+# rubocop-rspec
+
+# enable newer rubocop-rspec cops
+
+RSpec/IdenticalEqualityAssertion: # new in 2.4
+ Enabled: true
+
+RSpecRails/AvoidSetupHook: # new in 2.4
+ Enabled: true
+
+# Sometimes we're testing the operator
+Lint/BinaryOperatorWithIdenticalOperands:
+ Exclude:
+ - 'spec/**/*'
+
+Metrics/AbcSize:
+ Exclude:
+ - 'spec/**/*'
+
+Metrics/BlockLength:
+ Exclude:
+ - 'spec/**/*'
+
+Metrics/ModuleLength:
+ Exclude:
+ - 'spec/**/*'
+
+Metrics/MethodLength:
+ Exclude:
+ - 'spec/**/*'
+
+# we meant to do that
+Rails/SkipsModelValidations:
+ Exclude:
+ - 'spec/**/*'
+
+# we meant to do that
+Rails/TimeZone:
+ Exclude:
+ - 'spec/**/*'
\ No newline at end of file
diff --git a/.yarnrc b/.yarnrc
index cd6a666..c81d7dc 100644
--- a/.yarnrc
+++ b/.yarnrc
@@ -2,4 +2,4 @@
# yarn lockfile v1
-lastUpdateCheck 1780439312657
+lastUpdateCheck 1782867282598
diff --git a/Dockerfile b/Dockerfile
index 1749531..b231ec5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# The base stage scaffolds elements which are common to building and running
# the application, such as installing ca-certificates, creating the app user,
# and installing runtime system dependencies.
-FROM ruby:3.3-slim AS base
+FROM ruby:3.4-slim AS base
# ------------------------------------------------------------
# Declarative metadata
diff --git a/Gemfile b/Gemfile
index cffeeda..026f5c3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby '~> 3.3'
+ruby '~> 3.4'
gem 'addressable', '~> 2.8'
gem 'awesome_print', '~> 1.9'
@@ -28,14 +28,14 @@ gem 'pg', '~> 1.2'
gem 'pg_search', '~> 2.3'
gem 'puma', '~> 7.2'
gem 'puma-plugin-delayed_stop', '~> 0.1.2'
-gem 'rails', '~> 8.0.5'
+gem 'rails', '~> 8.1.3'
gem 'ruby-vips', '~> 2.3'
gem 'sprockets-rails', '~> 3.5.0'
group :development, :test do
gem 'brakeman'
gem 'bundle-audit'
- gem 'byebug', platforms: %i[mri mingw x64_mingw]
+ gem 'byebug', platforms: %i[mri windows]
gem 'colorize'
gem 'factory_bot_rails'
gem 'rspec-rails', '~> 8.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index b25def1..e943661 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,29 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (8.0.5)
- actionpack (= 8.0.5)
- activesupport (= 8.0.5)
+ action_text-trix (2.1.19)
+ railties
+ actioncable (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (8.0.5)
- actionpack (= 8.0.5)
- activejob (= 8.0.5)
- activerecord (= 8.0.5)
- activestorage (= 8.0.5)
- activesupport (= 8.0.5)
+ actionmailbox (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
mail (>= 2.8.0)
- actionmailer (8.0.5)
- actionpack (= 8.0.5)
- actionview (= 8.0.5)
- activejob (= 8.0.5)
- activesupport (= 8.0.5)
+ actionmailer (8.1.3)
+ actionpack (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activesupport (= 8.1.3)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
- actionpack (8.0.5)
- actionview (= 8.0.5)
- activesupport (= 8.0.5)
+ actionpack (8.1.3)
+ actionview (= 8.1.3)
+ activesupport (= 8.1.3)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,42 +33,43 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
- actiontext (8.0.5)
- actionpack (= 8.0.5)
- activerecord (= 8.0.5)
- activestorage (= 8.0.5)
- activesupport (= 8.0.5)
+ actiontext (8.1.3)
+ action_text-trix (~> 2.1.15)
+ actionpack (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (8.0.5)
- activesupport (= 8.0.5)
+ actionview (8.1.3)
+ activesupport (= 8.1.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (8.0.5)
- activesupport (= 8.0.5)
+ activejob (8.1.3)
+ activesupport (= 8.1.3)
globalid (>= 0.3.6)
- activemodel (8.0.5)
- activesupport (= 8.0.5)
- activerecord (8.0.5)
- activemodel (= 8.0.5)
- activesupport (= 8.0.5)
+ activemodel (8.1.3)
+ activesupport (= 8.1.3)
+ activerecord (8.1.3)
+ activemodel (= 8.1.3)
+ activesupport (= 8.1.3)
timeout (>= 0.4.0)
- activestorage (8.0.5)
- actionpack (= 8.0.5)
- activejob (= 8.0.5)
- activerecord (= 8.0.5)
- activesupport (= 8.0.5)
+ activestorage (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activesupport (= 8.1.3)
marcel (~> 1.0)
- activesupport (8.0.5)
+ activesupport (8.1.3)
base64
- benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
@@ -99,7 +102,7 @@ GEM
berkeley_library-logging (~> 0.3)
rest-client (~> 2.1)
typesafe_enum (~> 0.3)
- bigdecimal (4.0.1)
+ bigdecimal (4.1.2)
bindex (0.8.1)
brakeman (8.0.4)
racc
@@ -121,12 +124,12 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
colorize (1.1.0)
- concurrent-ruby (1.3.6)
+ concurrent-ruby (1.3.7)
connection_pool (3.0.2)
crack (1.0.1)
bigdecimal
rexml
- crass (1.0.6)
+ crass (1.0.7)
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
csv (3.3.5)
@@ -150,7 +153,7 @@ GEM
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
- faraday (2.14.2)
+ faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -162,7 +165,7 @@ GEM
foreman (0.90.0)
thor (~> 1.4)
geo_coord (0.2.0)
- globalid (1.3.0)
+ globalid (1.4.0)
activesupport (>= 6.1)
hashdiff (1.2.1)
hashie (5.1.0)
@@ -170,7 +173,7 @@ GEM
http-accept (1.7.0)
http-cookie (1.1.0)
domain_name (~> 0.5)
- i18n (1.14.8)
+ i18n (1.15.2)
concurrent-ruby (~> 1.0)
iiif-presentation (1.4.2)
activesupport (>= 3.2.18)
@@ -178,7 +181,7 @@ GEM
geo_coord
json
io-console (0.8.2)
- irb (1.17.0)
+ irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
@@ -188,7 +191,7 @@ GEM
activesupport (>= 7.0.0)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
- json (2.19.7)
+ json (2.20.0)
jwt (2.10.3)
base64
language_server-protocol (3.17.0.5)
@@ -222,13 +225,13 @@ GEM
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0224)
mini_mime (1.1.5)
- minitest (6.0.2)
+ minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
mutex_m (0.3.0)
net-http (0.9.1)
uri (>= 0.11.1)
- net-imap (0.6.4)
+ net-imap (0.6.4.1)
date
net-protocol
net-pop (0.1.2)
@@ -239,14 +242,14 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.7.5)
- nokogiri (1.19.3-aarch64-linux-gnu)
+ nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.19.3-arm64-darwin)
+ nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.19.3-x86_64-linux-gnu)
+ nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
observer (0.1.2)
- oj (3.16.15)
+ oj (3.17.3)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
okcomputer (1.19.1)
@@ -281,15 +284,12 @@ GEM
pg_search (2.3.7)
activerecord (>= 6.1)
activesupport (>= 6.1)
- pp (0.6.3)
+ pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
- psych (5.3.1)
- date
- stringio
public_suffix (7.0.5)
- puma (7.2.0)
+ puma (7.2.1)
nio4r (~> 2.0)
puma-plugin-delayed_stop (0.1.2)
puma (>= 5.0, < 8)
@@ -306,20 +306,20 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
- rails (8.0.5)
- actioncable (= 8.0.5)
- actionmailbox (= 8.0.5)
- actionmailer (= 8.0.5)
- actionpack (= 8.0.5)
- actiontext (= 8.0.5)
- actionview (= 8.0.5)
- activejob (= 8.0.5)
- activemodel (= 8.0.5)
- activerecord (= 8.0.5)
- activestorage (= 8.0.5)
- activesupport (= 8.0.5)
+ rails (8.1.3)
+ actioncable (= 8.1.3)
+ actionmailbox (= 8.1.3)
+ actionmailer (= 8.1.3)
+ actionpack (= 8.1.3)
+ actiontext (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activemodel (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
bundler (>= 1.15.0)
- railties (= 8.0.5)
+ railties (= 8.1.3)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@@ -327,9 +327,9 @@ GEM
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- railties (8.0.5)
- actionpack (= 8.0.5)
- activesupport (= 8.0.5)
+ railties (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -337,13 +337,18 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
- rake (13.3.1)
+ rake (13.4.2)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
- rdoc (7.2.0)
+ rbs (4.0.3)
+ logger
+ prism (>= 1.6.0)
+ tsort
+ rdoc (8.0.0)
erb
- psych (>= 4.0.0)
+ prism (>= 1.6.0)
+ rbs (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.3)
@@ -441,9 +446,8 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
- stringio (3.2.0)
thor (1.5.0)
- timeout (0.6.0)
+ timeout (0.6.1)
tsort (0.2.0)
typesafe_enum (0.3.1)
tzinfo (2.0.6)
@@ -462,14 +466,14 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)
- websocket-driver (0.8.0)
+ websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
- zeitwerk (2.7.5)
+ zeitwerk (2.8.2)
PLATFORMS
aarch64-linux-gnu
@@ -513,7 +517,7 @@ DEPENDENCIES
pg_search (~> 2.3)
puma (~> 7.2)
puma-plugin-delayed_stop (~> 0.1.2)
- rails (~> 8.0.5)
+ rails (~> 8.1.3)
rspec (~> 3.10)
rspec-rails (~> 8.0)
rspec_junit_formatter (~> 0.5)
@@ -531,7 +535,7 @@ DEPENDENCIES
webmock
RUBY VERSION
- ruby 3.3.10p183
+ ruby 3.4.9
BUNDLED WITH
- 2.5.22
+ 4.0.15
diff --git a/bin/brakeman b/bin/brakeman
new file mode 100755
index 0000000..ace1c9b
--- /dev/null
+++ b/bin/brakeman
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+ARGV.unshift("--ensure-latest")
+
+load Gem.bin_path("brakeman", "brakeman")
diff --git a/bin/bundler-audit b/bin/bundler-audit
new file mode 100755
index 0000000..e2ef226
--- /dev/null
+++ b/bin/bundler-audit
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "bundler/audit/cli"
+
+ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check")
+Bundler::Audit::CLI.start
diff --git a/bin/ci b/bin/ci
new file mode 100755
index 0000000..4137ad5
--- /dev/null
+++ b/bin/ci
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "active_support/continuous_integration"
+
+CI = ActiveSupport::ContinuousIntegration
+require_relative "../config/ci.rb"
diff --git a/bin/dev b/bin/dev
index 1ca73ac..52088a3 100755
--- a/bin/dev
+++ b/bin/dev
@@ -1,4 +1,2 @@
-#!/usr/bin/env bash
-# bin/dev: run Rails and JS bundler together in development
-
-exec foreman start -f Procfile.dev "$@"
+#!/usr/bin/env ruby
+exec './bin/rails', 'server', *ARGV
diff --git a/bin/lending/_zeitwerk.rb b/bin/lending/_zeitwerk.rb
old mode 100644
new mode 100755
diff --git a/bin/rubocop b/bin/rubocop
new file mode 100755
index 0000000..5a20504
--- /dev/null
+++ b/bin/rubocop
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+# Explicit RuboCop config increases performance slightly while avoiding config confusion.
+ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
+
+load Gem.bin_path("rubocop", "rubocop")
diff --git a/bin/setup b/bin/setup
index be3db3c..81be011 100755
--- a/bin/setup
+++ b/bin/setup
@@ -22,6 +22,7 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
+ system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
diff --git a/config/application.rb b/config/application.rb
index 0cf1595..59d64e7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -20,7 +20,7 @@
module UCBEARS
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 8.0
+ config.load_defaults 8.1
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml
new file mode 100644
index 0000000..e74b3af
--- /dev/null
+++ b/config/bundler-audit.yml
@@ -0,0 +1,5 @@
+# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
+# CVEs that are not relevant to the application can be enumerated on the ignore list below.
+
+ignore:
+ - CVE-THAT-DOES-NOT-APPLY
diff --git a/config/ci.rb b/config/ci.rb
new file mode 100644
index 0000000..f0bb2b8
--- /dev/null
+++ b/config/ci.rb
@@ -0,0 +1,19 @@
+# Run using bin/ci
+
+CI.run do
+ step 'Setup', 'bin/setup --skip-server'
+
+ step 'Style: Ruby', 'bin/rubocop'
+
+ step 'Security: Gem audit', 'bin/bundler-audit'
+ step 'Security: Importmap vulnerability audit', 'bin/importmap audit'
+ step 'Security: Brakeman code analysis', 'bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error'
+
+ # Optional: set a green GitHub commit status to unblock PR merge.
+ # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
+ # if success?
+ # step 'Signoff: All systems go. Ready for merge and deploy.', 'gh signoff'
+ # else
+ # failure 'Signoff: CI failed. Do not merge or deploy.', 'Fix the issues and try again.'
+ # end
+end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 883b0ba..7a930a7 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/integer/time'
+# rubocop:disable Metrics/BlockLength
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -45,6 +46,9 @@
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
+ # Highlight code that triggered redirect in logs.
+ config.action_dispatch.verbose_redirect_logs = true
+
# Allow cross-origin requests in development.
config.action_dispatch.default_headers['Access-Control-Allow-Origin'] = '*'
@@ -69,3 +73,4 @@
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!
end
+# rubocop:enable Metrics/BlockLength
diff --git a/config/environments/production.rb b/config/environments/production.rb
index bb42154..01b143c 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -15,9 +15,6 @@
# Turn on fragment caching in view templates.
config.action_controller.perform_caching = true
- # Serve static files from the app server.
- config.public_file_server.enabled = true
-
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
@@ -41,7 +38,7 @@
config.log_tags = [:request_id]
- # Change to "debug" to log everything (including potentially personally-identifiable information!)
+ # Change to "debug" to log everything (including potentially personally-identifiable information!).
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
# Prevent health checks from clogging up the logs.
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000..d51d713
--- /dev/null
+++ b/config/initializers/content_security_policy.rb
@@ -0,0 +1,29 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
+
+# Rails.application.configure do
+# config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+#
+# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+# config.content_security_policy_nonce_directives = %w(script-src style-src)
+#
+# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
+# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
+# # config.content_security_policy_nonce_auto = true
+#
+# # Report violations without enforcing the policy.
+# # config.content_security_policy_report_only = true
+# end
diff --git a/config/puma.rb b/config/puma.rb
index 2c9cfb3..7fee6bb 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -7,7 +7,8 @@
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
-# default is already 1.
+# default is already 1. You can set it to `auto` to automatically start a worker
+# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
diff --git a/config/routes.rb b/config/routes.rb
index b5d99c7..cbb322b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Metrics/BlockLength
Rails.application.routes.draw do
root 'sessions#index'
@@ -48,3 +49,4 @@
# Map OkComputer's /health/all.json to /health
get '/health', to: 'ok_computer/ok_computer#index', defaults: { format: :json }
end
+# rubocop:enable Metrics/BlockLength
diff --git a/public/400.html b/public/400.html
index 282dbc8..640de03 100644
--- a/public/400.html
+++ b/public/400.html
@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
- min-height: 100vh;
+ min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
+ #error-description {
+ fill: #d30001;
+ }
+
+ #error-id {
+ fill: #f0eff0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background: #101010;
+ color: #e0e0e0;
+ }
+
+ #error-description {
+ fill: #FF6161;
+ }
+
+ #error-id {
+ fill: #2c2c2c;
+ }
+ }
+
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
-
display: none;
@media(min-width: 48em) {
display: inline;
}
-
}
@@ -102,10 +123,10 @@
- The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.
+ The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.
diff --git a/public/404.html b/public/404.html
index c0670bc..d7f0f14 100644
--- a/public/404.html
+++ b/public/404.html
@@ -4,7 +4,7 @@
- The page you were looking for doesn’t exist (404 Not found)
+ The page you were looking for doesn't exist (404 Not found)
@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
- min-height: 100vh;
+ min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
+ #error-description {
+ fill: #d30001;
+ }
+
+ #error-id {
+ fill: #f0eff0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background: #101010;
+ color: #e0e0e0;
+ }
+
+ #error-description {
+ fill: #FF6161;
+ }
+
+ #error-id {
+ fill: #2c2c2c;
+ }
+ }
+
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
-
display: none;
@media(min-width: 48em) {
display: inline;
}
-
}
@@ -102,10 +123,10 @@
- The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.
+ The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.
diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html
new file mode 100644
index 0000000..43d2811
--- /dev/null
+++ b/public/406-unsupported-browser.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+ Your browser is not supported (406 Not Acceptable)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Your browser is not supported. Please upgrade your browser to continue.
+
+
+
+
+
+
diff --git a/public/422.html b/public/422.html
index 8bcf060..f12fb4a 100644
--- a/public/422.html
+++ b/public/422.html
@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
- min-height: 100vh;
+ min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
+ #error-description {
+ fill: #d30001;
+ }
+
+ #error-id {
+ fill: #f0eff0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background: #101010;
+ color: #e0e0e0;
+ }
+
+ #error-description {
+ fill: #FF6161;
+ }
+
+ #error-id {
+ fill: #2c2c2c;
+ }
+ }
+
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
-
display: none;
@media(min-width: 48em) {
display: inline;
}
-
}
@@ -102,10 +123,10 @@
- The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.
+ The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.
diff --git a/public/500.html b/public/500.html
index d77718c..e4eb18a 100644
--- a/public/500.html
+++ b/public/500.html
@@ -4,7 +4,7 @@
- We’re sorry, but something went wrong (500 Internal Server Error)
+ We're sorry, but something went wrong (500 Internal Server Error)
@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
- min-height: 100vh;
+ min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
+ #error-description {
+ fill: #d30001;
+ }
+
+ #error-id {
+ fill: #f0eff0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background: #101010;
+ color: #e0e0e0;
+ }
+
+ #error-description {
+ fill: #FF6161;
+ }
+
+ #error-id {
+ fill: #2c2c2c;
+ }
+ }
+
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
-
display: none;
@media(min-width: 48em) {
display: inline;
}
-
}
@@ -102,10 +123,10 @@
- We’re sorry, but something went wrong. If you’re the application owner check the logs for more information.
+ We're sorry, but something went wrong. If you're the application owner check the logs for more information.
diff --git a/public/icon.png b/public/icon.png
new file mode 100644
index 0000000..c4c9dbf
Binary files /dev/null and b/public/icon.png differ
diff --git a/public/icon.svg b/public/icon.svg
new file mode 100644
index 0000000..04b34bf
--- /dev/null
+++ b/public/icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/spec/.rubocop.yml b/spec/.rubocop.yml
index b7de565..a3d5081 100644
--- a/spec/.rubocop.yml
+++ b/spec/.rubocop.yml
@@ -1,139 +1,12 @@
inherit_from: ../.rubocop.yml
-plugins:
- - rubocop-rspec
- - rubocop-rspec_rails
-# Sometimes we're testing the operator
-Lint/BinaryOperatorWithIdenticalOperands:
- Enabled: false
-Metrics/AbcSize:
- Enabled: false
-
-Metrics/BlockLength:
- Enabled: false
-
-Metrics/ModuleLength:
- Enabled: false
-
-Metrics/MethodLength:
- Enabled: false
############################################################
# rubocop-rails
-# we meant to do that
-Rails/SkipsModelValidations:
- Enabled: false
-
-# we meant to do that
-Rails/TimeZone:
- Enabled: false
-
-############################################################
-# rubocop-rspec
-
-# believe me, it wasn't by choice
-RSpec/AnyInstance:
- Enabled: false
-
# This is used as plain Ruby not ActiveRecord. Pluck not available
-# in the context used
+# in the context used
Rails/Pluck:
- Enabled: false
-
-RSpec/VerifiedDoubleReference:
- Enabled: false
-
-# we meant to do that
-RSpec/BeforeAfterAll:
- Enabled: false
-
-RSpec/ContainExactly:
- Enabled: false
-
-# more words != more readable
-RSpec/ContextWording:
- Enabled: false
-
-# not everything we test is a class
-RSpec/DescribeClass:
- Enabled: false
-
-# explicit >>> implicit
-RSpec/DescribedClass:
- Enabled: false
-
-# more punctuation != more readable
-RSpec/DescribeSymbol:
- Enabled: false
-
-# setup cost / time >>> failure granularity
-RSpec/ExampleLength:
- Max: 25
- CountAsOne:
- - array
- - hash
- - heredoc
-
-# we meant to do that
-RSpec/ExpectInHook:
- Enabled: false
-
-# your naming scheme is not in possession of all the facts
-# RSpec/FilePath:
-# Enabled: false
-
-RSpec/SpecFilePathFormat:
- Enabled: false
-
-RSpec/SpecFilePathSuffix:
- Enabled: false
-
-# explicit >>> implicit
-RSpec/InstanceVariable:
- Enabled: false
-
-# maybe when 'all' has a corresponding 'none' matcher
-RSpec/IteratedExpectation:
- Enabled: false
-
-# we meant to do that
-RSpec/MessageSpies:
- Enabled: false
-
-# too late now
-RSpec/MultipleMemoizedHelpers:
- Enabled: false
-
-# setup cost / time >>> failure granularity
-RSpec/MultipleExpectations:
- Enabled: false
-
-# cure is worse than the disease
-RSpec/NestedGroups:
- Enabled: false
-
-# more quotation marks != more readable
-RSpec/SharedExamples:
- Enabled: false
-
-# we meant to do that
-RSpec/StubbedMock:
- Enabled: false
-
-# we meant to do that
-RSpec/VerifiedDoubles:
- Enabled: false
-
-############################################################
-# rubocop-rspec
-
-# enable newer rubocop-rspec cops
-
-RSpec/IdenticalEqualityAssertion: # new in 2.4
- Enabled: true
-
-RSpecRails/AvoidSetupHook: # new in 2.4
- Enabled: true
+ Enabled: false
\ No newline at end of file
diff --git a/spec/lib/lending/tileizer_spec.rb b/spec/lib/lending/tileizer_spec.rb
index 656d18b..b67a9e4 100644
--- a/spec/lib/lending/tileizer_spec.rb
+++ b/spec/lib/lending/tileizer_spec.rb
@@ -71,14 +71,16 @@ module Lending
end
describe :tileize_all do
+ # rubocop:disable RSpec/LeakyLocalVariable
sample_root = 'spec/data/lending/samples'
+ # rubocop:enable RSpec/LeakyLocalVariable
context 'with TIFF files' do
- include_examples :tileize_all_examples, File.join(sample_root, 'b135297126_C068087930_TIFF')
+ it_behaves_like :tileize_all_examples, File.join(sample_root, 'b135297126_C068087930_TIFF')
end
context 'with JPEG files' do
- include_examples :tileize_all_examples, File.join(sample_root, 'b135297126_C068087930_JPEG')
+ it_behaves_like :tileize_all_examples, File.join(sample_root, 'b135297126_C068087930_JPEG')
end
end
end
diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb
index ea56472..9be4f7d 100644
--- a/spec/models/item_spec.rb
+++ b/spec/models/item_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe Item, type: :model do
+describe Item do
let(:factory_names) do
%i[
inactive_item
diff --git a/spec/models/loan_spec.rb b/spec/models/loan_spec.rb
index b2d6afd..0a999cc 100644
--- a/spec/models/loan_spec.rb
+++ b/spec/models/loan_spec.rb
@@ -81,27 +81,19 @@
attr_reader :loans_by_date
before do
- @env_tz_actual = ENV.fetch('TZ', nil)
- @rails_tz_actual = Time.zone
-
- Time.zone = 'America/Los_Angeles'
-
- @loans_by_date = {}
-
- local_date = (Date.current - 7.days)
- year, month, day = %i[year month day].map { |attr| local_date.send(attr) }
- [2, 8, 14, 20].each do |hour|
- loan_date = Time.zone.local(year, month, day, hour)
- loan = create(:expired_loan, loan_date:, item_id: item.id, patron_identifier: user.borrower_id)
- (loans_by_date[local_date] ||= []) << loan
+ Time.use_zone 'America/Los_Angeles' do
+ @loans_by_date = {}
+
+ local_date = (Date.current - 7.days)
+ year, month, day = %i[year month day].map { |attr| local_date.send(attr) }
+ [2, 8, 14, 20].each do |hour|
+ loan_date = Time.zone.local(year, month, day, hour)
+ loan = create(:expired_loan, loan_date:, item_id: item.id, patron_identifier: user.borrower_id)
+ (loans_by_date[local_date] ||= []) << loan
+ end
end
end
- after do
- ENV['TZ'] = env_tz_actual
- Time.zone = rails_tz_actual
- end
-
it 'returns loans for the correct date regardless of the Ruby time zone' do
%w[UTC America/Los_Angeles].each do |tz|
ENV['TZ'] = tz
diff --git a/spec/models/settings_spec.rb b/spec/models/settings_spec.rb
index ed322b3..86ebde3 100644
--- a/spec/models/settings_spec.rb
+++ b/spec/models/settings_spec.rb
@@ -1,5 +1,5 @@
require 'rails_helper'
-RSpec.describe Settings, type: :model do
+describe Settings do
pending "add some examples to (or delete) #{__FILE__}"
end
diff --git a/spec/models/term_spec.rb b/spec/models/term_spec.rb
index 6104bbe..a459fe2 100644
--- a/spec/models/term_spec.rb
+++ b/spec/models/term_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe Term, type: :model do
+describe Term do
describe :create do
it 'requires a name' do
term = Term.create(start_date: Date.current, end_date: Date.current + 1.days)
diff --git a/spec/request/lending_request_spec.rb b/spec/request/lending_request_spec.rb
index eef753e..c3fbdf4 100644
--- a/spec/request/lending_request_spec.rb
+++ b/spec/request/lending_request_spec.rb
@@ -80,13 +80,13 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_show_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
# This is now treated as an http error in Rails 8
it 'returns 404 not found for /lending' do
get '/lending'
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -102,7 +102,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_edit_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -118,7 +118,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_manifest_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -146,7 +146,7 @@ def available
expect(item).not_to be_nil # just to be sure
patch lending_update_path(directory: item.directory), params: { item: { active: true, copies: 17 } }
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(:unprocessable_content)
item.reload
expect(item).not_to be_active
@@ -158,7 +158,7 @@ def available
expect(item).not_to be_nil # just to be sure
patch lending_update_path(directory: item.directory), params: { item: { active: true, copies: -1 } }
- expect(response.status).to eq(422)
+ expect(response).to have_http_status(:unprocessable_content)
item.reload
expect(item).not_to be_active
@@ -167,7 +167,7 @@ def available
it 'returns 404 not found for nonexistent items' do
patch lending_update_path(directory: 'not_a_directory'), params: { item: { active: true, copies: 17 } }
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
end
@@ -213,7 +213,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_activate_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -260,7 +260,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_deactivate_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -296,7 +296,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_destroy_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
it 'works for incomplete items that differ from complete items only by "file extension"' do
@@ -340,7 +340,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_reload_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
it 'succeeds for unchanged items' do
@@ -382,12 +382,12 @@ def available
describe :show do
it 'returns 403 Forbidden' do
get lending_show_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'returns 403 forbidden even for nonexistent items' do
get lending_deactivate_path(directory: 'not_a_directory')
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
@@ -405,7 +405,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_view_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
it 'shows a loan if one exists' do
@@ -503,7 +503,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_check_out_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
it 'fails if this user has already checked out the item' do
@@ -514,7 +514,7 @@ def available
get lending_check_out_path(directory: item.directory)
end.not_to change(Loan, :count)
- expect(response.status).to eq(422) # unprocessable entity
+ expect(response).to have_http_status(:unprocessable_content)
expect(response.body).to include(Item::MSG_CHECKED_OUT)
end
@@ -534,7 +534,7 @@ def available
get lending_check_out_path(directory: item.directory)
end.not_to change(Loan, :count)
- expect(response.status).to eq(422) # unprocessable entity
+ expect(response).to have_http_status(:unprocessable_content)
expect(response.body).to include(Item::MSG_CHECKOUT_LIMIT_REACHED)
end
@@ -610,7 +610,7 @@ def available
get lending_check_out_path(directory: item.directory)
end.not_to change(Loan, :count)
- expect(response.status).to eq(422) # unprocessable entity
+ expect(response).to have_http_status(:unprocessable_content)
expect(response.body).to include(Item::MSG_UNAVAILABLE)
end
@@ -621,7 +621,7 @@ def available
get lending_check_out_path(directory: item.directory)
end.not_to change(Loan, :count)
- expect(response.status).to eq(422) # unprocessable entity
+ expect(response).to have_http_status(:unprocessable_content)
expect(response.body).to include(Item::MSG_INACTIVE)
end
end
@@ -657,7 +657,7 @@ def available
it 'returns 404 not found for nonexistent items' do
get lending_return_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
@@ -671,31 +671,31 @@ def available
it 'returns 403 Forbidden if the item has not been checked out' do
get lending_manifest_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'returns 404 not found for nonexistent items' do
get lending_manifest_path(directory: 'not_a_directory')
- expect(response.status).to eq(404)
+ expect(response).to have_http_status(:not_found)
end
end
describe :edit do
it 'returns 403 forbidden' do
get lending_edit_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'returns 403 forbidden even for nonexistent items' do
get lending_edit_path(directory: 'not_a_directory')
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
describe :activate do
it 'returns 403 forbidden' do
get lending_activate_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it "doesn't activate the item" do
@@ -709,14 +709,14 @@ def available
it 'returns 403 forbidden even for nonexistent items' do
get lending_activate_path(directory: 'not_a_directory')
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
describe :inactivate do
it 'returns 403 forbidden' do
get lending_deactivate_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
item.reload
expect(item.active).to be(true)
@@ -724,14 +724,14 @@ def available
it 'returns 403 forbidden even for nonexistent items' do
get lending_deactivate_path(directory: 'not_a_directory')
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
describe :reload do
it 'returns 403 forbidden' do
get lending_reload_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it "doesn't reload the MARC metadata" do
@@ -747,7 +747,7 @@ def available
it 'returns 403 forbidden even for nonexistent items' do
get lending_reload_path(directory: 'not_a_directory')
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
end
@@ -815,34 +815,34 @@ def available
it 'GET lending_manifest_path returns 403 Forbidden' do
get lending_manifest_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'GET lending_edit_path returns 403 Forbidden' do
get lending_edit_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'GET lending_view_path returns 403 Forbidden' do
get lending_view_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
it 'DELETE lending_destroy_path returns 403 Forbidden' do
delete lending_destroy_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
# TODO: use PATCH
it 'GET lending_activate_path returns 403 Forbidden' do
get lending_activate_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
# TODO: use PATCH
it 'GET lending_deactivate_path returns 403 Forbidden' do
get lending_deactivate_path(directory: item.directory)
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
end
end
end
diff --git a/spec/request/stats_request_spec.rb b/spec/request/stats_request_spec.rb
index afab647..61f34e7 100644
--- a/spec/request/stats_request_spec.rb
+++ b/spec/request/stats_request_spec.rb
@@ -112,13 +112,13 @@
it 'rejects a non-date argument' do
get stats_download_path(date: 'not a date')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(:bad_request)
expect(response.body).to be_empty
end
it 'rejects a bad date' do
get stats_download_path(date: '9999-99-99')
- expect(response.status).to eq(400)
+ expect(response).to have_http_status(:bad_request)
expect(response.body).to be_empty
end
end
diff --git a/spec/requests/items_spec.rb b/spec/requests/items_spec.rb
index 78c88e3..b4ce6bf 100644
--- a/spec/requests/items_spec.rb
+++ b/spec/requests/items_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe '/items', type: :request do
+describe '/items', type: :request do
def expected_json(item)
renderer = ApplicationController.renderer.new(http_host: request.host)
@@ -54,7 +54,7 @@ def expected_json(item)
describe 'GET /index' do
it 'returns 403 Forbidden for HTML requests' do
get items_url, as: :html
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
expect(response.content_type).to start_with('text/html')
expect(response.body).to include('restricted to UC BEARS administrators')
end
@@ -65,7 +65,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.order(:title)
@@ -88,7 +88,7 @@ def expected_json(item)
links = link_header.split(', ')
expect(links).to include('; rel="profile"')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.order(:title)
@@ -147,7 +147,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.order(:title).first(Pagy.options[:limit])
@@ -163,7 +163,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.complete
@@ -181,7 +181,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.incomplete
@@ -199,7 +199,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.where(active: true)
@@ -217,7 +217,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.where(active: false)
@@ -235,7 +235,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = Item.inactive.complete
@@ -253,7 +253,7 @@ def expected_json(item)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_items = term_fall_2021.items.incomplete.where(active: true)
@@ -321,7 +321,7 @@ def expected_json(item)
item.reload
- actual_json = JSON.parse(response.body)
+ actual_json = response.parsed_body
expect(actual_json).to eq(expected_json(item))
expect(response.content_type).to start_with('application/json')
@@ -337,7 +337,7 @@ def expected_json(item)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)
@@ -377,7 +377,7 @@ def expected_json(item)
expect(response).to have_http_status(:forbidden)
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)
@@ -437,7 +437,7 @@ def expected_json(item)
get processing_url, as: :json
expect(response).to be_successful
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Array)
expect(parsed_response.size).to eq(symlinks.size)
diff --git a/spec/requests/terms_spec.rb b/spec/requests/terms_spec.rb
index 11f3508..e17a4e5 100644
--- a/spec/requests/terms_spec.rb
+++ b/spec/requests/terms_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe '/terms', type: :request do
+describe '/terms', type: :request do
def expected_json(term)
renderer = ApplicationController.renderer.new(http_host: request.host)
expected_json = renderer.render(template: 'terms/show', assigns: { term: })
@@ -65,7 +65,7 @@ def expected_json(term)
describe 'GET /index' do
it 'returns 403 Forbidden for HTML requests' do
get terms_url, as: :html
- expect(response.status).to eq(403)
+ expect(response).to have_http_status(:forbidden)
expect(response.content_type).to start_with('text/html')
expect(response.body).to include('restricted to UC BEARS administrators')
end
@@ -76,7 +76,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_terms = Term.all
@@ -101,7 +101,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_an(Array)
expected_terms = Term.all
@@ -121,7 +121,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to eq(expected_json(term))
end
end
@@ -170,7 +170,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to match(%r{^application/json})
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expected = {
@@ -206,7 +206,7 @@ def expected_json(term)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)
@@ -250,7 +250,7 @@ def expected_json(term)
expected_msg = I18n.t('activerecord.errors.messages.taken')
expected_msg_re = /#{Regexp.escape(expected_msg)}/
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)
@@ -286,7 +286,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to match(%r{^application/json})
- actual_json = JSON.parse(response.body)
+ actual_json = response.parsed_body
expect(actual_json).to eq(expected_json(term))
end
@@ -341,7 +341,7 @@ def expected_json(term)
expect(response).to be_successful
expect(response.content_type).to match(%r{^application/json})
- actual_json = JSON.parse(response.body)
+ actual_json = response.parsed_body
expect(actual_json).to eq(expected_json(term))
end
@@ -358,7 +358,7 @@ def expected_json(term)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to start_with('application/json')
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)
@@ -421,7 +421,7 @@ def expected_json(term)
expected_msg = I18n.t('activerecord.errors.messages.taken')
expected_msg_re = /#{Regexp.escape(expected_msg)}/
- parsed_response = JSON.parse(response.body)
+ parsed_response = response.parsed_body
expect(parsed_response).to be_a(Hash)
expect(parsed_response['success']).to be(false)