contact: hold the card, not the bytes
Every caller had a VCard in hand to make those bytes from, and Contact
built a second one back from them on each structured read; #insert
returns a card now so the first survives the round trip.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index b31ec49..01d4b02 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -5,12 +5,17 @@ require "pro_tacts/vcard"
require "pro_tacts/vcard/parser"
module ProTacts
- # One contact: its id, the vCard bytes served for it, the etag over
- # those bytes, and the structured read of them — the one model of a
- # contact (docs/plans/2026-09-01-contact-is-the-model.md). The id is
- # the vCard's UID and the name in its href (see
+ # One contact: its id, the card served for it, and the etag over that
+ # card's bytes — the one model of a contact
+ # (docs/plans/2026-09-01-contact-is-the-model.md). The id is the
+ # vCard's UID and the name in its href (see
# docs/plans/2026-01-12-carddav-architecture.md).
#
+ # A VCard rather than the bytes, because a contact that held bytes
+ # would have to make a card out of them to answer anything, and every
+ # caller handing one over already had a card in hand to make the bytes
+ # from. One card is built per contact and every read is asked of it.
+ #
# The etag hashes the card that goes out, so it changes exactly when
# what the client downloads changes. It is derived here and stored
# nowhere: a contact read back from the store hashes its card the same
@@ -20,22 +25,19 @@ module ProTacts
# both the ETag header and getetag properties carry.
#
# The structured accessors are that idea applied to the card's
- # contents: everything derives from the bytes, lazily, on the first
- # structured read. The CardDAV paths never take one — they answer in
- # bytes — and ctag and the listing reads build a contact per row
- # without reading structure, so deferring the parse keeps every
- # serving path as cheap as it was without the accessors. A regular
- # class rather than a Data one because the memoized parse needs an
- # ivar and Data freezes its instances.
+ # contents: everything derives from the bytes, and the card defers the
+ # walk that reads them until something asks. The CardDAV paths never
+ # ask — they answer in bytes — so the serving paths pay for no parse
+ # they do not use, and that laziness is VCard's to keep rather than
+ # this class's to arrange.
#
# The accessors are deliberately shallow, like the parser: a card can
# carry properties no accessor knows, and CardDAV still serves every
# one. #properties is the read for those.
class Contact
# @rbs @id: String
- # @rbs @vcard: String
+ # @rbs @vcard: VCard
# @rbs @etag: String
- # @rbs @properties: Array[VCard::Parser::Property]
# Ids end up in paths and arrive from client-supplied hrefs, so an id
# outside this charset cannot be served.
@@ -67,19 +69,19 @@ module ProTacts
# A contact from its id and its card. The only way to make one: an
# etag that came from anywhere but the card in hand is an etag that
# can be wrong.
- #: (id: String, vcard: String) -> Contact
+ #: (id: String, vcard: VCard) -> Contact
def self.for(id:, vcard:)
raise ArgumentError, "invalid contact id: #{id}" unless id.match?(ID_FORMAT)
new(id, vcard, etag_for(vcard))
end
- #: (String vcard) -> String
+ #: (VCard vcard) -> String
def self.etag_for(vcard)
- %("#{Digest::SHA256.hexdigest(vcard)}")
+ %("#{Digest::SHA256.hexdigest(vcard.to_s)}")
end
- #: (String id, String vcard, String etag) -> void
+ #: (String id, VCard vcard, String etag) -> void
def initialize(id, vcard, etag)
@id = id
@vcard = vcard
@@ -92,19 +94,14 @@ module ProTacts
attr_reader :etag
- # The card's properties, parsed once and memoized — the substrate
- # the typed accessors sit on, and the read for what none of them
- # models. Every line that read, and no complaint about one that did
- # not: a contact is served from its bytes, so a line this parser
- # cannot read costs an accessor its answer and costs the contact
- # nothing. There is no repair to make and nobody to make it, which
- # is why nothing here looks for one.
+ # The card's properties — the substrate the typed accessors sit on,
+ # and the read for what none of them models. Every line that read,
+ # and no complaint about one that did not: a contact is served from
+ # its bytes, so a line this parser cannot read costs an accessor its
+ # answer and costs the contact nothing. There is no repair to make
+ # and nobody to make it, which is why nothing here looks for one.
#: () -> Array[VCard::Parser::Property]
- def properties
- return @properties if defined?(@properties)
-
- @properties = VCard.new(@vcard).properties
- end
+ def properties = @vcard.properties
# FN's value (RFC 2426 section 3.1.1), in text form.
#: () -> String?
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 85a7c9b..bf26539 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -226,8 +226,9 @@ module ProTacts
# binary-flagged byte above 7 bits raises at the bind, and bytes
# that are not UTF-8 at all stop at the insert, which SQLite refuses
# to store as text. The body — the one binary input — is relabelled
- # where it is read, in write_card, and refused at the card itself
- # before that: VCard raises on bytes that are not text.
+ # and judged in the same breath where it is read, in write_card, so
+ # nothing that is not text gets this far; VCard's own raise is the
+ # assertion under that, and the bind is the third line.
#: (String id, String vcard) -> Contact
def put(id, vcard)
# The birthday half of the split a write makes, one arm per shape
@@ -254,12 +255,12 @@ module ProTacts
report_unrecognized_bday_lines([line])
property = bday_of(line)
birthday = property && Birthday.from_property(property)
- [birthday, birthday ? rest.to_s : vcard]
+ [birthday, birthday ? rest : card]
in [[], _]
# The rewrite arm: carry the unrendered lines out of the
# stored card, report the unrecognized ones' loss, and keep
# whatever an unseen model row holds.
- carried, lost = carried_and_lost_bday_lines(stored_vcard(id))
+ carried, lost = carried_and_lost_bday_lines(stored_card(id))
kept = existing && !existing.served? ? existing : nil
report_lost_bday_lines(lost)
[kept, card.insert(carried)]
@@ -267,7 +268,7 @@ module ProTacts
# More than one BDAY: cardinality-broken data, kept verbatim
# and reported like any other unrecognized line.
report_unrecognized_bday_lines(lines)
- [nil, vcard]
+ [nil, card]
end
# The Contact this returns is the composed one, and the logged
@@ -277,7 +278,7 @@ module ProTacts
@database.transaction do
cards
.insert_conflict(target: :id, update: {vcard: Sequel[:excluded][:vcard], updated_at: NOW})
- .insert(id: contact.id, vcard: stored)
+ .insert(id: contact.id, vcard: stored.to_s)
write_birthday(contact.id, birthday)
record(contact.id, "put", contact.etag)
reindex(contact.id, stored)
@@ -314,7 +315,7 @@ module ProTacts
def rebuild_index
@database.transaction do
card_properties.delete
- cards.order(:id).all.each { reindex(it.fetch(:id).to_s, it.fetch(:vcard).to_s) }
+ cards.order(:id).all.each { reindex(it.fetch(:id).to_s, VCard.new(it.fetch(:vcard).to_s)) }
end
end
@@ -358,13 +359,13 @@ module ProTacts
# parser cannot read. Takes the stored card — with the birthday
# already subtracted — so the index never sees a BDAY no stored
# card carries.
- #: (String id, String vcard) -> void
- def reindex(id, vcard)
+ #: (String id, VCard card) -> void
+ def reindex(id, card)
# Before the parse, so that a card which has stopped parsing does
# not keep the rows from when it did.
card_properties.where(card_id: id).delete
- properties_of(vcard).each.with_index do |property, position|
+ card.properties.each.with_index do |property, position|
card_properties.insert(
card_id: id,
position:,
@@ -378,12 +379,6 @@ module ProTacts
end
end
- # A card's properties: every line of it that read.
- #: (String vcard) -> Array[VCard::Parser::Property]
- def properties_of(vcard)
- VCard.new(vcard).properties
- end
-
# Applies whatever migrations the database has not seen. On one it is
# already current with, this is a read of Sequel's schema_info table
# and nothing else, which is cheap enough to do on every open and
@@ -396,10 +391,10 @@ module ProTacts
# The card currently stored for an id, or nil for a card being
# created. A plain `first` read: the primary key leaves `sole`
# nothing to catch, and no row is the ordinary answer.
- #: (String id) -> String?
- def stored_vcard(id)
+ #: (String id) -> VCard?
+ def stored_card(id)
row = cards.where(id:).first
- row && row.fetch(:vcard).to_s
+ row && VCard.new(row.fetch(:vcard).to_s)
end
# The BDAY a decision may be made of: the line's property, when it
@@ -420,11 +415,11 @@ module ProTacts
# it drops unwitnessed, which no client renders and no whitelist
# recognizes. Lines a client rendered are in neither pile: their
# absence is a deletion the rewrite already honors.
- #: (String? vcard) -> [Array[String], Array[VCard::Parser::Line]]
- def carried_and_lost_bday_lines(vcard)
- return [[], []] if vcard.nil?
+ #: (VCard? card) -> [Array[String], Array[VCard::Parser::Line]]
+ def carried_and_lost_bday_lines(card)
+ return [[], []] if card.nil?
- bdays, = VCard.new(vcard).extract("BDAY")
+ bdays, = card.extract("BDAY")
carried, rest = bdays.partition { |line|
property = bday_of(line)
property && Birthday.unrendered_value?(property.value)
@@ -482,19 +477,19 @@ module ProTacts
def contact_from(row, birthday)
Contact.for(
id: row.fetch(:id).to_s,
- vcard: with_birthday(row.fetch(:vcard).to_s, birthday),
+ vcard: with_birthday(VCard.new(row.fetch(:vcard).to_s), birthday),
)
end
# The card to serve: the stored card with its birthday composed
# back in, immediately before END:VCARD. A birthday with no wire
# form, or none at all, leaves the card exactly as it is.
- #: (String card, Birthday? birthday) -> String
+ #: (VCard card, Birthday? birthday) -> VCard
def with_birthday(card, birthday)
line = birthday && birthday.to_line
return card if line.nil?
- VCard.new(card).insert([line])
+ card.insert([line])
end
# The model's half of a write: hold the birthday the card gave up,
diff --git a/lib/pro_tacts/vcard.rb b/lib/pro_tacts/vcard.rb
index a2dd24a..da4d9ec 100644
--- a/lib/pro_tacts/vcard.rb
+++ b/lib/pro_tacts/vcard.rb
@@ -164,18 +164,22 @@ module ProTacts
# single-convention; a terminated line keeps its own, folds and
# all. With no END to anchor to there is no envelope worth
# respecting, and the lines are appended with CRLF.
- #: (Array[String] lines) -> String
+ #
+ # A card back, like #extract's remainder: text with text put into it
+ # is still text, so there is nothing here for a caller to re-read or
+ # re-judge, and one that wants the bytes asks #to_s for them.
+ #: (Array[String] lines) -> VCard
def insert(lines)
- return @bytes if lines.empty?
+ return self if lines.empty?
physical = physical_lines
index = physical.rindex { it.match?(END_LINE) }
if index
terminator = terminator_of(physical.fetch(index))
physical.insert(index, *lines.map { it.end_with?("\n") ? it : it + terminator })
- physical.join
+ VCard.new(physical.join)
else
- @bytes + lines.map { it.end_with?("\n") ? it : it + "\r\n" }.join
+ VCard.new(@bytes + lines.map { it.end_with?("\n") ? it : it + "\r\n" }.join)
end
end
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index a6f8f51..a458215 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -374,7 +374,7 @@ module ProTacts
# RFC 7232 section 2.3; must match the getetag reported for
# this contact in PROPFIND and REPORT.
response["ETag"] = contact.etag
- contact.vcard
+ contact.vcard.to_s
end
end
@@ -498,7 +498,7 @@ module ProTacts
# PUT that carried one stores a different card than it was handed
# and the client refetches; a card with nothing to subtract still
# stores octet for octet and still gets the tag.
- response["ETag"] = stored.etag if stored.vcard == vcard
+ response["ETag"] = stored.etag if stored.vcard.to_s == vcard
# A returned "" would land in the body and pin text/html and
# content-length onto the 204, which a bodyless status must not
@@ -620,7 +620,7 @@ module ProTacts
<d:propstat>
<d:prop>
<d:getetag>#{contact.etag}</d:getetag>
- <card:address-data>#{xml_escape(contact.vcard.chomp)}</card:address-data>
+ <card:address-data>#{xml_escape(contact.vcard.to_s.chomp)}</card:address-data>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
diff --git a/test/pro_tacts/admin/test_format.rb b/test/pro_tacts/admin/test_format.rb
index 1d184f1..7115cf5 100644
--- a/test/pro_tacts/admin/test_format.rb
+++ b/test/pro_tacts/admin/test_format.rb
@@ -2,13 +2,14 @@ require_relative "../../test_helper"
require "pro_tacts/admin/format"
require "pro_tacts/contact"
+require "pro_tacts/vcard"
class FormatTest < Minitest::Test
CARD = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Ada Lovelace\r\nN:Lovelace;Ada;;;\r\n" \
"BDAY:1985-12-10\r\nUID:ada\r\nEND:VCARD\r\n"
def contact(vcard = CARD, id: "aiden")
- ProTacts::Contact.for(id:, vcard:)
+ ProTacts::Contact.for(id:, vcard: ProTacts::VCard.new(vcard))
end
# Initials come from the structured N property (given + family),
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index a076379..a4004c1 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -3,6 +3,7 @@ require_relative "../test_helper"
require "digest"
require "pro_tacts/contact"
+require "pro_tacts/vcard"
class ContactTest < Minitest::Test
CARD = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Aiden\r\nEND:VCARD\r\n"
@@ -13,35 +14,30 @@ class ContactTest < Minitest::Test
"ADR;TYPE=home:;;12 Analytical Way;London;England;NW1 1AA;United Kingdom\r\n" \
"BDAY:1985-12-10\r\nNOTE:Countess\\, mathematician.\r\nUID:ada\r\nEND:VCARD\r\n"
- def contact(vcard)
- ProTacts::Contact.for(id: "aiden", vcard:)
+ def contact(vcard = CARD, id: "aiden")
+ ProTacts::Contact.for(id:, vcard: ProTacts::VCard.new(vcard))
end
def test_the_etag_hashes_the_card
- contact = ProTacts::Contact.for(id: "aiden", vcard: CARD)
-
assert_equal %("#{Digest::SHA256.hexdigest(CARD)}"), contact.etag
end
def test_the_etag_moves_with_the_card
- etag = ProTacts::Contact.for(id: "aiden", vcard: CARD).etag
+ etag = contact.etag
- assert_equal etag, ProTacts::Contact.for(id: "aiden", vcard: CARD).etag
- refute_equal etag, ProTacts::Contact.for(id: "aiden", vcard: CARD.sub("Aiden", "Aiden Smith")).etag
+ assert_equal etag, contact.etag
+ refute_equal etag, contact(CARD.sub("Aiden", "Aiden Smith")).etag
end
# The same card under two ids is two contacts with the same etag: the
# etag is about the bytes a client downloads, and the id is not in them.
def test_the_etag_ignores_the_id
- assert_equal(
- ProTacts::Contact.for(id: "aiden", vcard: CARD).etag,
- ProTacts::Contact.for(id: "znorth", vcard: CARD).etag,
- )
+ assert_equal contact(id: "aiden").etag, contact(id: "znorth").etag
end
def test_ids_outside_the_href_charset_raise
["John Smith", "../etc/passwd", "a/b", ""].each do |id|
- error = assert_raises(ArgumentError) { ProTacts::Contact.for(id:, vcard: CARD) }
+ error = assert_raises(ArgumentError) { contact(id:) }
assert_equal "invalid contact id: #{id}", error.message
end
@@ -49,7 +45,7 @@ class ContactTest < Minitest::Test
def test_uuids_and_slugs_are_ids
%w[AB12C345-6789-0DEF-1234-567890ABCDEF aiden znorth_2].each do |id|
- assert_equal id, ProTacts::Contact.for(id:, vcard: CARD).id
+ assert_equal id, contact(id:).id
end
end
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 6a7e221..0be1d93 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -43,7 +43,7 @@ class StoreTest < Minitest::Test
def test_a_stored_card_comes_back_byte_for_byte
with_store({"aiden" => AIDEN}) do |store|
- assert_equal AIDEN, store.contact("aiden").vcard
+ assert_equal AIDEN, store.contact("aiden").vcard.to_s
end
end
@@ -92,7 +92,7 @@ class StoreTest < Minitest::Test
updated = AIDEN.sub("Aiden", "Aiden Smith")
store.put("aiden", updated)
- assert_equal [updated], store.contacts.map { it.vcard }
+ assert_equal [updated], store.contacts.map { it.vcard.to_s }
end
end
@@ -129,7 +129,7 @@ class StoreTest < Minitest::Test
ProTacts::Store.connect(path) { it.put("aiden", AIDEN) }
ProTacts::Store.connect(path) do |store|
- assert_equal AIDEN, store.contact("aiden").vcard
+ assert_equal AIDEN, store.contact("aiden").vcard.to_s
end
end
end
@@ -149,7 +149,7 @@ class StoreTest < Minitest::Test
# Pure ASCII carries no such byte, so a binary-flagged id still
# finds its row.
- assert_equal AIDEN, store.contact("aiden".b).vcard
+ assert_equal AIDEN, store.contact("aiden".b).vcard.to_s
end
end
@@ -438,7 +438,7 @@ class StoreTest < Minitest::Test
# nothing to the index and cost the index nothing else.
def test_an_unparseable_card_is_still_stored_and_served
with_store({"broken" => "this is not a vCard\r\n"}) do |store|
- assert_equal "this is not a vCard\r\n", store.contact("broken").vcard
+ assert_equal "this is not a vCard\r\n", store.contact("broken").vcard.to_s
assert_empty indexed_names(store, "broken")
end
end
@@ -450,7 +450,7 @@ class StoreTest < Minitest::Test
unreadable = AIDEN.sub("FN:Aiden\r\n", "FN:Aiden\r\nTEL;HOME:+1-555-1234\r\n")
with_store({"aiden" => unreadable}) do |store|
assert_equal %w[BEGIN VERSION FN UID END], indexed_names(store, "aiden")
- assert_equal unreadable, store.contact("aiden").vcard
+ assert_equal unreadable, store.contact("aiden").vcard.to_s
end
end
@@ -465,7 +465,7 @@ class StoreTest < Minitest::Test
def test_a_birthday_is_stored_beside_the_card_and_served_within_it
with_store({"aiden" => AIDEN_BORN}) do |store|
assert_equal AIDEN, card_row(store, "aiden").fetch(:vcard)
- assert_equal AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\r\nEND:VCARD\r\n"), store.contact("aiden").vcard
+ assert_equal AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\r\nEND:VCARD\r\n"), store.contact("aiden").vcard.to_s
end
end
@@ -506,7 +506,7 @@ class StoreTest < Minitest::Test
store.put("aiden", AIDEN)
assert_nil birthday_row(store, "aiden")
- assert_equal AIDEN, store.contact("aiden").vcard
+ assert_equal AIDEN, store.contact("aiden").vcard.to_s
end
end
@@ -520,7 +520,7 @@ class StoreTest < Minitest::Test
store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
assert_equal ProTacts::Birthday.new(year: 1985), birthday_row(store, "aiden")
- assert_equal AIDEN.sub("FN:Aiden", "FN:Aiden Smith"), store.contact("aiden").vcard
+ assert_equal AIDEN.sub("FN:Aiden", "FN:Aiden Smith"), store.contact("aiden").vcard.to_s
end
end
@@ -538,7 +538,7 @@ class StoreTest < Minitest::Test
with_store({"aiden" => AIDEN_BORN}) do |store|
store.put("aiden", unmodeled)
- assert_equal unmodeled, store.contact("aiden").vcard, bday
+ assert_equal unmodeled, store.contact("aiden").vcard.to_s, bday
assert_nil birthday_row(store, "aiden"), bday
end
end
@@ -565,7 +565,7 @@ class StoreTest < Minitest::Test
store.put("aiden", shared)
messages = sentry_messages
- assert_equal shared, store.contact("aiden").vcard
+ assert_equal shared, store.contact("aiden").vcard.to_s
assert_nil birthday_row(store, "aiden")
assert_empty messages
end
@@ -578,7 +578,7 @@ class StoreTest < Minitest::Test
store.put("aiden", edited)
- assert_equal edited.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"), store.contact("aiden").vcard, line
+ assert_equal edited.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"), store.contact("aiden").vcard.to_s, line
assert_nil birthday_row(store, "aiden"), line
end
end
@@ -592,7 +592,7 @@ class StoreTest < Minitest::Test
with_store({"aiden" => AIDEN.sub("END:VCARD\r\n", "BDAY:--0412\r\nEND:VCARD\r\n")}) do |store|
store.put("aiden", AIDEN)
- assert_equal AIDEN, store.contact("aiden").vcard
+ assert_equal AIDEN, store.contact("aiden").vcard.to_s
assert_nil birthday_row(store, "aiden")
end
end
@@ -608,7 +608,7 @@ class StoreTest < Minitest::Test
store.put("aiden", edited)
messages = sentry_messages
- assert_equal edited, store.contact("aiden").vcard
+ assert_equal edited, store.contact("aiden").vcard.to_s
assert_equal 1, messages.length
end
end
@@ -622,7 +622,7 @@ class StoreTest < Minitest::Test
store.put("aiden", born)
assert_equal ProTacts::Birthday.new(year: 1985, month: 4, day: 12), birthday_row(store, "aiden")
- assert_equal born, store.contact("aiden").vcard
+ assert_equal born, store.contact("aiden").vcard.to_s
end
end
@@ -822,16 +822,16 @@ class StoreTest < Minitest::Test
ProTacts::Store.connect(path) do |store|
assert_equal AIDEN, card_row(store, "aiden").fetch(:vcard)
assert_equal ProTacts::Birthday.new(year: 1985, month: 4, day: 12), birthday_row(store, "aiden")
- assert_equal AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\r\nEND:VCARD\r\n"), store.contact("aiden").vcard
+ assert_equal AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\r\nEND:VCARD\r\n"), store.contact("aiden").vcard.to_s
# Unmodeled: byte-identical, no birthday row beside it.
- assert_equal ZED.sub("END:VCARD\r\n", "BDAY:--0412\r\nEND:VCARD\r\n"), store.contact("znorth").vcard
+ assert_equal ZED.sub("END:VCARD\r\n", "BDAY:--0412\r\nEND:VCARD\r\n"), store.contact("znorth").vcard.to_s
assert_nil birthday_row(store, "znorth")
# A BDAY sharing its line's bytes stays put: the line moves as
# one or not at all, where the first property alone would have
# moved the birthday and dropped the NOTE unwitnessed.
- assert_equal shared, store.contact("xavi").vcard
+ assert_equal shared, store.contact("xavi").vcard.to_s
assert_nil birthday_row(store, "xavi")
end
end
diff --git a/test/pro_tacts/vcard/test_lines.rb b/test/pro_tacts/vcard/test_lines.rb
index 2125757..f98e5e1 100644
--- a/test/pro_tacts/vcard/test_lines.rb
+++ b/test/pro_tacts/vcard/test_lines.rb
@@ -146,7 +146,7 @@ class VCardLinesTest < Minitest::Test
## insert
def test_insert_takes_the_end_lines_terminator_for_a_bare_line
- inserted = VCARD.new(CARD).insert(["BDAY:1985-04\r\n"])
+ inserted = VCARD.new(CARD).insert(["BDAY:1985-04\r\n"]).to_s
assert_equal CARD.sub("END:VCARD\r\n", "BDAY:1985-04\r\nEND:VCARD\r\n"), inserted
assert_operator inserted.index("BDAY"), :<, inserted.index("END:VCARD")
@@ -159,17 +159,17 @@ class VCardLinesTest < Minitest::Test
folded = "BDAY:1985-\r\n 04\r\n"
assert_equal CARD.sub("END:VCARD\r\n", "BDAY:1985-\r\n 04\r\nEND:VCARD\r\n"),
- VCARD.new(CARD).insert([folded])
+ VCARD.new(CARD).insert([folded]).to_s
assert_equal "BEGIN:VCARD\nEND:VCARD\n".sub("END:VCARD\n", "NOTE:x\nEND:VCARD\n"),
- VCARD.new("BEGIN:VCARD\nEND:VCARD\n").insert(["NOTE:x\n"])
+ VCARD.new("BEGIN:VCARD\nEND:VCARD\n").insert(["NOTE:x\n"]).to_s
end
def test_insert_appends_with_crlf_when_there_is_no_end
assert_equal "not a card\r\nBDAY:1985-04\r\n",
- VCARD.new("not a card\r\n").insert(["BDAY:1985-04\r\n"])
+ VCARD.new("not a card\r\n").insert(["BDAY:1985-04\r\n"]).to_s
end
def test_insert_with_nothing_leaves_the_card_alone
- assert_equal CARD, VCARD.new(CARD).insert([])
+ assert_equal CARD, VCARD.new(CARD).insert([]).to_s
end
end