diff --git a/test/test_cli.rb b/test/test_cli.rb new file mode 100644 index 0000000..1711049 --- /dev/null +++ b/test/test_cli.rb @@ -0,0 +1,95 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' + +class CliTest < Minitest::Test + def setup + @workdir = Dir.mktmpdir('prism-cli') + @cli = Prism::Cli.new(@workdir) + end + + def teardown + FileUtils.remove_entry(@workdir) + end + + def test_new_app_scaffolds_expected_files + app_dir = @cli.new_app('blog') + assert File.directory?(File.join(app_dir, 'app', 'controllers')) + assert File.directory?(File.join(app_dir, 'app', 'models')) + assert File.directory?(File.join(app_dir, 'db', 'migrate')) + assert File.file?(File.join(app_dir, 'app.rb')) + assert File.file?(File.join(app_dir, 'config.ru')) + assert File.file?(File.join(app_dir, 'app', 'views', 'layouts', 'application.erb')) + assert File.executable?(File.join(app_dir, 'bin', 'server')) + end + + def test_new_app_boot_file_references_app_class + app_dir = @cli.new_app('blog') + content = File.read(File.join(app_dir, 'app.rb')) + assert_includes content, 'class Blog < Prism::Application' + end + + def test_new_app_refuses_to_overwrite_existing_directory + @cli.new_app('blog') + assert_raises(Prism::Cli::UsageError) { @cli.new_app('blog') } + end + + def test_generate_migration_for_create_table + path = @cli.generate_migration('create_posts', ['title:string', 'body:text']) + content = File.read(path) + assert_includes content, 'class CreatePosts < Prism::Orm::Migration' + assert_includes content, 'create_table :posts do |t|' + assert_includes content, 't.string :title' + assert_includes content, 't.text :body' + assert_includes content, 't.timestamps' + assert_includes content, 'drop_table :posts' + end + + def test_generate_migration_filename_has_version_prefix + path = @cli.generate_migration('create_posts', []) + assert_match(/\A\d{14}_create_posts\.rb\z/, File.basename(path)) + end + + def test_generate_migration_avoids_version_collisions_within_the_same_second + first = @cli.generate_migration('create_posts', []) + second = @cli.generate_migration('create_comments', []) + first_version = File.basename(first).split('_', 2).first + second_version = File.basename(second).split('_', 2).first + refute_equal first_version, second_version + end + + def test_generated_migration_actually_runs + path = @cli.generate_migration('create_widgets', ['name:string']) + store = Prism::Orm::Store.new(File.join(@workdir, 'test.pstore')) + Prism::Orm::Migrator.new(store, File.dirname(path)).migrate! + assert store.table_exists?(:widgets) + assert_includes store.columns(:widgets), 'name' + end + + def test_generate_model_creates_model_and_migration + model_path, migration_path = @cli.generate_model('post', ['title:string']) + assert_includes File.read(model_path), 'class Post < Prism::Orm::Model' + assert_includes File.read(migration_path), 'create_table :posts do |t|' + end + + def test_bin_prism_new_via_subprocess + lib_dir = File.expand_path('../lib', __dir__) + bin_path = File.expand_path('../bin/prism', __dir__) + ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) + output = IO.popen([ruby, "-I#{lib_dir}", bin_path, 'new', 'shop'], chdir: @workdir, err: [:child, :out], &:read) + status = $?.exitstatus + assert_equal 0, status, output + assert File.file?(File.join(@workdir, 'shop', 'app.rb')) + end + + def test_bin_prism_reports_usage_error_and_nonzero_exit + lib_dir = File.expand_path('../lib', __dir__) + bin_path = File.expand_path('../bin/prism', __dir__) + ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) + output = IO.popen([ruby, "-I#{lib_dir}", bin_path, 'bogus'], chdir: @workdir, err: [:child, :out], &:read) + status = $?.exitstatus + refute_equal 0, status + assert_includes output, 'unknown command' + end +end diff --git a/test/test_integration.rb b/test/test_integration.rb new file mode 100644 index 0000000..61c8181 --- /dev/null +++ b/test/test_integration.rb @@ -0,0 +1,53 @@ +require 'minitest/autorun' +require 'prism' +require 'net/http' + +class IntegrationApp < Prism::Application + get '/ping' do + json(pong: true) + end + + get '/hello/:name' do + json(hello: params['name']) + end + + post '/sum' do + numbers = JSON.parse(request.body) + json(sum: numbers['a'] + numbers['b']) + end +end + +class IntegrationTest < Minitest::Test + def setup + @server = Prism::Server.new(IntegrationApp, port: 0).start + @port = @server.port + end + + def teardown + @server.stop + end + + def test_port_is_ephemeral + assert @port.positive? + end + + def test_get_with_param + response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/hello/Ruby")) + assert_equal '200', response.code + assert_equal({ 'hello' => 'Ruby' }, JSON.parse(response.body)) + end + + def test_post_with_body + uri = URI("http://127.0.0.1:#{@port}/sum") + response = Net::HTTP.start(uri.host, uri.port) do |http| + http.post(uri.path, JSON.generate(a: 2, b: 40), 'Content-Type' => 'application/json') + end + assert_equal '200', response.code + assert_equal 42, JSON.parse(response.body)['sum'] + end + + def test_not_found_over_http + response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}/nope")) + assert_equal '404', response.code + end +end diff --git a/test/test_integration_orm.rb b/test/test_integration_orm.rb new file mode 100644 index 0000000..024a398 --- /dev/null +++ b/test/test_integration_orm.rb @@ -0,0 +1,88 @@ +require 'minitest/autorun' +require 'prism' +require 'net/http' +require 'tmpdir' +require 'fileutils' + +class IntegrationOrmTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-e2e-orm') + @db_path = File.join(@dir, 'blog.pstore') + @views_dir = File.join(@dir, 'views', 'e2e_posts') + FileUtils.mkdir_p(@views_dir) + File.write(File.join(@views_dir, 'index.erb'), <<~ERB) + + ERB + + Prism::Orm.connect(@db_path) + Prism::Orm.store.create_table(:e2e_posts, %w[id title body created_at updated_at]) + + views_dir = File.dirname(@views_dir) + post_class = Class.new(Prism::Orm::Model) do + table_name :e2e_posts + validates :title, presence: true + end + Object.const_set(:E2ePost, post_class) + + controller_class = Class.new(Prism::Controller) do + views_root views_dir + + define_method(:index) { render(:index, locals: { posts: E2ePost.order(:id) }) } + define_method(:create) do + post = E2ePost.create(title: params['title'], body: params['body']) + json({ id: post.id }, 201) + end + end + Object.const_set(:E2ePostsController, controller_class) + + app_class = Class.new(Prism::Application) do + get '/posts', to: 'e2e_posts#index' + post '/posts', to: 'e2e_posts#create' + end + Object.const_set(:E2eBlogApp, app_class) + end + + def teardown + @server&.stop + %i[E2ePost E2ePostsController E2eBlogApp].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } + FileUtils.remove_entry(@dir) + end + + def test_post_then_get_round_trip_and_survives_a_process_restart + @server = Prism::Server.new(E2eBlogApp, port: 0).start + port = @server.port + + create_response = Net::HTTP.start('127.0.0.1', port) do |http| + http.post('/posts', URI.encode_www_form(title: 'Hello Prism', body: 'first real post'), + 'Content-Type' => 'application/x-www-form-urlencoded') + end + assert_equal '201', create_response.code + created_id = JSON.parse(create_response.body)['id'] + refute_nil created_id + + listing_response = Net::HTTP.get_response(URI("http://127.0.0.1:#{port}/posts")) + assert_equal '200', listing_response.code + assert_includes listing_response.body, 'Hello Prism: first real post' + + @server.stop + @server = nil + + # Simulate a process restart: reconnect to the same file with a brand new + # Store instance (no shared in-memory state with the writer above) and + # boot a second, independent server against it. + Prism::Orm.connect(@db_path) + restarted_server = Prism::Server.new(E2eBlogApp, port: 0).start + begin + restarted_response = Net::HTTP.get_response(URI("http://127.0.0.1:#{restarted_server.port}/posts")) + assert_equal '200', restarted_response.code + assert_includes restarted_response.body, 'Hello Prism: first real post', + 'data inserted before the restart must still render after reconnecting to the store file' + ensure + restarted_server.stop + end + end +end