feat: improve certificate download extensions

Update certificate download functionality to use appropriate file extensions:
- Use .p12 extension for PKCS#12 files
- Keep .pem extension for PEM-encoded files (CSR, CRT, private key)

This change ensures that downloaded certificate files have the correct extension based on their format, making it easier for users to identify and use the files correctly.
This commit is contained in:
oleghasjanov 2025-02-19 16:07:50 +02:00
parent 51035d1ddf
commit 5355397025
15 changed files with 281 additions and 262 deletions

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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');

View file

@ -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,

9
test/fixtures/files/user.crt vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUBgtGh4Pw8Luqq/HG4tqG3oIzfHIwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMTkxMjAwMDBaFw0yNTAy
MTkxMjAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDUVURLKdmhmEht7yz3MeQQtn9kMIaIzZDwggZvUg6J
5PlTabEixVfPzlRJixJBj37hh0Ree6mr19KECtPymy1L9U3oGfF18CJhdzc=
-----END CERTIFICATE-----

View file

@ -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 %>

View file

@ -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

View file

@ -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

View file

@ -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