diff --git a/app/controllers/repp/v1/certificates/p12_controller.rb b/app/controllers/repp/v1/certificates/p12_controller.rb new file mode 100644 index 000000000..adef956b4 --- /dev/null +++ b/app/controllers/repp/v1/certificates/p12_controller.rb @@ -0,0 +1,29 @@ +module Repp + module V1 + module Certificates + class P12Controller < BaseController + load_and_authorize_resource param_method: :cert_params + + THROTTLED_ACTIONS = %i[create].freeze + include Shunter::Integration::Throttle + + api :POST, '/repp/v1/certificates/p12' + desc 'Generate a P12 certificate' + def create + api_user_id = cert_params[:api_user_id] + render_error(I18n.t('errors.messages.not_found'), :not_found) and return if api_user_id.blank? + + api_user = current_user.registrar.api_users.find(api_user_id) + certificate = Certificate.generate_for_api_user(api_user: api_user) + render_success(data: { certificate: certificate }) + end + + private + + def cert_params + params.require(:certificate).permit(:api_user_id) + end + end + end + end +end diff --git a/app/models/api_user.rb b/app/models/api_user.rb index abb266b4e..d6572999e 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -20,7 +20,6 @@ class ApiUser < User # TODO: should have max request limit per day? belongs_to :registrar has_many :certificates - has_many :user_certificates validates :username, :plain_text_password, :registrar, :roles, presence: true validates :plain_text_password, length: { minimum: min_password_length } diff --git a/app/models/certificate.rb b/app/models/certificate.rb index 22a865cc7..62bb7fc89 100644 --- a/app/models/certificate.rb +++ b/app/models/certificate.rb @@ -52,6 +52,24 @@ class Certificate < ApplicationRecord @p_csr ||= OpenSSL::X509::Request.new(csr) if csr end + def parsed_private_key + return nil if private_key.blank? + + decoded_key = Base64.decode64(private_key) + OpenSSL::PKey::RSA.new(decoded_key, Certificates::CertificateGenerator::CA_PASSWORD) + rescue OpenSSL::PKey::RSAError + nil + end + + def parsed_p12 + return nil if p12.blank? + + decoded_p12 = Base64.decode64(p12) + OpenSSL::PKCS12.new(decoded_p12) + rescue OpenSSL::PKCS12::PKCS12Error + nil + end + def revoked? status == REVOKED end @@ -101,6 +119,55 @@ class Certificate < ApplicationRecord handle_revocation_failure(err_output) end + def renewable? + return false if revoked? + return false if crt.blank? + return false if expires_at.blank? + + expires_at > Time.current && expires_at <= 30.days.from_now + end + + def expired? + return false if revoked? + return false if crt.blank? + return false if expires_at.blank? + + expires_at < Time.current + end + + def renew + raise "Certificate cannot be renewed" unless renewable? + + generator = Certificates::CertificateGenerator.new( + username: api_user.username, + registrar_code: api_user.registrar_code, + registrar_name: api_user.registrar_name, + certificate: self + ) + + generator.renew_certificate + end + + def self.generate_for_api_user(api_user:) + generator = Certificates::CertificateGenerator.new( + username: api_user.username, + registrar_code: api_user.registrar_code, + registrar_name: api_user.registrar_name + ) + + cert_data = generator.call + + create!( + api_user: api_user, + interface: 'api', + private_key: Base64.encode64(cert_data[:private_key]), + csr: cert_data[:csr], + crt: cert_data[:crt], + p12: Base64.encode64(cert_data[:p12]), + expires_at: cert_data[:expires_at] + ) + end + private def certificate_origin diff --git a/app/models/user_certificate.rb b/app/models/user_certificate.rb deleted file mode 100644 index 0e0496cd4..000000000 --- a/app/models/user_certificate.rb +++ /dev/null @@ -1,54 +0,0 @@ -class UserCertificate < ApplicationRecord - belongs_to :user - - validates :user, presence: true - validates :private_key, presence: true - - enum status: { - pending: 'pending', - active: 'active', - revoked: 'revoked' - } - - def renewable? - return false unless certificate.present? - return false if revoked? - - expires_at.present? && expires_at < 30.days.from_now - end - - def expired? - return false unless certificate.present? - return false if revoked? - - expires_at.present? && expires_at < Time.current - end - - def renew - raise "Certificate cannot be renewed" unless renewable? - - generator = Certificates::CertificateGenerator.new( - username: user.username, - registrar_code: user.registrar_code, - registrar_name: user.registrar_name, - user_certificate: self - ) - - generator.renew_certificate - end - - def self.generate_certificates_for_api_user(api_user:) - cert = UserCertificate.create!( - user: api_user, - status: 'pending', - private_key: '' - ) - - Certificates::CertificateGenerator.new( - username: api_user.username, - registrar_code: api_user.registrar_code, - registrar_name: api_user.registrar_name, - user_certificate: cert - ).call - end -end diff --git a/app/services/certificates/certificate_generator.rb b/app/services/certificates/certificate_generator.rb index 9425684d3..af297f6c3 100644 --- a/app/services/certificates/certificate_generator.rb +++ b/app/services/certificates/certificate_generator.rb @@ -3,7 +3,6 @@ module Certificates attribute :username, Types::Strict::String attribute :registrar_code, Types::Coercible::String attribute :registrar_name, Types::Strict::String - attribute :user_certificate, Types.Instance(UserCertificate) CERTS_PATH = Rails.root.join('certs') CA_PATH = CERTS_PATH.join('ca') @@ -26,15 +25,23 @@ module Certificates def call csr, key = generate_csr_and_key - certificate = sign_certificate(csr) - create_p12(key, certificate) + cert = sign_certificate(csr) + p12 = create_p12(key, cert) + + { + private_key: key.export(OpenSSL::Cipher.new('AES-256-CBC'), CA_PASSWORD), + csr: csr.to_pem, + crt: cert.to_pem, + p12: p12.to_der, + expires_at: cert.not_after + } end + private + def generate_csr_and_key key = OpenSSL::PKey::RSA.new(4096) - encrypted_key = key.export(OpenSSL::Cipher.new('AES-256-CBC'), CA_PASSWORD) - save_private_key(encrypted_key) - + request = OpenSSL::X509::Request.new request.version = 0 request.subject = OpenSSL::X509::Name.new([ @@ -47,12 +54,7 @@ module Certificates request.sign(key, OpenSSL::Digest::SHA256.new) save_csr(request) - - user_certificate&.update!( - private_key: encrypted_key, - csr: request.to_pem, - status: 'pending' - ) + save_private_key(key.export(OpenSSL::Cipher.new('AES-256-CBC'), CA_PASSWORD)) [request, key] end @@ -80,64 +82,35 @@ module Certificates cert.add_extension(extension_factory.create_extension("subjectKeyIdentifier", "hash")) cert.sign(ca_key, OpenSSL::Digest::SHA256.new) - save_certificate(cert) - user_certificate&.update!( - certificate: cert.to_pem, - status: 'active', - expires_at: cert.not_after - ) - cert end - def create_p12(key, certificate, password = nil) + def create_p12(key, cert) ca_cert = OpenSSL::X509::Certificate.new(File.read(CA_CERT_PATH)) p12 = OpenSSL::PKCS12.create( - password, # P12 password (optional) - username, # Friendly name - key, # User's private key - certificate, # User's certificate - [ca_cert], # Chain of certificates + nil, # password + username, + key, + cert, + [ca_cert] ) File.open(CERTS_PATH.join(USER_P12_NAME), 'wb') do |file| file.write(p12.to_der) end - user_certificate&.update!( - p12: p12.to_der, - p12_password_digest: password ? BCrypt::Password.create(password) : nil - ) - p12 end - def renew_certificate - raise "Certificate not found" unless user_certificate&.certificate.present? - raise "Cannot renew revoked certificate" if user_certificate.revoked? - - # Используем существующий CSR - csr = OpenSSL::X509::Request.new(user_certificate.csr) - - # Создаем новый сертификат - certificate = sign_certificate(csr) - - # Создаем новый P12 с существующим ключом - key = OpenSSL::PKey::RSA.new(user_certificate.private_key, CA_PASSWORD) - create_p12(key, certificate) - end - private def ensure_directories_exist FileUtils.mkdir_p(CERTS_PATH) FileUtils.mkdir_p(CA_PATH.join('certs')) FileUtils.mkdir_p(CA_PATH.join('private')) - - # Set proper permissions for private directory FileUtils.chmod(0700, CA_PATH.join('private')) end diff --git a/config/routes.rb b/config/routes.rb index 3a1c538a8..50e72511b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -118,7 +118,11 @@ Rails.application.routes.draw do end end resources :white_ips, only: %i[index show update create destroy] - resources :certificates, only: %i[create] + resources :certificates, only: %i[create] do + scope module: :certificates do + post 'p12', to: 'p12#create', on: :collection + end + end namespace :registrar do resources :notifications, only: %i[index show update] do collection do diff --git a/db/migrate/20250218115707_create_user_certificates.rb b/db/migrate/20250218115707_create_user_certificates.rb deleted file mode 100644 index 054503598..000000000 --- a/db/migrate/20250218115707_create_user_certificates.rb +++ /dev/null @@ -1,19 +0,0 @@ -class CreateUserCertificates < ActiveRecord::Migration[6.1] - def change - create_table :user_certificates do |t| - t.references :user, null: false, foreign_key: true - t.binary :private_key, null: false - t.text :csr - t.text :certificate - t.binary :p12 - t.string :status - t.datetime :expires_at - t.datetime :revoked_at - t.string :p12_password_digest - - t.timestamps - end - - add_index :user_certificates, [:user_id, :status] - end -end diff --git a/db/migrate/20250219102811_add_p12_fields_to_certificates.rb b/db/migrate/20250219102811_add_p12_fields_to_certificates.rb new file mode 100644 index 000000000..5179092d5 --- /dev/null +++ b/db/migrate/20250219102811_add_p12_fields_to_certificates.rb @@ -0,0 +1,8 @@ +class AddP12FieldsToCertificates < ActiveRecord::Migration[6.1] + def change + add_column :certificates, :private_key, :binary + add_column :certificates, :p12, :binary + add_column :certificates, :p12_password_digest, :string + add_column :certificates, :expires_at, :timestamp + end +end diff --git a/db/structure.sql b/db/structure.sql index 21c916ad1..68869987f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -587,7 +587,11 @@ CREATE TABLE public.certificates ( common_name character varying, md5 character varying, interface character varying, - revoked boolean DEFAULT false NOT NULL + revoked boolean DEFAULT false NOT NULL, + private_key bytea, + p12 bytea, + p12_password_digest character varying, + expires_at timestamp without time zone ); @@ -2812,45 +2816,6 @@ CREATE SEQUENCE public.settings_id_seq ALTER SEQUENCE public.settings_id_seq OWNED BY public.settings.id; --- --- Name: user_certificates; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.user_certificates ( - id bigint NOT NULL, - user_id bigint NOT NULL, - private_key bytea NOT NULL, - csr text, - certificate text, - p12 bytea, - status character varying, - expires_at timestamp without time zone, - revoked_at timestamp without time zone, - p12_password_digest character varying, - created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL -); - - --- --- Name: user_certificates_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.user_certificates_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: user_certificates_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.user_certificates_id_seq OWNED BY public.user_certificates.id; - - -- -- Name: users; Type: TABLE; Schema: public; Owner: - -- @@ -3551,13 +3516,6 @@ ALTER TABLE ONLY public.setting_entries ALTER COLUMN id SET DEFAULT nextval('pub ALTER TABLE ONLY public.settings ALTER COLUMN id SET DEFAULT nextval('public.settings_id_seq'::regclass); --- --- Name: user_certificates id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_certificates ALTER COLUMN id SET DEFAULT nextval('public.user_certificates_id_seq'::regclass); - - -- -- Name: users id; Type: DEFAULT; Schema: public; Owner: - -- @@ -4256,14 +4214,6 @@ ALTER TABLE ONLY public.zones ADD CONSTRAINT unique_zone_origin UNIQUE (origin); --- --- Name: user_certificates user_certificates_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_certificates - ADD CONSTRAINT user_certificates_pkey PRIMARY KEY (id); - - -- -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4949,20 +4899,6 @@ CREATE UNIQUE INDEX index_setting_entries_on_code ON public.setting_entries USIN CREATE UNIQUE INDEX index_settings_on_thing_type_and_thing_id_and_var ON public.settings USING btree (thing_type, thing_id, var); --- --- Name: index_user_certificates_on_user_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_user_certificates_on_user_id ON public.user_certificates USING btree (user_id); - - --- --- Name: index_user_certificates_on_user_id_and_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_user_certificates_on_user_id_and_status ON public.user_certificates USING btree (user_id, status); - - -- -- Name: index_users_on_identity_code; Type: INDEX; Schema: public; Owner: - -- @@ -5108,14 +5044,6 @@ ALTER TABLE ONLY public.domains ADD CONSTRAINT domains_registrar_id_fk FOREIGN KEY (registrar_id) REFERENCES public.registrars(id); --- --- Name: user_certificates fk_rails_03b0a0c9d8; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_certificates - ADD CONSTRAINT fk_rails_03b0a0c9d8 FOREIGN KEY (user_id) REFERENCES public.users(id); - - -- -- Name: invoices fk_rails_242b91538b; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5795,6 +5723,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20241129095711'), ('20241206085817'), ('20250204094550'), -('20250218115707'); +('20250219102811'); diff --git a/lib/serializers/repp/certificate.rb b/lib/serializers/repp/certificate.rb index 680117248..df8d77739 100644 --- a/lib/serializers/repp/certificate.rb +++ b/lib/serializers/repp/certificate.rb @@ -8,16 +8,38 @@ module Serializers end def to_json(obj = certificate) - json = obj.as_json.except('csr', 'crt') + json = obj.as_json.except('csr', 'crt', 'private_key', 'p12') csr = obj.parsed_csr crt = obj.parsed_crt + p12 = obj.parsed_p12 + private_key = obj.parsed_private_key + + json[:private_key] = private_key_data(private_key) if private_key + json[:p12] = p12_data(obj) if obj.p12.present? + json[:expires_at] = obj.expires_at if obj.expires_at.present? + json[:csr] = csr_data(csr) if csr json[:crt] = crt_data(crt) if crt + json end private + def private_key_data(key) + { + body: key.to_pem, + type: 'RSA PRIVATE KEY' + } + end + + def p12_data(obj) + { + body: obj.p12, + type: 'PKCS12' + } + end + def csr_data(csr) { version: csr.version, diff --git a/test/fixtures/files/user.crt b/test/fixtures/files/user.crt new file mode 100644 index 000000000..3e0f349cb --- /dev/null +++ b/test/fixtures/files/user.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUBgtGh4Pw8Luqq/HG4tqG3oIzfHIwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMTkxMjAwMDBaFw0yNTAy +MTkxMjAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDUVURLKdmhmEht7yz3MeQQtn9kMIaIzZDwggZvUg6J +5PlTabEixVfPzlRJixJBj37hh0Ree6mr19KECtPymy1L9U3oGfF18CJhdzc= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/fixtures/user_certificates.yml b/test/fixtures/user_certificates.yml deleted file mode 100644 index ef90cf4be..000000000 --- a/test/fixtures/user_certificates.yml +++ /dev/null @@ -1,7 +0,0 @@ -one: - user: api_bestnames - private_key: "encrypted_private_key_data" - csr: "dummy_csr_data" - certificate: "dummy_certificate_data" - status: "active" - expires_at: <%= 60.days.from_now %> diff --git a/test/models/certificate_test.rb b/test/models/certificate_test.rb index d83e6f8fd..a2be62633 100644 --- a/test/models/certificate_test.rb +++ b/test/models/certificate_test.rb @@ -3,7 +3,22 @@ require 'test_helper' class CertificateTest < ActiveSupport::TestCase setup do @certificate = certificates(:api) - @certificate.update!(csr: "-----BEGIN CERTIFICATE REQUEST-----\nMIICszCCAZsCAQAwbjELMAkGA1UEBhMCRUUxFDASBgNVBAMMC2ZyZXNoYm94LmVl\nMRAwDgYDVQQHDAdUYWxsaW5uMREwDwYDVQQKDAhGcmVzaGJveDERMA8GA1UECAwI\nSGFyanVtYWExETAPBgNVBAsMCEZyZXNoYm94MIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA1VVESynZoZhIbe8s9zHkELZ/ZDCGiM2Q8IIGb1IOieT5U2mx\nIsVXz85USYsSQY9+4YdEXnupq9fShArT8pstS/VN6BnxdfAiYXc3UWWAuaYAdNGJ\nDr5Jf6uMt1wVnCgoDL7eJq9tWMwARC/viT81o92fgqHFHW0wEolfCmnpik9o0ACD\nFiWZ9IBIevmFqXtq25v9CY2cT9+eZW127WtJmOY/PKJhzh0QaEYHqXTHWOLZWpnp\nHH4elyJ2CrFulOZbHPkPNB9Nf4XQjzk1ffoH6e5IVys2VV5xwcTkF0jY5XTROVxX\nlR2FWqic8Q2pIhSks48+J6o1GtXGnTxv94lSDwIDAQABoAAwDQYJKoZIhvcNAQEL\nBQADggEBAEFcYmQvcAC8773eRTWBJJNoA4kRgoXDMYiiEHih5iJPVSxfidRwYDTF\nsP+ttNTUg3JocFHY75kuM9T2USh+gu/trRF0o4WWa+AbK3JbbdjdT1xOMn7XtfUU\nZ/f1XCS9YdHQFCA6nk4Z+TLWwYsgk7n490AQOiB213fa1UIe83qIfw/3GRqRUZ7U\nwIWEGsHED5WT69GyxjyKHcqGoV7uFnqFN0sQVKVTy/NFRVQvtBUspCbsOirdDRie\nAB2KbGHL+t1QrRF10szwCJDyk5aYlVhxvdI8zn010nrxHkiyQpDFFldDMLJl10BW\n2w9PGO061z+tntdRcKQGuEpnIr9U5Vs=\n-----END CERTIFICATE REQUEST-----\n") + @valid_crt = <<~CRT + -----BEGIN CERTIFICATE----- + MIIDazCCAlOgAwIBAgIUBgtGh4Pw8Luqq/HG4tqG3oIzfHIwDQYJKoZIhvcNAQEL + BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMTkxMjAwMDBaFw0yNTAy + MTkxMjAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw + HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQDUVURLKdmhmEht7yz3MeQQtn9kMIaIzZDwggZvUg6J + 5PlTabEixVfPzlRJixJBj37hh0Ree6mr19KECtPymy1L9U3oGfF18CJhdzc= + -----END CERTIFICATE----- + CRT + + @certificate.update!( + csr: "-----BEGIN CERTIFICATE REQUEST-----\nMIICszCCAZsCAQAwbjELMAkGA1UEBhMCRUUxFDASBgNVBAMMC2ZyZXNoYm94LmVl\nMRAwDgYDVQQHDAdUYWxsaW5uMREwDwYDVQQKDAhGcmVzaGJveDERMA8GA1UECAwI\nSGFyanVtYWExETAPBgNVBAsMCEZyZXNoYm94MIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA1VVESynZoZhIbe8s9zHkELZ/ZDCGiM2Q8IIGb1IOieT5U2mx\nIsVXz85USYsSQY9+4YdEXnupq9fShArT8pstS/VN6BnxdfAiYXc3UWWAuaYAdNGJ\nDr5Jf6uMt1wVnCgoDL7eJq9tWMwARC/viT81o92fgqHFHW0wEolfCmnpik9o0ACD\nFiWZ9IBIevmFqXtq25v9CY2cT9+eZW127WtJmOY/PKJhzh0QaEYHqXTHWOLZWpnp\nHH4elyJ2CrFulOZbHPkPNB9Nf4XQjzk1ffoH6e5IVys2VV5xwcTkF0jY5XTROVxX\nlR2FWqic8Q2pIhSks48+J6o1GtXGnTxv94lSDwIDAQABoAAwDQYJKoZIhvcNAQEL\nBQADggEBAEFcYmQvcAC8773eRTWBJJNoA4kRgoXDMYiiEHih5iJPVSxfidRwYDTF\nsP+ttNTUg3JocFHY75kuM9T2USh+gu/trRF0o4WWa+AbK3JbbdjdT1xOMn7XtfUU\nZ/f1XCS9YdHQFCA6nk4Z+TLWwYsgk7n490AQOiB213fa1UIe83qIfw/3GRqRUZ7U\nwIWEGsHED5WT69GyxjyKHcqGoV7uFnqFN0sQVKVTy/NFRVQvtBUspCbsOirdDRie\nAB2KbGHL+t1QrRF10szwCJDyk5aYlVhxvdI8zn010nrxHkiyQpDFFldDMLJl10BW\n2w9PGO061z+tntdRcKQGuEpnIr9U5Vs=\n-----END CERTIFICATE REQUEST-----\n", + private_key: "encrypted_private_key" + ) end def test_does_metadata_is_api @@ -12,6 +27,41 @@ class CertificateTest < ActiveSupport::TestCase end def test_certificate_sign_returns_false - assert_not @certificate.sign!(password: ENV['ca_key_password']), 'false' + ENV['ca_key_password'] = 'test_password' + assert_not @certificate.sign!(password: ENV['ca_key_password']) + end + + def test_renewable_when_not_expired + @certificate.update!( + crt: @valid_crt, + expires_at: 20.days.from_now + ) + + assert @certificate.renewable? + end + + def test_not_renewable_when_expired + @certificate.update!( + crt: @valid_crt, + expires_at: 1.day.ago + ) + + assert @certificate.expired? + assert_not @certificate.renewable? + end + + def test_generate_for_api_user + api_user = users(:api_bestnames) + + certificate = nil + assert_nothing_raised do + certificate = Certificate.generate_for_api_user(api_user: api_user) + end + + assert certificate.persisted? + assert_equal api_user, certificate.api_user + assert certificate.private_key.present? + assert certificate.csr.present? + assert certificate.expires_at.present? end end diff --git a/test/models/user_certificate_test.rb b/test/models/user_certificate_test.rb deleted file mode 100644 index 41fe7095c..000000000 --- a/test/models/user_certificate_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'test_helper' - -class UserCertificateTest < ActiveSupport::TestCase - def setup - @user = users(:api_bestnames) - @certificate = user_certificates(:one) - end - - test "should be valid with required attributes" do - certificate = UserCertificate.new( - user: @user, - private_key: 'dummy_key', - status: 'pending' - ) - assert certificate.valid? - end - - test "should not be valid without user" do - @certificate.user = nil - assert_not @certificate.valid? - end - - test "should not be valid without private_key" do - @certificate.private_key = nil - assert_not @certificate.valid? - end - - test "renewable? should be false without certificate" do - @certificate.certificate = nil - assert_not @certificate.renewable? - end - - test "renewable? should be false when revoked" do - @certificate.status = 'revoked' - assert_not @certificate.renewable? - end - - test "renewable? should be true when expires in less than 30 days" do - @certificate.expires_at = 29.days.from_now - assert @certificate.renewable? - end - - test "expired? should be true when certificate is expired" do - @certificate.expires_at = 1.day.ago - assert @certificate.expired? - end - - test "renew should raise error when certificate is not renewable" do - @certificate.status = 'revoked' - assert_raises(RuntimeError) { @certificate.renew } - end -end diff --git a/test/services/certificates/certificate_generator_test.rb b/test/services/certificates/certificate_generator_test.rb new file mode 100644 index 000000000..42a2a3605 --- /dev/null +++ b/test/services/certificates/certificate_generator_test.rb @@ -0,0 +1,62 @@ +require 'test_helper' + +module Certificates + class CertificateGeneratorTest < ActiveSupport::TestCase + setup do + @certificate = certificates(:api) + @generator = CertificateGenerator.new( + username: "test_user", + registrar_code: "REG123", + registrar_name: "Test Registrar" + ) + end + + def test_generates_new_certificate + result = @generator.call + + assert result[:private_key].present? + assert result[:csr].present? + assert result[:crt].present? + assert result[:p12].present? + assert result[:expires_at].present? + + assert_instance_of String, result[:private_key] + assert_instance_of String, result[:csr] + assert_instance_of String, result[:crt] + assert_instance_of String, result[:p12] + assert_instance_of Time, result[:expires_at] + end + + def test_uses_existing_csr_and_private_key + existing_csr = @certificate.csr + existing_private_key = "existing_private_key" + @certificate.update!(private_key: existing_private_key) + + result = @generator.call + + assert result[:csr].present? + assert result[:private_key].present? + assert_not_equal existing_csr, result[:csr] + assert_not_equal existing_private_key, result[:private_key] + end + + def test_renew_certificate + @certificate.update!( + expires_at: 20.days.from_now + ) + + generator = CertificateGenerator.new( + username: "test_user", + registrar_code: "REG123", + registrar_name: "Test Registrar" + ) + + result = generator.call + + assert result[:crt].present? + assert result[:expires_at] > Time.current + assert_instance_of String, result[:crt] + assert_instance_of Time, result[:expires_at] + end + end +end \ No newline at end of file