43 | With ShinyGems you can help ruby community develop better software. Many maintainers work on gems in their
44 | spare time and don't have enough capacity to take care of all the issues or develop new features. ShinyGems
45 | pulls data from RubyGems and GitHub to generate a list of gems you can help with.
46 |
47 |
48 |
49 |
50 |
51 |
52 |
At least 1 mln downloads
53 |
54 |
55 |
56 |
Commits in the last 365 days
57 |
58 |
59 |
60 |
Only "help wanted" issues
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
Browse gems or upload Gemfile
70 |
71 | Browse gems or upload your Gemfile to see if there are any gems you are using that require help.
72 |
4 | At ShinyGems, we are committed to protecting your privacy. This Privacy Policy outlines the type of personal data we
5 | collect, how we use it, and how we protect your information, in compliance with the General Data Protection
6 | Regulation (GDPR).
7 |
8 |
Website Owner
9 | The ShinyGems website is owned and operated by Karol Bąk, ul. Pszczelna 32/31, 30-409 Cracow, Poland.
10 |
11 |
Data Collection
12 | We collect the following types of data:
13 |
14 |
15 | Anonymous analytics data: We use cookies and other tracking technologies to collect anonymous analytics data.
16 | This data includes information about your device, browser, and how you interact with the website. We use this data
17 | to improve our website and provide a better user experience.
18 |
19 |
20 | Email addresses: We collect email addresses when you sign up. We use this
21 | information to send you notifications that you can opt-in or opt-out anytime. We
22 | won't use your email address for marketing emails unless you have provided consent.
23 |
24 |
25 | Github usernames: We collect Github usernames for authorization purposes.
26 | We do not use this information for any other purpose.
27 |
28 |
29 |
30 |
Data Usage
31 | We use the data we collect for the following purposes:
32 |
33 |
34 | To improve our website: We use anonymous analytics data to improve our website and provide a better user
35 | experience.
36 |
37 |
38 | To communicate with our users: We use email addresses to send you notifications about our products and services.
39 |
40 |
41 | For authorization: We use Github usernames for authorization purposes.
42 |
43 |
44 |
45 |
Data Access and Deletion
46 | You have the right to access and delete your personal data that we have collected. If you would like to access or
47 | delete your data, please contact us at hello@shinygems.dev.
48 |
49 |
Data Protection
50 | We take appropriate technical and organizational measures to ensure the security of your personal data against
51 | unauthorized access, alteration, disclosure, or destruction.
52 |
53 |
Changes to this Privacy Policy
54 | We may update this Privacy Policy from time to time. Any changes will be posted on this page, and the date of the
55 | latest update will be indicated at the top of the page.
56 |
57 |
58 |
--------------------------------------------------------------------------------
/slices/web/view.rb:
--------------------------------------------------------------------------------
1 | # auto_register: false
2 | # frozen_string_literal: true
3 |
4 | module Web
5 | class View < Hanami::View
6 | DEFAULT_DESCRIPTION = "Help maintain your favorite gems and make them shine ✨"
7 |
8 | setting :title
9 | setting :description
10 |
11 | config.paths = [File.join(__dir__, "templates")]
12 | config.layout = "application"
13 | config.part_namespace = Views::Parts
14 |
15 | expose :seo_title, layout: true do
16 | config.title || "ShinyGems"
17 | end
18 |
19 | expose :seo_description, layout: true do
20 | config.description || DEFAULT_DESCRIPTION
21 | end
22 |
23 | expose :current_user, layout: true
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/slices/web/views/favorites/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Favorites
6 | class Index < Web::View
7 | config.template = "favorites/index"
8 | config.title = "Favorites - ShinyGems"
9 |
10 | expose :gems
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/slices/web/views/gemfile/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Gemfile
6 | class Create < Web::View
7 | config.template = "gemfile/create"
8 | config.title = "Gems from your Gemfile - ShinyGems"
9 |
10 | expose :gems
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/slices/web/views/gems/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Gems
6 | class Index < Web::View
7 | config.template = "gems/index"
8 | config.title = "Browse gems - ShinyGems"
9 |
10 | expose :gems, :pager, :sort_by
11 |
12 | expose :pages, decorate: false do |pager:|
13 | total = pager.total_pages
14 | total.times.map { |i| i + 1 }.map do |page|
15 | next :gap unless page == 1 || page == total || page.between?(pager.current_page - 1, pager.current_page + 1)
16 | page
17 | end.chunk(&:itself).map(&:first)
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/slices/web/views/gems/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Gems
6 | class Show < Web::View
7 | config.template = "gems/show"
8 |
9 | expose :current_gem, as: :gem
10 | expose :total_favorites
11 | expose :favorite
12 |
13 | expose :seo_title, layout: true do |current_gem:|
14 | "#{current_gem.name} - ShinyGems"
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/slices/web/views/pages/error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Pages
6 | class Error < Web::View
7 | config.template = "pages/error"
8 | config.title = "Error - ShinyGems"
9 |
10 | expose :code
11 | expose :msg do |code:|
12 | Hanami::Http::Status.message_for(code)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/slices/web/views/pages/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Pages
6 | class Index < Web::View
7 | config.template = "pages/index"
8 | config.title = "ShinyGems - Help maintain your favourite gems and make them shine"
9 |
10 | expose :random_gems
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/slices/web/views/pages/not_authorized.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Pages
6 | class NotAuthorized < Web::View
7 | config.template = "pages/not_authorized"
8 | config.title = "Sign in - ShinyGems"
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/slices/web/views/pages/privacy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Pages
6 | class Privacy < Web::View
7 | config.template = "pages/privacy"
8 | config.title = "Privacy Policy - ShinyGems"
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/slices/web/views/parts/gem.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Parts
6 | class Gem < Hanami::View::Part
7 | include Deps["formatter"]
8 |
9 | decorate :repo
10 |
11 | def downloads
12 | formatter.separator(value.downloads)
13 | end
14 |
15 | def url
16 | "https://rubygems.org/gems/#{value.name}"
17 | end
18 |
19 | def license
20 | value.licenses&.join(", ")
21 | end
22 |
23 | def favorites_count
24 | formatter.separator(value.favorites_count || 0)
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/slices/web/views/parts/issue.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Parts
6 | class Issue < Hanami::View::Part
7 | decorate :labels
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/slices/web/views/parts/label.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Parts
6 | class Label < Hanami::View::Part
7 | def name
8 | value["name"]
9 | end
10 |
11 | def bg_color
12 | "##{value["color"]}"
13 | end
14 |
15 | def bg_light?
16 | r, g, b = value["color"].chars.each_slice(2).map(&:join).map(&:hex)
17 | (0.2126 * r) + (0.7152 * g) + (0.0722 * b) >= 128
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/slices/web/views/parts/repo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Web
4 | module Views
5 | module Parts
6 | class Repo < Hanami::View::Part
7 | include Deps["formatter"]
8 |
9 | decorate :issues
10 |
11 | def stars
12 | formatter.separator(value.stars)
13 | end
14 |
15 | def url
16 | "https://github.com/#{value.name}"
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/features/auth_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.feature "auth", type: :feature, js: true do
4 | scenario "sign in and out" do
5 | visit "/"
6 | expect(page).to have_content("ShinyGems")
7 | click_button "Sign in with GitHub"
8 |
9 | expect(page).to have_content("Successfully signed in")
10 | expect(page).to have_content("Logged as test")
11 | expect(page).not_to have_content("Sign in with GitHub")
12 |
13 | find('a[aria-label="sign out"]').click
14 | expect(page).to have_content("Successfully signed out")
15 | expect(page).to have_content("Sign in with GitHub")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/features/browse_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.feature "browse", type: :feature, js: true do
4 | let!(:gem) { Factory[:gem, repo: Factory[:repo, name: "my/repo"], downloads: 10, name: "a-special-gem"] }
5 |
6 | before do
7 | 30.times do
8 | Factory[:gem]
9 | end
10 | end
11 |
12 | scenario "browse gems" do
13 | visit "/gems"
14 | expect(page).to have_content("Browse gems")
15 | expect(page).to have_no_content(gem.name)
16 |
17 | click_link("2", class: "page-link")
18 | expect(page).to have_content(gem.name)
19 |
20 | click_link(gem.name)
21 | expect(page).to have_content("Some issue")
22 |
23 | expect(page).to have_content("GitHub Repository\nmy/repo")
24 | expect(page).to have_content("GitHub Stars\n1,521")
25 | expect(page).to have_content("RubyGems Downloads\n10")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/features/favorites_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.feature "favorites", type: :feature, js: true do
4 | let!(:gem) { Factory[:gem, repo: Factory[:repo, name: "my/repo"], downloads: 10, name: "a-special-gem"] }
5 |
6 | scenario "add and remove from favorites" do
7 | visit "/"
8 |
9 | within ".navbar" do
10 | click_link "Favorites"
11 | end
12 |
13 | expect(page).to have_content("You need to sign in to see this page.")
14 | click_button "Sign in with GitHub", class: "btn-primary"
15 |
16 | expect(page).to have_content("Successfully signed in")
17 | expect(page).to have_content("Logged as test")
18 | within ".navbar" do
19 | click_link "Favorites"
20 | end
21 |
22 | expect(page).to have_content("You don't have any favorites")
23 | click_link "Browse all gems"
24 |
25 | expect(page).to have_content("a-special-gem")
26 | click_link("a-special-gem")
27 |
28 | expect(page).to have_content("Some issue")
29 | click_button("Add to favorites")
30 |
31 | within ".navbar" do
32 | click_link "Favorites"
33 | end
34 | expect(page).to have_content("Favorites")
35 | expect(page).to have_content("a-special-gem")
36 | click_link("a-special-gem")
37 |
38 | expect(page).to have_content("Some issue")
39 | click_button("Remove from favorites")
40 |
41 | within ".navbar" do
42 | click_link "Favorites"
43 | end
44 | expect(page).to have_content("You don't have any favorites")
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/features/gemfile_upload_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.feature "gemfile upload", type: :feature, js: true do
4 | let!(:gem) { Factory[:gem, name: "hanami", repo: Factory[:repo, name: "my/repo"]] }
5 | let!(:other_gem) { Factory[:gem] }
6 |
7 | scenario "browse gems by uploaded gemfile" do
8 | visit "/gems"
9 | expect(page).to have_content("Browse gems")
10 | expect(page).to have_content(gem.name)
11 | expect(page).to have_content(other_gem.name)
12 |
13 | attach_file("gemfile", "#{SPEC_ROOT}/support/files/Gemfile.test", visible: false)
14 |
15 | expect(page).to have_content("Gems from your Gemfile")
16 | expect(page).to have_content(gem.name)
17 | expect(page).to have_no_content(other_gem.name)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/features/processing_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.feature "processing", type: :feature, js: true do
4 | around do |example|
5 | Sidekiq::Testing.inline! do
6 | example.call
7 | end
8 | end
9 |
10 | let(:rubygems_data) do
11 | {
12 | name: "some_gem",
13 | source_code_uri: "https://github.com/some/some_gem",
14 | info: "Just a sample gem",
15 | downloads: 1_526_321,
16 | version: "2.0.0",
17 | licenses: ["MIT"]
18 | }
19 | end
20 |
21 | let(:github_data) do
22 | {
23 | full_name: "some/some_gem",
24 | stargazers_count: 1_234,
25 | pushed_at: Time.now.to_s
26 | }
27 | end
28 |
29 | let(:issues) do
30 | [
31 | {
32 | id: 1234,
33 | title: "Sample issue",
34 | html_url: "https://github.com/some/some_gem/issues/1",
35 | comments: 5,
36 | created_at: "2023-01-26T19:06:43Z",
37 | labels: [{name: "help wanted", color: "333333"}]
38 | },
39 | {
40 | id: 1235,
41 | title: "Another issue",
42 | html_url: "https://github.com/some/some_gem/issues/2",
43 | comments: 10,
44 | created_at: "2023-01-26T19:06:43Z",
45 | labels: [{name: "help wanted", color: "333333"}]
46 | }
47 | ]
48 | end
49 |
50 | before do
51 | stub_request(:get, "https://rubygems.org/api/v1/search.json?page=1&query=downloads:%3E1000000")
52 | .to_return(status: 200, body: [rubygems_data].to_json)
53 | stub_request(:get, "https://rubygems.org/api/v1/search.json?page=2&query=downloads:%3E1000000")
54 | .to_return(status: 200, body: [].to_json)
55 | stub_request(:get, "https://rubygems.org/api/v1/gems/some_gem.json")
56 | .to_return(status: 200, body: rubygems_data.to_json)
57 | stub_request(:get, "https://api.github.com/repos/some/some_gem")
58 | .to_return(status: 200, body: github_data.to_json, headers: {"Content-Type" => "application/json; charset=utf-8"})
59 | stub_request(:get, "https://api.github.com/repos/some/some_gem/issues?labels=help%20wanted&per_page=100&state=open")
60 | .to_return(status: 200, body: issues.to_json, headers: {"Content-Type" => "application/json; charset=utf-8"})
61 | end
62 |
63 | scenario "discover and sync data" do
64 | ::Processing::Workers::DiscoverWorker.perform_async
65 |
66 | visit "/"
67 | within ".intro" do
68 | click_link "Browse gems"
69 | end
70 |
71 | expect(page).to have_content("some_gem")
72 | expect(page).to have_content("Just a sample gem")
73 |
74 | click_link "some_gem"
75 |
76 | expect(page).to have_content("some_gem")
77 | expect(page).to have_content("Just a sample gem")
78 | expect(page).to have_content("Sample issue")
79 | expect(page).to have_content("Another issue")
80 | expect(page).to have_content("RubyGems Downloads 1,526,321", normalize_ws: true)
81 | expect(page).to have_content("GitHub Stars 1,234", normalize_ws: true)
82 | expect(page).to have_content("GitHub Repository some/some_gem", normalize_ws: true)
83 | expect(page).to have_content("Current Version 2.0.0", normalize_ws: true)
84 | expect(page).to have_content("License MIT", normalize_ws: true)
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/slices/processing/repositories/gems_repository_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Repositories::GemsRepository, type: :database do
4 | subject(:repo) { described_class.new }
5 |
6 | describe "#by_id" do
7 | let!(:gem) { Factory[:gem] }
8 |
9 | context "gem exists" do
10 | it "returns gem" do
11 | expect(repo.by_id(gem.id)).to match_entity(gem)
12 | end
13 | end
14 |
15 | context "gem doesnt exist" do
16 | it "returns nil" do
17 | expect(repo.by_id(gem.id + 3)).to be_nil
18 | end
19 | end
20 |
21 | context "when 'with' present" do
22 | it "returns gem with associations" do
23 | result = repo.by_id(gem.id, with: [:repo])
24 | expect(result.name).to eq(gem.name)
25 | expect(result.repo).to eq(gem.repo)
26 | end
27 | end
28 | end
29 |
30 | describe "#pluck_ids_for_hour" do
31 | let!(:gem1) { Factory[:gem, id: 1] }
32 | let!(:gem2) { Factory[:gem, id: 2] }
33 | let!(:gem3) { Factory[:gem, id: 25] }
34 |
35 | it "returns ids as array" do
36 | expect(subject.pluck_ids_for_hour(1)).to eq([gem1.id, gem3.id])
37 | end
38 | end
39 |
40 | describe "#pluck_name_by_list" do
41 | let!(:gem1) { Factory[:gem] }
42 | let!(:gem2) { Factory[:gem] }
43 |
44 | it "returns names of existing gems" do
45 | expect(subject.pluck_name_by_list(["some_name", gem2.name])).to eq([gem2.name])
46 | end
47 | end
48 |
49 | describe "#replace_repo" do
50 | let!(:gem1) { Factory[:gem] }
51 | let!(:gem2) { Factory[:gem] }
52 | let!(:new_repo) { Factory[:repo] }
53 |
54 | it "replaces repos for proper gems" do
55 | subject.replace_repo(gem1.repo_id, new_repo.id)
56 |
57 | expect(repo.by_id(gem1.id).repo_id).to eq(new_repo.id)
58 | expect(repo.by_id(gem2.id).repo_id).not_to eq(new_repo.id)
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/spec/slices/processing/repositories/repos_repository_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Repositories::ReposRepository, type: :database do
4 | subject(:repository) { described_class.new }
5 |
6 | describe "#by_id" do
7 | let!(:repo) { Factory[:repo, issues: []] }
8 |
9 | context "repo exists" do
10 | it "returns gem" do
11 | expect(repository.by_id(repo.id)).to match_entity(repo)
12 | end
13 | end
14 |
15 | context "repo doesnt exist" do
16 | it "returns nil" do
17 | expect(repository.by_id(repo.id + 3)).to be_nil
18 | end
19 | end
20 |
21 | context "when 'with' present" do
22 | let!(:issue) { Factory[:issue, repo_id: repo.id] }
23 |
24 | it "returns repo with associations" do
25 | result = repository.by_id(repo.id, with: [:issues])
26 | expect(result.name).to eq(repo.name)
27 | expect(result.issues).to match([match_entity(issue)])
28 | end
29 | end
30 | end
31 |
32 | describe "#pluck_ids" do
33 | let!(:repo1) { Factory[:repo, id: 1] }
34 | let!(:repo2) { Factory[:repo, id: 2] }
35 | let!(:repo3) { Factory[:repo, id: 25] }
36 |
37 | it "returns ids as array" do
38 | expect(subject.pluck_ids_for_hour(1)).to eq([repo1.id, repo3.id])
39 | end
40 | end
41 |
42 | describe "#find_or_create" do
43 | context "already exists" do
44 | let!(:repo) { Factory[:repo] }
45 |
46 | it "returns repo" do
47 | expect(subject.find_or_create({name: repo.name})).to match(match_entity(repo))
48 | end
49 |
50 | it "doesn't create new repo" do
51 | expect { subject.find_or_create({name: repo.name}) }.not_to change { Hanami.app["persistence.rom"].relations[:repos].count }
52 | end
53 | end
54 |
55 | context "doesnt exist" do
56 | it "returns repo" do
57 | expect(subject.find_or_create({name: "name"})).to have_attributes(name: "name", id: kind_of(Integer))
58 | end
59 |
60 | it "creates new repo" do
61 | expect { subject.find_or_create({name: "name"}) }.to change { Hanami.app["persistence.rom"].relations[:repos].count }.by(1)
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/discover_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Services::Discover do
4 | let(:fake_gems_repository) { fake_repository(:processing, :gems) }
5 | let(:fake_repos_repository) { fake_repository(:processing, :repos) }
6 | let(:fake_list_fetcher) { instance_double(Processing::Services::Rubygems::ListFetcher) }
7 | let(:instance) { described_class.new(list_fetcher: fake_list_fetcher, gems_repository: fake_gems_repository, repos_repository: fake_repos_repository) }
8 |
9 | subject { instance.call }
10 |
11 | context "no gems returned from API" do
12 | before { allow(fake_list_fetcher).to receive(:call).with(page: 1).and_return(Dry::Monads::Success([])) }
13 |
14 | it "returns failure" do
15 | expect(subject).to eq(Dry::Monads::Failure(:no_results))
16 | end
17 | end
18 |
19 | context "gems returned from API" do
20 | let(:existing_gem) { Factory.structs[:gem] }
21 | let(:existing_repo) { Factory.structs[:repo, name: "some/repo"] }
22 | let(:gems_list) do
23 | [
24 | {"name" => existing_gem.name, "homepage_uri" => "https://github.com/some/repo"},
25 | {"name" => "gem_with_existing_repo", "homepage_uri" => "https://github.com/some/repo"},
26 | {"name" => "new_gem_without_repo", "homepage_uri" => ""}
27 | ]
28 | end
29 |
30 | before do
31 | allow(fake_list_fetcher).to receive(:call).with(page: 1).and_return(Dry::Monads::Success(gems_list))
32 | allow(fake_gems_repository).to receive(:pluck_name_by_list).with([existing_gem.name, "gem_with_existing_repo", "new_gem_without_repo"])
33 | .and_return([existing_gem.name])
34 | allow(fake_gems_repository).to receive(:transaction).and_yield
35 | allow(fake_gems_repository).to receive(:create)
36 | allow(fake_repos_repository).to receive(:find_or_create).with({name: "some/repo"})
37 | .and_return(existing_repo)
38 | allow(fake_gems_repository).to receive(:create).with(hash_including(name: "gem_with_existing_repo", repo_id: existing_repo.id))
39 | .and_return(Factory.structs[:gem, name: "gem_with_existing_repo"])
40 | end
41 |
42 | it "ignores existing gem" do
43 | expect(fake_gems_repository).not_to receive(:create).with(hash_including(name: existing_gem.name))
44 | subject
45 | end
46 |
47 | it "creates missing gem" do
48 | expect(fake_gems_repository).to receive(:create).with(hash_including(name: "gem_with_existing_repo", repo_id: existing_repo.id))
49 | subject
50 | end
51 |
52 | it "ignores gem without repo" do
53 | expect(fake_gems_repository).not_to receive(:create).with(hash_including(name: "new_gem_without_repo"))
54 | subject
55 | end
56 |
57 | it "returns Success with list of created gems" do
58 | expect(subject.success?).to be_truthy
59 | expect(subject.value!).to match([having_attributes(name: "gem_with_existing_repo")])
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/github/issues_list_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:octokit)
4 |
5 | RSpec.describe Processing::Services::Github::IssuesListFetcher do
6 | let(:fake_octokit) { instance_double(Octokit::Client) }
7 | let(:fake_response) do
8 | [
9 | {id: 231},
10 | {id: 233, pull_request: {id: 123}}
11 | ]
12 | end
13 |
14 | context "everything is ok" do
15 | subject { described_class.new(octokit: fake_octokit).call("some/repo") }
16 |
17 | before do
18 | allow(fake_octokit).to receive(:list_issues).with("some/repo", state: "open", labels: "help wanted")
19 | .and_return(fake_response)
20 | end
21 |
22 | it "fetches data from octokit and returns issues without PRs" do
23 | expect(subject.success?).to be_truthy
24 | expect(subject.value!).to eq([{id: 231}])
25 | end
26 | end
27 |
28 | context "api error" do
29 | subject { described_class.new(octokit: fake_octokit).call("some/repo") }
30 |
31 | before do
32 | allow(fake_octokit).to receive(:list_issues).with("some/repo", state: "open", labels: "help wanted")
33 | .and_raise(Octokit::NotFound)
34 | end
35 |
36 | it "returns failure" do
37 | expect(subject.success?).to be_falsey
38 | expect(subject.failure).to be_instance_of(Octokit::NotFound)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/github/repo_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:octokit)
4 |
5 | RSpec.describe Processing::Services::Github::RepoFetcher do
6 | let(:fake_octokit) { instance_double(Octokit::Client) }
7 | let(:fake_response) do
8 | {full_name: "some/repo", name: "repo"}
9 | end
10 |
11 | subject { described_class.new(octokit: fake_octokit).call("some/repo") }
12 |
13 | context "everything is ok" do
14 | before do
15 | allow(fake_octokit).to receive(:repo).with("some/repo").and_return(fake_response)
16 | end
17 |
18 | it "fetches data from octokit and returns repo data" do
19 | expect(subject.success?).to be_truthy
20 | expect(subject.value!).to eq(fake_response)
21 | end
22 | end
23 |
24 | context "api error" do
25 | before do
26 | allow(fake_octokit).to receive(:repo).with("some/repo").and_raise(Octokit::NotFound)
27 | end
28 |
29 | it "returns failure" do
30 | expect(subject.success?).to be_falsey
31 | expect(subject.failure).to be_instance_of(Octokit::NotFound)
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/issues_syncer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Services::IssuesSyncer do
4 | let(:fake_issues_repository) do
5 | fake_repository(:processing, :issues) do |repository|
6 | allow(repository).to receive(:transaction).and_yield
7 | allow(repository).to receive(:update).with(any_args)
8 | allow(repository).to receive(:delete).with(any_args)
9 | allow(repository).to receive(:create).with(any_args)
10 | end
11 | end
12 |
13 | let!(:existing_issue1) { Factory.structs[:issue, github_id: 100] }
14 | let!(:existing_issue2) { Factory.structs[:issue, github_id: 101] }
15 | let(:fake_issues_list_fetcher) { instance_double(Processing::Services::Github::IssuesListFetcher) }
16 | let(:repo) { Factory.structs[:repo, issues: [existing_issue1, existing_issue2]] }
17 | let(:fake_gh_list) do
18 | [
19 | {
20 | id: 100,
21 | title: "Issue1",
22 | comments: 5,
23 | html_url: "repo/issues/1",
24 | created_at: DateTime.new(2011, 4, 22, 13, 33, 48, 0),
25 | labels: [{name: "test", color: "324532"}]
26 | },
27 | {
28 | id: 103,
29 | title: "Issue4",
30 | comments: 65,
31 | html_url: "repo/issues/4",
32 | created_at: DateTime.new(2011, 4, 22, 13, 33, 48, 0),
33 | labels: []
34 | }
35 | ]
36 | end
37 |
38 | before do
39 | allow(fake_issues_list_fetcher).to receive(:call).with(repo.name).and_return(Dry::Monads::Success(fake_gh_list))
40 | end
41 |
42 | subject do
43 | described_class.new(issues_list_fetcher: fake_issues_list_fetcher, issues_repository: fake_issues_repository).call(repo)
44 | end
45 |
46 | it "returns success" do
47 | expect(subject.success?).to be_truthy
48 | end
49 |
50 | it "removes closed issues" do
51 | expect(fake_issues_repository).to receive(:delete).with(existing_issue2.id)
52 | subject
53 | end
54 |
55 | it "updates open issues on the list" do
56 | expect(fake_issues_repository).to receive(:update).with(existing_issue1.id, {
57 | title: "Issue1",
58 | comments: 5,
59 | url: "repo/issues/1",
60 | github_id: 100,
61 | created_at: DateTime.new(2011, 4, 22, 13, 33, 48, 0),
62 | labels: [{name: "test", color: "324532"}]
63 | })
64 | subject
65 | end
66 |
67 | it "creates open issues on the list when not present in DB" do
68 | expect(fake_issues_repository).to receive(:create).with({
69 | title: "Issue4",
70 | comments: 65,
71 | url: "repo/issues/4",
72 | repo_id: repo.id,
73 | github_id: 103,
74 | created_at: DateTime.new(2011, 4, 22, 13, 33, 48, 0),
75 | labels: []
76 | })
77 | subject
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/repo_syncer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Services::RepoSyncer do
4 | let(:fake_repo_fetcher) { instance_double(Processing::Services::Github::RepoFetcher) }
5 | let(:fake_repos_repository) { instance_double(Processing::Repositories::ReposRepository) }
6 | let(:fake_gems_repository) { instance_double(Processing::Repositories::GemsRepository) }
7 | let(:repo) { Factory.structs[:repo, name: "some/some_gem"] }
8 |
9 | subject do
10 | described_class.new(
11 | repo_fetcher: fake_repo_fetcher,
12 | repos_repository: fake_repos_repository,
13 | gems_repository: fake_gems_repository
14 | ).call(repo)
15 | end
16 |
17 | context "one of services returns failure" do
18 | before do
19 | allow(fake_repo_fetcher).to receive(:call).with("some/some_gem").and_return(Dry::Monads::Failure(:error))
20 | end
21 |
22 | it "returns failure" do
23 | expect(subject.success?).to be_falsey
24 | expect(subject.failure).to eq(:error)
25 | end
26 | end
27 |
28 | context "repo name didn't change" do
29 | let(:github_info) do
30 | {
31 | stargazers_count: 50000,
32 | full_name: "some/some_gem",
33 | pushed_at: DateTime.new(2022, 12, 12, 12, 0, 0)
34 | }
35 | end
36 | let(:expected_attributes) do
37 | {
38 | stars: 50000,
39 | pushed_at: DateTime.new(2022, 12, 12, 12, 0, 0)
40 | }
41 | end
42 |
43 | before do
44 | allow(fake_repo_fetcher).to receive(:call).with("some/some_gem").and_return(Dry::Monads::Success(github_info))
45 | allow(fake_repos_repository).to receive(:update).with(repo.id, expected_attributes).and_return(repo)
46 | end
47 |
48 | it "returns saved repo" do
49 | expect(subject.success?).to be_truthy
50 | expect(subject.value!).to eq(repo)
51 | end
52 | end
53 |
54 | context "repo name changed" do
55 | let(:existing_repo) { Factory.structs[:repo, name: "new/some_gem"] }
56 | let(:github_info) do
57 | {
58 | stargazers_count: 50000,
59 | full_name: "new/some_gem",
60 | pushed_at: DateTime.new(2022, 12, 12, 12, 0, 0)
61 | }
62 | end
63 | let(:expected_attributes) do
64 | {
65 | stars: 50000,
66 | pushed_at: DateTime.new(2022, 12, 12, 12, 0, 0)
67 | }
68 | end
69 |
70 | before do
71 | allow(fake_repo_fetcher).to receive(:call).with("some/some_gem").and_return(Dry::Monads::Success(github_info))
72 | allow(fake_repos_repository).to receive(:transaction).and_yield
73 | end
74 |
75 | it "replaces old repo with new one and returns new one" do
76 | expect(fake_repos_repository).to receive(:find_or_create).with({name: "new/some_gem"}).and_return(existing_repo)
77 | expect(fake_gems_repository).to receive(:replace_repo).with(repo.id, existing_repo.id)
78 | expect(fake_repos_repository).to receive(:delete).with(repo.id)
79 | expect(fake_repos_repository).to receive(:update).with(existing_repo.id, expected_attributes).and_return(existing_repo)
80 |
81 | expect(subject.success?).to be_truthy
82 | expect(subject.value!).to eq(existing_repo)
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/rubygems/gem_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:gems_api)
4 |
5 | RSpec.describe Processing::Services::Rubygems::GemFetcher do
6 | subject { described_class.new(gems_api: fake_gem_api).call("some_gem") }
7 |
8 | let(:fake_gem_api) { class_double(::Gems) }
9 |
10 | context "API returned data" do
11 | before { allow(fake_gem_api).to receive(:info).with("some_gem").and_return({"some" => "data"}) }
12 |
13 | it "returns data from API" do
14 | expect(subject.success?).to be_truthy
15 | expect(subject.value!).to eq({"some" => "data"})
16 | end
17 | end
18 |
19 | context "API returned error" do
20 | before { allow(fake_gem_api).to receive(:info).with("some_gem").and_raise(::Gems::NotFound) }
21 |
22 | it "returns failure" do
23 | expect(subject.success?).to be_falsey
24 | expect(subject.failure).to be_instance_of(Gems::NotFound)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/rubygems/list_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:gems_api)
4 |
5 | RSpec.describe Processing::Services::Rubygems::ListFetcher do
6 | subject { described_class.new(gems_api: fake_gem_api).call(page: 2) }
7 |
8 | let(:fake_gem_api) { class_double(::Gems) }
9 |
10 | context "API returned data" do
11 | before { allow(fake_gem_api).to receive(:search).with("downloads:>1000000", {page: 2}).and_return({"some" => "data"}) }
12 |
13 | it "returns data from API" do
14 | expect(subject.success?).to be_truthy
15 | expect(subject.value!).to eq({"some" => "data"})
16 | end
17 | end
18 |
19 | context "API returned error" do
20 | before { allow(fake_gem_api).to receive(:search).with("downloads:>1000000", {page: 2}).and_raise(::Gems::NotFound) }
21 |
22 | it "returns failure" do
23 | expect(subject.success?).to be_falsey
24 | expect(subject.failure).to be_instance_of(Gems::NotFound)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/slices/processing/services/syncer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Services::Syncer do
4 | let(:fake_rubygems_fetcher) { instance_double(Processing::Services::Rubygems::GemFetcher) }
5 | let(:fake_gem_repo) { instance_double(Processing::Repositories::GemsRepository) }
6 | let(:gem) { Factory.structs[:gem, name: "some_gem"] }
7 | let(:gem_info) do
8 | {
9 | "name" => "some_gem",
10 | "info" => "some description",
11 | "downloads" => 50000,
12 | "homepage_uri" => "https://github.com/test/some_gem",
13 | "version" => "2.0.0",
14 | "licenses" => ["MIT"]
15 | }
16 | end
17 | let(:expected_attributes) do
18 | {
19 | description: "some description",
20 | downloads: 50000,
21 | licenses: ["MIT"],
22 | version: "2.0.0"
23 | }
24 | end
25 |
26 | subject do
27 | described_class.new(
28 | gem_fetcher: fake_rubygems_fetcher,
29 | gems_repository: fake_gem_repo
30 | ).call(gem)
31 | end
32 |
33 | context "one of services returns failure" do
34 | before do
35 | allow(fake_rubygems_fetcher).to receive(:call).with("some_gem").and_return(Dry::Monads::Failure(:gem_info_fetch_failed))
36 | end
37 |
38 | it "returns failure" do
39 | expect(subject.success?).to be_falsey
40 | expect(subject.failure).to eq(:gem_info_fetch_failed)
41 | end
42 | end
43 |
44 | context "everything goes ok" do
45 | before do
46 | allow(fake_rubygems_fetcher).to receive(:call).with("some_gem").and_return(Dry::Monads::Success(gem_info))
47 | allow(fake_gem_repo).to receive(:update).with(gem.id, expected_attributes).and_return(gem)
48 | end
49 |
50 | it "returns saved gem" do
51 | expect(subject.success?).to be_truthy
52 | expect(subject.value!).to eq(gem)
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/slices/processing/workers/discover_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Workers::DiscoverWorker do
4 | let(:fake_discover) { instance_double(Processing::Services::Discover) }
5 |
6 | subject { described_class.new(discover: fake_discover).perform(1) }
7 |
8 | context "no_results failure" do
9 | before { allow(fake_discover).to receive(:call).and_return(Dry::Monads::Failure(:no_results)) }
10 |
11 | it "doesn't enqueue sync jobs" do
12 | subject
13 |
14 | expect(Processing::Workers::SyncWorker).not_to have_enqueued_sidekiq_job
15 | expect(Processing::Workers::SyncIssuesWorker).not_to have_enqueued_sidekiq_job
16 | expect(Processing::Workers::SyncRepoWorker).not_to have_enqueued_sidekiq_job
17 | end
18 |
19 | it "doesn't enqueue next page job" do
20 | subject
21 |
22 | expect(described_class).not_to have_enqueued_sidekiq_job
23 | end
24 | end
25 |
26 | context "gems returned by Discover" do
27 | let(:gem) { Factory.structs[:gem, id: 1, repo: Factory.structs[:repo, id: 5]] }
28 |
29 | before { allow(fake_discover).to receive(:call).and_return(Dry::Monads::Success[gem]) }
30 |
31 | it "enqueues sync jobs" do
32 | subject
33 |
34 | expect(Processing::Workers::SyncWorker).to have_enqueued_sidekiq_job(1)
35 | expect(Processing::Workers::SyncIssuesWorker).to have_enqueued_sidekiq_job(5)
36 | expect(Processing::Workers::SyncRepoWorker).to have_enqueued_sidekiq_job(5)
37 | end
38 |
39 | it "enqueues next page job" do
40 | subject
41 |
42 | expect(described_class).to have_enqueued_sidekiq_job(2)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/slices/processing/workers/sync_all_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Processing::Workers::SyncAllWorker do
4 | let(:fake_gem_repo) { instance_double(Processing::Repositories::GemsRepository) }
5 | let(:fake_repos_repo) { instance_double(Processing::Repositories::ReposRepository) }
6 |
7 | subject { described_class.new(gems_repository: fake_gem_repo, repos_repository: fake_repos_repo).perform(1673908001) }
8 |
9 | before do
10 | allow(fake_gem_repo).to receive(:pluck_ids_for_hour).with(22).and_return([1, 2, 3])
11 | allow(fake_repos_repo).to receive(:pluck_ids_for_hour).with(22).and_return([5, 6, 7])
12 | end
13 |
14 | it "schedules single sync jobs" do
15 | subject
16 | expect(Processing::Workers::SyncWorker).to have_enqueued_sidekiq_job(1)
17 | expect(Processing::Workers::SyncWorker).to have_enqueued_sidekiq_job(2)
18 | expect(Processing::Workers::SyncWorker).to have_enqueued_sidekiq_job(3)
19 | expect(Processing::Workers::SyncIssuesWorker).to have_enqueued_sidekiq_job(5)
20 | expect(Processing::Workers::SyncIssuesWorker).to have_enqueued_sidekiq_job(6)
21 | expect(Processing::Workers::SyncIssuesWorker).to have_enqueued_sidekiq_job(7)
22 | expect(Processing::Workers::SyncRepoWorker).to have_enqueued_sidekiq_job(5)
23 | expect(Processing::Workers::SyncRepoWorker).to have_enqueued_sidekiq_job(6)
24 | expect(Processing::Workers::SyncRepoWorker).to have_enqueued_sidekiq_job(7)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/slices/processing/workers/sync_issues_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:octokit)
4 |
5 | RSpec.describe Processing::Workers::SyncIssuesWorker do
6 | let(:fake_repos_repository) do
7 | fake_repository(:processing, :repos) do |repository|
8 | allow(repository).to receive(:by_id).with(1, with: [:issues]).and_return(repo)
9 | end
10 | end
11 | let(:fake_syncer) { instance_double(Processing::Services::IssuesSyncer) }
12 | let(:repo) { Factory.structs[:repo] }
13 |
14 | subject { described_class.new(repos_repository: fake_repos_repository, issues_syncer: fake_syncer).perform(1) }
15 |
16 | context "update successful" do
17 | it "calls sync" do
18 | expect(fake_syncer).to receive(:call).with(repo).and_return(Dry::Monads::Success())
19 | subject
20 | end
21 | end
22 |
23 | context "update failed" do
24 | it "raises error" do
25 | expect(fake_syncer).to receive(:call).with(repo).and_return(Dry::Monads::Failure(Octokit::Forbidden.new))
26 | expect { subject }.to raise_error(Octokit::Forbidden)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/slices/processing/workers/sync_repo_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:octokit)
4 |
5 | RSpec.describe Processing::Workers::SyncRepoWorker do
6 | let(:fake_repos_repository) do
7 | fake_repository(:processing, :repos) do |repository|
8 | allow(repository).to receive(:by_id).with(1).and_return(repo)
9 | end
10 | end
11 | let(:fake_syncer) { instance_double(Processing::Services::RepoSyncer) }
12 | let(:repo) { Factory.structs[:repo] }
13 |
14 | subject { described_class.new(repos_repository: fake_repos_repository, repo_syncer: fake_syncer).perform(1) }
15 |
16 | context "sync successful" do
17 | it "calls sync" do
18 | expect(fake_syncer).to receive(:call).with(repo).and_return(Dry::Monads::Success())
19 | subject
20 | end
21 | end
22 |
23 | context "sync failed" do
24 | it "raises error" do
25 | expect(fake_syncer).to receive(:call).with(repo).and_return(Dry::Monads::Failure(Octokit::Forbidden.new))
26 | expect { subject }.to raise_error(Octokit::Forbidden)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/slices/processing/workers/sync_worker_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Processing::Slice.prepare(:octokit)
4 |
5 | RSpec.describe Processing::Workers::SyncWorker do
6 | let(:fake_gem_repo) do
7 | fake_repository(:processing, :gems) do |repo|
8 | allow(repo).to receive(:by_id).with(1).and_return(gem)
9 | end
10 | end
11 | let(:fake_syncer) { instance_double(Processing::Services::Syncer) }
12 | let(:gem) { Factory.structs[:gem] }
13 |
14 | subject { described_class.new(gems_repository: fake_gem_repo, syncer: fake_syncer).perform(1) }
15 |
16 | context "sync successful" do
17 | it "calls sync" do
18 | expect(fake_syncer).to receive(:call).with(gem).and_return(Dry::Monads::Success())
19 | subject
20 | end
21 | end
22 |
23 | context "sync failed" do
24 | it "raises error" do
25 | expect(fake_syncer).to receive(:call).with(gem).and_return(Dry::Monads::Failure(Gems::GemError.new))
26 | expect { subject }.to raise_error(Gems::GemError)
27 | end
28 | end
29 |
30 | context "gem not found" do
31 | it "deletes gem" do
32 | expect(fake_syncer).to receive(:call).with(gem).and_return(Dry::Monads::Failure(Gems::NotFound.new))
33 | expect(fake_gem_repo).to receive(:delete).with(1)
34 | subject
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/favorites/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Favorites::Create do
4 | include_context "authorized user"
5 |
6 | subject do
7 | described_class.new(gems_repository: fake_gem_repository, favorites_repository: fake_favorites_repository)
8 | .call(env.merge({name: "test"}))
9 | end
10 |
11 | let(:gem) { Factory.structs[:gem, name: "test"] }
12 | let(:fake_gem_repository) do
13 | fake_repository(:web, :gem) do |repository|
14 | allow(repository).to receive(:by_name).with(gem.name).and_return(gem)
15 | end
16 | end
17 | let(:fake_favorites_repository) do
18 | fake_repository(:web, :favorites) do |repository|
19 | allow(repository).to receive(:create).with(gem_id: gem.id, user_id: user.id)
20 | end
21 | end
22 |
23 | it "redirects to gem" do
24 | expect(subject.headers["Location"]).to eq("/gems/test")
25 | end
26 |
27 | it "saves favorite" do
28 | expect(fake_favorites_repository).to receive(:create).with(gem_id: gem.id, user_id: user.id)
29 | subject
30 | end
31 |
32 | it "sets flash message" do
33 | expect(subject.flash.next).to eq({success: "Gem added to favorites"})
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/favorites/delete_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Favorites::Delete do
4 | include_context "authorized user"
5 |
6 | subject do
7 | described_class.new(gems_repository: fake_gem_repository, favorites_repository: fake_favorites_repository)
8 | .call(env.merge({name: "test"}))
9 | end
10 |
11 | let(:gem) { Factory.structs[:gem, name: "test"] }
12 | let(:fake_gem_repository) do
13 | fake_repository(:web, :gem) do |repository|
14 | allow(repository).to receive(:by_name).with(gem.name).and_return(gem)
15 | end
16 | end
17 | let(:fake_favorites_repository) do
18 | fake_repository(:web, :favorites) do |repository|
19 | allow(repository).to receive(:unlink).with(gem_id: gem.id, user_id: user.id)
20 | end
21 | end
22 |
23 | it "redirects to gem" do
24 | expect(subject.headers["Location"]).to eq("/gems/test")
25 | end
26 |
27 | it "unlinks favorite" do
28 | expect(fake_favorites_repository).to receive(:unlink).with(gem_id: gem.id, user_id: user.id)
29 | subject
30 | end
31 |
32 | it "sets flash message" do
33 | expect(subject.flash.next).to eq({success: "Gem removed from favorites"})
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/favorites/index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Favorites::Index do
4 | include_context "authorized user"
5 |
6 | let(:fake_gems_repository) do
7 | fake_repository(:web, :gems) do |repo|
8 | allow(repo).to receive(:user_favorites).with(user.id).and_return([gem])
9 | end
10 | end
11 | let(:gem) { OpenStruct.new(**Factory.structs[:gem].attributes, issues_count: 10) }
12 |
13 | subject { described_class.new(gems_repository: fake_gems_repository).call(env) }
14 |
15 | it "calls repo with proper attributes" do
16 | expect(fake_gems_repository).to receive(:user_favorites)
17 | .with(user.id)
18 | .and_return([gem])
19 | subject
20 | end
21 |
22 | it "is successful" do
23 | expect(subject).to be_successful
24 | end
25 |
26 | it "exposes proper data" do
27 | expect(subject[:gems]).to eq([gem])
28 | end
29 |
30 | it "render view" do
31 | expect(subject.body[0]).to include("Favorites")
32 | expect(subject.body[0]).to include(gem.name)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/gemfile/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Gemfile::Create do
4 | let(:fake_parser) { instance_double(Web::Services::GemfileParser) }
5 | let(:instance) { described_class.new(gemfile_parser: fake_parser, gems_repository: fake_gems_repository) }
6 | let(:fake_gems_repository) { fake_repository(:web, :gems) }
7 | let!(:gem) { OpenStruct.new(**Factory.structs[:gem].attributes, issues_count: 10, name: "hanami") }
8 |
9 | subject { instance.call({gemfile: {tempfile: File.open("#{SPEC_ROOT}/support/files/Gemfile.test")}}) }
10 |
11 | context "gemfile parse successful" do
12 | before do
13 | allow(fake_parser).to receive(:call).with("gem \"hanami\", \"~> 2.0\"\n")
14 | .and_return(Dry::Monads::Success(["hanami"]))
15 | allow(fake_gems_repository).to receive(:by_list).with(["hanami"]).and_return([gem])
16 | end
17 |
18 | it "exposes proper data" do
19 | expect(subject[:gems].to_a).to eq([gem])
20 | end
21 |
22 | it "render view" do
23 | expect(subject.body[0]).to include("Gems from your Gemfile")
24 | expect(subject.body[0]).to include(gem.name)
25 | end
26 | end
27 |
28 | context "gemfile parse successful but there are no gems in DB" do
29 | before do
30 | allow(fake_parser).to receive(:call).with("gem \"hanami\", \"~> 2.0\"\n")
31 | .and_return(Dry::Monads::Success(["hanami"]))
32 | allow(fake_gems_repository).to receive(:by_list).with(["hanami"]).and_return([])
33 | end
34 |
35 | it "exposes proper data" do
36 | expect(subject[:gems].to_a).to eq([])
37 | end
38 |
39 | it "render view" do
40 | expect(subject.body[0]).to include("Gems from your Gemfile")
41 | expect(subject.body[0]).to include("Couldn't find any gems from your gemfile in our database")
42 | end
43 | end
44 |
45 | context "gemfile parse failed" do
46 | before do
47 | allow(fake_parser).to receive(:call).with("gem \"hanami\", \"~> 2.0\"\n")
48 | .and_return(Dry::Monads::Failure(:no_gems_in_gemfile))
49 | end
50 |
51 | it "redirects to gem index" do
52 | expect(subject.headers["Location"]).to eq("/gems")
53 | end
54 |
55 | it "sets flash" do
56 | expect(subject.flash.next).to eq({warning: "No gems found in file, are you sure it's a correct Gemfile?"})
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/gems/index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Gems::Index do
4 | let(:fake_gems_repository) do
5 | fake_repository(:web, :gems) do |repo|
6 | allow(repo).to receive(:index).with(any_args).and_return(result)
7 | end
8 | end
9 | let!(:gem) { OpenStruct.new(**Factory.structs[:gem].attributes, issues_count: 10) }
10 | let(:result) { double(to_a: [gem], pager: pager_dbl) }
11 | let(:pager_dbl) { instance_double(ROM::SQL::Plugin::Pagination::Pager, total_pages: 3, current_page: 1) }
12 |
13 | subject { described_class.new(gems_repository: fake_gems_repository).call(env) }
14 |
15 | context "invalid params" do
16 | let(:env) { {page: "hehe"} }
17 |
18 | it "returns bad request" do
19 | puts gem.attributes
20 | expect(subject.status).to eq(400)
21 | end
22 | end
23 |
24 | context "valid params" do
25 | let(:env) { {page: 1, sort_by: "name"} }
26 |
27 | it "calls repo with proper attributes" do
28 | expect(fake_gems_repository).to receive(:index)
29 | .with(page: 1, order: "name")
30 | .and_return(result)
31 | subject
32 | end
33 |
34 | it "is successful" do
35 | expect(subject).to be_successful
36 | end
37 |
38 | it "exposes proper data" do
39 | expect(subject[:gems].to_a).to eq([gem])
40 | end
41 |
42 | it "render view" do
43 | expect(subject.body[0]).to include("Browse gems")
44 | expect(subject.body[0]).to include(gem.name)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/gems/show_old_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Gems::ShowOld do
4 | let(:fake_gems_repository) do
5 | fake_repository(:web, :gems) do |repo|
6 | allow(repo).to receive(:by_id).with(gem.id).and_return(gem)
7 | end
8 | end
9 | let(:gem) { Factory.structs[:gem, name: "test"] }
10 |
11 | subject { described_class.new(gems_repository: fake_gems_repository).call({id: gem.id}) }
12 |
13 | it "is redirects to new endpoint" do
14 | expect(subject.status).to eq(301)
15 | expect(subject.headers["Location"]).to eq("/gems/test")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/gems/show_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Gems::Show do
4 | include_context "authorized user"
5 |
6 | let(:fake_gems_repository) do
7 | fake_repository(:web, :gems) do |repo|
8 | allow(repo).to receive(:by_name).with(gem.name, with: {repo: [:issues, :gems]}).and_return(gem)
9 | allow(repo).to receive(:by_name).with("eo", with: {repo: [:issues, :gems]}).and_return(nil)
10 | end
11 | end
12 | let(:fake_favorites_repository) do
13 | fake_repository(:web, :favorites) do |repo|
14 | allow(repo).to receive(:favorite?).with(user_id: user.id, gem_id: gem.id).and_return(true)
15 | allow(repo).to receive(:total_favorites).with(gem.id).and_return(5)
16 | end
17 | end
18 | let(:repo) { Factory.structs[:repo] }
19 | let(:gem) { Factory.structs[:gem, repo: repo] }
20 | let(:instance) { described_class.new(gems_repository: fake_gems_repository, favorites_repository: fake_favorites_repository) }
21 |
22 | before do
23 | # bug in factory?
24 | allow(gem).to receive(:repo).and_return(repo)
25 | end
26 |
27 | context "gem not found" do
28 | subject { instance.call(env.merge({name: "eo"})) }
29 |
30 | it "returns not found" do
31 | expect(subject.status).to eq(404)
32 | end
33 | end
34 |
35 | context "gem exists" do
36 | subject { instance.call(env.merge({name: gem.name})) }
37 |
38 | it "is successful" do
39 | expect(subject).to be_successful
40 | end
41 |
42 | it "exposes proper data" do
43 | expect(subject[:current_gem]).to eq(gem)
44 | expect(subject[:favorite]).to eq(true)
45 | expect(subject[:total_favorites]).to eq(5)
46 | end
47 |
48 | it "render view" do
49 | expect(subject.body[0]).to include(gem.name)
50 | expect(subject.body[0]).to include(repo.issues[0].title)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/pages/index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Pages::Index do
4 | let(:fake_gems_repository) do
5 | fake_repository(:web, :gems) do |repo|
6 | allow(repo).to receive(:random).with(3).and_return(result)
7 | end
8 | end
9 | let!(:gem) { OpenStruct.new(**Factory.structs[:gem].attributes, issues_count: 10) }
10 | let(:result) { [gem] }
11 |
12 | subject { described_class.new(gems_repository: fake_gems_repository).call({}) }
13 |
14 | it "calls repo with proper attributes" do
15 | expect(fake_gems_repository).to receive(:random)
16 | .with(3)
17 | .and_return(result)
18 | subject
19 | end
20 |
21 | it "is successful" do
22 | expect(subject).to be_successful
23 | end
24 |
25 | it "exposes proper data" do
26 | expect(subject[:random_gems]).to eq([gem])
27 | end
28 |
29 | it "render view" do
30 | expect(subject.body[0]).to include("Help maintain your favourite gems")
31 | expect(subject.body[0]).to include(gem.name)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/session/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Session::Create do
4 | subject { described_class.new(users_repository: fake_repo).call(env) }
5 |
6 | let(:auth_mock) { OmniAuth.config.mock_auth[:github] }
7 | let(:user) { Factory.structs[:user, id: 2] }
8 | let(:fake_repo) { instance_double(Web::Repositories::UsersRepository, {auth: user}) }
9 | let(:env) { {"omniauth.auth" => OmniAuth.config.mock_auth[:github]} }
10 |
11 | it "redirects to homepage" do
12 | expect(subject.headers["Location"]).to eq("/")
13 | end
14 |
15 | it "saves user id returned from repo to session" do
16 | expect(fake_repo).to receive(:auth).with(auth_mock).and_return(user)
17 | expect(subject.session[:user_id]).to eq(2)
18 | end
19 |
20 | it "sets flash message" do
21 | expect(subject.flash.next).to eq({success: "Successfully signed in"})
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/session/destroy_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Session::Destroy do
4 | subject { described_class.new.call(env) }
5 |
6 | let(:env) { {"rack.session" => {"user_id" => 3}} }
7 |
8 | it "redirects to homepage" do
9 | expect(subject.headers["Location"]).to eq("/")
10 | end
11 |
12 | it "clears user id from session" do
13 | expect(subject.session[:user_id]).to be_nil
14 | end
15 |
16 | it "sets flash message" do
17 | expect(subject.flash.next).to eq({success: "Successfully signed out"})
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/slices/web/actions/session/failure_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Actions::Session::Failure do
4 | subject { described_class.new.call({}) }
5 |
6 | it "redirects to homepage" do
7 | expect(subject.headers["Location"]).to eq("/")
8 | end
9 |
10 | it "sets flash message" do
11 | expect(subject.flash.next).to eq({warning: "Error: couldn't sign in"})
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/slices/web/lib/errors_mapper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::ErrorsMapper do
4 | it "returns human error for key" do
5 | expect(described_class.new.call(:gemfile_parse_failed)).to eq("Couldn't parse gemfile")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/slices/web/lib/formatter_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Formatter do
4 | let(:instance) { described_class.new }
5 |
6 | describe "#separator" do
7 | it "returns number with separators" do
8 | expect(instance.separator(2342315)).to eq("2,342,315")
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/slices/web/repositories/favorites_repository_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Repositories::FavoritesRepository, type: :database do
4 | subject(:repository) { described_class.new }
5 |
6 | describe "#favorite?" do
7 | let(:gem) { Factory[:gem] }
8 | let(:user) { Factory[:user] }
9 |
10 | context "when user has favorite" do
11 | before { Factory[:favorite, user_id: user.id, gem_id: gem.id] }
12 |
13 | it "returns true" do
14 | expect(repository.favorite?(user_id: user.id, gem_id: gem.id)).to be_truthy
15 | end
16 | end
17 |
18 | context "without favorite" do
19 | it "returns false" do
20 | expect(repository.favorite?(user_id: user.id, gem_id: gem.id)).to be_falsey
21 | end
22 | end
23 | end
24 |
25 | describe "#total_favorites" do
26 | let(:gem1) { Factory[:gem] }
27 | let(:gem2) { Factory[:gem] }
28 | let(:user1) { Factory[:user] }
29 | let(:user2) { Factory[:user] }
30 | let(:user3) { Factory[:user] }
31 |
32 | before do
33 | Factory[:favorite, user_id: user1.id, gem_id: gem1.id]
34 | Factory[:favorite, user_id: user2.id, gem_id: gem1.id]
35 | Factory[:favorite, user_id: user3.id, gem_id: gem2.id]
36 | end
37 |
38 | it "returns proper count of favorites" do
39 | expect(repository.total_favorites(gem1.id)).to eq(2)
40 | expect(repository.total_favorites(gem2.id)).to eq(1)
41 | end
42 | end
43 |
44 | describe "#unlink" do
45 | let(:gem) { Factory[:gem] }
46 | let(:user) { Factory[:user] }
47 |
48 | before { Factory[:favorite, user_id: user.id, gem_id: gem.id] }
49 |
50 | it "removes association" do
51 | repository.unlink(user_id: user.id, gem_id: gem.id)
52 | expect(repository.favorite?(user_id: user.id, gem_id: gem.id)).to be_falsey
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/slices/web/repositories/gems_repository_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Repositories::GemsRepository, type: :database do
4 | subject(:repository) { described_class.new }
5 |
6 | describe "#by_id" do
7 | let!(:repo) { Factory[:repo] }
8 | let!(:gem) { Factory[:gem, repo_id: repo.id] }
9 |
10 | context "gem exists" do
11 | it "returns gem" do
12 | expect(repository.by_id(gem.id)).to match_entity(gem)
13 | end
14 | end
15 |
16 | context "gem doesnt exist" do
17 | it "returns nil" do
18 | expect(repository.by_id(gem.id + 3)).to be_nil
19 | end
20 | end
21 |
22 | context "when 'with' present" do
23 | it "returns gem with associations" do
24 | result = repository.by_id(gem.id, with: [:repo])
25 | expect(result.name).to eq(gem.name)
26 | expect(result.repo).to match(match_entity(repo))
27 | end
28 | end
29 | end
30 |
31 | describe "#by_name" do
32 | let!(:repo) { Factory[:repo] }
33 | let!(:gem) { Factory[:gem, repo_id: repo.id] }
34 |
35 | context "gem exists" do
36 | it "returns gem" do
37 | expect(repository.by_name(gem.name)).to match_entity(gem)
38 | end
39 | end
40 |
41 | context "gem doesnt exist" do
42 | it "returns nil" do
43 | expect(repository.by_name("invalid")).to be_nil
44 | end
45 | end
46 |
47 | context "when 'with' present" do
48 | it "returns gem with associations" do
49 | result = repository.by_name(gem.name, with: [:repo])
50 | expect(result.name).to eq(gem.name)
51 | expect(result.repo).to match(match_entity(repo))
52 | end
53 | end
54 | end
55 |
56 | describe "#index" do
57 | let!(:gem1) { Factory[:gem, repo: Factory[:repo], downloads: 3000] }
58 | let!(:gem2) { Factory[:gem, repo: Factory[:repo], downloads: 5000] }
59 | let!(:gem3) { Factory[:gem, repo: Factory[:repo], downloads: 4000] }
60 | let!(:gem4) { Factory[:gem, repo: Factory[:repo, issues: []]] }
61 | let!(:gem5) { Factory[:gem, repo: Factory[:repo, pushed_at: DateTime.now - 400]] }
62 |
63 | it "returns paginated data except for gems without issues or outdated" do
64 | expect(subject.index(per_page: 2, page: 1).to_a.map(&:id))
65 | .to eq([gem2.id, gem3.id])
66 | expect(subject.index(per_page: 2, page: 2).to_a.map(&:id))
67 | .to eq([gem1.id])
68 | end
69 |
70 | it "returns pager" do
71 | expect(subject.index(per_page: 2, page: 1).pager).not_to be_nil
72 | end
73 | end
74 |
75 | describe "#by_list" do
76 | let!(:gem1) { Factory[:gem, repo: Factory[:repo]] }
77 | let!(:gem2) { Factory[:gem, repo: Factory[:repo]] }
78 | let!(:gem3) { Factory[:gem, repo: Factory[:repo, stars: 20, issues: []]] }
79 | let!(:gem4) { Factory[:gem, repo: Factory[:repo, pushed_at: DateTime.now - 400]] }
80 |
81 | it "returns gems from given list except for gems without issues or outdated" do
82 | expect(subject.by_list([gem1.name, gem2.name, gem3.name, gem4.name]).to_a.map(&:id))
83 | .to eq([gem1.id, gem2.id])
84 | end
85 | end
86 |
87 | describe "#user_favorites" do
88 | let!(:gem1) { Factory[:gem, repo: Factory[:repo]] }
89 | let!(:gem2) { Factory[:gem, repo: Factory[:repo]] }
90 | let!(:gem3) { Factory[:gem, repo: Factory[:repo, stars: 20, issues: []]] }
91 | let!(:gem4) { Factory[:gem, repo: Factory[:repo, pushed_at: DateTime.now - 400]] }
92 | let!(:user) { Factory[:user] }
93 |
94 | before { Factory[:favorite, user_id: user.id, gem_id: gem1.id] }
95 |
96 | it "returns favorite gems from user id except for gems without issues or outdated" do
97 | expect(subject.user_favorites(user.id).to_a.map(&:id))
98 | .to eq([gem1.id])
99 | end
100 | end
101 |
102 | describe "#random" do
103 | let!(:gem1) { Factory[:gem, repo: Factory[:repo]] }
104 | let!(:gem2) { Factory[:gem, repo: Factory[:repo]] }
105 |
106 | it "returns random N gems" do
107 | expect(subject.random(1).to_a.map(&:id)).to eq([gem1.id]).or(eq([gem2.id]))
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/spec/slices/web/repositories/users_repository_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Repositories::UsersRepository, type: :database do
4 | subject(:repo) { described_class.new }
5 |
6 | describe "#by_id" do
7 | let!(:user) { Factory[:user] }
8 |
9 | context "user exists" do
10 | it "returns user" do
11 | expect(repo.by_id(user.id)).to eq(user)
12 | end
13 | end
14 |
15 | context "user doesnt exist" do
16 | it "returns nil" do
17 | expect(repo.by_id(user.id + 3)).to be_nil
18 | end
19 | end
20 | end
21 |
22 | describe "#auth" do
23 | let(:expected_attributes) do
24 | {
25 | username: "test",
26 | avatar: "http://localhost/avatar.png",
27 | github_id: 235352,
28 | email: "test@test.com"
29 | }
30 | end
31 |
32 | context "user exists" do
33 | let!(:user) { Factory[:user, github_id: 235352] }
34 |
35 | it "returns updated user" do
36 | user = repo.auth(OmniAuth.config.mock_auth[:github])
37 | expect(user.attributes).to include(expected_attributes)
38 | end
39 |
40 | it "doesnt create new user" do
41 | expect { repo.auth(OmniAuth.config.mock_auth[:github]) }.not_to change { repo.users.to_a.size }
42 | end
43 | end
44 |
45 | context "user doesnt exist" do
46 | it "returns user" do
47 | user = repo.auth(OmniAuth.config.mock_auth[:github])
48 | expect(user.attributes).to include(expected_attributes)
49 | end
50 |
51 | it "saves user to DB" do
52 | expect { repo.auth(OmniAuth.config.mock_auth[:github]) }.to change { repo.users.to_a.size }.by(1)
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/slices/web/services/gemfile_parser_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Services::GemfileParser do
4 | subject { described_class.new.call(content) }
5 |
6 | context "content is valid gemfile" do
7 | let(:content) do
8 | 'source "https://rubygems.org"
9 |
10 | gem "hanami", "~> 2.0"
11 | gem "hanami-router", "~> 2.0"'
12 | end
13 |
14 | it "returns gems" do
15 | expect(subject.success?).to be_truthy
16 | expect(subject.value!).to eq(["hanami", "hanami-router"])
17 | end
18 | end
19 |
20 | context "content is valid gemfile but empty" do
21 | let(:content) do
22 | 'source "https://rubygems.org"'
23 | end
24 |
25 | it "returns gems" do
26 | expect(subject.success?).to be_falsey
27 | expect(subject.failure).to eq(:no_gems_in_gemfile)
28 | end
29 | end
30 |
31 | context "content is not a valid ruby code" do
32 | let(:content) do
33 | 'source https://rubygems.org"
34 |
35 | gem "hanami", "~> 2.0"
36 | gem "hanami-router", "~> 2.0"'
37 | end
38 |
39 | it "returns failure" do
40 | expect(subject.success?).to be_falsey
41 | expect(subject.failure).to eq(:gemfile_parse_failed)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/slices/web/views/gems/index_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Views::Gems::Index do
4 | describe "pages exposure" do
5 | subject { described_class.new.call(pager: fake_pager, sort_by: "downloads", gems: [], context: context) }
6 |
7 | let(:context) { Web::Views::Context.new(request: request_dbl, response: response_dbl) }
8 | let(:response_dbl) { instance_double(Hanami::Action::Response, flash: []) }
9 | let(:request_dbl) { instance_double(Hanami::Action::Request, session: {}, flash: {}) }
10 | let(:fake_pager) { instance_double(ROM::SQL::Plugin::Pagination::Pager, current_page: 5, total_pages: 25) }
11 |
12 | before do
13 | allow(response_dbl).to receive(:[]).with(:current_user).and_return(nil)
14 | end
15 |
16 | it "returns array with proper gaps" do
17 | expect(subject[:pages]).to eq([1, :gap, 4, 5, 6, :gap, 25])
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/slices/web/views/pages/error_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Views::Pages::Error do
4 | describe "exposures" do
5 | subject { described_class.new.call(code: 404, context: context) }
6 |
7 | let(:context) { Web::Views::Context.new(request: request_dbl, response: response_dbl) }
8 | let(:response_dbl) { instance_double(Hanami::Action::Response, flash: []) }
9 | let(:request_dbl) { instance_double(Hanami::Action::Request, session: {}, flash: {}) }
10 |
11 | before do
12 | allow(response_dbl).to receive(:[]).with(:current_user).and_return(nil)
13 | end
14 |
15 | it "exposes code and message" do
16 | expect(subject[:code].value).to eq(404)
17 | expect(subject[:msg].value).to eq("Not Found")
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/slices/web/views/parts/gem_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Views::Parts::Gem do
4 | let(:gem) { Factory.structs[:gem, downloads: 34522342, name: "test", licenses: ["MIT", "ABC"]] }
5 | let(:instance) { described_class.new(value: gem) }
6 |
7 | describe "#downloads" do
8 | it "returns formatted value" do
9 | expect(instance.downloads).to eq("34,522,342")
10 | end
11 | end
12 |
13 | describe "#url" do
14 | it "returns formatted value" do
15 | expect(instance.url).to eq("https://rubygems.org/gems/test")
16 | end
17 | end
18 |
19 | describe "#license" do
20 | it "returns joined licenses" do
21 | expect(instance.license).to eq("MIT, ABC")
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/slices/web/views/parts/label_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Views::Parts::Label do
4 | let(:label_light) { {"name" => "test", "color" => "dddddd"} }
5 | let(:label_dark) { {"name" => "dark", "color" => "222222"} }
6 | let(:instance_light) { described_class.new(value: label_light) }
7 | let(:instance_dark) { described_class.new(value: label_dark) }
8 |
9 | describe "#name" do
10 | it "returns name" do
11 | expect(instance_light.name).to eq("test")
12 | expect(instance_dark.name).to eq("dark")
13 | end
14 | end
15 |
16 | describe "#bg_color" do
17 | it "returns formatter color" do
18 | expect(instance_light.bg_color).to eq("#dddddd")
19 | expect(instance_dark.bg_color).to eq("#222222")
20 | end
21 | end
22 |
23 | describe "#bg_light?" do
24 | it "returns if color is light" do
25 | expect(instance_light.bg_light?).to be_truthy
26 | expect(instance_dark.bg_light?).to be_falsey
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/slices/web/views/parts/repo_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Web::Views::Parts::Repo do
4 | let(:repo) { Factory.structs[:repo, stars: 34522342, name: "test/test"] }
5 | let(:instance) { described_class.new(value: repo) }
6 |
7 | describe "#stars" do
8 | it "returns formatted value" do
9 | expect(instance.stars).to eq("34,522,342")
10 | end
11 | end
12 |
13 | describe "#url" do
14 | it "returns formatted value" do
15 | expect(instance.url).to eq("https://github.com/test/test")
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "pathname"
4 | SPEC_ROOT = Pathname(__dir__).realpath.freeze
5 |
6 | ENV["HANAMI_ENV"] ||= "test"
7 |
8 | require_relative "support/cov"
9 | require "hanami/prepare"
10 | require "dry/system/stubs"
11 | require "ostruct"
12 |
13 | Web::Slice.container.enable_stubs!
14 |
15 | RSpec.configure do |config|
16 | config.after do
17 | Web::Slice.container.unstub
18 | end
19 | end
20 |
21 | require_relative "support/rspec"
22 | require_relative "support/database_cleaner"
23 | require_relative "support/factory"
24 | require_relative "support/capybara"
25 | require_relative "support/sidekiq"
26 | require_relative "support/matchers"
27 | require_relative "support/fake_repositories"
28 | require_relative "support/webmock"
29 | require_relative "support/csrf_fix"
30 | require_relative "support/omniauth_mock"
31 | require_relative "support/auth"
32 |
--------------------------------------------------------------------------------
/spec/support/auth.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_context "authorized user" do
4 | let!(:user) { Factory.structs[:user] }
5 | let!(:fake_auth_repo) do
6 | fake_repository(:web, :users, stub_container: true) do |repo|
7 | allow(repo).to receive(:by_id).with(user.id).and_return(user)
8 | end
9 | end
10 | let!(:env) { {"rack.session" => {user_id: user.id}} }
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/capybara.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "capybara/rspec"
4 | require "capybara/cuprite"
5 |
6 | Capybara.app = Hanami.app
7 | Capybara.javascript_driver = :cuprite
8 | Capybara.register_driver(:cuprite) do |app|
9 | Capybara::Cuprite::Driver.new(app, window_size: [1200, 800], browser_options: {"disable-smooth-scrolling" => true})
10 | end
11 |
--------------------------------------------------------------------------------
/spec/support/cov.rb:
--------------------------------------------------------------------------------
1 | require "simplecov"
2 |
3 | SimpleCov.start do
4 | add_filter "/spec/"
5 | minimum_coverage 95
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/csrf_fix.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RemoveCsrfParamMiddleware
4 | def initialize(app)
5 | @app = app
6 | end
7 |
8 | def call(env)
9 | return @app.call(env) if env["CONTENT_TYPE"]&.start_with?("multipart")
10 |
11 | params = Rack::Utils.parse_query(env["rack.input"].read, "&")
12 | params.delete("_csrf_token")
13 | env["rack.input"] = StringIO.new(Rack::Utils.build_query(params))
14 | @app.call(env)
15 | end
16 | end
17 |
18 | class ShinyGems::App
19 | config.middleware.use RemoveCsrfParamMiddleware
20 | end
21 |
--------------------------------------------------------------------------------
/spec/support/database_cleaner.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "database_cleaner-sequel"
4 |
5 | Hanami.app.prepare(:persistence)
6 | DatabaseCleaner[:sequel, db: Hanami.app["persistence.db"]]
7 |
8 | RSpec.configure do |config|
9 | config.before(:suite) do
10 | DatabaseCleaner.clean_with(:truncation)
11 | end
12 |
13 | config.around(:each, type: :database) do |example|
14 | DatabaseCleaner.strategy = :transaction
15 | DatabaseCleaner.cleaning do
16 | example.run
17 | end
18 | end
19 |
20 | config.around(:each, type: :feature) do |example|
21 | DatabaseCleaner.strategy = :truncation
22 | DatabaseCleaner.cleaning do
23 | example.run
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/support/factories/favorite.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Factory.define(:favorite) do |f|
4 | f.association(:gem)
5 | f.association(:user)
6 | end
7 |
--------------------------------------------------------------------------------
/spec/support/factories/gem.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Factory.define(:gem) do |f|
4 | f.sequence(:name) { |n| "gem#{n}" }
5 | f.description "This is a gem"
6 | f.downloads { 2453432 }
7 | f.association(:repo)
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/factories/issue.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Factory.define(:issue) do |f|
4 | f.sequence(:title) { |n| "Some issue #{n}" }
5 | f.sequence(:url) { |n| "https://github.com/repo/gem/issues/#{n}" }
6 | f.sequence(:github_id) { |n| 1000 + n }
7 | f.comments 5
8 | f.labels { [{"name" => "test", "color" => "dddddd"}] }
9 | f.created_at { DateTime.now }
10 |
11 | f.trait :with_repo do |t|
12 | t.association(:repo)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/support/factories/repo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Factory.define(:repo) do |f|
4 | f.sequence(:name) { |n| "repo/gem#{n}" }
5 | f.stars { 1521 }
6 | f.association(:issues, count: 3)
7 | f.pushed_at { DateTime.now - 30 }
8 | f.association(:gems, count: 0)
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/factories/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Factory.define(:user) do |f|
4 | f.username "testname"
5 | f.sequence(:github_id) { |n| 1000 + n }
6 | f.email "test@test.com"
7 | f.avatar "http://localhost/avatar.png"
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/factory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rom-factory"
4 |
5 | Factory = ROM::Factory.configure do |config|
6 | config.rom = Hanami.app["persistence.rom"]
7 | end
8 |
9 | Dir[File.dirname(__FILE__) + "/factories/*.rb"].each { |file| require file }
10 |
--------------------------------------------------------------------------------
/spec/support/fake_repositories.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ShinyGems
4 | module Support
5 | module FakeRepositories
6 | def fake_repository(namespace, name, stub_container: false, &block)
7 | instance_double("#{namespace.capitalize}::Repositories::#{name.capitalize}Repository").tap do |double|
8 | block&.call(double)
9 | Object.const_get("#{namespace.capitalize}::Slice").container.stub("repositories.#{name}_repository", double) if stub_container
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
16 | RSpec.configure do |config|
17 | config.include ShinyGems::Support::FakeRepositories
18 | end
19 |
--------------------------------------------------------------------------------
/spec/support/files/Gemfile.test:
--------------------------------------------------------------------------------
1 | gem "hanami", "~> 2.0"
2 |
--------------------------------------------------------------------------------
/spec/support/matchers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define :match_entity do |expected|
4 | match do |actual|
5 | filtered(actual) == filtered(expected)
6 | end
7 |
8 | def filtered(entity)
9 | entity.attributes.reject { |_, value| value.is_a?(ROM::Struct) || (value.is_a?(Array) && value.all? { |el| el.is_a?(ROM::Struct) }) }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/omniauth_mock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "omniauth"
4 |
5 | OmniAuth.config.test_mode = true
6 | OmniAuth.config.request_validation_phase = false
7 | OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new({
8 | provider: "github",
9 | uid: 235352,
10 | info: {
11 | nickname: "test",
12 | image: "http://localhost/avatar.png",
13 | email: "test@test.com"
14 | },
15 | credentials: {
16 | token: "abc"
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/spec/support/rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.configure do |config|
4 | config.expect_with :rspec do |expectations|
5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6 | end
7 |
8 | config.mock_with :rspec do |mocks|
9 | mocks.verify_partial_doubles = true
10 | end
11 |
12 | config.shared_context_metadata_behavior = :apply_to_host_groups
13 |
14 | config.filter_run_when_matching :focus
15 |
16 | config.disable_monkey_patching!
17 | config.warnings = true
18 |
19 | if config.files_to_run.one?
20 | config.default_formatter = "doc"
21 | end
22 |
23 | config.order = :random
24 | Kernel.srand config.seed
25 | end
26 |
--------------------------------------------------------------------------------
/spec/support/sidekiq.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rspec-sidekiq"
4 |
5 | Sidekiq.configure_client do |config|
6 | config.logger.level = Logger::WARN
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/webmock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "webmock/rspec"
4 |
5 | WebMock.disable_net_connect!(allow_localhost: true)
6 |
--------------------------------------------------------------------------------
/web.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | HANAMI_SLICES=web bundle exec puma -C config/puma.rb
4 |
--------------------------------------------------------------------------------
/worker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | HANAMI_SLICES=processing bundle exec sidekiq -r ./config/sidekiq.rb -e production
4 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@esbuild/android-arm64@0.19.5":
6 | version "0.19.5"
7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz#276c5f99604054d3dbb733577e09adae944baa90"
8 | integrity sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==
9 |
10 | "@esbuild/android-arm@0.19.5":
11 | version "0.19.5"
12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.5.tgz#4a3cbf14758166abaae8ba9c01a80e68342a4eec"
13 | integrity sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==
14 |
15 | "@esbuild/android-x64@0.19.5":
16 | version "0.19.5"
17 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.5.tgz#21a3d11cd4613d2d3c5ccb9e746c254eb9265b0a"
18 | integrity sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==
19 |
20 | "@esbuild/darwin-arm64@0.19.5":
21 | version "0.19.5"
22 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz#714cb839f467d6a67b151ee8255886498e2b9bf6"
23 | integrity sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==
24 |
25 | "@esbuild/darwin-x64@0.19.5":
26 | version "0.19.5"
27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz#2c553e97a6d2b4ae76a884e35e6cbab85a990bbf"
28 | integrity sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==
29 |
30 | "@esbuild/freebsd-arm64@0.19.5":
31 | version "0.19.5"
32 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz#d554f556718adb31917a0da24277bf84b6ee87f3"
33 | integrity sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==
34 |
35 | "@esbuild/freebsd-x64@0.19.5":
36 | version "0.19.5"
37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz#288f7358a3bb15d99e73c65c9adaa3dabb497432"
38 | integrity sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==
39 |
40 | "@esbuild/linux-arm64@0.19.5":
41 | version "0.19.5"
42 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz#95933ae86325c93cb6b5e8333d22120ecfdc901b"
43 | integrity sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==
44 |
45 | "@esbuild/linux-arm@0.19.5":
46 | version "0.19.5"
47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz#0acef93aa3e0579e46d33b666627bddb06636664"
48 | integrity sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==
49 |
50 | "@esbuild/linux-ia32@0.19.5":
51 | version "0.19.5"
52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz#b6e5c9e80b42131cbd6b1ddaa48c92835f1ed67f"
53 | integrity sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==
54 |
55 | "@esbuild/linux-loong64@0.19.5":
56 | version "0.19.5"
57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz#e5f0cf95a180158b01ff5f417da796a1c09dfbea"
58 | integrity sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==
59 |
60 | "@esbuild/linux-mips64el@0.19.5":
61 | version "0.19.5"
62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz#ae36fb86c7d5f641f3a0c8472e83dcb6ea36a408"
63 | integrity sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==
64 |
65 | "@esbuild/linux-ppc64@0.19.5":
66 | version "0.19.5"
67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz#7960cb1666f0340ddd9eef7b26dcea3835d472d0"
68 | integrity sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==
69 |
70 | "@esbuild/linux-riscv64@0.19.5":
71 | version "0.19.5"
72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz#32207df26af60a3a9feea1783fc21b9817bade19"
73 | integrity sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==
74 |
75 | "@esbuild/linux-s390x@0.19.5":
76 | version "0.19.5"
77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz#b38d5681db89a3723862dfa792812397b1510a7d"
78 | integrity sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==
79 |
80 | "@esbuild/linux-x64@0.19.5":
81 | version "0.19.5"
82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz#46feba2ad041a241379d150f415b472fe3885075"
83 | integrity sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==
84 |
85 | "@esbuild/netbsd-x64@0.19.5":
86 | version "0.19.5"
87 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz#3b5c1fb068f26bfc681d31f682adf1bea4ef0702"
88 | integrity sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==
89 |
90 | "@esbuild/openbsd-x64@0.19.5":
91 | version "0.19.5"
92 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz#ca6830316ca68056c5c88a875f103ad3235e00db"
93 | integrity sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==
94 |
95 | "@esbuild/sunos-x64@0.19.5":
96 | version "0.19.5"
97 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz#9efc4eb9539a7be7d5a05ada52ee43cda0d8e2dd"
98 | integrity sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==
99 |
100 | "@esbuild/win32-arm64@0.19.5":
101 | version "0.19.5"
102 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz#29f8184afa7a02a956ebda4ed638099f4b8ff198"
103 | integrity sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==
104 |
105 | "@esbuild/win32-ia32@0.19.5":
106 | version "0.19.5"
107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz#f3de07afb292ecad651ae4bb8727789de2d95b05"
108 | integrity sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==
109 |
110 | "@esbuild/win32-x64@0.19.5":
111 | version "0.19.5"
112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz#faad84c41ba12e3a0acb52571df9bff37bee75f6"
113 | integrity sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==
114 |
115 | "@isaacs/cliui@^8.0.2":
116 | version "8.0.2"
117 | resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
118 | integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
119 | dependencies:
120 | string-width "^5.1.2"
121 | string-width-cjs "npm:string-width@^4.2.0"
122 | strip-ansi "^7.0.1"
123 | strip-ansi-cjs "npm:strip-ansi@^6.0.1"
124 | wrap-ansi "^8.1.0"
125 | wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
126 |
127 | "@pkgjs/parseargs@^0.11.0":
128 | version "0.11.0"
129 | resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
130 | integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
131 |
132 | "@popperjs/core@^2.11.8":
133 | version "2.11.8"
134 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
135 | integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
136 |
137 | ansi-regex@^5.0.1:
138 | version "5.0.1"
139 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
140 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
141 |
142 | ansi-regex@^6.0.1:
143 | version "6.0.1"
144 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
145 | integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
146 |
147 | ansi-styles@^4.0.0:
148 | version "4.3.0"
149 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
150 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
151 | dependencies:
152 | color-convert "^2.0.1"
153 |
154 | ansi-styles@^6.1.0:
155 | version "6.2.1"
156 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
157 | integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
158 |
159 | anymatch@~3.1.2:
160 | version "3.1.3"
161 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
162 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
163 | dependencies:
164 | normalize-path "^3.0.0"
165 | picomatch "^2.0.4"
166 |
167 | balanced-match@^1.0.0:
168 | version "1.0.2"
169 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
170 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
171 |
172 | binary-extensions@^2.0.0:
173 | version "2.2.0"
174 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
175 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
176 |
177 | bootstrap-icons@^1.11.1:
178 | version "1.11.1"
179 | resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.11.1.tgz#79e32494871d8c98e9d14f4bcdc278cee9b1dafd"
180 | integrity sha512-F0DDp7nKUX+x/QtpfRZ+XHFya60ng9nfdpdS59vDDfs4Uhuxp7zym/QavMsu/xx51txkoM9eVmpE7D08N35blw==
181 |
182 | bootstrap@^5.3.2:
183 | version "5.3.2"
184 | resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.2.tgz#97226583f27aae93b2b28ab23f4c114757ff16ae"
185 | integrity sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==
186 |
187 | brace-expansion@^2.0.1:
188 | version "2.0.1"
189 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
190 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
191 | dependencies:
192 | balanced-match "^1.0.0"
193 |
194 | braces@~3.0.2:
195 | version "3.0.3"
196 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
197 | integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
198 | dependencies:
199 | fill-range "^7.1.1"
200 |
201 | "chokidar@>=3.0.0 <4.0.0":
202 | version "3.5.3"
203 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
204 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
205 | dependencies:
206 | anymatch "~3.1.2"
207 | braces "~3.0.2"
208 | glob-parent "~5.1.2"
209 | is-binary-path "~2.1.0"
210 | is-glob "~4.0.1"
211 | normalize-path "~3.0.0"
212 | readdirp "~3.6.0"
213 | optionalDependencies:
214 | fsevents "~2.3.2"
215 |
216 | color-convert@^2.0.1:
217 | version "2.0.1"
218 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
219 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
220 | dependencies:
221 | color-name "~1.1.4"
222 |
223 | color-name@~1.1.4:
224 | version "1.1.4"
225 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
226 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
227 |
228 | cross-spawn@^7.0.0:
229 | version "7.0.3"
230 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
231 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
232 | dependencies:
233 | path-key "^3.1.0"
234 | shebang-command "^2.0.0"
235 | which "^2.0.1"
236 |
237 | eastasianwidth@^0.2.0:
238 | version "0.2.0"
239 | resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
240 | integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
241 |
242 | emoji-regex@^8.0.0:
243 | version "8.0.0"
244 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
245 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
246 |
247 | emoji-regex@^9.2.2:
248 | version "9.2.2"
249 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
250 | integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
251 |
252 | esbuild-sass-plugin@^2.16.0:
253 | version "2.16.0"
254 | resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz#2908ab5e104cfc980118c46d0b409cbab8aa32dd"
255 | integrity sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==
256 | dependencies:
257 | resolve "^1.22.6"
258 | sass "^1.7.3"
259 |
260 | esbuild@^0.19.0:
261 | version "0.19.5"
262 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.5.tgz#53a0e19dfbf61ba6c827d51a80813cf071239a8c"
263 | integrity sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==
264 | optionalDependencies:
265 | "@esbuild/android-arm" "0.19.5"
266 | "@esbuild/android-arm64" "0.19.5"
267 | "@esbuild/android-x64" "0.19.5"
268 | "@esbuild/darwin-arm64" "0.19.5"
269 | "@esbuild/darwin-x64" "0.19.5"
270 | "@esbuild/freebsd-arm64" "0.19.5"
271 | "@esbuild/freebsd-x64" "0.19.5"
272 | "@esbuild/linux-arm" "0.19.5"
273 | "@esbuild/linux-arm64" "0.19.5"
274 | "@esbuild/linux-ia32" "0.19.5"
275 | "@esbuild/linux-loong64" "0.19.5"
276 | "@esbuild/linux-mips64el" "0.19.5"
277 | "@esbuild/linux-ppc64" "0.19.5"
278 | "@esbuild/linux-riscv64" "0.19.5"
279 | "@esbuild/linux-s390x" "0.19.5"
280 | "@esbuild/linux-x64" "0.19.5"
281 | "@esbuild/netbsd-x64" "0.19.5"
282 | "@esbuild/openbsd-x64" "0.19.5"
283 | "@esbuild/sunos-x64" "0.19.5"
284 | "@esbuild/win32-arm64" "0.19.5"
285 | "@esbuild/win32-ia32" "0.19.5"
286 | "@esbuild/win32-x64" "0.19.5"
287 |
288 | fill-range@^7.1.1:
289 | version "7.1.1"
290 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
291 | integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
292 | dependencies:
293 | to-regex-range "^5.0.1"
294 |
295 | foreground-child@^3.1.0:
296 | version "3.1.1"
297 | resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
298 | integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
299 | dependencies:
300 | cross-spawn "^7.0.0"
301 | signal-exit "^4.0.1"
302 |
303 | fs-extra@^11.1.0:
304 | version "11.1.1"
305 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
306 | integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
307 | dependencies:
308 | graceful-fs "^4.2.0"
309 | jsonfile "^6.0.1"
310 | universalify "^2.0.0"
311 |
312 | fsevents@~2.3.2:
313 | version "2.3.3"
314 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
315 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
316 |
317 | function-bind@^1.1.2:
318 | version "1.1.2"
319 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
320 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
321 |
322 | glob-parent@~5.1.2:
323 | version "5.1.2"
324 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
325 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
326 | dependencies:
327 | is-glob "^4.0.1"
328 |
329 | glob@^10.3.3:
330 | version "10.3.10"
331 | resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
332 | integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
333 | dependencies:
334 | foreground-child "^3.1.0"
335 | jackspeak "^2.3.5"
336 | minimatch "^9.0.1"
337 | minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
338 | path-scurry "^1.10.1"
339 |
340 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
341 | version "4.2.11"
342 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
343 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
344 |
345 | hanami-assets@^2.2.1:
346 | version "2.2.1"
347 | resolved "https://registry.yarnpkg.com/hanami-assets/-/hanami-assets-2.2.1.tgz#c2ef2e7da75df61d7b8b2d94729178cb8663c296"
348 | integrity sha512-/zE7COY/seadmeBR+TVGD29DnJClxWi/En6YQQIH51s5/xhUHiuucbLdicmTmhw2ousqQ1mN9uX27RPkLujs+g==
349 | dependencies:
350 | esbuild "^0.19.0"
351 | fs-extra "^11.1.0"
352 | glob "^10.3.3"
353 |
354 | hasown@^2.0.0:
355 | version "2.0.0"
356 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
357 | integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
358 | dependencies:
359 | function-bind "^1.1.2"
360 |
361 | immutable@^4.0.0:
362 | version "4.3.4"
363 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
364 | integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
365 |
366 | is-binary-path@~2.1.0:
367 | version "2.1.0"
368 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
369 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
370 | dependencies:
371 | binary-extensions "^2.0.0"
372 |
373 | is-core-module@^2.13.0:
374 | version "2.13.1"
375 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
376 | integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
377 | dependencies:
378 | hasown "^2.0.0"
379 |
380 | is-extglob@^2.1.1:
381 | version "2.1.1"
382 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
383 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
384 |
385 | is-fullwidth-code-point@^3.0.0:
386 | version "3.0.0"
387 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
388 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
389 |
390 | is-glob@^4.0.1, is-glob@~4.0.1:
391 | version "4.0.3"
392 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
393 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
394 | dependencies:
395 | is-extglob "^2.1.1"
396 |
397 | is-number@^7.0.0:
398 | version "7.0.0"
399 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
400 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
401 |
402 | isexe@^2.0.0:
403 | version "2.0.0"
404 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
405 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
406 |
407 | jackspeak@^2.3.5:
408 | version "2.3.6"
409 | resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
410 | integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
411 | dependencies:
412 | "@isaacs/cliui" "^8.0.2"
413 | optionalDependencies:
414 | "@pkgjs/parseargs" "^0.11.0"
415 |
416 | jsonfile@^6.0.1:
417 | version "6.1.0"
418 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
419 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
420 | dependencies:
421 | universalify "^2.0.0"
422 | optionalDependencies:
423 | graceful-fs "^4.1.6"
424 |
425 | "lru-cache@^9.1.1 || ^10.0.0":
426 | version "10.0.1"
427 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
428 | integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
429 |
430 | minimatch@^9.0.1:
431 | version "9.0.3"
432 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
433 | integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
434 | dependencies:
435 | brace-expansion "^2.0.1"
436 |
437 | "minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
438 | version "7.0.4"
439 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
440 | integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
441 |
442 | normalize-path@^3.0.0, normalize-path@~3.0.0:
443 | version "3.0.0"
444 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
445 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
446 |
447 | path-key@^3.1.0:
448 | version "3.1.1"
449 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
450 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
451 |
452 | path-parse@^1.0.7:
453 | version "1.0.7"
454 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
455 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
456 |
457 | path-scurry@^1.10.1:
458 | version "1.10.1"
459 | resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
460 | integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
461 | dependencies:
462 | lru-cache "^9.1.1 || ^10.0.0"
463 | minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
464 |
465 | picomatch@^2.0.4, picomatch@^2.2.1:
466 | version "2.3.1"
467 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
468 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
469 |
470 | readdirp@~3.6.0:
471 | version "3.6.0"
472 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
473 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
474 | dependencies:
475 | picomatch "^2.2.1"
476 |
477 | resolve@^1.22.6:
478 | version "1.22.8"
479 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
480 | integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
481 | dependencies:
482 | is-core-module "^2.13.0"
483 | path-parse "^1.0.7"
484 | supports-preserve-symlinks-flag "^1.0.0"
485 |
486 | sass@^1.7.3:
487 | version "1.69.5"
488 | resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde"
489 | integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==
490 | dependencies:
491 | chokidar ">=3.0.0 <4.0.0"
492 | immutable "^4.0.0"
493 | source-map-js ">=0.6.2 <2.0.0"
494 |
495 | shebang-command@^2.0.0:
496 | version "2.0.0"
497 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
498 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
499 | dependencies:
500 | shebang-regex "^3.0.0"
501 |
502 | shebang-regex@^3.0.0:
503 | version "3.0.0"
504 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
505 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
506 |
507 | signal-exit@^4.0.1:
508 | version "4.1.0"
509 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
510 | integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
511 |
512 | "source-map-js@>=0.6.2 <2.0.0":
513 | version "1.0.2"
514 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
515 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
516 |
517 | "string-width-cjs@npm:string-width@^4.2.0":
518 | version "4.2.3"
519 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
520 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
521 | dependencies:
522 | emoji-regex "^8.0.0"
523 | is-fullwidth-code-point "^3.0.0"
524 | strip-ansi "^6.0.1"
525 |
526 | string-width@^4.1.0:
527 | version "4.2.3"
528 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
529 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
530 | dependencies:
531 | emoji-regex "^8.0.0"
532 | is-fullwidth-code-point "^3.0.0"
533 | strip-ansi "^6.0.1"
534 |
535 | string-width@^5.0.1, string-width@^5.1.2:
536 | version "5.1.2"
537 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
538 | integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
539 | dependencies:
540 | eastasianwidth "^0.2.0"
541 | emoji-regex "^9.2.2"
542 | strip-ansi "^7.0.1"
543 |
544 | "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
545 | version "6.0.1"
546 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
547 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
548 | dependencies:
549 | ansi-regex "^5.0.1"
550 |
551 | strip-ansi@^6.0.0, strip-ansi@^6.0.1:
552 | version "6.0.1"
553 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
554 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
555 | dependencies:
556 | ansi-regex "^5.0.1"
557 |
558 | strip-ansi@^7.0.1:
559 | version "7.1.0"
560 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
561 | integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
562 | dependencies:
563 | ansi-regex "^6.0.1"
564 |
565 | supports-preserve-symlinks-flag@^1.0.0:
566 | version "1.0.0"
567 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
568 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
569 |
570 | to-regex-range@^5.0.1:
571 | version "5.0.1"
572 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
573 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
574 | dependencies:
575 | is-number "^7.0.0"
576 |
577 | universalify@^2.0.0:
578 | version "2.0.1"
579 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
580 | integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
581 |
582 | which@^2.0.1:
583 | version "2.0.2"
584 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
585 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
586 | dependencies:
587 | isexe "^2.0.0"
588 |
589 | "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
590 | version "7.0.0"
591 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
592 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
593 | dependencies:
594 | ansi-styles "^4.0.0"
595 | string-width "^4.1.0"
596 | strip-ansi "^6.0.0"
597 |
598 | wrap-ansi@^8.1.0:
599 | version "8.1.0"
600 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
601 | integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
602 | dependencies:
603 | ansi-styles "^6.1.0"
604 | string-width "^5.0.1"
605 | strip-ansi "^7.0.1"
606 |
--------------------------------------------------------------------------------