Compare commits
4 commits
54bdd52e75
...
13992e1dbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13992e1dbe | ||
|
|
ed626b5066 | ||
|
|
9058d1ae2e | ||
|
|
1acd3e30ae |
50 changed files with 1337 additions and 5 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -30,3 +30,5 @@
|
||||||
|
|
||||||
# Ignore master key for decrypting credentials and more.
|
# Ignore master key for decrypting credentials and more.
|
||||||
/config/master.key
|
/config/master.key
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="TEST" name="annotaterb (v4.22.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="TEST" name="annotaterb (v4.22.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="backport (v1.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.21, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.21, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="debug (v1.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="debug (v1.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="dotenv (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="dotenv (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="TEST" name="dotenv-rails (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="TEST" name="dotenv-rails (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -68,8 +70,11 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.8, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.8, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="irb (v1.18.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="irb (v1.18.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="jaro_winkler (v1.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="kamal (v2.11.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="kamal (v2.11.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="kramdown (v2.5.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="kramdown-parser-gfm (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -88,6 +93,7 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.19.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.19.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="observer (v0.1.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -110,9 +116,11 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rake (v13.4.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rake (v13.4.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="rbs (v3.9.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.12.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.12.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="reverse_markdown (v3.0.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.4, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.4, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.86.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.86.2, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -122,6 +130,7 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ruby-graphviz (v1.2.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ruby-graphviz (v1.2.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="solargraph (v0.57.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.12, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.12, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.10, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.10, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.4.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.4.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -129,6 +138,7 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="thor (v1.5.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="thor (v1.5.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.20, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.20, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="tilt (v2.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
|
@ -138,6 +148,9 @@
|
||||||
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.8.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.8.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="yard (v0.9.37, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="yard-activesupport-concern (v0.0.1, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="yard-solargraph (v0.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
|
||||||
</component>
|
</component>
|
||||||
<component name="RModuleSettingsStorage">
|
<component name="RModuleSettingsStorage">
|
||||||
|
|
|
||||||
3
Gemfile
3
Gemfile
|
|
@ -49,4 +49,7 @@ group :development, :test do
|
||||||
|
|
||||||
gem "annotaterb"
|
gem "annotaterb"
|
||||||
gem "dotenv-rails"
|
gem "dotenv-rails"
|
||||||
|
|
||||||
|
# Ruby language server for IDE support
|
||||||
|
gem "solargraph", require: false
|
||||||
end
|
end
|
||||||
40
Gemfile.lock
40
Gemfile.lock
|
|
@ -76,6 +76,7 @@ GEM
|
||||||
activerecord (>= 6.0.0)
|
activerecord (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
|
backport (1.2.0)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.21)
|
bcrypt (3.1.21)
|
||||||
bcrypt_pbkdf (1.1.2)
|
bcrypt_pbkdf (1.1.2)
|
||||||
|
|
@ -94,6 +95,7 @@ GEM
|
||||||
debug (1.11.1)
|
debug (1.11.1)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
|
diff-lcs (1.6.2)
|
||||||
dotenv (3.2.0)
|
dotenv (3.2.0)
|
||||||
dotenv-rails (3.2.0)
|
dotenv-rails (3.2.0)
|
||||||
dotenv (= 3.2.0)
|
dotenv (= 3.2.0)
|
||||||
|
|
@ -117,6 +119,7 @@ GEM
|
||||||
prism (>= 1.3.0)
|
prism (>= 1.3.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
|
jaro_winkler (1.6.1)
|
||||||
json (2.19.5)
|
json (2.19.5)
|
||||||
kamal (2.11.0)
|
kamal (2.11.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
|
|
@ -129,6 +132,10 @@ GEM
|
||||||
sshkit (>= 1.23.0, < 2.0)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
thor (~> 1.3)
|
thor (~> 1.3)
|
||||||
zeitwerk (>= 2.6.18, < 3.0)
|
zeitwerk (>= 2.6.18, < 3.0)
|
||||||
|
kramdown (2.5.1)
|
||||||
|
rexml (>= 3.3.9)
|
||||||
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
kramdown (~> 2.0)
|
||||||
language_server-protocol (3.17.0.5)
|
language_server-protocol (3.17.0.5)
|
||||||
lint_roller (1.1.0)
|
lint_roller (1.1.0)
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
|
|
@ -176,6 +183,7 @@ GEM
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-x86_64-linux-musl)
|
nokogiri (1.19.3-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
observer (0.1.2)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
parallel (2.1.0)
|
parallel (2.1.0)
|
||||||
parser (3.3.11.1)
|
parser (3.3.11.1)
|
||||||
|
|
@ -243,6 +251,8 @@ GEM
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.4.2)
|
rake (13.4.2)
|
||||||
|
rbs (3.9.5)
|
||||||
|
logger
|
||||||
rdoc (7.2.0)
|
rdoc (7.2.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
|
@ -250,6 +260,8 @@ GEM
|
||||||
regexp_parser (2.12.0)
|
regexp_parser (2.12.0)
|
||||||
reline (0.6.3)
|
reline (0.6.3)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
|
reverse_markdown (3.0.0)
|
||||||
|
nokogiri
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rubocop (1.86.2)
|
rubocop (1.86.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
|
|
@ -283,6 +295,27 @@ GEM
|
||||||
rexml
|
rexml
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
|
solargraph (0.57.0)
|
||||||
|
backport (~> 1.2)
|
||||||
|
benchmark (~> 0.4)
|
||||||
|
bundler (~> 2.0)
|
||||||
|
diff-lcs (~> 1.4)
|
||||||
|
jaro_winkler (~> 1.6, >= 1.6.1)
|
||||||
|
kramdown (~> 2.3)
|
||||||
|
kramdown-parser-gfm (~> 1.1)
|
||||||
|
logger (~> 1.6)
|
||||||
|
observer (~> 0.1)
|
||||||
|
ostruct (~> 0.6)
|
||||||
|
parser (~> 3.0)
|
||||||
|
prism (~> 1.4)
|
||||||
|
rbs (>= 3.6.1, <= 4.0.0.dev.4)
|
||||||
|
reverse_markdown (~> 3.0)
|
||||||
|
rubocop (~> 1.76)
|
||||||
|
thor (~> 1.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
yard (~> 0.9, >= 0.9.24)
|
||||||
|
yard-activesupport-concern (~> 0.0)
|
||||||
|
yard-solargraph (~> 0.1)
|
||||||
solid_cable (3.0.12)
|
solid_cable (3.0.12)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
|
|
@ -312,6 +345,7 @@ GEM
|
||||||
thruster (0.1.20-aarch64-linux)
|
thruster (0.1.20-aarch64-linux)
|
||||||
thruster (0.1.20-arm64-darwin)
|
thruster (0.1.20-arm64-darwin)
|
||||||
thruster (0.1.20-x86_64-linux)
|
thruster (0.1.20-x86_64-linux)
|
||||||
|
tilt (2.6.1)
|
||||||
timeout (0.6.1)
|
timeout (0.6.1)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
|
|
@ -325,6 +359,11 @@ GEM
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
|
yard (0.9.37)
|
||||||
|
yard-activesupport-concern (0.0.1)
|
||||||
|
yard (>= 0.8)
|
||||||
|
yard-solargraph (0.1.0)
|
||||||
|
yard (~> 0.9)
|
||||||
zeitwerk (2.7.5)
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
|
@ -351,6 +390,7 @@ DEPENDENCIES
|
||||||
rails (~> 8.0.4)
|
rails (~> 8.0.4)
|
||||||
rails-erd
|
rails-erd
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
solargraph
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,26 @@
|
||||||
class ApplicationController < ActionController::API
|
class ApplicationController < ActionController::API
|
||||||
|
include Renderable
|
||||||
|
|
||||||
before_action :authenticate!
|
before_action :authenticate!
|
||||||
|
|
||||||
|
rescue_from Unauthorized do
|
||||||
|
render_error("Unauthorized", :unauthorized)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def authenticate!
|
def authenticate!
|
||||||
token = request.headers["Authorization"]&.split(" ")&.last
|
token = request.headers["Authorization"]&.split(" ")&.last
|
||||||
@current_user = User.find_by(token:)
|
@current_session = Session.find_by(token: token)
|
||||||
|
raise Unauthorized unless @current_session
|
||||||
|
|
||||||
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
|
@current_user = @current_session.user
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_user
|
def current_user
|
||||||
@current_user
|
@current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_session
|
||||||
|
@current_session
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
19
app/controllers/concerns/renderable.rb
Normal file
19
app/controllers/concerns/renderable.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Renderable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def render_success(data = nil, status = :ok, meta = {})
|
||||||
|
body = { success: true }
|
||||||
|
body[:data] = data if data
|
||||||
|
body[:meta] = meta if meta.present?
|
||||||
|
render json: body, status:
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_error(message, status = :unprocessable_entity, details: nil)
|
||||||
|
body = { success: false, error: { message: } }
|
||||||
|
body[:error][:details] = details if details.present?
|
||||||
|
render json: body, status:
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
26
app/controllers/sessions_controller.rb
Normal file
26
app/controllers/sessions_controller.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
class SessionsController < ApplicationController
|
||||||
|
skip_before_action :authenticate!, only: [ :create ]
|
||||||
|
|
||||||
|
def create
|
||||||
|
user = User.find_by(email: login_params[:email])
|
||||||
|
|
||||||
|
unless user.present? && user.authenticate(login_params[:password])
|
||||||
|
raise Unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
session = user.sessions.create!
|
||||||
|
|
||||||
|
render_success(user: user, token: session.token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@current_session.destroy!
|
||||||
|
render_success(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def login_params
|
||||||
|
params.permit(:email, :password)
|
||||||
|
end
|
||||||
|
end
|
||||||
28
app/controllers/users_controller.rb
Normal file
28
app/controllers/users_controller.rb
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
class UsersController < ApplicationController
|
||||||
|
skip_before_action :authenticate!, only: [ :create ]
|
||||||
|
|
||||||
|
def create
|
||||||
|
user = User.new(user_params)
|
||||||
|
|
||||||
|
if user.save
|
||||||
|
session = user.sessions.create!
|
||||||
|
render_success({ user: user, token: session.token }, :created)
|
||||||
|
else
|
||||||
|
render_error("Failed to create user", :unprocessable_entity, details: user.errors.full_messages)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def user_params
|
||||||
|
params.require(:user).permit(:name, :email, :password, :password_confirmation)
|
||||||
|
end
|
||||||
|
end
|
||||||
1
app/errors/unauthorized.rb
Normal file
1
app/errors/unauthorized.rb
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
class Unauthorized < StandardError; end
|
||||||
25
app/models/session.rb
Normal file
25
app/models/session.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: sessions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# ip_address :string
|
||||||
|
# token :string
|
||||||
|
# user_agent :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_sessions_on_token (token) UNIQUE
|
||||||
|
# index_sessions_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
class Session < ApplicationRecord
|
||||||
|
has_secure_token
|
||||||
|
belongs_to :user
|
||||||
|
end
|
||||||
|
|
@ -18,4 +18,5 @@ class User < ApplicationRecord
|
||||||
has_secure_password
|
has_secure_password
|
||||||
has_many :price_reports, dependent: :nullify
|
has_many :price_reports, dependent: :nullify
|
||||||
has_many :moderation_logs, foreign_key: :admin_id, dependent: :nullify
|
has_many :moderation_logs, foreign_key: :admin_id, dependent: :nullify
|
||||||
|
has_many :sessions, dependent: :destroy
|
||||||
end
|
end
|
||||||
|
|
|
||||||
35
bruno/auth/login-invalid.bru
Normal file
35
bruno/auth/login-invalid.bru
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
meta {
|
||||||
|
name: Login - Invalid Credentials
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/login
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"email": "nonexistent@example.com",
|
||||||
|
"password": "wrongpassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 401
|
||||||
|
res.body.success: equals false
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Invalid login returns error envelope", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(false);
|
||||||
|
expect(data).to.have.property("error");
|
||||||
|
expect(data.error).to.have.property("message");
|
||||||
|
});
|
||||||
|
}
|
||||||
64
bruno/auth/login.bru
Normal file
64
bruno/auth/login.bru
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
meta {
|
||||||
|
name: Login
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/login
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"email": "{{userEmail}}",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: 200
|
||||||
|
res.body.success: true
|
||||||
|
res("data.token"): isNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.data && body.data.token) {
|
||||||
|
bru.setVar("authToken", body.data.token);
|
||||||
|
bru.setVar("userId", body.data.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Login returns wrapped success response", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(true);
|
||||||
|
expect(data).to.have.property("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Data contains user object and token string", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.data).to.have.property("user");
|
||||||
|
expect(data.data).to.have.property("token");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User object has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.data.user).to.have.property("id");
|
||||||
|
expect(data.data.user).to.have.property("email");
|
||||||
|
expect(data.data.user).to.have.property("name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Token is a non-empty string", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.data.token).to.be.a("string");
|
||||||
|
expect(data.data.token.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
38
bruno/auth/logout.bru
Normal file
38
bruno/auth/logout.bru
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
meta {
|
||||||
|
name: Logout
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/logout
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
res.body.success: equals true
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
bru.deleteVar("authToken");
|
||||||
|
bru.deleteVar("userId");
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Logout returns { success: true }", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Logout response has no data envelope", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.not.have.property("data");
|
||||||
|
});
|
||||||
|
}
|
||||||
60
bruno/auth/signup.bru
Normal file
60
bruno/auth/signup.bru
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
meta {
|
||||||
|
name: Signup
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/signup
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirmation": "password123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.data && body.data.token) {
|
||||||
|
bru.setVar("authToken", body.data.token);
|
||||||
|
bru.setVar("userId", body.data.user.id);
|
||||||
|
bru.setVar("userEmail", "test@example.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Signup returns wrapped success response", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Signup response contains data envelope", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Data contains user and token", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.data).to.have.property("user");
|
||||||
|
expect(data.data).to.have.property("token");
|
||||||
|
expect(data.data.user).to.have.property("id");
|
||||||
|
expect(data.data.user).to.have.property("email");
|
||||||
|
});
|
||||||
|
}
|
||||||
28
bruno/auth/unauthorized.bru
Normal file
28
bruno/auth/unauthorized.bru
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
meta {
|
||||||
|
name: Unauthorized Access
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/users/{{userId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer invalid_token_here
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 401
|
||||||
|
res.body.success: equals false
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Invalid token returns 401 with error shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(false);
|
||||||
|
expect(data).to.have.property("error");
|
||||||
|
expect(data.error).to.have.property("message");
|
||||||
|
});
|
||||||
|
}
|
||||||
8
bruno/bruno.json
Normal file
8
bruno/bruno.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Price Point API",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
49
bruno/chains/create-chain.bru
Normal file
49
bruno/chains/create-chain.bru
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
meta {
|
||||||
|
name: Create Chain
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/chains
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"chain": {
|
||||||
|
"name": "Test Chain Store"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 201
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.id) {
|
||||||
|
bru.setVar("chainId", body.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Create chain returns chain object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Created chain has correct name", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.name).to.equal("Test Chain Store");
|
||||||
|
});
|
||||||
|
}
|
||||||
23
bruno/chains/delete-chain.bru
Normal file
23
bruno/chains/delete-chain.bru
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
meta {
|
||||||
|
name: Delete Chain
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/chains/{{chainId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 204
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Delete chain returns 204 no content", function() {
|
||||||
|
expect(res.getStatus()).to.equal(204);
|
||||||
|
});
|
||||||
|
}
|
||||||
32
bruno/chains/get-chain.bru
Normal file
32
bruno/chains/get-chain.bru
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
meta {
|
||||||
|
name: Get Chain
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/chains/{{chainId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Get chain returns chain object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Chain id matches requested id", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.id).to.equal(Number(bru.getVar("chainId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
32
bruno/chains/list-chains.bru
Normal file
32
bruno/chains/list-chains.bru
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
meta {
|
||||||
|
name: List Chains
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/chains
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("List chains returns an array", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.be.an("array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Each chain has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
data.forEach(function(chain) {
|
||||||
|
expect(chain).to.have.property("id");
|
||||||
|
expect(chain).to.have.property("name");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
36
bruno/chains/update-chain.bru
Normal file
36
bruno/chains/update-chain.bru
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
meta {
|
||||||
|
name: Update Chain
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
patch {
|
||||||
|
url: {{baseUrl}}/chains/{{chainId}}
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"chain": {
|
||||||
|
"name": "Updated Chain Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Update chain returns updated object", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data.name).to.equal("Updated Chain Name");
|
||||||
|
});
|
||||||
|
}
|
||||||
22
bruno/collection.bru
Normal file
22
bruno/collection.bru
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
meta {
|
||||||
|
name: Price Point API
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Response status is not a server error", function() {
|
||||||
|
expect(res.getStatus()).to.be.below(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Response time is under 2 seconds", function() {
|
||||||
|
expect(res.getResponseTime()).to.be.below(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Response body is valid JSON", function() {
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
4
bruno/environments/Local.bru
Normal file
4
bruno/environments/Local.bru
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
vars {
|
||||||
|
baseUrl: http://localhost:3000
|
||||||
|
userEmail: test@example.com
|
||||||
|
}
|
||||||
57
bruno/price-reports/create-price-report.bru
Normal file
57
bruno/price-reports/create-price-report.bru
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
meta {
|
||||||
|
name: Create Price Report
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/products/{{productId}}/price_reports
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"price_report": {
|
||||||
|
"price": "9.99",
|
||||||
|
"discount_price": "7.99",
|
||||||
|
"store_id": {{storeId}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 201
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.id) {
|
||||||
|
bru.setVar("priceReportId", body.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Create price report returns report object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("price");
|
||||||
|
expect(data).to.have.property("discount_price");
|
||||||
|
expect(data).to.have.property("store_id");
|
||||||
|
expect(data).to.have.property("product_id");
|
||||||
|
expect(data).to.have.property("user_id");
|
||||||
|
expect(data).to.have.property("reported_at");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Price report has correct values", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.product_id).to.equal(Number(bru.getVar("productId")));
|
||||||
|
expect(data.store_id).to.equal(Number(bru.getVar("storeId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
23
bruno/price-reports/delete-price-report.bru
Normal file
23
bruno/price-reports/delete-price-report.bru
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
meta {
|
||||||
|
name: Delete Price Report
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/price_reports/{{priceReportId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 204
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Delete price report returns 204 no content", function() {
|
||||||
|
expect(res.getStatus()).to.equal(204);
|
||||||
|
});
|
||||||
|
}
|
||||||
37
bruno/price-reports/get-price-report.bru
Normal file
37
bruno/price-reports/get-price-report.bru
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
meta {
|
||||||
|
name: Get Price Report
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/price_reports/{{priceReportId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Get price report returns report object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("price");
|
||||||
|
expect(data).to.have.property("discount_price");
|
||||||
|
expect(data).to.have.property("store_id");
|
||||||
|
expect(data).to.have.property("product_id");
|
||||||
|
expect(data).to.have.property("user_id");
|
||||||
|
expect(data).to.have.property("reported_at");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Price report id matches requested id", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.id).to.equal(Number(bru.getVar("priceReportId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
35
bruno/price-reports/list-price-reports.bru
Normal file
35
bruno/price-reports/list-price-reports.bru
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
meta {
|
||||||
|
name: List Price Reports
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/products/{{productId}}/price_reports
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("List price reports returns an array", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.be.an("array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Each price report has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
data.forEach(function(report) {
|
||||||
|
expect(report).to.have.property("id");
|
||||||
|
expect(report).to.have.property("price");
|
||||||
|
expect(report).to.have.property("store_id");
|
||||||
|
expect(report).to.have.property("product_id");
|
||||||
|
expect(report).to.have.property("user_id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
43
bruno/price-reports/update-price-report.bru
Normal file
43
bruno/price-reports/update-price-report.bru
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
meta {
|
||||||
|
name: Update Price Report
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
patch {
|
||||||
|
url: {{baseUrl}}/price_reports/{{priceReportId}}
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"price_report": {
|
||||||
|
"price": "12.99",
|
||||||
|
"discount_price": "10.99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Update price report returns updated object", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("price");
|
||||||
|
expect(data).to.have.property("discount_price");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Updated values are reflected", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(String(data.price)).to.equal("12.99");
|
||||||
|
expect(String(data.discount_price)).to.equal("10.99");
|
||||||
|
});
|
||||||
|
}
|
||||||
52
bruno/products/create-product.bru
Normal file
52
bruno/products/create-product.bru
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
meta {
|
||||||
|
name: Create Product
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/products
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"product": {
|
||||||
|
"name": "Test Product",
|
||||||
|
"barcode": "1234567890123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 201
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.id) {
|
||||||
|
bru.setVar("productId", body.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Create product returns product object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("barcode");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Created product has correct values", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.name).to.equal("Test Product");
|
||||||
|
expect(data.barcode).to.equal("1234567890123");
|
||||||
|
});
|
||||||
|
}
|
||||||
23
bruno/products/delete-product.bru
Normal file
23
bruno/products/delete-product.bru
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
meta {
|
||||||
|
name: Delete Product
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/products/{{productId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 204
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Delete product returns 204 no content", function() {
|
||||||
|
expect(res.getStatus()).to.equal(204);
|
||||||
|
});
|
||||||
|
}
|
||||||
33
bruno/products/get-product.bru
Normal file
33
bruno/products/get-product.bru
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
meta {
|
||||||
|
name: Get Product
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/products/{{productId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Get product returns product object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("barcode");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Product id matches requested id", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.id).to.equal(Number(bru.getVar("productId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
33
bruno/products/list-products.bru
Normal file
33
bruno/products/list-products.bru
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
meta {
|
||||||
|
name: List Products
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/products
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("List products returns an array", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.be.an("array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Each product has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
data.forEach(function(product) {
|
||||||
|
expect(product).to.have.property("id");
|
||||||
|
expect(product).to.have.property("name");
|
||||||
|
expect(product).to.have.property("barcode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
37
bruno/products/update-product.bru
Normal file
37
bruno/products/update-product.bru
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
meta {
|
||||||
|
name: Update Product
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
patch {
|
||||||
|
url: {{baseUrl}}/products/{{productId}}
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"product": {
|
||||||
|
"name": "Updated Product Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Update product returns updated object", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("barcode");
|
||||||
|
expect(data.name).to.equal("Updated Product Name");
|
||||||
|
});
|
||||||
|
}
|
||||||
56
bruno/stores/create-store.bru
Normal file
56
bruno/stores/create-store.bru
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
meta {
|
||||||
|
name: Create Store
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/stores
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"name": "Test Store Location",
|
||||||
|
"latitude": 40.7128,
|
||||||
|
"longitude": -74.006,
|
||||||
|
"chain_id": {{chainId}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 201
|
||||||
|
}
|
||||||
|
|
||||||
|
script:post-response {
|
||||||
|
const body = res.getBody();
|
||||||
|
if (body && body.id) {
|
||||||
|
bru.setVar("storeId", body.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Create store returns store object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("latitude");
|
||||||
|
expect(data).to.have.property("longitude");
|
||||||
|
expect(data).to.have.property("chain_id");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Created store has correct values", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.name).to.equal("Test Store Location");
|
||||||
|
expect(data.chain_id).to.equal(Number(bru.getVar("chainId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
23
bruno/stores/delete-store.bru
Normal file
23
bruno/stores/delete-store.bru
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
meta {
|
||||||
|
name: Delete Store
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/stores/{{storeId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 204
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Delete store returns 204 no content", function() {
|
||||||
|
expect(res.getStatus()).to.equal(204);
|
||||||
|
});
|
||||||
|
}
|
||||||
35
bruno/stores/get-store.bru
Normal file
35
bruno/stores/get-store.bru
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
meta {
|
||||||
|
name: Get Store
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/stores/{{storeId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Get store returns store object directly", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data).to.have.property("latitude");
|
||||||
|
expect(data).to.have.property("longitude");
|
||||||
|
expect(data).to.have.property("chain_id");
|
||||||
|
expect(data).to.have.property("created_at");
|
||||||
|
expect(data).to.have.property("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Store id matches requested id", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.id).to.equal(Number(bru.getVar("storeId")));
|
||||||
|
});
|
||||||
|
}
|
||||||
35
bruno/stores/list-stores.bru
Normal file
35
bruno/stores/list-stores.bru
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
meta {
|
||||||
|
name: List Stores
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/stores
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("List stores returns an array", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.be.an("array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Each store has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
data.forEach(function(store) {
|
||||||
|
expect(store).to.have.property("id");
|
||||||
|
expect(store).to.have.property("name");
|
||||||
|
expect(store).to.have.property("latitude");
|
||||||
|
expect(store).to.have.property("longitude");
|
||||||
|
expect(store).to.have.property("chain_id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
36
bruno/stores/update-store.bru
Normal file
36
bruno/stores/update-store.bru
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
meta {
|
||||||
|
name: Update Store
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
patch {
|
||||||
|
url: {{baseUrl}}/stores/{{storeId}}
|
||||||
|
body: json
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"name": "Updated Store Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Update store returns updated object", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("id");
|
||||||
|
expect(data).to.have.property("name");
|
||||||
|
expect(data.name).to.equal("Updated Store Name");
|
||||||
|
});
|
||||||
|
}
|
||||||
26
bruno/users/delete-user.bru
Normal file
26
bruno/users/delete-user.bru
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
meta {
|
||||||
|
name: Delete User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{baseUrl}}/users/{{userId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
res.body.success: equals true
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Delete user returns { success: true }", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
34
bruno/users/get-user.bru
Normal file
34
bruno/users/get-user.bru
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
meta {
|
||||||
|
name: Get User
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/users/{{userId}}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Authorization: Bearer {{authToken}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: equals 200
|
||||||
|
res.body.success: equals true
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("Get user returns wrapped success response", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data).to.have.property("success");
|
||||||
|
expect(data.success).to.equal(true);
|
||||||
|
expect(data).to.have.property("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User data has correct shape", function() {
|
||||||
|
const data = res.getBody();
|
||||||
|
expect(data.data).to.have.property("id");
|
||||||
|
expect(data.data).to.have.property("email");
|
||||||
|
expect(data.data).to.have.property("name");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,11 @@ Rails.application.routes.draw do
|
||||||
resources :products do
|
resources :products do
|
||||||
resources :price_reports, only: [:index, :create]
|
resources :price_reports, only: [:index, :create]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post 'login', to: 'sessions#create'
|
||||||
|
delete 'logout', to: 'sessions#destroy'
|
||||||
|
post 'signup', to: 'users#create'
|
||||||
|
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
|
|
||||||
13
db/migrate/20260609155259_create_sessions.rb
Normal file
13
db/migrate/20260609155259_create_sessions.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateSessions < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :sessions do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.string :token
|
||||||
|
t.string :user_agent
|
||||||
|
t.string :ip_address
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :sessions, :token, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20260609155729_remove_token_from_users.rb
Normal file
5
db/migrate/20260609155729_remove_token_from_users.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class RemoveTokenFromUsers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_column :users, :token, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
14
db/schema.rb
generated
14
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_05_30_204821) do
|
ActiveRecord::Schema[8.0].define(version: 2026_06_09_155729) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
|
@ -81,6 +81,17 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_30_204821) do
|
||||||
t.index ["barcode"], name: "index_products_on_barcode", unique: true
|
t.index ["barcode"], name: "index_products_on_barcode", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "sessions", force: :cascade do |t|
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.string "token"
|
||||||
|
t.string "user_agent"
|
||||||
|
t.string "ip_address"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["token"], name: "index_sessions_on_token", unique: true
|
||||||
|
t.index ["user_id"], name: "index_sessions_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "stores", force: :cascade do |t|
|
create_table "stores", force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.float "latitude"
|
t.float "latitude"
|
||||||
|
|
@ -118,6 +129,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_30_204821) do
|
||||||
add_foreign_key "price_reports", "products"
|
add_foreign_key "price_reports", "products"
|
||||||
add_foreign_key "price_reports", "stores"
|
add_foreign_key "price_reports", "stores"
|
||||||
add_foreign_key "price_reports", "users"
|
add_foreign_key "price_reports", "users"
|
||||||
|
add_foreign_key "sessions", "users"
|
||||||
add_foreign_key "stores", "chains"
|
add_foreign_key "stores", "chains"
|
||||||
add_foreign_key "votes", "users"
|
add_foreign_key "votes", "users"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
3
sig/application_controller.rbs
Normal file
3
sig/application_controller.rbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
class ApplicationController
|
||||||
|
@current_user: User
|
||||||
|
end
|
||||||
7
test/controllers/sessions_controller_test.rb
Normal file
7
test/controllers/sessions_controller_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/users_controller_test.rb
Normal file
7
test/controllers/users_controller_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
34
test/fixtures/sessions.yml
vendored
Normal file
34
test/fixtures/sessions.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: sessions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# ip_address :string
|
||||||
|
# token :string
|
||||||
|
# user_agent :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_sessions_on_token (token) UNIQUE
|
||||||
|
# index_sessions_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
one:
|
||||||
|
user: one
|
||||||
|
token: MyString
|
||||||
|
user_agent: MyString
|
||||||
|
ip_address: MyString
|
||||||
|
|
||||||
|
two:
|
||||||
|
user: two
|
||||||
|
token: MyString
|
||||||
|
user_agent: MyString
|
||||||
|
ip_address: MyString
|
||||||
28
test/models/session_test.rb
Normal file
28
test/models/session_test.rb
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: sessions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# ip_address :string
|
||||||
|
# token :string
|
||||||
|
# user_agent :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_sessions_on_token (token) UNIQUE
|
||||||
|
# index_sessions_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SessionTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue