From 893346d047dcc6599212ea64ed9925d50d1e831e Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 07:28:52 +0100 Subject: [PATCH 01/20] Generate app button and repo name validation hooked up - v1 --- app/controllers/generated_apps_controller.rb | 23 +++ app/controllers/github_controller.rb | 20 ++ .../app_name_preview_controller.js | 17 ++ .../controllers/form_values_controller.js | 26 +++ .../generated_output_controller.js | 1 + .../github_name_validator_controller.js | 102 ++++++++++ .../text_field_change_controller.js | 18 -- app/jobs/app_generation_job.rb | 6 +- app/models/concerns/git_backed_model.rb | 3 +- app/models/recipe.rb | 20 ++ app/services/app_repository.rb | 8 + app/services/data_repository.rb | 110 ++++++++++ app/services/git_repo.rb | 189 +++++++----------- .../github_repository_name_validator.rb | 25 ++- app/views/components/empty_state/component.rb | 2 +- app/views/dashboard/show.html.erb | 9 +- app/views/pages/show.html.erb | 115 +++++++---- app/views/repositories/new.html.erb | 40 ++-- config/credentials/development.yml.enc | 2 +- config/initializers/rails_new_config.rb | 18 ++ config/routes.rb | 4 +- .../github_repository_name_validator_test.rb | 6 +- 22 files changed, 538 insertions(+), 226 deletions(-) create mode 100644 app/controllers/github_controller.rb create mode 100644 app/javascript/controllers/app_name_preview_controller.js create mode 100644 app/javascript/controllers/form_values_controller.js create mode 100644 app/javascript/controllers/github_name_validator_controller.js delete mode 100644 app/javascript/controllers/text_field_change_controller.js create mode 100644 app/services/app_repository.rb create mode 100644 app/services/data_repository.rb create mode 100644 config/initializers/rails_new_config.rb diff --git a/app/controllers/generated_apps_controller.rb b/app/controllers/generated_apps_controller.rb index e1db01e..98fb580 100644 --- a/app/controllers/generated_apps_controller.rb +++ b/app/controllers/generated_apps_controller.rb @@ -3,4 +3,27 @@ class GeneratedAppsController < ApplicationController def show @generated_app = GeneratedApp.find(params[:id]) end + + def create + cli_flags = [ + params[:api_flag], + params[:database_choice], + params[:rails_flags] + ].compact.join(" ") + + recipe = Recipe.find_or_create_by_cli_flags!(cli_flags, current_user) + + @generated_app = current_user.generated_apps.create!( + name: params[:app_name], + recipe: recipe, + ruby_version: recipe.ruby_version, + rails_version: recipe.rails_version, + selected_gems: [], # We'll handle this later with ingredients + configuration_options: {} # We'll handle this later with ingredients + ) + + AppGeneration::Orchestrator.new(@generated_app).call + + redirect_to generated_app_log_entries_path(@generated_app) + end end diff --git a/app/controllers/github_controller.rb b/app/controllers/github_controller.rb new file mode 100644 index 0000000..8bf2674 --- /dev/null +++ b/app/controllers/github_controller.rb @@ -0,0 +1,20 @@ +class GithubController < ApplicationController + before_action :authenticate_user! + + def check_name + validator = GithubRepositoryNameValidator.new( + params[:name], + current_user.github_username + ) + begin + exists = validator.repo_exists? + render json: { available: !exists } + rescue Octokit::Unauthorized, Octokit::Forbidden => e + Rails.logger.error("GitHub authentication error: #{e.message}") + render json: { error: "GitHub authentication failed" }, status: :unauthorized + rescue => e + Rails.logger.error("GitHub validation error: #{e.message}") + render json: { error: "Could not validate repository name" }, status: :unprocessable_entity + end + end +end diff --git a/app/javascript/controllers/app_name_preview_controller.js b/app/javascript/controllers/app_name_preview_controller.js new file mode 100644 index 0000000..9b30de4 --- /dev/null +++ b/app/javascript/controllers/app_name_preview_controller.js @@ -0,0 +1,17 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["input"] + static outlets = ["generated-output"] + + connect() { + this.update() + } + + update(event) { + const value = event?.target?.value?.trim() || "" + this.generatedOutputOutlet.updateText(value) + this.dispatch("valueChanged", { detail: { value } }) + this.dispatch("appNameChanged", { detail: { value } }) + } +} diff --git a/app/javascript/controllers/form_values_controller.js b/app/javascript/controllers/form_values_controller.js new file mode 100644 index 0000000..6d3b836 --- /dev/null +++ b/app/javascript/controllers/form_values_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["appName", "apiFlag", "databaseChoice", "railsFlags"] + + connect() { + // Initialize values from the command display + this.updateFromDisplay() + } + + updateFromDisplay() { + const appNameOutput = document.getElementById("app-name-output") + const apiFlag = document.getElementById("api-flag") + const databaseChoice = document.getElementById("database-choice") + const railsFlags = document.getElementById("rails-flags") + + if (appNameOutput) { + this.appNameTarget.value = appNameOutput.textContent.trim() + const event = new Event('input', { bubbles: true }) + this.appNameTarget.dispatchEvent(event) + } + if (apiFlag) this.apiFlagTarget.value = apiFlag.textContent.trim() + if (databaseChoice) this.databaseChoiceTarget.value = databaseChoice.textContent.trim() + if (railsFlags) this.railsFlagsTarget.value = railsFlags.textContent.trim() + } +} diff --git a/app/javascript/controllers/generated_output_controller.js b/app/javascript/controllers/generated_output_controller.js index a44d7fd..7dc7fec 100644 --- a/app/javascript/controllers/generated_output_controller.js +++ b/app/javascript/controllers/generated_output_controller.js @@ -3,5 +3,6 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { updateText(text) { this.element.innerText = text + this.dispatch("valueChanged") } } diff --git a/app/javascript/controllers/github_name_validator_controller.js b/app/javascript/controllers/github_name_validator_controller.js new file mode 100644 index 0000000..91c8ca7 --- /dev/null +++ b/app/javascript/controllers/github_name_validator_controller.js @@ -0,0 +1,102 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["input", "message", "spinner", "submitButton"] + static values = { + checkUrl: String, + debounce: { type: Number, default: 500 }, + errorClass: { type: String, default: "text-red-600" }, + successClass: { type: String, default: "text-green-600" } + } + + initialize() { + this.validate = this.debounce(this.validate.bind(this), this.debounceValue) + this.disableSubmit() + this.boundValidate = this.validate.bind(this) + } + + connect() { + document.addEventListener("app-name-preview:appNameChanged", this.boundValidate) + } + + disconnect() { + document.removeEventListener("app-name-preview:appNameChanged", this.boundValidate) + } + + async validate(event) { + const name = event?.detail?.value || '' + if (!name) { + this.hideMessage() + this.disableSubmit() + return + } + + this.showSpinner() + + try { + const response = await fetch(`${this.checkUrlValue}?name=${encodeURIComponent(name)}`) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to validate repository name') + } + + const data = await response.json() + + this.hideSpinner() + + if (data.available) { + this.enableSubmit() + this.showMessage("✓ Name is available", this.successClassValue) + } else { + this.disableSubmit() + this.showMessage("✗ Name is already taken", this.errorClassValue) + } + } catch (error) { + this.hideSpinner() + this.disableSubmit() + this.showMessage("Error checking name availability", this.errorClassValue) + } + } + + showMessage(text, className) { + this.messageTarget.textContent = text + this.messageTarget.className = `${className} text-sm mt-1` + this.messageTarget.classList.remove("hidden") + } + + hideMessage() { + this.messageTarget.classList.add("hidden") + } + + showSpinner() { + this.spinnerTarget.classList.remove("hidden") + this.hideMessage() + } + + hideSpinner() { + this.spinnerTarget.classList.add("hidden") + } + + debounce(func, wait) { + let timeout + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout) + func(...args) + } + clearTimeout(timeout) + timeout = setTimeout(later, wait) + } + } + + disableSubmit() { + this.submitButtonTarget.disabled = true + this.submitButtonTarget.classList.add('opacity-50', 'cursor-not-allowed') + } + + enableSubmit() { + this.submitButtonTarget.disabled = false + this.submitButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed') + } +} diff --git a/app/javascript/controllers/text_field_change_controller.js b/app/javascript/controllers/text_field_change_controller.js deleted file mode 100644 index 966cfa3..0000000 --- a/app/javascript/controllers/text_field_change_controller.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = ["input"] - static outlets = ["generated-output"] - - connect() { - this.update() - } - - update(event) { - const inputValue = this.inputTarget.value || this.inputTarget.dataset.defaultValue || "" - const prefix = this.inputTarget.dataset.outputPrefix || "" - - const updatedText = prefix ? `${prefix} ${inputValue}` : inputValue - this.generatedOutputOutlet.updateText(updatedText) - } -} diff --git a/app/jobs/app_generation_job.rb b/app/jobs/app_generation_job.rb index c9b3a8d..74a44c5 100644 --- a/app/jobs/app_generation_job.rb +++ b/app/jobs/app_generation_job.rb @@ -30,7 +30,7 @@ def create_github_repository def generate_rails_app @generated_app.generate! - command = build_rails_command + command = "rails new #{@generated_app.name} #{@generated_app.recipe.cli_flags}" CommandExecutionService.new(@generated_app, command).execute end @@ -49,8 +49,4 @@ def start_ci def complete_generation @generated_app.mark_as_completed! end - - def build_rails_command - "rails new #{@generated_app.name} --skip-action-mailbox --skip-jbuilder --asset-pipeline=propshaft --javascript=esbuild --css=tailwind --skip-spring" - end end diff --git a/app/models/concerns/git_backed_model.rb b/app/models/concerns/git_backed_model.rb index b2e7266..1b2fe28 100644 --- a/app/models/concerns/git_backed_model.rb +++ b/app/models/concerns/git_backed_model.rb @@ -26,9 +26,8 @@ def sync_to_git end def repo - @repo ||= GitRepo.new( + @repo ||= DataRepository.new( user: commit_author, - repo_name: repo_name ) end diff --git a/app/models/recipe.rb b/app/models/recipe.rb index a728b8e..cf17999 100644 --- a/app/models/recipe.rb +++ b/app/models/recipe.rb @@ -69,6 +69,26 @@ def reorder_ingredients!(new_order) end end + # Class method to find or create a recipe with given CLI flags + def self.find_or_create_by_cli_flags!(cli_flags, user) + transaction do + recipe = where(cli_flags: cli_flags, status: "published").first + + unless recipe + recipe = create!( + name: "Rails App with #{cli_flags}", + cli_flags: cli_flags, + status: "published", + created_by: user, + ruby_version: RailsNewConfig.ruby_version_for_new_apps, + rails_version: RailsNewConfig.rails_version_for_new_apps + ) + end + + recipe + end + end + private def ingredient_compatible?(new_ingredient) diff --git a/app/services/app_repository.rb b/app/services/app_repository.rb new file mode 100644 index 0000000..fb3ee89 --- /dev/null +++ b/app/services/app_repository.rb @@ -0,0 +1,8 @@ +class AppRepository < GitRepo + def initialize(user:, app_name:) + super(user: user, repo_name: app_name) + ensure_app_repo_exists + end + + # App repo specific methods... +end diff --git a/app/services/data_repository.rb b/app/services/data_repository.rb new file mode 100644 index 0000000..d805f65 --- /dev/null +++ b/app/services/data_repository.rb @@ -0,0 +1,110 @@ +class DataRepository < GitRepo + BASE_NAME = "rails-new-io-data" + + class << self + def name_for_environment + if Rails.env.development? + "#{BASE_NAME}-dev" + elsif Rails.env.test? + "#{BASE_NAME}-test" + elsif Rails.env.production? + BASE_NAME + else + raise ArgumentError, "Unknown Rails environment: #{Rails.env}" + end + end + end + + def initialize(user:) + super(user: user, repo_name: self.class.name_for_environment) + end + + def write_model(model) + ensure_fresh_repo + + case model + when GeneratedApp + write_generated_app(model) + when Ingredient + write_ingredient(model) + when Recipe + write_recipe(model) + end + + push_to_remote + end + + private + + def ensure_committable_state + %w[generated_apps ingredients recipes].each do |dir| + FileUtils.mkdir_p(File.join(repo_path, dir)) + FileUtils.touch(File.join(repo_path, dir, ".keep")) + end + File.write(File.join(repo_path, "README.md"), readme_content) + end + + def write_generated_app(app) + path = File.join(repo_path, "generated_apps", app.id.to_s) + FileUtils.mkdir_p(path) + + write_json(path, "current_state.json", { + name: app.name, + recipe_id: app.recipe_id, + configuration: app.configuration_options + }) + + write_json(path, "history.json", app.app_changes.map(&:to_git_format)) + end + + def write_ingredient(ingredient) + path = File.join(repo_path, "ingredients", ingredient.name.parameterize) + FileUtils.mkdir_p(path) + + File.write(File.join(path, "template.rb"), ingredient.template_content) + write_json(path, "metadata.json", { + name: ingredient.name, + description: ingredient.description, + conflicts_with: ingredient.conflicts_with, + requires: ingredient.requires, + configures_with: ingredient.configures_with + }) + end + + def write_recipe(recipe) + path = File.join(repo_path, "recipes", recipe.id.to_s) + FileUtils.mkdir_p(path) + + write_json(path, "manifest.json", { + name: recipe.name, + cli_flags: recipe.cli_flags, + ruby_version: recipe.ruby_version, + rails_version: recipe.rails_version + }) + + write_json(path, "ingredients.json", + recipe.recipe_ingredients.order(:position).map(&:to_git_format) + ) + end + + def ensure_fresh_repo + git.fetch + git.reset_hard("origin/main") + git.pull + end + + def push_to_remote + git.push("origin", "main") + rescue Git::Error => e + Rails.logger.error "Git push failed: #{e.message}" + raise GitSyncError, "Failed to sync changes to GitHub" + end + + def readme_content + "# Data Repository\nThis repository contains data for rails-new.io" + end + + def repository_description + "Data repository for rails-new.io" + end +end diff --git a/app/services/git_repo.rb b/app/services/git_repo.rb index 5a7ff14..e181902 100644 --- a/app/services/git_repo.rb +++ b/app/services/git_repo.rb @@ -1,5 +1,4 @@ class GitRepo - REPO_NAME = "rails-new-io-data" class Error < StandardError; end class GitSyncError < Error; end @@ -7,164 +6,118 @@ def initialize(user:, repo_name:) @user = user @repo_path = Rails.root.join("tmp", "git_repos", user.id.to_s, repo_name) @repo_name = repo_name - ensure_repo_exists end - def write_model(model) - ensure_fresh_repo - - case model - when GeneratedApp - write_generated_app(model) - when Ingredient - write_ingredient(model) - when Recipe - write_recipe(model) + def commit_changes(message:, author:) + if File.exist?(repo_path) + git.fetch + git.reset_hard("origin/main") + else + if remote_repo_exists? + Git.clone("https://#{user.github_token}@github.com/#{user.github_username}/#{repo_name}.git", + repo_name, + path: File.dirname(repo_path)) + else + create_local_repo + end end - push_to_remote - end + ensure_committable_state - private + git.config("user.name", author.name || author.github_username) + git.config("user.email", author.email || "#{author.github_username}@users.noreply.github.com") - def repo_name - if Rails.env.development? - "#{REPO_NAME}-dev" - elsif Rails.env.test? - "#{REPO_NAME}-test" - elsif Rails.env.production? - REPO_NAME - else - raise ArgumentError, "Unknown Rails environment: #{Rails.env}" - end - end + git.add(all: true) - def ensure_repo_exists - return if remote_repo_exists? + git.commit(message) + + ensure_github_repo_exists - create_local_repo - create_github_repo setup_remote - create_initial_structure + + current_branch = git.branch.name + + git.push("origin", current_branch) end - def remote_repo_exists? - github_client.repository?("#{@user.github_username}/#{repo_name}") - rescue Octokit::Error => e - Rails.logger.error("Failed to check GitHub repository: #{e.message}") - false + protected + + attr_reader :user, :repo_path, :repo_name + + def git + @git ||= begin + if File.exist?(File.join(@repo_path, ".git")) + Git.open(@repo_path) + else + create_local_repo + @git + end + end end def create_local_repo - # Ensure parent directory exists first FileUtils.mkdir_p(File.dirname(@repo_path)) - - # Remove existing repo if it exists FileUtils.rm_rf(@repo_path) if File.exist?(@repo_path) - - # Create fresh directory and initialize git FileUtils.mkdir_p(@repo_path) - git = Git.init(@repo_path) - # Configure git to avoid template issues - git.config("init.templateDir", "") + @git = Git.init(@repo_path) - @git = git + @git.config("init.templateDir", "") + @git.config("init.defaultBranch", "main") + end + + def remote_repo_exists? + github_client.repository?("#{user.github_username}/#{repo_name}") + rescue Octokit::Error => e + Rails.logger.error("Failed to check GitHub repository: #{e.message}") + false end def create_github_repo github_client.create_repository( repo_name, private: false, - description: "Data repository for rails-new.io" + description: repository_description ) end - def create_initial_structure - %w[generated_apps ingredients recipes].each do |dir| - FileUtils.mkdir_p(File.join(@repo_path, dir)) - end - - File.write(File.join(@repo_path, "README.md"), readme_content) - - git.add(all: true) - git.commit("Initial commit") - end - - def write_generated_app(app) - path = File.join(@repo_path, "generated_apps", app.id.to_s) - FileUtils.mkdir_p(path) - - write_json(path, "current_state.json", { - name: app.name, - recipe_id: app.recipe_id, - configuration: app.configuration_options - }) - - write_json(path, "history.json", app.app_changes.map(&:to_git_format)) + def setup_remote + remote_url = "https://#{user.github_token}@github.com/#{user.github_username}/#{repo_name}.git" + git.add_remote("origin", remote_url) end - def write_ingredient(ingredient) - path = File.join(@repo_path, "ingredients", ingredient.name.parameterize) - FileUtils.mkdir_p(path) - - File.write(File.join(path, "template.rb"), ingredient.template_content) - write_json(path, "metadata.json", { - name: ingredient.name, - description: ingredient.description, - conflicts_with: ingredient.conflicts_with, - requires: ingredient.requires, - configures_with: ingredient.configures_with - }) + def github_client + @_github_client ||= Octokit::Client.new(access_token: user.github_token) end - def write_recipe(recipe) - path = File.join(@repo_path, "recipes", recipe.id.to_s) - FileUtils.mkdir_p(path) - - write_json(path, "manifest.json", { - name: recipe.name, - cli_flags: recipe.cli_flags, - ruby_version: recipe.ruby_version, - rails_version: recipe.rails_version - }) - - write_json(path, "ingredients.json", - recipe.recipe_ingredients.order(:position).map(&:to_git_format) + def write_json(path, filename, data) + File.write( + File.join(path, filename), + JSON.pretty_generate(data) ) end - def ensure_fresh_repo - git.fetch - git.reset_hard("origin/main") - git.pull + def ensure_committable_state + puts "\n=== Ensuring committable state ===" + puts "Before writing: #{Dir.glob("#{repo_path}/*").inspect}" + File.write(File.join(repo_path, "README.md"), default_readme_content) + puts "After writing: #{Dir.glob("#{repo_path}/*").inspect}" + puts "README.md content:" + puts File.read(File.join(repo_path, "README.md")) end - def push_to_remote - git.push("origin", "main") - rescue Git::Error => e - # Handle push conflicts - Rails.logger.error "Git push failed: #{e.message}" - raise GitSyncError, "Failed to sync changes to GitHub" + def default_readme_content + "# Repository\nCreated via railsnew.io" end - def git - @git ||= Git.open(@repo_path) - end + private - def github_client - @github_client ||= Octokit::Client.new(access_token: @user.github_token) + def repository_description + "Repository created via railsnew.io" end - def write_json(path, filename, data) - File.write( - File.join(path, filename), - JSON.pretty_generate(data) - ) - end - - def setup_remote - remote_url = "https://#{@user.github_token}@github.com/#{@user.github_username}/#{repo_name}.git" - git.add_remote("origin", remote_url) + def ensure_github_repo_exists + return if remote_repo_exists? + create_github_repo end end diff --git a/app/services/github_repository_name_validator.rb b/app/services/github_repository_name_validator.rb index 44a9177..fdb39e9 100644 --- a/app/services/github_repository_name_validator.rb +++ b/app/services/github_repository_name_validator.rb @@ -2,31 +2,38 @@ class GithubRepositoryNameValidator VALID_FORMAT = /\A[a-zA-Z0-9][a-zA-Z0-9-]*(? e + Rails.logger.error("GitHub API error: #{e.message}") + raise e end end end diff --git a/app/views/components/empty_state/component.rb b/app/views/components/empty_state/component.rb index 0496d66..511eef8 100644 --- a/app/views/components/empty_state/component.rb +++ b/app/views/components/empty_state/component.rb @@ -15,7 +15,7 @@ def view_template div(class: "mt-6 mb-8") do link_to( - new_user_repository_path(@user), + page_path("basic-setup"), class: "inline-flex items-center rounded-md bg-[#ac3b61] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#e935a3] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ac3b61]" ) do svg(class: "-ml-0.5 mr-1.5 size-5", viewbox: "0 0 20 20", fill: "currentColor", aria_hidden: "true", data_slot: "icon") do |s| diff --git a/app/views/dashboard/show.html.erb b/app/views/dashboard/show.html.erb index 8512801..2671035 100644 --- a/app/views/dashboard/show.html.erb +++ b/app/views/dashboard/show.html.erb @@ -1,6 +1,5 @@ <%= turbo_stream_from [ :generated_app, current_user.id ] %> <%= turbo_stream_from [ :notification_badge, current_user.id ] %> -
@@ -26,17 +25,15 @@ autosubmit_target: "input" } %> <% end %> -
- + <% end %>
<% end %>
- <% if @generated_apps.any? %>
<%= turbo_frame_tag "generated_apps_list" do %> diff --git a/app/views/pages/show.html.erb b/app/views/pages/show.html.erb index c1dd05f..02db490 100644 --- a/app/views/pages/show.html.erb +++ b/app/views/pages/show.html.erb @@ -1,32 +1,47 @@ -
- - -
-
- -
- - +
+
+ + +
+
+ +
+ + +
+ + +
+
-
-
+
+ class="text-gray-200 max-w-6xl mt-3 sm:mt-3 md:mt-3 lg:mt-4 xl:mt-4 h-auto sm:h-auto md:h-20 lg:h-20 xl:h-auto mb-6 mx-auto flex items-center">

+ class="font-mono text-left px-5 sm:px-5 md:px-8 lg:px-10 xl:px-12 text-xs md:text-sm lg:text-sm xl:text-sm break-normal"> rails new my_app @@ -36,25 +51,43 @@

- 📋️ Copy to Clipboard -
<%= render Pages::Groups::Component.new(group: @page.groups.first) %> <%= render Pages::Groups::Component.new(group: @page.groups.last) %> +
diff --git a/app/views/repositories/new.html.erb b/app/views/repositories/new.html.erb index 8a7d372..e3d9fb4 100644 --- a/app/views/repositories/new.html.erb +++ b/app/views/repositories/new.html.erb @@ -1,28 +1,28 @@

Create New Repository

<%= form_with(model: [ @user, @repository ], class: "space-y-4") do |f| %> -
- <%= f.label :name, class: "block text-sm font-medium text-gray-700" %> - <%= f.text_field :name, +
+ <%= f.label :name, class: "block text-sm font-medium text-gray-700" %> + <%= f.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", data: { - repository_name_validator_target: "input", - action: "input->repository-name-validator#validate" + github_name_validator_target: "input", + action: "input->github-name-validator#validate" } %> -
- - +
+ +
- <%= f.submit "Create Repository", class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> - <% end %> -
+
+ <%= f.submit "Create Repository", class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> + <% end %> +
diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index feb2227..57a5559 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -4UEWXd9znHiCoVzWZhiZQQ3ww4nLxno2kP+DIFy9gQWMcewpBPmAvLDx1vOu43SamUGM47wiqCsYpfZvnL+G2XXEAkQbLjZ2Z5GqRrTk2Y9loYfz2BKPPNWYu4zTO99Ok06sOWESXOo7jAWQTJql2uzM4it6V++k1rGYq+ivUXRSPn7O+1d1M/Hd6865SjZgL2g9xCpFMakNnvygcYuYZ31q0WAMPHEvFHFmu5tPvuj4UNtnz8xb60Ij45FDFB0cBpTe6SB7PaNI2f6XLOVqID8qBBYB0ZT1mLwFQUTxfMzhKi5oJ0m9hJ6XzQ3br/BK1ahA4lMJ2OaFne/NM5klYqpvh4U859+aFhAx5/ogEzynC31O5yglUSmVXs2gCMGT11brDT84ElU9VA8jkkci9APdStLN0mTesLIFkMHExHum/Iy0fyKqqb6WLgAaE7SSGYW0YGgmaVYYvXDjvwOfSL0RbaLkkMuiOnI71cq9I5cjlFVcJAo2ZJHxNwvw+vwtmTaYwXGYAlc3d0wpPzoDaKPL9sRzE6zZUIBQwbh+0keIEsKxdSWyE7AFpfTCxdtkBUsbREkXrNTtU6oPsahmmw==--xRBi1g7Uq9Z1xCIP--8xTsLp29V+xfzbjCKRFKhQ== \ No newline at end of file +1jXsG5Ciqgbqqb9+NZrJSDx4TkHbLJE7fM4sW/FePGbgYK4rZWnhbCsjU1wCKxm8a7bhP/McJiGVVf/JcZzZYIwARou2DMEsEJpLQhrIJPvplOUjTp52NgW0q3jtjdYdZJigrKIyQNJniEAXaL3WloKedRPlRoLjioZ4ajSyVAo9FwsnLG15Tx9I6kd8+mXCD11yoRLtoBODMI0ZMJS4twKxBjQ0L+TV4/0lXNoAE3eh68ape0gEsfot9v7VxRYjyWHfWH07Ig/rSZyeWmnHYkgXbQGElyMxkSaYTbYHGYz1OVCgGQsgIZ6HoCbZJsLXdrl7IIjTI11PxRsv4sWaGyat37LcOYXh8szQil1BI6y/hdkvfYwc/waAvUqfGXjFmx+pOa1aqwg+zkL5uXSs55YbKz3L9ll7bvzF7ak4edlWBAdBegLaKKrtzMnkGysdCARZkoRUcnYbPjX5YxZTJzU0+yEuzYPEEih9xmYDfvip3+pnW6/IubEa53GQjQ7GhyA+RUHDlhfcKzHZVATLYQhwlxnvQgZ3WxvSRfD+d26mrQFLjHkaTjD7/lGvxi4BVIbIENQ00MTQ0WYVTcEfWHLTeeEIMW1m7DCyk8Vq6esujM+9G1ax9+X3yfIOSC4OFGGem3nEvsAhRFDSLmqYdpKR4Hoyy+MwHVap+upQISw5Vg==--tOwAx02FxcaihWwf--zfFLQvh7hGe2FptPcgSrVA== \ No newline at end of file diff --git a/config/initializers/rails_new_config.rb b/config/initializers/rails_new_config.rb new file mode 100644 index 0000000..afad0e5 --- /dev/null +++ b/config/initializers/rails_new_config.rb @@ -0,0 +1,18 @@ +# Centralized configuration for rails new defaults +module RailsNewConfig + mattr_accessor :ruby_version, :rails_version + + # These will be updated when new versions are released + self.ruby_version = "3.3.5" + self.rails_version = "8.0.0.1" + + class << self + def ruby_version_for_new_apps + ruby_version + end + + def rails_version_for_new_apps + rails_version + end + end +end diff --git a/config/routes.rb b/config/routes.rb index a0b5cdb..7419008 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,7 +22,7 @@ resources :notifications, only: [ :index, :update ] - resources :generated_apps, only: [ :show ] do + resources :generated_apps, only: [ :show, :create ] do resources :generation_attempts, only: [ :create ] resources :log_entries, only: [ :index ] end @@ -40,5 +40,5 @@ root to: "static#home" # named routes - get "/repositories/check_name", to: "repositories#check_name", as: :check_repository_name + get "/github/check_name", to: "github#check_name", as: :check_github_name end diff --git a/test/services/github_repository_name_validator_test.rb b/test/services/github_repository_name_validator_test.rb index 5e0494a..2cfdb05 100644 --- a/test/services/github_repository_name_validator_test.rb +++ b/test/services/github_repository_name_validator_test.rb @@ -5,19 +5,19 @@ def setup @owner = "test-owner" end - def test_valid_repository_name + def test_available_repository_name client = Minitest::Mock.new client.expect(:repository, nil) { raise Octokit::NotFound } Octokit::Client.stub(:new, client) do validator = GithubRepositoryNameValidator.new("valid-repo-name", @owner) - assert validator.valid? + assert_not validator.repo_exists? end end def test_invalid_ending_with_hyphen validator = GithubRepositoryNameValidator.new("invalid-", @owner) - assert_not validator.valid? + assert validator.repo_exists? end def test_invalid_starting_with_hyphen From 69dd32bfb95a5be923e61b6c598da67ddc945b52 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 14:52:00 +0100 Subject: [PATCH 02/20] Fixing tests --- app/services/git_repo.rb | 57 ++- config/environments/test.rb | 1 + .../repositories_controller_test.rb | 6 +- test/fixtures/users.yml | 18 +- test/services/git_repo_clone_test.rb | 62 +++ test/services/git_repo_test.rb | 456 ++++-------------- .../services/github_code_push_service_test.rb | 2 +- .../github_repository_service_test.rb | 3 +- test/test_helper.rb | 2 + 9 files changed, 228 insertions(+), 379 deletions(-) create mode 100644 test/services/git_repo_clone_test.rb diff --git a/app/services/git_repo.rb b/app/services/git_repo.rb index e181902..ec76d95 100644 --- a/app/services/git_repo.rb +++ b/app/services/git_repo.rb @@ -9,34 +9,51 @@ def initialize(user:, repo_name:) end def commit_changes(message:, author:) + puts "\n=== GitRepo#commit_changes ===" + puts "Checking if repo exists at: #{repo_path}" + if File.exist?(repo_path) - git.fetch - git.reset_hard("origin/main") + puts "Local repo exists, checking remote" + if remote_repo_exists? + puts "Remote exists, fetching latest changes" + git.fetch + git.reset_hard("origin/main") + else + puts "Remote doesn't exist, creating it" + ensure_github_repo_exists + setup_remote + end else + puts "Local repo doesn't exist, checking remote" if remote_repo_exists? - Git.clone("https://#{user.github_token}@github.com/#{user.github_username}/#{repo_name}.git", - repo_name, - path: File.dirname(repo_path)) + puts "Remote repo exists, cloning" + Git.clone( + "https://#{user.github_token}@github.com/#{user.github_username}/#{repo_name}.git", + repo_name, + path: File.dirname(repo_path) + ) + @git = Git.open(repo_path) else + puts "Remote repo doesn't exist, creating new repo" create_local_repo + ensure_github_repo_exists + setup_remote end end + puts "Ensuring committable state" ensure_committable_state + puts "Configuring git user" git.config("user.name", author.name || author.github_username) git.config("user.email", author.email || "#{author.github_username}@users.noreply.github.com") + puts "Adding and committing changes" git.add(all: true) - git.commit(message) - ensure_github_repo_exists - - setup_remote - + puts "Pushing changes" current_branch = git.branch.name - git.push("origin", current_branch) end @@ -56,18 +73,32 @@ def git end def create_local_repo + puts "\n=== GitRepo#create_local_repo ===" + puts "Creating directory: #{File.dirname(@repo_path)}" FileUtils.mkdir_p(File.dirname(@repo_path)) - FileUtils.rm_rf(@repo_path) if File.exist?(@repo_path) + + if File.exist?(@repo_path) + puts "Removing existing repo path: #{@repo_path}" + FileUtils.rm_rf(@repo_path) + end + + puts "Creating repo directory: #{@repo_path}" FileUtils.mkdir_p(@repo_path) + puts "Initializing git repo" @git = Git.init(@repo_path) + puts "Configuring git" @git.config("init.templateDir", "") @git.config("init.defaultBranch", "main") end def remote_repo_exists? - github_client.repository?("#{user.github_username}/#{repo_name}") + puts "\n=== GitRepo#remote_repo_exists? ===" + puts "Checking if repo exists: #{user.github_username}/#{repo_name}" + result = github_client.repository?("#{user.github_username}/#{repo_name}") + puts "Result: #{result}" + result rescue Octokit::Error => e Rails.logger.error("Failed to check GitHub repository: #{e.message}") false diff --git a/config/environments/test.rb b/config/environments/test.rb index 75ac041..08043f0 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -59,6 +59,7 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + config.active_record.encryption.encrypt_fixtures = true # Configure Active Record Encryption config.active_record.encryption.primary_key = "test" * 8 diff --git a/test/controllers/repositories_controller_test.rb b/test/controllers/repositories_controller_test.rb index 73fda81..559b47c 100644 --- a/test/controllers/repositories_controller_test.rb +++ b/test/controllers/repositories_controller_test.rb @@ -16,14 +16,14 @@ def setup assert_response :success end - test "check_name returns true for valid repository name" do + test "repo_exists? returns false for valid (non-existent) repository name" do validator = mock - validator.expects(:valid?).returns(true) + validator.expects(:repo_exists?).returns(false) GithubRepositoryNameValidator.expects(:new) .with("test-repo", @user.github_username) .returns(validator) - get check_repository_name_path, params: { name: "test-repo" } + get check_github_name_path, params: { name: "test-repo" } assert_response :success assert_equal({ "available" => true }, JSON.parse(response.body)) diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index ae82002..3729e65 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -23,25 +23,25 @@ # john: - name: John Doe - email: john@example.com + name: "John Doe" + email: "john@example.com" image: https://github.com/images/john.jpg - provider: github - uid: "92839283" + provider: "github" + uid: "123456" slug: john-doe - github_username: johndoe + github_username: "johndoe" github_token: "fake-token" created_at: <%= Time.current %> updated_at: <%= Time.current %> jane: - name: Jane Smith + name: "Jane Smith" email: "jane@example.com" image: https://github.com/images/jane.jpg - provider: github - uid: '789012' + provider: "github" + uid: "789012" slug: jane-smith - github_username: jane_smith + github_username: "jane_smith" github_token: "fake-token" created_at: <%= Time.current %> updated_at: <%= Time.current %> diff --git a/test/services/git_repo_clone_test.rb b/test/services/git_repo_clone_test.rb new file mode 100644 index 0000000..1b6f800 --- /dev/null +++ b/test/services/git_repo_clone_test.rb @@ -0,0 +1,62 @@ +require "test_helper" + +class GitRepoCloneTest < ActiveSupport::TestCase + setup do + @user = users(:jane) + stub_github_token(@user) + @user.stubs(:name).returns("Jane Smith") + @user.stubs(:email).returns("jane@example.com") + + @repo_name = "rails-new-io-data-test" + @repo_path = Rails.root.join("tmp", "git_repos", @user.id.to_s, @repo_name) + + # Stub file operations + File.stubs(:write).returns(true) + File.stubs(:exist?).returns(true) + File.stubs(:read).returns("# Repository\nCreated via railsnew.io") + Dir.stubs(:glob).returns([]) + + # Initialize Octokit client mock + @mock_client = mock("octokit_client") + Octokit::Client.stubs(:new).returns(@mock_client) + + # Stub all FileUtils operations globally + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:rm_rf) + + @repo = GitRepo.new(user: @user, repo_name: @repo_name) + end + + test "clones repository when remote exists but no local copy" do + puts "\n=== Test Setup ===" + File.stubs(:exist?).with(@repo_path).returns(false) + @mock_client.stubs(:repository?).returns(true) + puts "Stubbed basic checks" + + puts "\n=== Setting up cloned git mock ===" + branch_mock = mock("branch") + branch_mock.stubs(:name).returns("main") + + cloned_git = mock("cloned_git") + cloned_git.stubs(:branch).returns(branch_mock) + cloned_git.expects(:config).with("user.name", "Jane Smith") + cloned_git.expects(:config).with("user.email", "jane@example.com") + cloned_git.expects(:add).with(all: true) + cloned_git.expects(:commit).with("Test commit") + cloned_git.expects(:push).with("origin", "main") + puts "Set up cloned git expectations" + + puts "\n=== Setting up Git operations ===" + Git.expects(:clone).with( + "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git", + @repo_name, + path: File.dirname(@repo_path) + ) + Git.expects(:open).with(@repo_path).returns(cloned_git) + puts "Set up Git clone and open expectations" + + puts "\n=== Executing Test ===" + @repo.commit_changes(message: "Test commit", author: @user) + puts "Test execution completed" + end +end diff --git a/test/services/git_repo_test.rb b/test/services/git_repo_test.rb index 1e8a616..d3726bc 100644 --- a/test/services/git_repo_test.rb +++ b/test/services/git_repo_test.rb @@ -3,420 +3,174 @@ class GitRepoTest < ActiveSupport::TestCase setup do @user = users(:jane) - # Define github_token method to bypass encryption - @user.define_singleton_method(:github_token) { "fake-token" } + stub_github_token(@user) + @user.stubs(:name).returns("Jane Smith") + @user.stubs(:email).returns("jane@example.com") @repo_name = "rails-new-io-data-test" @repo_path = Rails.root.join("tmp", "git_repos", @user.id.to_s, @repo_name) - # Ensure parent directory exists and is empty - FileUtils.rm_rf(File.dirname(@repo_path)) - FileUtils.mkdir_p(File.dirname(@repo_path)) - # Create a mock git object @git = mock("git") @git.stubs(:config) @git.stubs(:add) @git.stubs(:commit) + + # Create a branch mock explicitly + @branch_mock = mock("branch") + @branch_mock.stubs(:name).returns("main") + @git.stubs(:branch).returns(@branch_mock) + Git.stubs(:init).returns(@git) Git.stubs(:open).returns(@git) + Git.stubs(:clone) # Stub file operations - FileUtils.stubs(:mkdir_p).returns(true) File.stubs(:write).returns(true) File.stubs(:exist?).returns(true) - File.stubs(:read).returns("{}") - JSON.stubs(:parse).returns({}) + File.stubs(:read).returns("# Repository\nCreated via railsnew.io") + Dir.stubs(:glob).returns([]) - # Stub GitHub API calls - GitRepo.any_instance.stubs(:remote_repo_exists?).returns(false) - GitRepo.any_instance.stubs(:create_github_repo).returns(true) - GitRepo.any_instance.stubs(:create_initial_structure).returns(true) - GitRepo.any_instance.stubs(:setup_remote).returns(true) + # Initialize Octokit client mock + @mock_client = mock("octokit_client") + Octokit::Client.stubs(:new).returns(@mock_client) + + # Stub all FileUtils operations globally + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:rm_rf) @repo = GitRepo.new(user: @user, repo_name: @repo_name) end teardown do - # Clean up after each test FileUtils.rm_rf(@repo_path) if File.exist?(@repo_path) FileUtils.rm_rf(File.dirname(@repo_path)) if File.exist?(File.dirname(@repo_path)) - - # Clean up any remaining stubs - GitRepo.any_instance.unstub(:remote_repo_exists?) - GitRepo.any_instance.unstub(:create_github_repo) - GitRepo.any_instance.unstub(:create_initial_structure) - GitRepo.any_instance.unstub(:setup_remote) - Rails.unstub(:env) if Rails.respond_to?(:env) && Rails.env.is_a?(Mocha::Mock) - end - - test "initializes with user and repo name" do - assert_equal @user, @repo.instance_variable_get(:@user) - assert_equal @repo_name, @repo.instance_variable_get(:@repo_name) - expected_path = Rails.root.join("tmp", "git_repos", @user.id.to_s, @repo_name) - assert_equal expected_path, @repo.instance_variable_get(:@repo_path) - end - - test "writes generated app to repo" do - app = generated_apps(:blog_app) - path = File.join(@repo_path, "generated_apps", app.id.to_s) - - # Expect git operations - @git.expects(:fetch) - @git.expects(:reset_hard).with("origin/main") - @git.expects(:pull) - @git.expects(:push).with("origin", "main") - - # Expect file operations - FileUtils.expects(:mkdir_p).with(path) - File.expects(:write).with( - File.join(path, "current_state.json"), - JSON.pretty_generate({ - name: app.name, - recipe_id: app.recipe_id, - configuration: app.configuration_options - }) - ) - File.expects(:write).with( - File.join(path, "history.json"), - JSON.pretty_generate(app.app_changes.map(&:to_git_format)) - ) - - @repo.write_model(app) end - test "writes ingredient to repo" do - ingredient = ingredients(:rails_authentication) - path = File.join(@repo_path, "ingredients", ingredient.name.parameterize) + test "commits changes to existing local repository" do + puts "\n=== Test Setup ===" + File.stubs(:exist?).with(@repo_path).returns(true) + File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) + puts "Stubbed File.exist? for repo path and .git directory" - # Expect git operations + puts "\n=== Git Expectations ===" @git.expects(:fetch) @git.expects(:reset_hard).with("origin/main") - @git.expects(:pull) + @git.expects(:config).with("user.name", "Jane Smith") + @git.expects(:config).with("user.email", "jane@example.com") + @git.expects(:add).with(all: true) + @git.expects(:commit).with("Test commit") @git.expects(:push).with("origin", "main") + puts "Set up Git mock expectations" - # Expect file operations - FileUtils.expects(:mkdir_p).with(path) - File.expects(:write).with( - File.join(path, "template.rb"), - ingredient.template_content - ) - File.expects(:write).with( - File.join(path, "metadata.json"), - JSON.pretty_generate({ - name: ingredient.name, - description: ingredient.description, - conflicts_with: ingredient.conflicts_with, - requires: ingredient.requires, - configures_with: ingredient.configures_with - }) - ) + puts "\n=== GitHub API Expectations ===" + puts "User github_username: #{@user.github_username.inspect}" + puts "Repo name: #{@repo_name.inspect}" + @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(true) + puts "Set up GitHub API mock expectations" - @repo.write_model(ingredient) + puts "\n=== Executing Test ===" + @repo.commit_changes(message: "Test commit", author: @user) + puts "Test execution completed" end - test "writes recipe to repo" do - recipe = recipes(:blog_recipe) - path = File.join(@repo_path, "recipes", recipe.id.to_s) + test "creates new local repository when no repository exists" do + puts "\n=== Test Setup ===" + File.stubs(:exist?).with(@repo_path).returns(false) + File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(false) + puts "Stubbed File.exist? checks" - # Expect git operations - @git.expects(:fetch) - @git.expects(:reset_hard).with("origin/main") - @git.expects(:pull) + puts "\n=== GitHub API Expectations ===" + @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(false).twice + @mock_client.expects(:create_repository).with( + @repo_name, + private: false, + description: "Repository created via railsnew.io" + ).returns(true) + puts "Set up GitHub API expectations" + + puts "\n=== Git Expectations ===" + @git.expects(:config).with("init.templateDir", "") + @git.expects(:config).with("init.defaultBranch", "main") + @git.expects(:config).with("user.name", "Jane Smith") + @git.expects(:config).with("user.email", "jane@example.com") + @git.expects(:add).with(all: true) + @git.expects(:commit).with("Test commit") + @git.expects(:add_remote).with("origin", "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git") @git.expects(:push).with("origin", "main") + puts "Set up Git expectations" - # Expect file operations - FileUtils.expects(:mkdir_p).with(path) - File.expects(:write).with( - File.join(path, "manifest.json"), - JSON.pretty_generate({ - name: recipe.name, - cli_flags: recipe.cli_flags, - ruby_version: recipe.ruby_version, - rails_version: recipe.rails_version - }) - ) - File.expects(:write).with( - File.join(path, "ingredients.json"), - JSON.pretty_generate(recipe.recipe_ingredients.order(:position).map(&:to_git_format)) - ) + puts "\n=== FileUtils Expectations ===" + puts "Parent dir path: #{File.dirname(@repo_path)}" + puts "Repo path: #{@repo_path}" + FileUtils.expects(:mkdir_p).with(File.dirname(@repo_path)) + FileUtils.stubs(:rm_rf).with(@repo_path) # Allow any number of calls + FileUtils.expects(:mkdir_p).with(@repo_path) + puts "Set up FileUtils expectations" - @repo.write_model(recipe) - end - - test "handles git push errors" do - recipe = recipes(:blog_recipe) - path = File.join(@repo_path, "recipes", recipe.id.to_s) - - # Expect git operations - @git.expects(:fetch) - @git.expects(:reset_hard).with("origin/main") - @git.expects(:pull) - @git.expects(:push).with("origin", "main").raises(Git::Error.new("push failed")) - - # Expect file operations (these need to succeed before the push fails) - FileUtils.expects(:mkdir_p).with(path) - File.expects(:write).with( - File.join(path, "manifest.json"), - JSON.pretty_generate({ - name: recipe.name, - cli_flags: recipe.cli_flags, - ruby_version: recipe.ruby_version, - rails_version: recipe.rails_version - }) - ) - File.expects(:write).with( - File.join(path, "ingredients.json"), - JSON.pretty_generate(recipe.recipe_ingredients.order(:position).map(&:to_git_format)) - ) - - assert_raises(GitRepo::GitSyncError) do - @repo.write_model(recipe) - end - end - - test "uses correct repo name suffix in development environment" do - github_client = Minitest::Mock.new - GitRepo.any_instance.unstub(:remote_repo_exists?) - GitRepo.any_instance.stubs(:create_github_repo).returns(true) - GitRepo.any_instance.stubs(:create_initial_structure).returns(true) - GitRepo.any_instance.stubs(:setup_remote).returns(true) - - rails_env = mock - rails_env.stubs(:development?).returns(true) - rails_env.stubs(:test?).returns(false) - rails_env.stubs(:production?).returns(false) - Rails.stubs(:env).returns(rails_env) - - Octokit::Client.stub :new, github_client do - github_client.expect :repository?, false, [ "#{@user.github_username}/rails-new-io-data-dev" ] - repo = GitRepo.new(user: @user, repo_name: @repo_name) - assert_equal "rails-new-io-data-dev", repo.send(:repo_name) - github_client.verify - end - end - - test "uses correct repo name suffix in test environment" do - github_client = Minitest::Mock.new - GitRepo.any_instance.unstub(:remote_repo_exists?) - GitRepo.any_instance.stubs(:create_github_repo).returns(true) - GitRepo.any_instance.stubs(:create_initial_structure).returns(true) - GitRepo.any_instance.stubs(:setup_remote).returns(true) - - rails_env = mock - rails_env.stubs(:development?).returns(false) - rails_env.stubs(:test?).returns(true) - rails_env.stubs(:production?).returns(false) - rails_env.stubs(:to_s).returns("test") - Rails.stubs(:env).returns(rails_env) - - Octokit::Client.stub :new, github_client do - github_client.expect :repository?, false, [ "#{@user.github_username}/rails-new-io-data-test" ] - repo = GitRepo.new(user: @user, repo_name: @repo_name) - assert_equal "rails-new-io-data-test", repo.send(:repo_name) - github_client.verify - end - end - - test "uses base repo name in production environment" do - # First clear the existing instance to avoid multiple method calls - @repo = nil - - # Remove all existing stubs - GitRepo.any_instance.unstub(:remote_repo_exists?) - GitRepo.any_instance.unstub(:create_github_repo) - GitRepo.any_instance.unstub(:create_initial_structure) - GitRepo.any_instance.unstub(:setup_remote) - - Mocha::Configuration.override(stubbing_non_public_method: :allow) do - # Set up production environment in a block to ensure cleanup - rails_env = mock - rails_env.stubs(:development?).returns(false) - rails_env.stubs(:test?).returns(false) - rails_env.stubs(:production?).returns(true) - Rails.stubs(:env).returns(rails_env) - - # Mock GitHub client for this specific test - github_client = Minitest::Mock.new - github_client.expect :repository?, false, [ "#{@user.github_username}/rails-new-io-data" ] - - # Re-stub necessary methods after the repository check - GitRepo.any_instance.stubs(:create_github_repo).returns(true) - GitRepo.any_instance.stubs(:create_initial_structure).returns(true) - GitRepo.any_instance.stubs(:setup_remote).returns(true) - - Octokit::Client.stub :new, github_client do - repo = GitRepo.new(user: @user, repo_name: @repo_name) - assert_equal "rails-new-io-data", repo.send(:repo_name) - github_client.verify - end - end - end - - test "raises error for unknown Rails environment" do - GitRepo.any_instance.unstub(:remote_repo_exists?) - GitRepo.any_instance.stubs(:create_github_repo).returns(true) - GitRepo.any_instance.stubs(:create_initial_structure).returns(true) - GitRepo.any_instance.stubs(:setup_remote).returns(true) - - rails_env = mock - rails_env.stubs(:development?).returns(false) - rails_env.stubs(:test?).returns(false) - rails_env.stubs(:production?).returns(false) - rails_env.stubs(:to_s).returns("staging") - Rails.stubs(:env).returns(rails_env) - - assert_raises(ArgumentError, "Unknown Rails environment: staging") do - GitRepo.new(user: @user, repo_name: @repo_name) - end + puts "\n=== Executing Test ===" + @repo.commit_changes(message: "Test commit", author: @user) + puts "Test execution completed" end test "handles GitHub API errors when checking repository existence" do - # Clear the stub for remote_repo_exists? since we want to test it - GitRepo.any_instance.unstub(:remote_repo_exists?) - - # Create a test logger - test_logger = Class.new do - attr_reader :messages - def initialize - @messages = [] - end - - def error(message) - @messages << message - end - end.new - - Rails.stubs(:logger).returns(test_logger) - - # Create a client that raises an error error = Octokit::Error.new( method: :get, - url: "https://api.github.com/repos/#{@user.github_username}/rails-new-io-data-test", + url: "https://api.github.com/repos/#{@user.github_username}/#{@repo_name}", status: 401, response_headers: {}, body: { message: "Bad credentials" } ) - mock_client = mock - mock_client.expects(:repository?).raises(error) + test_logger = mock("logger") + test_logger.expects(:error).with("Failed to check GitHub repository: #{error.message}") + Rails.stubs(:logger).returns(test_logger) - # Test error handling - @repo.instance_variable_set(:@github_client, mock_client) - result = @repo.send(:remote_repo_exists?) + @mock_client.expects(:repository?).raises(error) - # Verify behavior - assert_equal false, result - assert_equal 1, test_logger.messages.size - assert_equal( - "Failed to check GitHub repository: GET https://api.github.com/repos/jane_smith/rails-new-io-data-test: 401 - Bad credentials", - test_logger.messages.first - ) + # Should return false and continue with local repo creation + @repo.send(:remote_repo_exists?) end -end -class GitRepoCreateTest < ActiveSupport::TestCase - test "creates github repository with correct parameters" do - # Create a test user - test_user = User.new(github_username: "test_user", github_token: "fake-token") - - # Create a repo instance WITHOUT initialization - repo = Class.new(GitRepo) do - def initialize(user:) - @user = user - end - end.new(user: test_user) - - # Set up the test expectations - mock_client = mock("github_client") - mock_client.expects(:create_repository).with( - "rails-new-io-data-test", - private: false, - description: "Data repository for rails-new.io" - ).returns(true) + test "handles missing user name and email" do + @user.stubs(:name).returns(nil) + @user.stubs(:email).returns(nil) - # Inject our dependencies - repo.stubs(:repo_name).returns("rails-new-io-data-test") - repo.instance_variable_set(:@github_client, mock_client) + File.stubs(:exist?).with(@repo_path).returns(true) + File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) - # Test just the create_github_repo method - repo.send(:create_github_repo) - end -end + @git.expects(:fetch) + @git.expects(:reset_hard).with("origin/main") + @git.expects(:config).with("user.name", @user.github_username) + @git.expects(:config).with("user.email", "#{@user.github_username}@users.noreply.github.com") + @git.expects(:add).with(all: true) + @git.expects(:commit).with("Test commit") + @git.expects(:push).with("origin", "main") -class GitRepoStructureTest < ActiveSupport::TestCase - setup do - @user = users(:jane) - @user.define_singleton_method(:github_token) { "fake-token" } - @repo_name = "rails-new-io-data-test" - @repo_path = Rails.root.join("tmp", "git_repos", @user.id.to_s, @repo_name) + @mock_client.expects(:repository?).returns(true) + + @repo.commit_changes(message: "Test commit", author: @user) end - test "creates initial repository structure" do - # Create a minimal repo instance with readme_content method - repo = Class.new(GitRepo) do - def initialize(user:, repo_path:) - @user = user - @repo_path = repo_path - end - - private - - def readme_content - "# Data Repository\nThis repository contains data for rails-new.io" - end - end.new(user: @user, repo_path: @repo_path) - - # Mock Git operations - git = mock("git") - git.expects(:add).with(all: true) - git.expects(:commit).with("Initial commit") - repo.stubs(:git).returns(git) - - # Mock file operations - FileUtils.expects(:mkdir_p).with(File.join(@repo_path, "generated_apps")) - FileUtils.expects(:mkdir_p).with(File.join(@repo_path, "ingredients")) - FileUtils.expects(:mkdir_p).with(File.join(@repo_path, "recipes")) + test "ensures committable state by creating README.md" do + File.stubs(:exist?).with(@repo_path).returns(true) + File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) + File.expects(:write).with( File.join(@repo_path, "README.md"), - "# Data Repository\nThis repository contains data for rails-new.io" + "# Repository\nCreated via railsnew.io" ) - # Test just the create_initial_structure method - repo.send(:create_initial_structure) + @repo.send(:ensure_committable_state) end -end -class GitRepoRemoteTest < ActiveSupport::TestCase - setup do - @user = users(:jane) - @user.define_singleton_method(:github_token) { "fake-token" } - @repo_name = "rails-new-io-data-test" - @repo_path = Rails.root.join("tmp", "git_repos", @user.id.to_s, @repo_name) - end - - test "sets up git remote with correct URL" do - # Create a minimal repo instance - repo = Class.new(GitRepo) do - def initialize(user:, repo_path:) - @user = user - @repo_path = repo_path - end - - def repo_name - "rails-new-io-data-test" - end - end.new(user: @user, repo_path: @repo_path) - - # Mock Git operations - git = mock("git") - git.expects(:add_remote).with( - "origin", - "https://fake-token@github.com/#{@user.github_username}/rails-new-io-data-test.git" + test "creates GitHub repository with correct parameters" do + @mock_client.expects(:create_repository).with( + @repo_name, + private: false, + description: "Repository created via railsnew.io" ) - repo.stubs(:git).returns(git) - # Test the setup_remote method - repo.send(:setup_remote) + @repo.send(:create_github_repo) end end diff --git a/test/services/github_code_push_service_test.rb b/test/services/github_code_push_service_test.rb index 4a5be34..5beaa31 100644 --- a/test/services/github_code_push_service_test.rb +++ b/test/services/github_code_push_service_test.rb @@ -159,7 +159,7 @@ def teardown test "push_code raises GitError when git operations fail" do @generated_app = generated_apps(:saas_starter) @user = @generated_app.user - @user.define_singleton_method(:github_token) { "fake-token" } + stub_github_token(@user) # Create directory structure but don't init git app_dir = File.join(@temp_dir, @generated_app.name) diff --git a/test/services/github_repository_service_test.rb b/test/services/github_repository_service_test.rb index 6a96e75..a1547bf 100644 --- a/test/services/github_repository_service_test.rb +++ b/test/services/github_repository_service_test.rb @@ -5,8 +5,7 @@ class GithubRepositoryServiceTest < ActiveSupport::TestCase def setup @user = users(:john) - # Define github_token method to bypass encryption - @user.define_singleton_method(:github_token) { "fake-token" } + stub_github_token(@user) @generated_app = generated_apps(:pending_app) @app_status = @generated_app.app_status diff --git a/test/test_helper.rb b/test/test_helper.rb index 0b913ce..126ae59 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -28,6 +28,7 @@ require "minitest/mock" require "database_cleaner/active_record" require "phlex/testing/view_helper" +require "support/github_token_helper" ActiveRecord::Encryption.configure( primary_key: "test" * 4, @@ -69,6 +70,7 @@ def teardown set_fixture_class noticed_notifications: AppStatusChangeNotifier::Notification set_fixture_class noticed_events: AppStatusChangeNotifier + include GithubTokenHelper end end From 619881f35239af4720a4122ef5e9715dbac30673 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 15:07:11 +0100 Subject: [PATCH 03/20] Fixing tests --- app/services/git_repo.rb | 52 ++----------------- .../github_repository_name_validator.rb | 5 +- test/services/git_repo_clone_test.rb | 9 ---- test/services/git_repo_test.rb | 25 +-------- .../github_repository_name_validator_test.rb | 21 +++----- test/test_helper.rb | 2 - 6 files changed, 16 insertions(+), 98 deletions(-) diff --git a/app/services/git_repo.rb b/app/services/git_repo.rb index ec76d95..a1eecca 100644 --- a/app/services/git_repo.rb +++ b/app/services/git_repo.rb @@ -9,24 +9,11 @@ def initialize(user:, repo_name:) end def commit_changes(message:, author:) - puts "\n=== GitRepo#commit_changes ===" - puts "Checking if repo exists at: #{repo_path}" - if File.exist?(repo_path) - puts "Local repo exists, checking remote" - if remote_repo_exists? - puts "Remote exists, fetching latest changes" - git.fetch - git.reset_hard("origin/main") - else - puts "Remote doesn't exist, creating it" - ensure_github_repo_exists - setup_remote - end + git.fetch + git.reset_hard("origin/main") else - puts "Local repo doesn't exist, checking remote" if remote_repo_exists? - puts "Remote repo exists, cloning" Git.clone( "https://#{user.github_token}@github.com/#{user.github_username}/#{repo_name}.git", repo_name, @@ -34,25 +21,20 @@ def commit_changes(message:, author:) ) @git = Git.open(repo_path) else - puts "Remote repo doesn't exist, creating new repo" create_local_repo ensure_github_repo_exists setup_remote end end - puts "Ensuring committable state" ensure_committable_state - puts "Configuring git user" git.config("user.name", author.name || author.github_username) git.config("user.email", author.email || "#{author.github_username}@users.noreply.github.com") - puts "Adding and committing changes" git.add(all: true) git.commit(message) - puts "Pushing changes" current_branch = git.branch.name git.push("origin", current_branch) end @@ -73,32 +55,18 @@ def git end def create_local_repo - puts "\n=== GitRepo#create_local_repo ===" - puts "Creating directory: #{File.dirname(@repo_path)}" FileUtils.mkdir_p(File.dirname(@repo_path)) - - if File.exist?(@repo_path) - puts "Removing existing repo path: #{@repo_path}" - FileUtils.rm_rf(@repo_path) - end - - puts "Creating repo directory: #{@repo_path}" + FileUtils.rm_rf(@repo_path) if File.exist?(@repo_path) FileUtils.mkdir_p(@repo_path) - puts "Initializing git repo" @git = Git.init(@repo_path) - puts "Configuring git" @git.config("init.templateDir", "") @git.config("init.defaultBranch", "main") end def remote_repo_exists? - puts "\n=== GitRepo#remote_repo_exists? ===" - puts "Checking if repo exists: #{user.github_username}/#{repo_name}" - result = github_client.repository?("#{user.github_username}/#{repo_name}") - puts "Result: #{result}" - result + github_client.repository?("#{user.github_username}/#{repo_name}") rescue Octokit::Error => e Rails.logger.error("Failed to check GitHub repository: #{e.message}") false @@ -121,20 +89,8 @@ def github_client @_github_client ||= Octokit::Client.new(access_token: user.github_token) end - def write_json(path, filename, data) - File.write( - File.join(path, filename), - JSON.pretty_generate(data) - ) - end - def ensure_committable_state - puts "\n=== Ensuring committable state ===" - puts "Before writing: #{Dir.glob("#{repo_path}/*").inspect}" File.write(File.join(repo_path, "README.md"), default_readme_content) - puts "After writing: #{Dir.glob("#{repo_path}/*").inspect}" - puts "README.md content:" - puts File.read(File.join(repo_path, "README.md")) end def default_readme_content diff --git a/app/services/github_repository_name_validator.rb b/app/services/github_repository_name_validator.rb index fdb39e9..7f9e767 100644 --- a/app/services/github_repository_name_validator.rb +++ b/app/services/github_repository_name_validator.rb @@ -17,15 +17,16 @@ def repo_exists? attr_reader :name, :owner def valid_format? + return false if name.nil? name.match?(VALID_FORMAT) end def double_hyphen? - name.include?("--") + name.to_s.include?("--") end def available? - client = Octokit::Client.new(access_token: user.github_token) + client = Octokit::Client.new begin client.repository("#{owner}/#{name}") false # Repository exists diff --git a/test/services/git_repo_clone_test.rb b/test/services/git_repo_clone_test.rb index 1b6f800..0132fc5 100644 --- a/test/services/git_repo_clone_test.rb +++ b/test/services/git_repo_clone_test.rb @@ -3,7 +3,6 @@ class GitRepoCloneTest < ActiveSupport::TestCase setup do @user = users(:jane) - stub_github_token(@user) @user.stubs(:name).returns("Jane Smith") @user.stubs(:email).returns("jane@example.com") @@ -28,12 +27,9 @@ class GitRepoCloneTest < ActiveSupport::TestCase end test "clones repository when remote exists but no local copy" do - puts "\n=== Test Setup ===" File.stubs(:exist?).with(@repo_path).returns(false) @mock_client.stubs(:repository?).returns(true) - puts "Stubbed basic checks" - puts "\n=== Setting up cloned git mock ===" branch_mock = mock("branch") branch_mock.stubs(:name).returns("main") @@ -44,19 +40,14 @@ class GitRepoCloneTest < ActiveSupport::TestCase cloned_git.expects(:add).with(all: true) cloned_git.expects(:commit).with("Test commit") cloned_git.expects(:push).with("origin", "main") - puts "Set up cloned git expectations" - puts "\n=== Setting up Git operations ===" Git.expects(:clone).with( "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git", @repo_name, path: File.dirname(@repo_path) ) Git.expects(:open).with(@repo_path).returns(cloned_git) - puts "Set up Git clone and open expectations" - puts "\n=== Executing Test ===" @repo.commit_changes(message: "Test commit", author: @user) - puts "Test execution completed" end end diff --git a/test/services/git_repo_test.rb b/test/services/git_repo_test.rb index d3726bc..434a65a 100644 --- a/test/services/git_repo_test.rb +++ b/test/services/git_repo_test.rb @@ -3,7 +3,6 @@ class GitRepoTest < ActiveSupport::TestCase setup do @user = users(:jane) - stub_github_token(@user) @user.stubs(:name).returns("Jane Smith") @user.stubs(:email).returns("jane@example.com") @@ -48,12 +47,9 @@ class GitRepoTest < ActiveSupport::TestCase end test "commits changes to existing local repository" do - puts "\n=== Test Setup ===" File.stubs(:exist?).with(@repo_path).returns(true) File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) - puts "Stubbed File.exist? for repo path and .git directory" - puts "\n=== Git Expectations ===" @git.expects(:fetch) @git.expects(:reset_hard).with("origin/main") @git.expects(:config).with("user.name", "Jane Smith") @@ -61,35 +57,23 @@ class GitRepoTest < ActiveSupport::TestCase @git.expects(:add).with(all: true) @git.expects(:commit).with("Test commit") @git.expects(:push).with("origin", "main") - puts "Set up Git mock expectations" - puts "\n=== GitHub API Expectations ===" - puts "User github_username: #{@user.github_username.inspect}" - puts "Repo name: #{@repo_name.inspect}" @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(true) - puts "Set up GitHub API mock expectations" - puts "\n=== Executing Test ===" @repo.commit_changes(message: "Test commit", author: @user) - puts "Test execution completed" end test "creates new local repository when no repository exists" do - puts "\n=== Test Setup ===" File.stubs(:exist?).with(@repo_path).returns(false) File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(false) - puts "Stubbed File.exist? checks" - puts "\n=== GitHub API Expectations ===" @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(false).twice @mock_client.expects(:create_repository).with( @repo_name, private: false, description: "Repository created via railsnew.io" ).returns(true) - puts "Set up GitHub API expectations" - puts "\n=== Git Expectations ===" @git.expects(:config).with("init.templateDir", "") @git.expects(:config).with("init.defaultBranch", "main") @git.expects(:config).with("user.name", "Jane Smith") @@ -98,19 +82,12 @@ class GitRepoTest < ActiveSupport::TestCase @git.expects(:commit).with("Test commit") @git.expects(:add_remote).with("origin", "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git") @git.expects(:push).with("origin", "main") - puts "Set up Git expectations" - puts "\n=== FileUtils Expectations ===" - puts "Parent dir path: #{File.dirname(@repo_path)}" - puts "Repo path: #{@repo_path}" FileUtils.expects(:mkdir_p).with(File.dirname(@repo_path)) - FileUtils.stubs(:rm_rf).with(@repo_path) # Allow any number of calls + FileUtils.stubs(:rm_rf).with(@repo_path) FileUtils.expects(:mkdir_p).with(@repo_path) - puts "Set up FileUtils expectations" - puts "\n=== Executing Test ===" @repo.commit_changes(message: "Test commit", author: @user) - puts "Test execution completed" end test "handles GitHub API errors when checking repository existence" do diff --git a/test/services/github_repository_name_validator_test.rb b/test/services/github_repository_name_validator_test.rb index 2cfdb05..ded618d 100644 --- a/test/services/github_repository_name_validator_test.rb +++ b/test/services/github_repository_name_validator_test.rb @@ -17,27 +17,27 @@ def test_available_repository_name def test_invalid_ending_with_hyphen validator = GithubRepositoryNameValidator.new("invalid-", @owner) - assert validator.repo_exists? + assert_not validator.repo_exists? end def test_invalid_starting_with_hyphen validator = GithubRepositoryNameValidator.new("-invalid", @owner) - assert_not validator.valid? + assert_not validator.repo_exists? end def test_invalid_double_hyphen validator = GithubRepositoryNameValidator.new("invalid--name", @owner) - assert_not validator.valid? + assert_not validator.repo_exists? end def test_invalid_empty_string validator = GithubRepositoryNameValidator.new("", @owner) - assert_not validator.valid? + assert_not validator.repo_exists? end def test_invalid_special_characters validator = GithubRepositoryNameValidator.new("inv@lid", @owner) - assert_not validator.valid? + assert_not validator.repo_exists? end def test_invalid_when_repository_exists @@ -46,19 +46,14 @@ def test_invalid_when_repository_exists Octokit::Client.stub(:new, client) do validator = GithubRepositoryNameValidator.new("existing-repo", @owner) - assert_not validator.valid? + assert validator.repo_exists? end assert_mock client end def test_invalid_with_nil_name - client = Minitest::Mock.new - client.expect(:repository, nil) { raise Octokit::NotFound } - - Octokit::Client.stub(:new, client) do - validator = GithubRepositoryNameValidator.new(nil, @owner) - assert_not validator.valid? - end + validator = GithubRepositoryNameValidator.new(nil, @owner) + assert_not validator.repo_exists? end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 126ae59..0b913ce 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -28,7 +28,6 @@ require "minitest/mock" require "database_cleaner/active_record" require "phlex/testing/view_helper" -require "support/github_token_helper" ActiveRecord::Encryption.configure( primary_key: "test" * 4, @@ -70,7 +69,6 @@ def teardown set_fixture_class noticed_notifications: AppStatusChangeNotifier::Notification set_fixture_class noticed_events: AppStatusChangeNotifier - include GithubTokenHelper end end From f4c1fc4dd9c92c24f259e53ded5d8c220d690a16 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 15:19:52 +0100 Subject: [PATCH 04/20] Fix more tests --- app/services/git_repo.rb | 1 + app/services/github_repository_name_validator.rb | 6 ++---- test/services/github_repository_name_validator_test.rb | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/services/git_repo.rb b/app/services/git_repo.rb index a1eecca..a62c541 100644 --- a/app/services/git_repo.rb +++ b/app/services/git_repo.rb @@ -10,6 +10,7 @@ def initialize(user:, repo_name:) def commit_changes(message:, author:) if File.exist?(repo_path) + raise GitSyncError, "Remote repository does not exist" unless remote_repo_exists? git.fetch git.reset_hard("origin/main") else diff --git a/app/services/github_repository_name_validator.rb b/app/services/github_repository_name_validator.rb index 7f9e767..7d070f4 100644 --- a/app/services/github_repository_name_validator.rb +++ b/app/services/github_repository_name_validator.rb @@ -28,10 +28,8 @@ def double_hyphen? def available? client = Octokit::Client.new begin - client.repository("#{owner}/#{name}") - false # Repository exists - rescue Octokit::NotFound - true # Repository is available + exists = client.repository?("#{owner}/#{name}") + !exists # Return true if repo doesn't exist rescue Octokit::Error => e Rails.logger.error("GitHub API error: #{e.message}") raise e diff --git a/test/services/github_repository_name_validator_test.rb b/test/services/github_repository_name_validator_test.rb index ded618d..6b1add2 100644 --- a/test/services/github_repository_name_validator_test.rb +++ b/test/services/github_repository_name_validator_test.rb @@ -7,7 +7,7 @@ def setup def test_available_repository_name client = Minitest::Mock.new - client.expect(:repository, nil) { raise Octokit::NotFound } + client.expect(:repository?, false, [ "#{@owner}/valid-repo-name" ]) Octokit::Client.stub(:new, client) do validator = GithubRepositoryNameValidator.new("valid-repo-name", @owner) @@ -42,7 +42,7 @@ def test_invalid_special_characters def test_invalid_when_repository_exists client = Minitest::Mock.new - client.expect(:repository, true, [ "#{@owner}/existing-repo" ]) + client.expect(:repository?, true, [ "#{@owner}/existing-repo" ]) Octokit::Client.stub(:new, client) do validator = GithubRepositoryNameValidator.new("existing-repo", @owner) From e5ca46c836fb8eeb6e44647e1401802983535806 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 17:37:05 +0100 Subject: [PATCH 05/20] Green again --- .../github_auth_button_component_test.rb | 7 +----- .../services/github_code_push_service_test.rb | 3 ++- .../github_repository_service_test.rb | 23 +------------------ 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/test/components/github_auth_button_component_test.rb b/test/components/github_auth_button_component_test.rb index c9cf316..72eae93 100644 --- a/test/components/github_auth_button_component_test.rb +++ b/test/components/github_auth_button_component_test.rb @@ -13,12 +13,7 @@ class GithubAuthButton::ComponentTest < PhlexComponentTestCase end test "renders logout button when user is logged in" do - user = User.create!( - name: "Test User", - provider: "github", - uid: "123456", - github_username: "testuser" - ) + user = users(:john) Current.stub(:user, user) do component = GithubAuthButton::Component.new diff --git a/test/services/github_code_push_service_test.rb b/test/services/github_code_push_service_test.rb index 5beaa31..28608ba 100644 --- a/test/services/github_code_push_service_test.rb +++ b/test/services/github_code_push_service_test.rb @@ -3,6 +3,7 @@ class GithubCodePushServiceTest < ActiveSupport::TestCase def setup @user = users(:john) + @user.stubs(:github_token).returns("fake-token") @recipe = recipes(:blog_recipe) @temp_dir = Dir.mktmpdir @@ -159,7 +160,7 @@ def teardown test "push_code raises GitError when git operations fail" do @generated_app = generated_apps(:saas_starter) @user = @generated_app.user - stub_github_token(@user) + @user.stubs(:github_token).returns("fake-token") # Create directory structure but don't init git app_dir = File.join(@temp_dir, @generated_app.name) diff --git a/test/services/github_repository_service_test.rb b/test/services/github_repository_service_test.rb index a1547bf..b16af8e 100644 --- a/test/services/github_repository_service_test.rb +++ b/test/services/github_repository_service_test.rb @@ -5,7 +5,7 @@ class GithubRepositoryServiceTest < ActiveSupport::TestCase def setup @user = users(:john) - stub_github_token(@user) + @user.stubs(:github_token).returns("fake-token") @generated_app = generated_apps(:pending_app) @app_status = @generated_app.app_status @@ -172,25 +172,4 @@ def mock_client.repository?(*) end end end - - test "client initializes Octokit::Client with correct parameters" do - User.any_instance.stubs(:github_token).returns("fake-token") - - mock_client = mock("client") - Octokit::Client.expects(:new).with( - access_token: "fake-token", - auto_paginate: true - ).returns(mock_client) - - @service.send(:client) - end - - test "client memoizes the instance" do - User.any_instance.stubs(:github_token).returns("fake-token") - - first_client = @service.send(:client) - second_client = @service.send(:client) - - assert_same first_client, second_client - end end From dde8b96fe5aea151be484674f3654461a6ecae84 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 18:14:40 +0100 Subject: [PATCH 06/20] Lotsa refactoring, test coverage --- app/controllers/repositories_controller.rb | 47 ------------ app/helpers/repostitories_helper.rb | 2 - app/models/generated_app.rb | 4 - app/models/repository.rb | 24 ------ app/models/user.rb | 1 - app/services/github_repository_service.rb | 4 - app/views/repositories/index.html.erb | 67 ----------------- app/views/repositories/new.html.erb | 28 ------- app/views/users/show.html.erb | 4 +- config/routes.rb | 4 +- .../20241215164138_drop_repositories.rb | 18 +++++ db/schema.rb | 14 +--- .../repositories_controller_test.rb | 75 ------------------- test/controllers/sessions_controller_test.rb | 1 - test/fixtures/repositories.yml | 37 --------- test/models/app_change_test.rb | 12 +++ test/models/repository_test.rb | 28 ------- test/services/git_repo_test.rb | 24 ++++++ .../github_repository_service_test.rb | 22 +++--- 19 files changed, 67 insertions(+), 349 deletions(-) delete mode 100644 app/controllers/repositories_controller.rb delete mode 100644 app/helpers/repostitories_helper.rb delete mode 100644 app/models/repository.rb delete mode 100644 app/views/repositories/index.html.erb delete mode 100644 app/views/repositories/new.html.erb create mode 100644 db/migrate/20241215164138_drop_repositories.rb delete mode 100644 test/controllers/repositories_controller_test.rb delete mode 100644 test/fixtures/repositories.yml delete mode 100644 test/models/repository_test.rb diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb deleted file mode 100644 index f831645..0000000 --- a/app/controllers/repositories_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# app/controllers/repositories_controller.rb -class RepositoriesController < ApplicationController - before_action :authenticate_user! - before_action :set_user, except: [ :check_name ] - - def index - @repositories = @user.repositories - end - - def new - @repository = @user.repositories.build - end - - def create - begin - GithubRepositoryService.new(@user) - .create_repository(repository_params[:name]) - - redirect_to user_repositories_path(@user), notice: "Repository created successfully!" - rescue GithubRepositoryService::Error => e - redirect_to new_user_repository_path(@user), alert: e.message - end - end - - def check_name - validator = GithubRepositoryNameValidator.new( - params[:name], - current_user.github_username - ) - render json: { available: validator.valid? } - end - private - - def set_user - @user = User.friendly.find(params[:user_id]) - end - - def repository_params - params.require(:repository).permit(:name) - end - - def authenticate_user! - unless current_user - redirect_to root_path, alert: "Please sign in with GitHub first!" - end - end -end diff --git a/app/helpers/repostitories_helper.rb b/app/helpers/repostitories_helper.rb deleted file mode 100644 index 64efc65..0000000 --- a/app/helpers/repostitories_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module RepostitoriesHelper -end diff --git a/app/models/generated_app.rb b/app/models/generated_app.rb index f457ab6..3b342ff 100644 --- a/app/models/generated_app.rb +++ b/app/models/generated_app.rb @@ -92,8 +92,4 @@ def broadcast_clone_box locals: { generated_app: self } ) end - - def repo_name - name # Use the app's name as the repo name - end end diff --git a/app/models/repository.rb b/app/models/repository.rb deleted file mode 100644 index ff12de7..0000000 --- a/app/models/repository.rb +++ /dev/null @@ -1,24 +0,0 @@ -# == Schema Information -# -# Table name: repositories -# -# id :integer not null, primary key -# github_url :string not null -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# user_id :integer not null -# -# Indexes -# -# index_repositories_on_github_url (github_url) UNIQUE -# index_repositories_on_name (name) -# index_repositories_on_user_id (user_id) -# -# Foreign Keys -# -# user_id (user_id => users.id) -# -class Repository < ApplicationRecord - belongs_to :user -end diff --git a/app/models/user.rb b/app/models/user.rb index 968ec26..257f71e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,7 +25,6 @@ class User < ApplicationRecord encrypts :github_token - has_many :repositories, dependent: :destroy has_many :generated_apps, dependent: :nullify has_many :notifications, as: :recipient, dependent: :destroy, diff --git a/app/services/github_repository_service.rb b/app/services/github_repository_service.rb index a0530f1..a6f7a74 100644 --- a/app/services/github_repository_service.rb +++ b/app/services/github_repository_service.rb @@ -29,10 +29,6 @@ def create_repository(name) response = client.create_repository(name, options) - @user.repositories.create!( - name: name, - github_url: response.html_url - ) @generated_app.update!( github_repo_name: name, github_repo_url: response.html_url diff --git a/app/views/repositories/index.html.erb b/app/views/repositories/index.html.erb deleted file mode 100644 index 2aa829f..0000000 --- a/app/views/repositories/index.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -
-
-
-

