Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ CLAUDE.md
.claude/
.memory-bank
AGENT.md
own
own
AGENT.md

6 changes: 4 additions & 2 deletions app/controllers/admin/domain_versions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ def show
@domain = Domain.find(params[:domain_id] || params[:id])
else
@version = Version::DomainVersion.find(params[:id])
@domain = Domain.find(@version.item_id)
@resolver = Version::DomainVersion::Resolver.new(@version)
@domain = @resolver.domain
end

@versions = Version::DomainVersion.where(item_id: @domain.id).order(created_at: :desc, id: :desc)
item_id = @resolver&.item_id || @domain.id
@versions = Version::DomainVersion.where(item_id: item_id).order(created_at: :desc, id: :desc)
@versions_map = @versions.all.map(&:id)

get_page if params[:page].blank?
Expand Down
102 changes: 102 additions & 0 deletions app/models/version/domain_version/resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
class Version::DomainVersion < PaperTrail::Version
class Resolver
attr_reader :version

def initialize(version)
@version = version
end

def domain
@domain ||= live_domain || reconstruct_domain
end

def deleted?
live_domain.nil?
end

def domain_name
domain.name.presence || object_value('name') || changes_value('name')
end

def registrar
return @registrar if defined?(@registrar)

@registrar = registrar_id && Registrar.find_by(id: registrar_id)
end

def registrar_id
domain.registrar_id || changes_value('registrar_id') || object_value('registrar_id')
end

def item_id
version.item_id
end

private

def live_domain
return @live_domain if defined?(@live_domain)

@live_domain = Domain.find_by(id: version.item_id)
end

def reconstruct_domain
reify_with_changes || earliest_reifiable_version&.reify || build_from_changes
end

def reify_with_changes
reified = version.reify
return nil unless reified

apply_changes(reified)
stamp_timestamps(reified)
reified
end

def earliest_reifiable_version
sibling_versions.where.not(object: nil).first
end

def build_from_changes
record = Domain.new
apply_changes(record)
stamp_timestamps(record)
record
end

def apply_changes(record)
changes = version.object_changes || {}
changes.slice(*Domain.column_names).each do |attr, values|
value = values.is_a?(Array) ? values.last : values
record.public_send("#{attr}=", value)
end
end

def stamp_timestamps(record)
record.created_at ||= earliest_version_created_at
record.updated_at ||= version.created_at
end

def earliest_version_created_at
@earliest_version_created_at ||=
sibling_versions.pluck(:created_at).first || version.created_at
end

def sibling_versions
Version::DomainVersion
.where(item_id: version.item_id)
.order(created_at: :asc, id: :asc)
end

def object_value(key)
version.object.is_a?(Hash) ? version.object[key] : nil
end

def changes_value(key)
changes = version.object_changes
return nil unless changes.is_a?(Hash) && changes[key].is_a?(Array)

changes[key].last
end
end
end
45 changes: 26 additions & 19 deletions app/views/admin/domain_versions/show.haml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
- deleted = @resolver&.deleted?
- if @version
- children = HashWithIndifferentAccess.new(@version.children)
- nameservers = Nameserver.all_versions_for(children[:nameservers], @domain.updated_at)
- dnskeys = Dnskey.all_versions_for(children[:dnskeys], @domain.updated_at)
- tech_contacts = Contact.all_versions_for(children[:tech_contacts], @domain.updated_at)
- admin_contacts = Contact.all_versions_for(children[:admin_contacts], @domain.updated_at)
- registrant = Contact.all_versions_for(children[:registrant], @domain.updated_at)
- version_timestamp = @domain.updated_at || @version.created_at
- nameservers = Nameserver.all_versions_for(children[:nameservers], version_timestamp)
- dnskeys = Dnskey.all_versions_for(children[:dnskeys], version_timestamp)
- tech_contacts = Contact.all_versions_for(children[:tech_contacts], version_timestamp)
- admin_contacts = Contact.all_versions_for(children[:admin_contacts], version_timestamp)
- registrant = Contact.all_versions_for(children[:registrant], version_timestamp)
- event = @version.event
- creator = plain_username(@version.terminator)
- else
Expand Down Expand Up @@ -33,19 +35,17 @@
.panel-body
%dl.dl-horizontal
%dt= t(:name)
- if !@domain.name
- domain_name = Domain.find(@version.item_id).try(:name)
- domain_name = (@resolver ? @resolver.domain_name : @domain.name).presence || t('.unknown_name')
- if deleted
%dd= domain_name
- else
- domain_name = @domain.name
%dd= link_to(domain_name, admin_domain_path(@version ? @version.item_id : @domain.id))
%dd= link_to(domain_name, admin_domain_path(@version ? @version.item_id : @domain.id))

%dt= t('.created')
%dd
= l(@domain.created_at, format: :short)
%dd= l(@domain.created_at, format: :short)

%dt= t('.updated')
%dd
= l(@domain.updated_at, format: :short)
%dd= l(@domain.updated_at, format: :short)

%br