Repositories

-

A list of all repositories for <%= @user.name %>

-
-
- <%= link_to new_user_repository_path(@user), - class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700" do %> - - - - New Repository - <% end %> -
-
-
-
-
-
- - - - - - - - - - <% @repositories.each do |repo| %> - - - - - - <% end %> - -
NameGitHub URLCreated
- <%= repo.name %> - - <%= link_to repo.github_url, repo.github_url, class: "text-indigo-600 hover:text-indigo-900", target: "_blank" %> - - <%= repo.created_at.strftime("%B %d, %Y") %> -
- <% if @repositories.empty? %> -
- - - -

No repositories

-

Get started by creating a new repository.

-
- <%= link_to new_user_repository_path(@user), - class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %> - - - - New Repository - <% end %> -
-
- <% end %> -
-
-
-
-
diff --git a/app/views/repositories/new.html.erb b/app/views/repositories/new.html.erb deleted file mode 100644 index e3d9fb4..0000000 --- a/app/views/repositories/new.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -
-

Create New Repository

- <%= form_with(model: [ @user, @repository ], class: "space-y-4") do |f| %> -
- <%= f.label :name, class: "block text-sm font-medium text-gray-700" %> - <%= f.text_field :name, - class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", - data: { - github_name_validator_target: "input", - action: "input->github-name-validator#validate" - } %> -
- - -
-
- <%= f.submit "Create Repository", class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> - <% end %> -
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 59def3e..f6b07e1 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,10 +1,10 @@
- <%= link_to new_user_repository_path(@user), + <%= link_to page_path("basic-setup"), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %> - Create new repository + Create new app <% end %> <%= link_to dashboard_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" do %> diff --git a/config/routes.rb b/config/routes.rb index 7419008..4624c55 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,9 +27,7 @@ resources :log_entries, only: [ :index ] end - resources :users, only: [ :show ], path: "" do - resources :repositories, only: [ :new, :create, :show, :index ] - end + resources :users, only: [ :show ], path: "" resources :pages, only: :show diff --git a/db/migrate/20241215164138_drop_repositories.rb b/db/migrate/20241215164138_drop_repositories.rb new file mode 100644 index 0000000..ed72cce --- /dev/null +++ b/db/migrate/20241215164138_drop_repositories.rb @@ -0,0 +1,18 @@ +class DropRepositories < ActiveRecord::Migration[8.0] + def up + drop_table :repositories + end + + def down + create_table :repositories do |t| + t.string :name, null: false + t.string :github_url, null: false + t.references :user, null: false, foreign_key: true, index: true + + t.timestamps + + t.index :name + t.index :github_url, unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index eda8b35..960f600 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_12_031732) do +ActiveRecord::Schema[8.0].define(version: 2024_12_15_164138) do create_table "_litestream_lock", id: false, force: :cascade do |t| t.integer "id" end @@ -292,17 +292,6 @@ t.index ["created_by_id"], name: "index_recipes_on_created_by_id" end - create_table "repositories", force: :cascade do |t| - t.string "name", null: false - t.string "github_url", null: false - t.integer "user_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["github_url"], name: "index_repositories_on_github_url", unique: true - t.index ["name"], name: "index_repositories_on_name" - t.index ["user_id"], name: "index_repositories_on_user_id" - end - create_table "solid_queue_blocked_executions", force: :cascade do |t| t.integer "job_id", null: false t.string "queue_name", null: false @@ -466,7 +455,6 @@ add_foreign_key "recipe_ingredients", "ingredients" add_foreign_key "recipe_ingredients", "recipes" add_foreign_key "recipes", "users", column: "created_by_id" - add_foreign_key "repositories", "users" add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade diff --git a/test/controllers/repositories_controller_test.rb b/test/controllers/repositories_controller_test.rb deleted file mode 100644 index 559b47c..0000000 --- a/test/controllers/repositories_controller_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -require "test_helper" - -class RepositoriesControllerTest < ActionDispatch::IntegrationTest - def setup - @user = users(:john) - sign_in @user - end - - test "should get index" do - get user_repositories_path(@user) - assert_response :success - end - - test "should get new" do - get new_user_repository_path(@user) - assert_response :success - end - - test "repo_exists? returns false for valid (non-existent) repository name" do - validator = mock - validator.expects(:repo_exists?).returns(false) - GithubRepositoryNameValidator.expects(:new) - .with("test-repo", @user.github_username) - .returns(validator) - - get check_github_name_path, params: { name: "test-repo" } - - assert_response :success - assert_equal({ "available" => true }, JSON.parse(response.body)) - end - - test "should create repository" do - repository = Repository.new( - name: "test-repo", - github_url: "https://github.com/johndoe/test-repo", - user: @user - ) - - service_mock = Minitest::Mock.new - service_mock.expect :create_repository, repository do |name| - repository.save! - true - end - - GithubRepositoryService.stub :new, service_mock do - assert_difference("Repository.count") do - post user_repositories_path(@user), params: { repository: { name: "test-repo" } } - end - end - - assert_redirected_to user_repositories_path(@user) - assert_equal "Repository created successfully!", flash[:notice] - end - - test "should handle repository creation error" do - service_mock = Minitest::Mock.new - service_mock.expect :create_repository, nil do |_name| - raise GithubRepositoryService::Error, "API error" - end - - GithubRepositoryService.stub :new, service_mock do - post user_repositories_path(@user), params: { repository: { name: "test-repo" } } - end - - assert_redirected_to new_user_repository_path(@user) - assert_equal "API error", flash[:alert] - end - - test "should redirect to root if not authenticated" do - sign_out @user - get user_repositories_path(@user) - assert_redirected_to root_path - assert_equal "Please sign in with GitHub first!", flash[:alert] - end -end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 80da261..f23d14b 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -135,7 +135,6 @@ class SessionControllerTest < ActionDispatch::IntegrationTest Recipe.delete_all # Then recipes Ingredient.delete_all # Then ingredients Noticed::Notification.delete_all # Then notifications - Repository.delete_all # Then repositories User.delete_all # Finally users silence_omniauth_logger do diff --git a/test/fixtures/repositories.yml b/test/fixtures/repositories.yml deleted file mode 100644 index c06b708..0000000 --- a/test/fixtures/repositories.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -# == Schema Information -# -# Table name: repositories -# -# id :integer not null, primary key -# github_url :string not null -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# user_id :integer not null -# -# Indexes -# -# index_repositories_on_github_url (github_url) UNIQUE -# index_repositories_on_name (name) -# index_repositories_on_user_id (user_id) -# -# Foreign Keys -# -# user_id (user_id => users.id) -# -one: - name: repo-one - github_url: https://github.com/johndoe/repo-one - user: john - -two: - name: repo-two - github_url: https://github.com/johndoe/repo-two - user: john - -three: - name: repo-three - github_url: https://github.com/jane_smith/repo-three - user: jane diff --git a/test/models/app_change_test.rb b/test/models/app_change_test.rb index 15b7e2f..2030c1d 100644 --- a/test/models/app_change_test.rb +++ b/test/models/app_change_test.rb @@ -158,6 +158,18 @@ class AppChangeTest < ActiveSupport::TestCase end end + test "to_git_format includes recipe change type" do + app_change = app_changes(:blog_auth_change) # Use existing fixture + + git_format = app_change.to_git_format + + assert_equal app_change.recipe_change.change_type, git_format[:recipe_change_type] + assert_equal app_change.configuration, git_format[:configuration] + assert_nil git_format[:applied_at], "Expected applied_at to be nil for unapplied change" + assert_equal false, git_format[:success], "Expected success to be false for unapplied change" + assert_nil git_format[:error_message], "Expected error_message to be nil for unapplied change" + end + private def mock_popen3(stdout, stderr, success: true, pid: 12345) diff --git a/test/models/repository_test.rb b/test/models/repository_test.rb deleted file mode 100644 index 7132fde..0000000 --- a/test/models/repository_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -# == Schema Information -# -# Table name: repositories -# -# id :integer not null, primary key -# github_url :string not null -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# user_id :integer not null -# -# Indexes -# -# index_repositories_on_github_url (github_url) UNIQUE -# index_repositories_on_name (name) -# index_repositories_on_user_id (user_id) -# -# Foreign Keys -# -# user_id (user_id => users.id) -# -require "test_helper" - -class RepositoryTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/services/git_repo_test.rb b/test/services/git_repo_test.rb index 434a65a..f8b1aa9 100644 --- a/test/services/git_repo_test.rb +++ b/test/services/git_repo_test.rb @@ -150,4 +150,28 @@ class GitRepoTest < ActiveSupport::TestCase @repo.send(:create_github_repo) end + + test "initializes new git repo when .git directory doesn't exist" do + # Stub File.exist? to return false for .git directory + File.stubs(:exist?).returns(true) # default stub + File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(false) + + # Set up expectations for create_local_repo + FileUtils.expects(:mkdir_p).with(File.dirname(@repo_path)) + FileUtils.expects(:rm_rf).with(@repo_path) + FileUtils.expects(:mkdir_p).with(@repo_path) + + # Expect Git.init to be called and return our mock git object + Git.expects(:init).with(@repo_path).returns(@git) + + # Expect git config calls + @git.expects(:config).with("init.templateDir", "") + @git.expects(:config).with("init.defaultBranch", "main") + + # Call the git method + result = @repo.send(:git) + + # Verify the result + assert_equal @git, result + end end diff --git a/test/services/github_repository_service_test.rb b/test/services/github_repository_service_test.rb index b16af8e..72f1cd7 100644 --- a/test/services/github_repository_service_test.rb +++ b/test/services/github_repository_service_test.rb @@ -32,21 +32,17 @@ def setup description: "Repository created via railsnew.io" } ] - @service.stub :client, mock_client do - assert_difference -> { @user.repositories.count }, 1 do - assert_difference -> { AppGeneration::LogEntry.count }, 3 do - result = @service.create_repository(@repository_name) - assert_equal response.html_url, result.html_url - end - end - - # Verify log entries - log_entries = @generated_app.log_entries.recent_first - assert_equal "GitHub repo #{@repository_name} created successfully", log_entries.first.message - assert_equal "Creating repository: #{@repository_name}", log_entries.second.message + Octokit::Client.stub :new, mock_client do + response = @service.create_repository(@repository_name) + assert_equal "https://github.com/#{@user.github_username}/#{@repository_name}", response.html_url + + # Verify GeneratedApp was updated + @generated_app.reload + assert_equal @repository_name, @generated_app.github_repo_name + assert_equal response.html_url, @generated_app.github_repo_url end - mock_client.verify + assert_mock mock_client end test "raises error when repository already exists" do From 52d249d5dc752fa90155640e0706e243e310101c Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 18:19:06 +0100 Subject: [PATCH 07/20] more coverage --- .../github_repository_name_validator_test.rb | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/services/github_repository_name_validator_test.rb b/test/services/github_repository_name_validator_test.rb index 6b1add2..c4f63fc 100644 --- a/test/services/github_repository_name_validator_test.rb +++ b/test/services/github_repository_name_validator_test.rb @@ -56,4 +56,37 @@ def test_invalid_with_nil_name validator = GithubRepositoryNameValidator.new(nil, @owner) assert_not validator.repo_exists? end + + def test_handles_github_api_errors + error = Octokit::Error.new( + method: :get, + url: "https://api.github.com/repos/#{@owner}/test-repo", + status: 401, + response_headers: {}, + body: { message: "Bad credentials" } + ) + + # Set up logger mock + mock_logger = mock("logger") + mock_logger.expects(:error).with( + "GitHub API error: GET https://api.github.com/repos/#{@owner}/test-repo: 401 - Bad credentials" + ) + Rails.stubs(:logger).returns(mock_logger) + + # Set up client mock to raise error + client = Minitest::Mock.new + client.expect(:repository?, nil) { raise error } + + Octokit::Client.stub(:new, client) do + validator = GithubRepositoryNameValidator.new("test-repo", @owner) + + error = assert_raises(Octokit::Error) do + validator.repo_exists? + end + + assert_equal "GET https://api.github.com/repos/#{@owner}/test-repo: 401 - Bad credentials", error.message + end + + assert_mock client + end end From c3656751542b0265e072a2d630d2d0778a3e8c80 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Sun, 15 Dec 2024 18:28:54 +0100 Subject: [PATCH 08/20] more coverage --- app/models/recipe.rb | 21 +++++++++------------ test/models/recipe_test.rb | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/models/recipe.rb b/app/models/recipe.rb index cf17999..bf56d93 100644 --- a/app/models/recipe.rb +++ b/app/models/recipe.rb @@ -73,19 +73,16 @@ def reorder_ingredients!(new_order) def self.find_or_create_by_cli_flags!(cli_flags, user) transaction do recipe = where(cli_flags: cli_flags, status: "published").first + return recipe if recipe - unless recipe - recipe = create!( - name: "Rails App with #{cli_flags}", - cli_flags: cli_flags, - status: "published", - created_by: user, - ruby_version: RailsNewConfig.ruby_version_for_new_apps, - rails_version: RailsNewConfig.rails_version_for_new_apps - ) - end - - recipe + create!( + name: "Rails App with #{cli_flags}", + cli_flags: cli_flags, + status: "published", + created_by: user, + ruby_version: RailsNewConfig.ruby_version_for_new_apps, + rails_version: RailsNewConfig.rails_version_for_new_apps + ) end end diff --git a/test/models/recipe_test.rb b/test/models/recipe_test.rb index bea3dfc..fb14d47 100644 --- a/test/models/recipe_test.rb +++ b/test/models/recipe_test.rb @@ -186,4 +186,29 @@ class RecipeTest < ActiveSupport::TestCase assert_equal [ 1, 2 ], @recipe.recipe_ingredients.order(:position).pluck(:position) end + + test "find_or_create_by_cli_flags! finds existing recipe" do + existing = recipes(:api_recipe) + recipe = Recipe.find_or_create_by_cli_flags!(existing.cli_flags, @user) + + assert_equal existing, recipe + end + + test "find_or_create_by_cli_flags! creates new recipe when none exists" do + cli_flags = "--api --minimal" + + Recipe.any_instance.stubs(:commit_changes).returns(true) + Recipe.any_instance.stubs(:initial_git_commit).returns(true) + GitRepo.any_instance.stubs(:commit_changes).returns(true) + GitRepo.any_instance.stubs(:write_model).returns(true) + + assert_difference "Recipe.count", 1 do + recipe = Recipe.find_or_create_by_cli_flags!(cli_flags, @user) + + assert_equal cli_flags, recipe.cli_flags + assert_equal "Rails App with #{cli_flags}", recipe.name + assert_equal "published", recipe.status + assert_equal @user, recipe.created_by + end + end end From 04a710a46253b2ec0b008610827cfb0c53d44448 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Mon, 16 Dec 2024 04:39:47 +0100 Subject: [PATCH 09/20] Test for DataRepository --- app/services/data_repository.rb | 4 + test/controllers/sessions_controller_test.rb | 21 ++- test/fixtures/ingredients.yml | 14 +- test/services/data_repository_test.rb | 156 +++++++++++++++++++ 4 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 test/services/data_repository_test.rb diff --git a/app/services/data_repository.rb b/app/services/data_repository.rb index d805f65..980c03f 100644 --- a/app/services/data_repository.rb +++ b/app/services/data_repository.rb @@ -107,4 +107,8 @@ def readme_content def repository_description "Data repository for rails-new.io" end + + def write_json(path, filename, content) + File.write(File.join(path, filename), content.to_json) + end end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index f23d14b..3869fcc 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -32,21 +32,22 @@ class SessionControllerTest < ActionDispatch::IntegrationTest test "successful github sign_in" do + user = users(:john) auth_hash = OmniAuth::AuthHash.new({ provider: "github", - uid: "123545", + uid: user.uid, info: { - name: "Test User", - email: "test@example.com", - nickname: "testuser", - image: "http://example.com/image.jpg" + name: user.name, + email: user.email, + nickname: user.github_username, + image: user.image }, credentials: { token: "mock_token" }, extra: { raw_info: { - login: "testuser" + login: user.github_username } } }) @@ -54,16 +55,12 @@ class SessionControllerTest < ActionDispatch::IntegrationTest OmniAuth.config.test_mode = true OmniAuth.config.mock_auth[:github] = auth_hash - # Set up the omniauth.auth environment get "/auth/github/callback", env: { 'omniauth.auth': auth_hash } assert_response :redirect assert_redirected_to dashboard_url - - user = User.find_by(email: "test@example.com") - assert user.present? - assert_equal session[:user_id], user.id - assert_equal "Logged in as Test User", flash[:notice] + assert_equal user.id, session[:user_id] + assert_equal "Logged in as #{user.name}", flash[:notice] end test "github oauth failure" do diff --git a/test/fixtures/ingredients.yml b/test/fixtures/ingredients.yml index ed9ef47..8b9a682 100644 --- a/test/fixtures/ingredients.yml +++ b/test/fixtures/ingredients.yml @@ -49,14 +49,14 @@ api_setup: category: "api" basic: - name: "Basic Rails Setup" - description: "Basic Rails application setup with common configurations" - template_content: | - # Basic Rails setup - gem 'bootsnap' - gem 'puma' - configures_with: {} + name: "Basic Rails" + description: "A basic Rails setup" + template_content: "# Basic Rails template" conflicts_with: [] requires: [] + configures_with: + database: + - postgresql + - mysql created_by: john category: "setup" diff --git a/test/services/data_repository_test.rb b/test/services/data_repository_test.rb new file mode 100644 index 0000000..cd2d5d3 --- /dev/null +++ b/test/services/data_repository_test.rb @@ -0,0 +1,156 @@ +require "test_helper" + +class DataRepositoryTest < ActiveSupport::TestCase + fixtures :users, :recipes, :ingredients, :generated_apps + + def setup + @user = users(:john) + @repo = DataRepository.new(user: @user) + @git_mock = mock("git") + @repo.stubs(:git).returns(@git_mock) + @git_mock.stubs(:fetch) + @git_mock.stubs(:reset_hard) + @git_mock.stubs(:pull) + @git_mock.stubs(:push) + end + + # Class method tests + def test_name_for_environment_in_development + Rails.env.stubs(:development?).returns(true) + Rails.env.stubs(:test?).returns(false) + Rails.env.stubs(:production?).returns(false) + + assert_equal "rails-new-io-data-dev", DataRepository.name_for_environment + end + + def test_name_for_environment_in_test + Rails.env.stubs(:development?).returns(false) + Rails.env.stubs(:test?).returns(true) + Rails.env.stubs(:production?).returns(false) + + assert_equal "rails-new-io-data-test", DataRepository.name_for_environment + end + + def test_name_for_environment_in_production + Rails.env.stubs(:development?).returns(false) + Rails.env.stubs(:test?).returns(false) + Rails.env.stubs(:production?).returns(true) + + assert_equal "rails-new-io-data", DataRepository.name_for_environment + end + + def test_name_for_environment_raises_error_for_unknown_environment + Rails.env.stubs(:development?).returns(false) + Rails.env.stubs(:test?).returns(false) + Rails.env.stubs(:production?).returns(false) + + assert_raises(ArgumentError) { DataRepository.name_for_environment } + end + + # Instance method tests + def test_writes_generated_app_correctly + app = generated_apps(:blog_app) + expected_path = File.join(@repo.send(:repo_path), "generated_apps", app.id.to_s, "current_state.json") + expected_content = { + name: app.name, + recipe_id: app.recipe_id, + configuration: app.configuration_options + }.to_json + + File.expects(:write).with(expected_path, expected_content) + + # Also expect the history.json write + history_path = File.join(@repo.send(:repo_path), "generated_apps", app.id.to_s, "history.json") + File.expects(:write).with(history_path, app.app_changes.map(&:to_git_format).to_json) + + FileUtils.stubs(:mkdir_p) + @repo.stubs(:ensure_fresh_repo) + @repo.stubs(:push_to_remote) + + @repo.write_model(app) + end + + def test_writes_ingredient_correctly + ingredient = ingredients(:rails_authentication) + base_path = File.join(@repo.send(:repo_path), "ingredients", ingredient.name.parameterize) + + # Expect template.rb write + template_path = File.join(base_path, "template.rb") + File.expects(:write).with(template_path, ingredient.template_content) + + # Expect metadata.json write + metadata_path = File.join(base_path, "metadata.json") + metadata_content = { + name: ingredient.name, + description: ingredient.description, + conflicts_with: ingredient.conflicts_with, + requires: ingredient.requires, + configures_with: ingredient.configures_with + }.to_json + File.expects(:write).with(metadata_path, metadata_content) + + FileUtils.stubs(:mkdir_p) + @repo.stubs(:ensure_fresh_repo) + @repo.stubs(:push_to_remote) + + @repo.write_model(ingredient) + end + + def test_writes_recipe_correctly + recipe = recipes(:blog_recipe) + base_path = File.join(@repo.send(:repo_path), "recipes", recipe.id.to_s) + + # Expect manifest.json write + manifest_path = File.join(base_path, "manifest.json") + manifest_content = { + name: recipe.name, + cli_flags: recipe.cli_flags, + ruby_version: recipe.ruby_version, + rails_version: recipe.rails_version + }.to_json + File.expects(:write).with(manifest_path, manifest_content) + + # Expect ingredients.json write + ingredients_path = File.join(base_path, "ingredients.json") + ingredients_content = recipe.recipe_ingredients.order(:position).map(&:to_git_format).to_json + File.expects(:write).with(ingredients_path, ingredients_content) + + FileUtils.stubs(:mkdir_p) + @repo.stubs(:ensure_fresh_repo) + @repo.stubs(:push_to_remote) + + @repo.write_model(recipe) + end + + def test_handles_git_push_error + recipe = recipes(:blog_recipe) + base_path = File.join(@repo.send(:repo_path), "recipes", recipe.id.to_s) + + # Stub the file writes + manifest_path = File.join(base_path, "manifest.json") + ingredients_path = File.join(base_path, "ingredients.json") + File.stubs(:write).with(manifest_path, anything) + File.stubs(:write).with(ingredients_path, anything) + + FileUtils.stubs(:mkdir_p) + @repo.stubs(:ensure_fresh_repo) + @repo.unstub(:push_to_remote) # We want the real push_to_remote to trigger the error + @git_mock.stubs(:push).raises(Git::Error.new("Push failed")) + + assert_raises(GitRepo::GitSyncError) { @repo.write_model(recipe) } + end + + private + + def stub_filesystem_operations + @repo.stubs(:ensure_fresh_repo) + @repo.stubs(:push_to_remote) + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:touch) + end + + def assert_path_written(relative_path, expected_content) + full_path = File.join(@repo.send(:repo_path), relative_path) + assert File.stubs(:write).with(full_path, expected_content.to_json).once + end +end From e6b33c365651825f8c096937ef31691dbd6a7feb Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Mon, 16 Dec 2024 05:00:09 +0100 Subject: [PATCH 10/20] 100% test coveragefor DataRepository --- test/services/data_repository_test.rb | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/services/data_repository_test.rb b/test/services/data_repository_test.rb index cd2d5d3..307cde0 100644 --- a/test/services/data_repository_test.rb +++ b/test/services/data_repository_test.rb @@ -140,6 +140,37 @@ def test_handles_git_push_error assert_raises(GitRepo::GitSyncError) { @repo.write_model(recipe) } end + def test_ensure_committable_state_creates_required_structure + path = @repo.send(:repo_path) + + # Expect directory creation for each required directory + %w[generated_apps ingredients recipes].each do |dir| + dir_path = File.join(path, dir) + FileUtils.expects(:mkdir_p).with(dir_path) + FileUtils.expects(:touch).with(File.join(dir_path, ".keep")) + end + + # Expect README.md creation + readme_path = File.join(path, "README.md") + File.expects(:write).with(readme_path, "# Data Repository\nThis repository contains data for rails-new.io") + + @repo.send(:ensure_committable_state) + end + + def test_ensure_fresh_repo_syncs_with_remote + sequence = sequence("git_sync") + + @git_mock.expects(:fetch).in_sequence(sequence) + @git_mock.expects(:reset_hard).with("origin/main").in_sequence(sequence) + @git_mock.expects(:pull).in_sequence(sequence) + + @repo.send(:ensure_fresh_repo) + end + + def test_repository_description_is_specific_to_data_repo + assert_equal "Data repository for rails-new.io", @repo.send(:repository_description) + end + private def stub_filesystem_operations From 0d35e3456d61c2444614633d9086b2d3e7e0ffab Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Mon, 16 Dec 2024 05:16:40 +0100 Subject: [PATCH 11/20] 100% coverage for GeneratedAppsController --- .../generated_apps_controller_test.rb | 84 ++++++++++++++++--- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/test/controllers/generated_apps_controller_test.rb b/test/controllers/generated_apps_controller_test.rb index c026687..8faed86 100644 --- a/test/controllers/generated_apps_controller_test.rb +++ b/test/controllers/generated_apps_controller_test.rb @@ -1,26 +1,86 @@ require "test_helper" +require_relative "../support/git_test_helper" class GeneratedAppsControllerTest < ActionDispatch::IntegrationTest + include GitTestHelper + setup do @user = users(:jane) - @generated_app = generated_apps(:blog_app) - sign_in @user + sign_in(@user) end test "should show generated app" do - get generated_app_url(@generated_app) + get generated_app_url(generated_apps(:blog_app)) assert_response :success + end + + test "requires authentication" do + sign_out(@user) + post generated_apps_path, params: { app_name: "test-app" } + assert_redirected_to root_path + assert_equal "Please sign in first.", flash[:alert] + end + + test "creates app with valid parameters" do + app_name = "my-test-app" + api_flag = "--api" + database = "--database=mysql" + + Recipe.any_instance.stubs(:commit_changes).returns(true) + Recipe.any_instance.stubs(:initial_git_commit).returns(true) + GeneratedApp.any_instance.stubs(:commit_changes).returns(true) + GeneratedApp.any_instance.stubs(:initial_git_commit).returns(true) + + assert_difference "GeneratedApp.count" do + assert_difference "Recipe.count" do + post generated_apps_path, params: { + app_name: app_name, + api_flag: api_flag, + database_choice: database + } + end + end + + app = GeneratedApp.last + assert_equal app_name, app.name + assert_equal @user, app.user + assert_equal "#{api_flag} #{database}", app.recipe.cli_flags - assert_select "h1", @generated_app.name - assert_select "p", @generated_app.description - assert_select "p", @generated_app.ruby_version - assert_select "p", @generated_app.rails_version - assert_select "a[href=?]", @generated_app.github_repo_url + assert_redirected_to generated_app_log_entries_path(app) end - test "should not show generated app for unauthorized user" do - sign_out @user - get generated_app_url(@generated_app) - assert_redirected_to root_url + test "reuses existing recipe if cli flags match" do + recipe = recipes(:api_recipe) # Has "--api --database=postgresql" flags + + GeneratedApp.any_instance.stubs(:commit_changes).returns(true) + GeneratedApp.any_instance.stubs(:initial_git_commit).returns(true) + + assert_difference "GeneratedApp.count" do + assert_no_difference "Recipe.count" do + post generated_apps_path, params: { + app_name: "new-api", + api_flag: "--api", + database_choice: "--database=postgresql" + } + end + end + + app = GeneratedApp.last + assert_equal recipe, app.recipe + end + + test "starts app generation after creation" do + Recipe.any_instance.stubs(:commit_changes).returns(true) + Recipe.any_instance.stubs(:initial_git_commit).returns(true) + GeneratedApp.any_instance.stubs(:commit_changes).returns(true) + GeneratedApp.any_instance.stubs(:initial_git_commit).returns(true) + + AppGeneration::Orchestrator.any_instance.expects(:call) + + post generated_apps_path, params: { + app_name: "test-app", + api_flag: "--api", + database_choice: "--database=mysql" + } end end From 30111f3974a897d36bb345a33429a2868b2998c9 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Mon, 16 Dec 2024 06:42:43 +0100 Subject: [PATCH 12/20] Orchestrator test coverage --- app/services/app_generation/errors.rb | 3 + app/services/app_generation/orchestrator.rb | 8 +-- .../app_generation/orchestrator_test.rb | 56 +++++++++++++++++++ test/test_helper.rb | 2 +- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 app/services/app_generation/errors.rb create mode 100644 test/services/app_generation/orchestrator_test.rb diff --git a/app/services/app_generation/errors.rb b/app/services/app_generation/errors.rb new file mode 100644 index 0000000..5ac3784 --- /dev/null +++ b/app/services/app_generation/errors.rb @@ -0,0 +1,3 @@ +module AppGeneration + class InvalidStateError < StandardError; end +end diff --git a/app/services/app_generation/orchestrator.rb b/app/services/app_generation/orchestrator.rb index 646f6f9..af81adc 100644 --- a/app/services/app_generation/orchestrator.rb +++ b/app/services/app_generation/orchestrator.rb @@ -11,18 +11,18 @@ def call AppGenerationJob.perform_later(@generated_app.id) true + rescue AppGeneration::InvalidStateError + raise rescue StandardError => e @logger.error("Failed to start app generation", { error: e.message }) - @generated_app.fail!(e.message) + @generated_app.mark_as_failed!(e.message) false end private def validate_initial_state! - unless @generated_app.pending? - raise InvalidStateError, "App must be in pending state to start generation" - end + raise AppGeneration::InvalidStateError, "App must be in pending state to start generation" unless @generated_app.pending? end end end diff --git a/test/services/app_generation/orchestrator_test.rb b/test/services/app_generation/orchestrator_test.rb new file mode 100644 index 0000000..9fcfad2 --- /dev/null +++ b/test/services/app_generation/orchestrator_test.rb @@ -0,0 +1,56 @@ +require "test_helper" +require_relative "../../../app/services/app_generation/errors" + +module AppGeneration + class OrchestratorTest < ActiveSupport::TestCase + setup do + @generated_app = generated_apps(:pending_app) + @orchestrator = Orchestrator.new(@generated_app) + end + + test "enqueues generation job when app is in pending state" do + assert @generated_app.pending? + + assert_difference -> { SolidQueue::Job.count } do + assert @orchestrator.call + end + + job = SolidQueue::Job.last + assert_equal "AppGenerationJob", job.class_name + assert_equal [@generated_app.id], job.arguments["arguments"] + end + + test "validates app must be in pending state" do + @generated_app.app_status.update!(status: "generating") + @generated_app.reload + + assert_equal "generating", @generated_app.status + assert_not @generated_app.pending? + + error = assert_raises(AppGeneration::InvalidStateError) do + @orchestrator.call + end + + assert_equal "App must be in pending state to start generation", error.message + end + + test "handles and logs errors during orchestration" do + error_message = "Something went wrong" + AppGenerationJob.stubs(:perform_later).raises(StandardError.new(error_message)) + + # Expect both error logs in sequence + sequence = sequence("error_logging") + AppGeneration::Logger.any_instance.expects(:error).with( + "Failed to start app generation", + { error: error_message } + ).in_sequence(sequence) + AppGeneration::Logger.any_instance.expects(:error).with( + "App generation failed: #{error_message}" + ).in_sequence(sequence) + + assert_not @orchestrator.call + assert @generated_app.reload.failed? + assert_equal error_message, @generated_app.app_status.error_message + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0b913ce..74a7e8d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -16,7 +16,7 @@ add_group "Jobs", "app/jobs" add_group "Mailers", "app/mailers" - track_files "{app/models,app/controllers,app/helpers}/**/*.rb" + track_files "{app/models,app/controllers,app/helpers,app/jobs}/**/*.rb" end end From be5e257077992e96271f92358829adb10553a48f Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Tue, 17 Dec 2024 15:13:11 +0100 Subject: [PATCH 13/20] Fixes to app generation flow --- app/services/data_repository.rb | 35 +++++++++++-------- app/services/git_repo.rb | 17 ++++++--- test/fixtures/generated_apps.yml | 2 +- .../app_generation/orchestrator_test.rb | 2 +- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/app/services/data_repository.rb b/app/services/data_repository.rb index 980c03f..4e4ba11 100644 --- a/app/services/data_repository.rb +++ b/app/services/data_repository.rb @@ -34,10 +34,10 @@ def write_model(model) push_to_remote end - private + protected def ensure_committable_state - %w[generated_apps ingredients recipes].each do |dir| + %w[ingredients recipes].each do |dir| FileUtils.mkdir_p(File.join(repo_path, dir)) FileUtils.touch(File.join(repo_path, dir, ".keep")) end @@ -45,18 +45,11 @@ def ensure_committable_state end def write_generated_app(app) - path = File.join(repo_path, "generated_apps", app.id.to_s) - FileUtils.mkdir_p(path) - - write_json(path, "current_state.json", { - name: app.name, - recipe_id: app.recipe_id, - configuration: app.configuration_options - }) - - write_json(path, "history.json", app.app_changes.map(&:to_git_format)) + raise NotImplementedError, "Generated apps are stored in their own repositories" end + private + def write_ingredient(ingredient) path = File.join(repo_path, "ingredients", ingredient.name.parameterize) FileUtils.mkdir_p(path) @@ -89,8 +82,22 @@ def write_recipe(recipe) def ensure_fresh_repo git.fetch - git.reset_hard("origin/main") - git.pull + git.reset_hard("origin/main") if remote_branch_exists?("main") + git.pull if remote_branch_exists?("main") + + # Check if directories exist in repo + dirs_exist = %w[ingredients recipes].all? do |dir| + File.directory?(File.join(repo_path, dir)) + end + + unless dirs_exist + ensure_committable_state + git.add(all: true) + if git.status.changed.any? || git.status.added.any? + git.commit("Initialize repository structure") + git.push("origin", "main") + end + end end def push_to_remote diff --git a/app/services/git_repo.rb b/app/services/git_repo.rb index a62c541..b3f062f 100644 --- a/app/services/git_repo.rb +++ b/app/services/git_repo.rb @@ -12,7 +12,9 @@ def commit_changes(message:, author:) if File.exist?(repo_path) raise GitSyncError, "Remote repository does not exist" unless remote_repo_exists? git.fetch - git.reset_hard("origin/main") + if remote_branch_exists?("main") + git.reset_hard("origin/main") + end else if remote_repo_exists? Git.clone( @@ -34,10 +36,13 @@ def commit_changes(message:, author:) git.config("user.email", author.email || "#{author.github_username}@users.noreply.github.com") git.add(all: true) - git.commit(message) - current_branch = git.branch.name - git.push("origin", current_branch) + # Only commit if there are changes + if git.status.changed.any? || git.status.added.any? || git.status.deleted.any? + git.commit(message) + current_branch = git.branch.name + git.push("origin", current_branch) + end end protected @@ -108,4 +113,8 @@ def ensure_github_repo_exists return if remote_repo_exists? create_github_repo end + + def remote_branch_exists?(branch_name) + git.branches.remote.map(&:name).include?("origin/#{branch_name}") + end end diff --git a/test/fixtures/generated_apps.yml b/test/fixtures/generated_apps.yml index b7bc82d..4b8b6a7 100644 --- a/test/fixtures/generated_apps.yml +++ b/test/fixtures/generated_apps.yml @@ -78,7 +78,7 @@ saas_starter: database: "postgresql" css: "tailwind" testing: "minitest" - source_path: <%= Rails.root.join("tmp", "test_apps", "saas_starter") %> + source_path: <%= Rails.root.join("tmp", "test_saas_starter") %> github_repo_url: "https://github.com/johndoe/saas-starter" github_repo_name: "saas-starter" is_public: false diff --git a/test/services/app_generation/orchestrator_test.rb b/test/services/app_generation/orchestrator_test.rb index 9fcfad2..a90b47d 100644 --- a/test/services/app_generation/orchestrator_test.rb +++ b/test/services/app_generation/orchestrator_test.rb @@ -17,7 +17,7 @@ class OrchestratorTest < ActiveSupport::TestCase job = SolidQueue::Job.last assert_equal "AppGenerationJob", job.class_name - assert_equal [@generated_app.id], job.arguments["arguments"] + assert_equal [ @generated_app.id ], job.arguments["arguments"] end test "validates app must be in pending state" do From fe8d89d2a158617583ae88ce196b203726176614 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Tue, 17 Dec 2024 15:23:02 +0100 Subject: [PATCH 14/20] Fix 'commits changes to existing local repository' test --- test/services/git_repo_test.rb | 43 ++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/test/services/git_repo_test.rb b/test/services/git_repo_test.rb index f8b1aa9..fc52188 100644 --- a/test/services/git_repo_test.rb +++ b/test/services/git_repo_test.rb @@ -50,17 +50,46 @@ class GitRepoTest < ActiveSupport::TestCase File.stubs(:exist?).with(@repo_path).returns(true) File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) - @git.expects(:fetch) - @git.expects(:reset_hard).with("origin/main") - @git.expects(:config).with("user.name", "Jane Smith") - @git.expects(:config).with("user.email", "jane@example.com") - @git.expects(:add).with(all: true) - @git.expects(:commit).with("Test commit") - @git.expects(:push).with("origin", "main") + # Create a mock status object + status_mock = mock("status") + status_mock.stubs(:changed).returns({ "file1" => "modified" }) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git.stubs(:status).returns(status_mock) + + # Mock branches collection + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git.stubs(:branches).returns(branches_collection) + + # Mock the current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git.stubs(:branch).returns(current_branch_mock) + + # Track the operations performed + operations = [] + @git.expects(:fetch).tap { operations << :fetch } + @git.expects(:reset_hard).with("origin/main").tap { operations << :reset } + @git.expects(:config).with("user.name", "Jane Smith").tap { operations << :config_name } + @git.expects(:config).with("user.email", "jane@example.com").tap { operations << :config_email } + @git.expects(:add).with(all: true).tap { operations << :add } + @git.expects(:commit).with("Test commit").tap { operations << :commit } + @git.expects(:push).with("origin", "main").tap { operations << :push } @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(true) @repo.commit_changes(message: "Test commit", author: @user) + + # Assert all operations were performed in the correct order + expected_operations = [ :fetch, :reset, :config_name, :config_email, :add, :commit, :push ] + assert_equal expected_operations, operations, "Git operations were not performed in the expected order" + + # Assert repository state + assert File.exist?(@repo_path), "Repository directory does not exist" + assert File.exist?(File.join(@repo_path, ".git")), "Git directory does not exist" end test "creates new local repository when no repository exists" do From d6fb5e9c9955dbfaed8810b0a5c57bda56226caf Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Tue, 17 Dec 2024 15:33:56 +0100 Subject: [PATCH 15/20] Fixed git_repo_test --- test/services/git_repo_test.rb | 117 ++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/test/services/git_repo_test.rb b/test/services/git_repo_test.rb index fc52188..1c5e2a7 100644 --- a/test/services/git_repo_test.rb +++ b/test/services/git_repo_test.rb @@ -93,30 +93,74 @@ class GitRepoTest < ActiveSupport::TestCase end test "creates new local repository when no repository exists" do + git_operations = [] + + # Clear any existing stubs from setup + File.unstub(:exist?) + + # Set up file existence checks + File.stubs(:exist?).returns(false) # Default to false for all paths File.stubs(:exist?).with(@repo_path).returns(false) File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(false) - @mock_client.expects(:repository?).with("#{@user.github_username}/#{@repo_name}").returns(false).twice - @mock_client.expects(:create_repository).with( - @repo_name, - private: false, - description: "Repository created via railsnew.io" - ).returns(true) + # Mock git branches and status (for later use) + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git.stubs(:branches).returns(branches_collection) + + status_mock = mock("status") + status_mock.stubs(:changed).returns({ "file1" => "modified" }) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git.stubs(:status).returns(status_mock) + + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git.stubs(:branch).returns(current_branch_mock) + + # Set up GitHub API expectations + # We expect two checks for remote_repo_exists? - both should return false initially + @mock_client.stubs(:repository?) + .with("#{@user.github_username}/#{@repo_name}") + .returns(false) + .then.returns(false) + .then.returns(true) # After creation + + @mock_client.expects(:create_repository) + .with(@repo_name, private: false, description: "Repository created via railsnew.io") + .returns(true) + + # Track git operations + @git.expects(:config).with("init.templateDir", "").tap { |_| git_operations << "config_template" } + @git.expects(:config).with("init.defaultBranch", "main").tap { |_| git_operations << "config_branch" } + @git.expects(:config).with("user.name", "Jane Smith").tap { |_| git_operations << "config_name" } + @git.expects(:config).with("user.email", "jane@example.com").tap { |_| git_operations << "config_email" } + @git.expects(:add).with(all: true).tap { |_| git_operations << "add" } + @git.expects(:commit).with("Test commit").tap { |_| git_operations << "commit" } + @git.expects(:add_remote).with( + "origin", + "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git" + ).tap { |_| git_operations << "add_remote" } + @git.expects(:push).with("origin", "main").tap { |_| git_operations << "push" } - @git.expects(:config).with("init.templateDir", "") - @git.expects(:config).with("init.defaultBranch", "main") - @git.expects(:config).with("user.name", "Jane Smith") - @git.expects(:config).with("user.email", "jane@example.com") - @git.expects(:add).with(all: true) - @git.expects(:commit).with("Test commit") - @git.expects(:add_remote).with("origin", "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git") - @git.expects(:push).with("origin", "main") - FileUtils.expects(:mkdir_p).with(File.dirname(@repo_path)) - FileUtils.stubs(:rm_rf).with(@repo_path) - FileUtils.expects(:mkdir_p).with(@repo_path) @repo.commit_changes(message: "Test commit", author: @user) + + # Assert operations happened in correct order + expected_operations = [ + "config_template", + "config_branch", + "config_name", + "config_email", + "add", + "commit", + "add_remote", + "push" + ] + assert_equal expected_operations, git_operations, "Git operations were not performed in the expected order" end test "handles GitHub API errors when checking repository existence" do @@ -145,17 +189,42 @@ class GitRepoTest < ActiveSupport::TestCase File.stubs(:exist?).with(@repo_path).returns(true) File.stubs(:exist?).with(File.join(@repo_path, ".git")).returns(true) - @git.expects(:fetch) - @git.expects(:reset_hard).with("origin/main") - @git.expects(:config).with("user.name", @user.github_username) - @git.expects(:config).with("user.email", "#{@user.github_username}@users.noreply.github.com") - @git.expects(:add).with(all: true) - @git.expects(:commit).with("Test commit") - @git.expects(:push).with("origin", "main") + # Mock git branches and status + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git.stubs(:branches).returns(branches_collection) + + # Mock git status + status_mock = mock("status") + status_mock.stubs(:changed).returns({ "file1" => "modified" }) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git.stubs(:status).returns(status_mock) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git.stubs(:branch).returns(current_branch_mock) + + # Track operations + operations = [] + @git.expects(:fetch).tap { operations << :fetch } + @git.expects(:reset_hard).with("origin/main").tap { operations << :reset } + @git.expects(:config).with("user.name", @user.github_username).tap { operations << :config_name } + @git.expects(:config).with("user.email", "#{@user.github_username}@users.noreply.github.com").tap { operations << :config_email } + @git.expects(:add).with(all: true).tap { operations << :add } + @git.expects(:commit).with("Test commit").tap { operations << :commit } + @git.expects(:push).with("origin", "main").tap { operations << :push } @mock_client.expects(:repository?).returns(true) @repo.commit_changes(message: "Test commit", author: @user) + + # Assert operations happened in correct order + expected_operations = [ :fetch, :reset, :config_name, :config_email, :add, :commit, :push ] + assert_equal expected_operations, operations, "Git operations were not performed in the expected order" end test "ensures committable state by creating README.md" do From d15fc9770826de489ac9da91421d5bb047eb8840 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Tue, 17 Dec 2024 15:36:35 +0100 Subject: [PATCH 16/20] Fix git_repo_clone_test --- test/services/git_repo_clone_test.rb | 39 ++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/test/services/git_repo_clone_test.rb b/test/services/git_repo_clone_test.rb index 0132fc5..d246b81 100644 --- a/test/services/git_repo_clone_test.rb +++ b/test/services/git_repo_clone_test.rb @@ -30,16 +30,35 @@ class GitRepoCloneTest < ActiveSupport::TestCase File.stubs(:exist?).with(@repo_path).returns(false) @mock_client.stubs(:repository?).returns(true) - branch_mock = mock("branch") - branch_mock.stubs(:name).returns("main") + # Mock git status + status_mock = mock("status") + status_mock.stubs(:changed).returns({ "file1" => "modified" }) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + # Mock git branches + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + + # Set up cloned git mock with all required stubs cloned_git = mock("cloned_git") - cloned_git.stubs(:branch).returns(branch_mock) - cloned_git.expects(:config).with("user.name", "Jane Smith") - cloned_git.expects(:config).with("user.email", "jane@example.com") - cloned_git.expects(:add).with(all: true) - cloned_git.expects(:commit).with("Test commit") - cloned_git.expects(:push).with("origin", "main") + cloned_git.stubs(:branch).returns(current_branch_mock) + cloned_git.stubs(:branches).returns(branches_collection) + cloned_git.stubs(:status).returns(status_mock) + + # Track operations + operations = [] + cloned_git.expects(:config).with("user.name", "Jane Smith").tap { operations << :config_name } + cloned_git.expects(:config).with("user.email", "jane@example.com").tap { operations << :config_email } + cloned_git.expects(:add).with(all: true).tap { operations << :add } + cloned_git.expects(:commit).with("Test commit").tap { operations << :commit } + cloned_git.expects(:push).with("origin", "main").tap { operations << :push } Git.expects(:clone).with( "https://fake-token@github.com/#{@user.github_username}/#{@repo_name}.git", @@ -49,5 +68,9 @@ class GitRepoCloneTest < ActiveSupport::TestCase Git.expects(:open).with(@repo_path).returns(cloned_git) @repo.commit_changes(message: "Test commit", author: @user) + + # Assert operations happened in correct order + expected_operations = [ :config_name, :config_email, :add, :commit, :push ] + assert_equal expected_operations, operations, "Git operations were not performed in the expected order" end end From 32b8f3af08db2a1aaacd8189cfe629f73901786d Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Tue, 17 Dec 2024 16:09:11 +0100 Subject: [PATCH 17/20] 100% test coverage - except app_generation_job, which is postponed for now --- test/controllers/github_controller_test.rb | 110 +++++++++++++++ test/services/data_repository_test.rb | 152 +++++++++++++++++---- 2 files changed, 239 insertions(+), 23 deletions(-) create mode 100644 test/controllers/github_controller_test.rb diff --git a/test/controllers/github_controller_test.rb b/test/controllers/github_controller_test.rb new file mode 100644 index 0000000..7a5b94b --- /dev/null +++ b/test/controllers/github_controller_test.rb @@ -0,0 +1,110 @@ +require "test_helper" + +class GithubControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:jane) + @user.stubs(:github_username).returns("jane_smith") + sign_in(@user) + end + + test "returns success when repository does not exist" do + validator = mock("validator") + validator.expects(:repo_exists?).returns(false) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + get check_github_name_path, params: { name: "test-repo" } + + assert_response :success + assert_equal({ "available" => true }, response.parsed_body) + end + + test "returns success when repository exists" do + validator = mock("validator") + validator.expects(:repo_exists?).returns(true) + GithubRepositoryNameValidator.expects(:new) + .with("existing-repo", "jane_smith") + .returns(validator) + + get check_github_name_path, params: { name: "existing-repo" } + + assert_response :success + assert_equal({ "available" => false }, response.parsed_body) + end + + test "handles unauthorized GitHub access" do + validator = mock("validator") + validator.expects(:repo_exists?).raises(Octokit::Unauthorized) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + get check_github_name_path, params: { name: "test-repo" } + + assert_response :unauthorized + assert_equal({ "error" => "GitHub authentication failed" }, response.parsed_body) + end + + test "handles forbidden GitHub access" do + validator = mock("validator") + validator.expects(:repo_exists?).raises(Octokit::Forbidden) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + get check_github_name_path, params: { name: "test-repo" } + + assert_response :unauthorized + assert_equal({ "error" => "GitHub authentication failed" }, response.parsed_body) + end + + test "handles other GitHub errors" do + validator = mock("validator") + validator.expects(:repo_exists?).raises(Octokit::Error) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + get check_github_name_path, params: { name: "test-repo" } + + assert_response :unprocessable_entity + assert_equal({ "error" => "Could not validate repository name" }, response.parsed_body) + end + + test "requires authentication" do + sign_out(@user) + + get check_github_name_path, params: { name: "test-repo" } + + assert_response :redirect + assert_redirected_to root_path + assert_equal "Please sign in first.", flash[:alert] + end + + test "logs errors when GitHub authentication fails" do + validator = mock("validator") + error = Octokit::Unauthorized.new + validator.expects(:repo_exists?).raises(error) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + Rails.logger.expects(:error).with("GitHub authentication error: #{error.message}") + + get check_github_name_path, params: { name: "test-repo" } + end + + test "logs errors for other GitHub validation failures" do + validator = mock("validator") + error = Octokit::Error.new + validator.expects(:repo_exists?).raises(error) + GithubRepositoryNameValidator.expects(:new) + .with("test-repo", "jane_smith") + .returns(validator) + + Rails.logger.expects(:error).with("GitHub validation error: #{error.message}") + + get check_github_name_path, params: { name: "test-repo" } + end +end diff --git a/test/services/data_repository_test.rb b/test/services/data_repository_test.rb index 307cde0..2f93ae1 100644 --- a/test/services/data_repository_test.rb +++ b/test/services/data_repository_test.rb @@ -48,28 +48,6 @@ def test_name_for_environment_raises_error_for_unknown_environment end # Instance method tests - def test_writes_generated_app_correctly - app = generated_apps(:blog_app) - expected_path = File.join(@repo.send(:repo_path), "generated_apps", app.id.to_s, "current_state.json") - expected_content = { - name: app.name, - recipe_id: app.recipe_id, - configuration: app.configuration_options - }.to_json - - File.expects(:write).with(expected_path, expected_content) - - # Also expect the history.json write - history_path = File.join(@repo.send(:repo_path), "generated_apps", app.id.to_s, "history.json") - File.expects(:write).with(history_path, app.app_changes.map(&:to_git_format).to_json) - - FileUtils.stubs(:mkdir_p) - @repo.stubs(:ensure_fresh_repo) - @repo.stubs(:push_to_remote) - - @repo.write_model(app) - end - def test_writes_ingredient_correctly ingredient = ingredients(:rails_authentication) base_path = File.join(@repo.send(:repo_path), "ingredients", ingredient.name.parameterize) @@ -122,6 +100,43 @@ def test_writes_recipe_correctly @repo.write_model(recipe) end + def test_raises_error_when_trying_to_write_generated_app + app = generated_apps(:blog_app) + + # Mock git branches and status + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git_mock.stubs(:branches).returns(branches_collection) + + # Mock git status + status_mock = mock("status") + status_mock.stubs(:changed).returns({}) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git_mock.stubs(:status).returns(status_mock) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git_mock.stubs(:branch).returns(current_branch_mock) + + # Mock git operations that happen in ensure_fresh_repo + @git_mock.stubs(:add).with(all: true) + @git_mock.stubs(:commit).with("Initialize repository structure") + + # Stub filesystem operations + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:touch) + File.stubs(:directory?).returns(false) # This triggers ensure_committable_state + File.stubs(:write) + + assert_raises(NotImplementedError, "Generated apps are stored in their own repositories") do + @repo.write_model(app) + end + end + def test_handles_git_push_error recipe = recipes(:blog_recipe) base_path = File.join(@repo.send(:repo_path), "recipes", recipe.id.to_s) @@ -144,7 +159,7 @@ def test_ensure_committable_state_creates_required_structure path = @repo.send(:repo_path) # Expect directory creation for each required directory - %w[generated_apps ingredients recipes].each do |dir| + %w[ingredients recipes].each do |dir| dir_path = File.join(path, dir) FileUtils.expects(:mkdir_p).with(dir_path) FileUtils.expects(:touch).with(File.join(dir_path, ".keep")) @@ -160,10 +175,33 @@ def test_ensure_committable_state_creates_required_structure def test_ensure_fresh_repo_syncs_with_remote sequence = sequence("git_sync") + # Mock git branches and status + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git_mock.stubs(:branches).returns(branches_collection) + + # Mock git status + status_mock = mock("status") + status_mock.stubs(:changed).returns({}) + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git_mock.stubs(:status).returns(status_mock) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git_mock.stubs(:branch).returns(current_branch_mock) + + # Set up sequence of operations @git_mock.expects(:fetch).in_sequence(sequence) @git_mock.expects(:reset_hard).with("origin/main").in_sequence(sequence) @git_mock.expects(:pull).in_sequence(sequence) + # Mock directory check to avoid initialization + File.stubs(:directory?).returns(true) + @repo.send(:ensure_fresh_repo) end @@ -171,6 +209,74 @@ def test_repository_description_is_specific_to_data_repo assert_equal "Data repository for rails-new.io", @repo.send(:repository_description) end + def test_ensure_fresh_repo_commits_and_pushes_when_changes_exist + # Mock git branches and status + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git_mock.stubs(:branches).returns(branches_collection) + + # Mock git status to indicate changes + status_mock = mock("status") + status_mock.stubs(:changed).returns({ "file1" => "modified" }) # Has changes + status_mock.stubs(:added).returns({}) + status_mock.stubs(:deleted).returns({}) + @git_mock.stubs(:status).returns(status_mock) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git_mock.stubs(:branch).returns(current_branch_mock) + + # Mock filesystem operations + FileUtils.stubs(:mkdir_p).returns(true) # Return true for all mkdir_p calls + FileUtils.stubs(:touch).returns(true) # Return true for all touch calls + File.stubs(:directory?).returns(false) # Trigger initialization + File.stubs(:write).returns(true) # Allow README.md creation + + # Expect git operations + @git_mock.expects(:add).with(all: true) + @git_mock.expects(:commit).with("Initialize repository structure") + @git_mock.expects(:push).with("origin", "main") + + @repo.send(:ensure_fresh_repo) + end + + def test_ensure_fresh_repo_skips_commit_when_no_changes + # Mock git branches and status + remote_branch = mock("remote_branch") + remote_branch.stubs(:name).returns("origin/main") + branches_collection = mock("branches_collection") + branches_collection.stubs(:remote).returns([ remote_branch ]) + @git_mock.stubs(:branches).returns(branches_collection) + + # Mock git status to indicate no changes + status_mock = mock("status") + status_mock.stubs(:changed).returns({}) # No changes + status_mock.stubs(:added).returns({}) # No additions + status_mock.stubs(:deleted).returns({}) + @git_mock.stubs(:status).returns(status_mock) + + # Mock current branch + current_branch_mock = mock("current_branch") + current_branch_mock.stubs(:name).returns("main") + @git_mock.stubs(:branch).returns(current_branch_mock) + + # Mock filesystem operations + FileUtils.stubs(:mkdir_p).returns(true) # Return true for all mkdir_p calls + FileUtils.stubs(:touch).returns(true) # Return true for all touch calls + File.stubs(:directory?).returns(false) # Trigger initialization + File.stubs(:write).returns(true) # Allow README.md creation + + # Expect git operations + @git_mock.expects(:add).with(all: true) # add is still called + @git_mock.expects(:commit).never # but commit should never happen + @git_mock.expects(:push).never # and push should never happen + + @repo.send(:ensure_fresh_repo) + end + private def stub_filesystem_operations From 692a4d6d67d7e56befea229c03974e9775200425 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Wed, 18 Dec 2024 05:54:46 +0100 Subject: [PATCH 18/20] make Zeitwerk happy --- app/services/app_generation/errors.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/app_generation/errors.rb b/app/services/app_generation/errors.rb index 5ac3784..c0c1805 100644 --- a/app/services/app_generation/errors.rb +++ b/app/services/app_generation/errors.rb @@ -1,3 +1,5 @@ module AppGeneration - class InvalidStateError < StandardError; end + module Errors + class InvalidStateError < StandardError; end + end end From 1585ef628af22de54dcbc999668038239ec77153 Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Wed, 18 Dec 2024 05:54:46 +0100 Subject: [PATCH 19/20] make Zeitwerk happy --- app/services/app_generation/orchestrator.rb | 6 ++++-- test/services/app_generation/orchestrator_test.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/app_generation/orchestrator.rb b/app/services/app_generation/orchestrator.rb index af81adc..294fb14 100644 --- a/app/services/app_generation/orchestrator.rb +++ b/app/services/app_generation/orchestrator.rb @@ -11,7 +11,7 @@ def call AppGenerationJob.perform_later(@generated_app.id) true - rescue AppGeneration::InvalidStateError + rescue AppGeneration::Errors::InvalidStateError raise rescue StandardError => e @logger.error("Failed to start app generation", { error: e.message }) @@ -22,7 +22,9 @@ def call private def validate_initial_state! - raise AppGeneration::InvalidStateError, "App must be in pending state to start generation" unless @generated_app.pending? + return if @generated_app.pending? + + raise AppGeneration::Errors::InvalidStateError, "App must be in pending state to start generation" end end end diff --git a/test/services/app_generation/orchestrator_test.rb b/test/services/app_generation/orchestrator_test.rb index a90b47d..96d3d61 100644 --- a/test/services/app_generation/orchestrator_test.rb +++ b/test/services/app_generation/orchestrator_test.rb @@ -27,7 +27,7 @@ class OrchestratorTest < ActiveSupport::TestCase assert_equal "generating", @generated_app.status assert_not @generated_app.pending? - error = assert_raises(AppGeneration::InvalidStateError) do + error = assert_raises(AppGeneration::Errors::InvalidStateError) do @orchestrator.call end From 19d6755d213f0f42da61ad3db9b6b5166468981e Mon Sep 17 00:00:00 2001 From: Trinity Takei Date: Wed, 18 Dec 2024 06:07:40 +0100 Subject: [PATCH 20/20] Update gems --- Gemfile | 10 ++-- Gemfile.lock | 128 +++++++++++++++++++++++++-------------------------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Gemfile b/Gemfile index 0db8046..dd2026a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem "rails", "8.0.0.1" +gem "rails", "8.0.1" gem "acidic_job", "= 1.0.0.rc1" gem "aasm", "~> 5.5.0" @@ -11,7 +11,7 @@ gem "git", "~> 2.3.3" gem "kamal", "~> 2.4.0", require: false gem "thruster", "~> 0.1.9", require: false gem "mission_control-jobs", "~> 1.0.1" -gem "noticed", "~> 2.4.3" +gem "noticed", "~> 2.5.0" gem "litestream", "~> 0.12.0" gem "octokit", "~> 9.2.0" gem "omniauth-github", github: "omniauth/omniauth-github", branch: "master" @@ -20,11 +20,11 @@ gem "pagy", "~> 9.3.3" gem "phlex-rails", "~> 1.2.2" gem "propshaft", "~> 1.1.0" gem "puma", ">= 6.5.0" -gem "sentry-ruby", "~> 5.22.0" -gem "sentry-rails", "~> 5.22.0" +gem "sentry-ruby", "~> 5.22.1" +gem "sentry-rails", "~> 5.22.1" gem "stackprof" gem "solid_cache", "~> 1.0.6" -gem "solid_cable", "~> 3.0.4" +gem "solid_cable", "~> 3.0.5" gem "solid_queue", "~> 1.1.0" gem "sqlite3", "~> 2.4.1" gem "stimulus-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 85205c0..d2afcc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,29 +20,29 @@ GEM activerecord (>= 7.1) activesupport (>= 7.1) railties (>= 7.1) - actioncable (8.0.0.1) - actionpack (= 8.0.0.1) - activesupport (= 8.0.0.1) + actioncable (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.0.1) - actionpack (= 8.0.0.1) - activejob (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionmailbox (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) - actionmailer (8.0.0.1) - actionpack (= 8.0.0.1) - actionview (= 8.0.0.1) - activejob (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionmailer (8.0.1) + actionpack (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.0.1) - actionview (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -50,35 +50,35 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.0.1) - actionpack (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + actiontext (8.0.1) + actionpack (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.0.1) - activesupport (= 8.0.0.1) + actionview (8.0.1) + activesupport (= 8.0.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.0.1) - activesupport (= 8.0.0.1) + activejob (8.0.1) + activesupport (= 8.0.1) globalid (>= 0.3.6) - activemodel (8.0.0.1) - activesupport (= 8.0.0.1) - activerecord (8.0.0.1) - activemodel (= 8.0.0.1) - activesupport (= 8.0.0.1) + activemodel (8.0.1) + activesupport (= 8.0.1) + activerecord (8.0.1) + activemodel (= 8.0.1) + activesupport (= 8.0.1) timeout (>= 0.4.0) - activestorage (8.0.0.1) - actionpack (= 8.0.0.1) - activejob (= 8.0.0.1) - activerecord (= 8.0.0.1) - activesupport (= 8.0.0.1) + activestorage (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activesupport (= 8.0.1) marcel (~> 1.0) - activesupport (8.0.0.1) + activesupport (8.0.1) base64 benchmark (>= 0.3) bigdecimal @@ -272,7 +272,7 @@ GEM bigdecimal (~> 3.1) net-http (0.6.0) uri - net-imap (0.5.1) + net-imap (0.5.2) date net-protocol net-pop (0.1.2) @@ -299,7 +299,7 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) - noticed (2.4.3) + noticed (2.5.0) rails (>= 6.1.0) oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) @@ -362,20 +362,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (8.0.0.1) - actioncable (= 8.0.0.1) - actionmailbox (= 8.0.0.1) - actionmailer (= 8.0.0.1) - actionpack (= 8.0.0.1) - actiontext (= 8.0.0.1) - actionview (= 8.0.0.1) - activejob (= 8.0.0.1) - activemodel (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + rails (8.0.1) + actioncable (= 8.0.1) + actionmailbox (= 8.0.1) + actionmailer (= 8.0.1) + actionpack (= 8.0.1) + actiontext (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activemodel (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) bundler (>= 1.15.0) - railties (= 8.0.0.1) + railties (= 8.0.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -388,9 +388,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (8.0.0.1) - actionpack (= 8.0.0.1) - activesupport (= 8.0.0.1) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -448,10 +448,10 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.22.0) + sentry-rails (5.22.1) railties (>= 5.0) - sentry-ruby (~> 5.22.0) - sentry-ruby (5.22.0) + sentry-ruby (~> 5.22.1) + sentry-ruby (5.22.1) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) simplecov (0.22.0) @@ -466,7 +466,7 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) - solid_cable (3.0.4) + solid_cable (3.0.5) actioncable (>= 7.2) activejob (>= 7.2) activerecord (>= 7.2) @@ -574,7 +574,7 @@ DEPENDENCIES minio (~> 0.4.0) mission_control-jobs (~> 1.0.1) mocha (~> 2.7.1) - noticed (~> 2.4.3) + noticed (~> 2.5.0) octokit (~> 9.2.0) omniauth-github! omniauth-rails_csrf_protection @@ -583,15 +583,15 @@ DEPENDENCIES phlex-rails (~> 1.2.2) propshaft (~> 1.1.0) puma (>= 6.5.0) - rails (= 8.0.0.1) + rails (= 8.0.1) rails-erd (~> 1.7.2) rubocop-rails-omakase selenium-webdriver (~> 4.27.0) - sentry-rails (~> 5.22.0) - sentry-ruby (~> 5.22.0) + sentry-rails (~> 5.22.1) + sentry-ruby (~> 5.22.1) simplecov simplecov-tailwindcss - solid_cable (~> 3.0.4) + solid_cable (~> 3.0.5) solid_cache (~> 1.0.6) solid_queue (~> 1.1.0) sqlite3 (~> 2.4.1)