Expand Down Expand Up @@ -112,12 +112,18 @@
\...#{ns[:public_key].to_s[-20,20]}
%br

- if @domain.registrar
- registrar = @resolver ? @resolver.registrar : @domain.registrar
- registrar_id = @resolver ? @resolver.registrar_id : @domain.registrar&.id
- if registrar
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
= link_to admin_registrar_path(registrar), target: "registrar_#{registrar.id}" do
= registrar.name
- elsif registrar_id
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
= link_to admin_registrar_path(@domain.registrar), target: "registrar_#{@domain.registrar.id}" do
= @domain.registrar.name
%span{:style => "margin: 20px 20px; clear:both;"}
= "Registrar ID: #{registrar_id}"
%span{:style => "margin: 20px 20px; clear:both;"}

- if @version && (prev = @versions_map[(@versions_map.index(@version.id) - 1)]) && @versions_map.index(@version.id) != 0
= link_to(t(:previous),
Expand All @@ -137,8 +143,9 @@
.col-md-4
.panel.panel-default{:style => "min-height:450px;"}
%ul.nav.nav-pills.nav-stacked
%li{class: ('active' if @version.nil?)}
= link_to t('.current_state'), admin_domain_version_path(current: 1, domain_id: @domain.id)
- unless deleted
%li{class: ('active' if @version.nil?)}
= link_to t('.current_state'), admin_domain_version_path(current: 1, domain_id: @domain.id)
- @versions.each do |vs|
%li{class: (@version && vs.id == @version.id) && :active}
= link_to admin_domain_version_path(vs) do
Expand Down
1 change: 1 addition & 0 deletions config/locales/admin/domain_versions.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ en:
admin_contacts: Admin. contacts
tech_contacts: Tech. contacts
current_state: Current state
unknown_name: N/A
104 changes: 104 additions & 0 deletions test/integration/admin_area/domain_versions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,108 @@ def test_show_with_invalid_domain_id
get admin_domain_version_path(999999), params: { current: true }
end
end

def test_show_create_version_of_deleted_domain
deleted_item_id = next_unused_domain_id
create_version = Version::DomainVersion.create!(
item_type: 'Domain',
item_id: deleted_item_id,
event: 'create',
whodunnit: users(:admin).id.to_s,
object: nil,
object_changes: {
'name' => [nil, 'ghost.test'],
'registrar_id' => [nil, @registrar.id],
'registrant_id' => [nil, contacts(:john).id],
},
created_at: Time.zone.parse('2024-01-01')
)

get admin_domain_version_path(create_version.id)

assert_response :ok
assert_includes response.body, 'ghost.test'
assert_includes response.body, @registrar.name
assert_no_match %r{href="#{admin_domain_path(deleted_item_id)}"}, response.body
assert_no_match %r{admin_domain_version[^"]*current=1}, response.body
end

def test_show_update_version_of_deleted_domain
deleted_item_id = next_unused_domain_id
Version::DomainVersion.create!(
item_type: 'Domain',
item_id: deleted_item_id,
event: 'create',
whodunnit: users(:admin).id.to_s,
object: nil,
object_changes: {
'name' => [nil, 'gone.test'],
'registrar_id' => [nil, @registrar.id],
},
created_at: Time.zone.parse('2024-01-01')
)
update_version = Version::DomainVersion.create!(
item_type: 'Domain',
item_id: deleted_item_id,
event: 'update',
whodunnit: users(:admin).id.to_s,
object: {
'name' => 'gone.test',
'registrar_id' => @registrar.id,
},
object_changes: {
'statuses' => [[], ['serverHold']],
},
created_at: Time.zone.parse('2024-02-01')
)

get admin_domain_version_path(update_version.id)

assert_response :ok
assert_includes response.body, 'gone.test'
assert_includes response.body, @registrar.name
assert_no_match %r{href="#{admin_domain_path(deleted_item_id)}"}, response.body
end

def test_show_destroy_version_of_deleted_domain
deleted_item_id = next_unused_domain_id
Version::DomainVersion.create!(
item_type: 'Domain',
item_id: deleted_item_id,
event: 'create',
whodunnit: users(:admin).id.to_s,
object: nil,
object_changes: {
'name' => [nil, 'bye.test'],
'registrar_id' => [nil, @registrar.id],
},
created_at: Time.zone.parse('2024-01-01')
)
destroy_version = Version::DomainVersion.create!(
item_type: 'Domain',
item_id: deleted_item_id,
event: 'destroy',
whodunnit: users(:admin).id.to_s,
object: {
'name' => 'bye.test',
'registrar_id' => @registrar.id,
},
object_changes: nil,
created_at: Time.zone.parse('2024-03-01')
)

get admin_domain_version_path(destroy_version.id)

assert_response :ok
assert_includes response.body, 'bye.test'
end

private

def next_unused_domain_id
@next_unused_domain_id ||= Domain.maximum(:id).to_i +
Version::DomainVersion.maximum(:item_id).to_i +
1000
@next_unused_domain_id += 1
end
end
Loading
Loading