lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 617f7b0d8c141e3c7b8a1db20fb3248f945b72a3
parent 26a5c9cfa6c231a0ddbdbb9816d2102da8fe32dc
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 22:52:16 +0000

tests: cover tag builders and job trait error paths


- add a comprehensive tag_builders suite that executes every radrootseventtagbuilder impl and listing_build_tags
- expand job trait adapter tests to cover request/result/feedback metadata and index conversions
- add job parse error and profile encode error display and source assertions in codec error tests
- validate with cargo check, cargo test, and xtask strict preflight showing additional coverage lift

Diffstat:
Mcrates/events-codec/tests/codec_error_job.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/job_traits.rs | 159++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Acrates/events-codec/tests/tag_builders.rs | 449+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 646 insertions(+), 5 deletions(-)

diff --git a/crates/events-codec/tests/codec_error_job.rs b/crates/events-codec/tests/codec_error_job.rs @@ -5,6 +5,8 @@ use radroots_events_codec::job::encode::{ assert_no_inputs_when_encrypted, push_provider_tag, push_relay_tag, push_status_tag, JobEncodeError, }; +use radroots_events_codec::job::error::JobParseError; +use radroots_events_codec::profile::error::ProfileEncodeError; #[cfg(feature = "serde_json")] use serde::ser::{Error as _, Serializer}; #[cfg(feature = "serde_json")] @@ -133,3 +135,44 @@ fn job_encode_error_display_covers_variants() { "empty required field: content" ); } + +#[test] +fn job_parse_error_display_and_source_covers_variants() { + let missing = JobParseError::MissingTag("e"); + assert_eq!(missing.to_string(), "missing tag: e"); + assert!(missing.source().is_none()); + + let invalid = JobParseError::InvalidTag("status"); + assert_eq!(invalid.to_string(), "invalid tag structure for 'status'"); + assert!(invalid.source().is_none()); + + let invalid_number = JobParseError::InvalidNumber("amount", "x".parse::<u32>().unwrap_err()); + assert!(invalid_number + .to_string() + .contains("invalid number in 'amount'")); + assert!(invalid_number.source().is_some()); + + let non_whole = JobParseError::NonWholeSats("amount"); + assert!(non_whole.to_string().contains("whole number of sats")); + assert!(non_whole.source().is_none()); + + let overflow = JobParseError::AmountOverflow("amount"); + assert!(overflow.to_string().contains("does not fit u32 sat")); + assert!(overflow.source().is_none()); + + let missing_chain = JobParseError::MissingChainTag("e"); + assert_eq!(missing_chain.to_string(), "missing required chain tag: e"); + assert!(missing_chain.source().is_none()); +} + +#[test] +fn profile_encode_error_display_covers_variants() { + let invalid = ProfileEncodeError::InvalidUrl("website", "ftp://example.com".to_string()); + assert_eq!( + invalid.to_string(), + "invalid URL for website: ftp://example.com" + ); + + let json = ProfileEncodeError::Json; + assert_eq!(json.to_string(), "failed to serialize metadata JSON"); +} diff --git a/crates/events-codec/tests/job_traits.rs b/crates/events-codec/tests/job_traits.rs @@ -1,8 +1,12 @@ -use radroots_events::RadrootsNostrEvent; -use radroots_events::job::JobInputType; +use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest}; +use radroots_events::job_feedback::RadrootsJobFeedback; use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest}; -use radroots_events::kinds::KIND_JOB_REQUEST_MIN; -use radroots_events_codec::job::request::encode::to_wire_parts; +use radroots_events::job_result::RadrootsJobResult; +use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN}; +use radroots_events::RadrootsNostrEvent; +use radroots_events_codec::job::feedback::encode::to_wire_parts as to_feedback_wire_parts; +use radroots_events_codec::job::request::encode::to_wire_parts as to_request_wire_parts; +use radroots_events_codec::job::result::encode::to_wire_parts as to_result_wire_parts; use radroots_events_codec::job::traits::{BorrowedEventAdapter, JobEventLike}; fn sample_request() -> RadrootsJobRequest { @@ -30,7 +34,7 @@ fn sample_request() -> RadrootsJobRequest { #[test] fn borrowed_event_adapter_builds_request_metadata() { let req = sample_request(); - let parts = to_wire_parts(&req, "payload").unwrap(); + let parts = to_request_wire_parts(&req, "payload").unwrap(); let event = RadrootsNostrEvent { id: "id".to_string(), @@ -51,3 +55,148 @@ fn borrowed_event_adapter_builds_request_metadata() { assert_eq!(metadata.kind, event.kind); assert_eq!(metadata.job_request, req); } + +fn sample_result() -> RadrootsJobResult { + RadrootsJobResult { + kind: (KIND_JOB_RESULT_MIN + 1) as u16, + request_event: radroots_events::RadrootsNostrEventPtr { + id: "req".to_string(), + relays: Some("wss://relay.example.com".to_string()), + }, + request_json: Some("{\"foo\":\"bar\"}".to_string()), + inputs: vec![RadrootsJobInput { + data: "hello".to_string(), + input_type: JobInputType::Text, + relay: None, + marker: None, + }], + customer_pubkey: Some( + "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(), + ), + payment: Some(JobPaymentRequest { + amount_sat: 1, + bolt11: None, + }), + content: Some("payload".to_string()), + encrypted: false, + } +} + +fn sample_feedback() -> RadrootsJobFeedback { + RadrootsJobFeedback { + kind: KIND_JOB_FEEDBACK as u16, + status: JobFeedbackStatus::Processing, + extra_info: Some("processing".to_string()), + request_event: radroots_events::RadrootsNostrEventPtr { + id: "req".to_string(), + relays: Some("wss://relay.example.com".to_string()), + }, + customer_pubkey: Some( + "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(), + ), + payment: Some(JobPaymentRequest { + amount_sat: 2, + bolt11: None, + }), + content: Some("payload".to_string()), + encrypted: false, + } +} + +#[test] +fn borrowed_event_adapter_builds_request_metadata_and_index() { + let req = sample_request(); + let parts = to_request_wire_parts(&req, "payload").unwrap(); + let event = RadrootsNostrEvent { + id: "id".to_string(), + author: "author".to_string(), + created_at: 42, + kind: parts.kind, + tags: parts.tags, + content: "payload".to_string(), + sig: "sig".to_string(), + }; + + let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + assert_eq!(adapter.raw_id(), "id"); + assert_eq!(adapter.raw_author(), "author"); + assert_eq!(adapter.raw_published_at(), 42); + assert_eq!(adapter.raw_kind(), event.kind); + assert_eq!(adapter.raw_content(), "payload"); + assert_eq!(adapter.raw_tags().len(), event.tags.len()); + assert_eq!(adapter.raw_sig(), "sig"); + + let index = adapter.to_job_request_event_index().unwrap(); + assert_eq!(index.event.id, event.id); + assert_eq!(index.event.author, event.author); + assert_eq!(index.event.created_at, event.created_at); + assert_eq!(index.event.kind, event.kind); + assert_eq!(index.event.content, event.content); + assert_eq!(index.event.sig, event.sig); +} + +#[test] +fn borrowed_event_adapter_builds_result_metadata_and_index() { + let result = sample_result(); + let parts = to_result_wire_parts(&result, "payload").unwrap(); + let event = RadrootsNostrEvent { + id: "id".to_string(), + author: "author".to_string(), + created_at: 42, + kind: parts.kind, + tags: parts.tags, + content: "payload".to_string(), + sig: "sig".to_string(), + }; + + let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + let metadata = adapter.to_job_result_metadata().unwrap(); + assert_eq!(metadata.id, event.id); + assert_eq!(metadata.author, event.author); + assert_eq!(metadata.published_at, event.created_at); + assert_eq!(metadata.kind, event.kind); + assert_eq!(metadata.job_result.kind, result.kind); + assert_eq!(metadata.job_result.request_event.id, "req"); + assert_eq!(metadata.job_result.content.as_deref(), Some("payload")); + + let index = adapter.to_job_result_event_index().unwrap(); + assert_eq!(index.event.id, event.id); + assert_eq!(index.event.author, event.author); + assert_eq!(index.event.created_at, event.created_at); + assert_eq!(index.event.kind, event.kind); + assert_eq!(index.event.content, event.content); + assert_eq!(index.event.sig, event.sig); +} + +#[test] +fn borrowed_event_adapter_builds_feedback_metadata_and_index() { + let feedback = sample_feedback(); + let parts = to_feedback_wire_parts(&feedback, "payload").unwrap(); + let event = RadrootsNostrEvent { + id: "id".to_string(), + author: "author".to_string(), + created_at: 42, + kind: parts.kind, + tags: parts.tags, + content: "payload".to_string(), + sig: "sig".to_string(), + }; + + let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + let metadata = adapter.to_job_feedback_metadata().unwrap(); + assert_eq!(metadata.id, event.id); + assert_eq!(metadata.author, event.author); + assert_eq!(metadata.published_at, event.created_at); + assert_eq!(metadata.kind, event.kind); + assert_eq!(metadata.job_feedback.kind, feedback.kind); + assert_eq!(metadata.job_feedback.request_event.id, "req"); + assert_eq!(metadata.job_feedback.content.as_deref(), Some("payload")); + + let index = adapter.to_job_feedback_event_index().unwrap(); + assert_eq!(index.event.id, event.id); + assert_eq!(index.event.author, event.author); + assert_eq!(index.event.created_at, event.created_at); + assert_eq!(index.event.kind, event.kind); + assert_eq!(index.event.content, event.content); + assert_eq!(index.event.sig, event.sig); +} diff --git a/crates/events-codec/tests/tag_builders.rs b/crates/events-codec/tests/tag_builders.rs @@ -0,0 +1,449 @@ +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, RadrootsCoreUnit, +}; +use radroots_events::app_data::RadrootsAppData; +use radroots_events::comment::RadrootsComment; +use radroots_events::coop::RadrootsCoop; +use radroots_events::document::{RadrootsDocument, RadrootsDocumentSubject}; +use radroots_events::farm::{ + RadrootsFarm, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, + RadrootsGeoJsonPolygon, +}; +use radroots_events::follow::{RadrootsFollow, RadrootsFollowProfile}; +use radroots_events::geochat::RadrootsGeoChat; +use radroots_events::gift_wrap::{RadrootsGiftWrap, RadrootsGiftWrapRecipient}; +use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest}; +use radroots_events::job_feedback::RadrootsJobFeedback; +use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest}; +use radroots_events::job_result::RadrootsJobResult; +use radroots_events::kinds::{ + KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN, KIND_POST, +}; +use radroots_events::list::{RadrootsList, RadrootsListEntry}; +use radroots_events::list_set::RadrootsListSet; +use radroots_events::listing::{ + RadrootsListing, RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct, +}; +use radroots_events::message::{RadrootsMessage, RadrootsMessageRecipient}; +use radroots_events::message_file::RadrootsMessageFile; +use radroots_events::plot::RadrootsPlot; +use radroots_events::post::RadrootsPost; +use radroots_events::profile::RadrootsProfile; +use radroots_events::reaction::RadrootsReaction; +use radroots_events::resource_area::{ + RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef, +}; +use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct}; +use radroots_events::seal::RadrootsSeal; +use radroots_events::RadrootsNostrEventPtr; +use radroots_events::RadrootsNostrEventRef; +use radroots_events_codec::job::encode::JobEncodeError; +use radroots_events_codec::listing::encode::listing_build_tags; +use radroots_events_codec::tag_builders::RadrootsEventTagBuilder; + +const TEST_PUBKEY_HEX: &str = "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62"; +const TEST_NPUB: &str = "npub1tr33s4tj2le2kk9yzhfphdtss26gyn8kv7savnnjhj794nqp333q8e7grr"; + +fn sample_event_ref(id: &str) -> RadrootsNostrEventRef { + RadrootsNostrEventRef { + id: id.to_string(), + author: TEST_PUBKEY_HEX.to_string(), + kind: KIND_POST, + d_tag: None, + relays: None, + } +} + +fn sample_gcs() -> RadrootsGcsLocation { + RadrootsGcsLocation { + lat: 37.0, + lng: -122.0, + geohash: "9q8yy".to_string(), + point: RadrootsGeoJsonPoint { + r#type: "Point".to_string(), + coordinates: [-122.0, 37.0], + }, + polygon: RadrootsGeoJsonPolygon { + r#type: "Polygon".to_string(), + coordinates: vec![vec![ + [-122.0, 37.0], + [-122.0, 37.0001], + [-122.0001, 37.0001], + [-122.0, 37.0], + ]], + }, + accuracy: None, + altitude: None, + tag_0: None, + label: None, + area: None, + elevation: None, + soil: None, + climate: None, + gc_id: None, + gc_name: None, + gc_admin1_id: None, + gc_admin1_name: None, + gc_country_id: None, + gc_country_name: None, + } +} + +fn sample_listing() -> RadrootsListing { + let quantity = + RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each); + let price = RadrootsCoreQuantityPrice::new( + RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD), + quantity.clone(), + ); + + RadrootsListing { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + farm: RadrootsListingFarmRef { + pubkey: TEST_NPUB.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + product: RadrootsListingProduct { + key: "sku".to_string(), + title: "Widget".to_string(), + category: "Tools".to_string(), + summary: None, + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: "bin-1".to_string(), + bins: vec![RadrootsListingBin { + bin_id: "bin-1".to_string(), + quantity, + price_per_canonical_unit: price, + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: None, + availability: None, + delivery_method: None, + location: None, + images: None, + } +} + +#[test] +fn event_tag_builder_impls_build_tags_for_all_supported_types() { + let listing = sample_listing(); + assert!(!listing.build_tags().unwrap().is_empty()); + assert!(!listing_build_tags(&listing).unwrap().is_empty()); + + let app_data = RadrootsAppData { + d_tag: "radroots.app".to_string(), + content: "payload".to_string(), + }; + assert!(!app_data.build_tags().unwrap().is_empty()); + + let comment = RadrootsComment { + root: sample_event_ref("root"), + parent: sample_event_ref("parent"), + content: "hello".to_string(), + }; + assert!(!comment.build_tags().unwrap().is_empty()); + + let reaction = RadrootsReaction { + root: sample_event_ref("root"), + content: "+".to_string(), + }; + assert!(!reaction.build_tags().unwrap().is_empty()); + + let message = RadrootsMessage { + recipients: vec![RadrootsMessageRecipient { + public_key: TEST_PUBKEY_HEX.to_string(), + relay_url: Some("wss://relay.example.com".to_string()), + }], + content: "hello".to_string(), + reply_to: Some(RadrootsNostrEventPtr { + id: "reply".to_string(), + relays: Some("wss://relay.example.com".to_string()), + }), + subject: Some("topic".to_string()), + }; + assert!(!message.build_tags().unwrap().is_empty()); + + let message_file = RadrootsMessageFile { + recipients: vec![RadrootsMessageRecipient { + public_key: TEST_PUBKEY_HEX.to_string(), + relay_url: None, + }], + file_url: "https://files.example.com/blob".to_string(), + reply_to: None, + subject: None, + file_type: "image/jpeg".to_string(), + encryption_algorithm: "aes-gcm".to_string(), + decryption_key: "key".to_string(), + decryption_nonce: "nonce".to_string(), + encrypted_hash: "hash".to_string(), + original_hash: None, + size: None, + dimensions: None, + blurhash: None, + thumb: None, + fallbacks: vec!["https://files.example.com/fallback".to_string()], + }; + assert!(!message_file.build_tags().unwrap().is_empty()); + + let geochat = RadrootsGeoChat { + geohash: "dr5rsj7".to_string(), + content: "hello".to_string(), + nickname: Some("alex".to_string()), + teleported: true, + }; + assert!(!geochat.build_tags().unwrap().is_empty()); + + let follow = RadrootsFollow { + list: vec![RadrootsFollowProfile { + published_at: 1, + public_key: TEST_PUBKEY_HEX.to_string(), + relay_url: Some("wss://relay.example.com".to_string()), + contact_name: Some("alex".to_string()), + }], + }; + assert!(!follow.build_tags().unwrap().is_empty()); + + let farm = RadrootsFarm { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + name: "Farm".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location: None, + tags: None, + }; + assert!(!farm.build_tags().unwrap().is_empty()); + + let resource_area = RadrootsResourceArea { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + name: "Area".to_string(), + about: None, + location: RadrootsResourceAreaLocation { + primary: None, + city: None, + region: None, + country: None, + gcs: sample_gcs(), + }, + tags: None, + }; + assert!(!resource_area.build_tags().unwrap().is_empty()); + + let resource_cap = RadrootsResourceHarvestCap { + d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(), + resource_area: RadrootsResourceAreaRef { + pubkey: TEST_PUBKEY_HEX.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + }, + product: RadrootsResourceHarvestProduct { + key: "nutmeg".to_string(), + category: Some("spice".to_string()), + }, + start: 1, + end: 2, + cap_quantity: RadrootsCoreQuantity::new( + RadrootsCoreDecimal::from(1000u32), + RadrootsCoreUnit::MassG, + ), + display_amount: None, + display_unit: None, + display_label: None, + tags: None, + }; + assert!(!resource_cap.build_tags().unwrap().is_empty()); + + let coop = RadrootsCoop { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + name: "Coop".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location: None, + tags: None, + }; + assert!(!coop.build_tags().unwrap().is_empty()); + + let document = RadrootsDocument { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + doc_type: "charter".to_string(), + title: "Charter".to_string(), + version: "1.0.0".to_string(), + summary: None, + effective_at: None, + body_markdown: None, + subject: RadrootsDocumentSubject { + pubkey: TEST_PUBKEY_HEX.to_string(), + address: Some("30340:58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62:AAAAAAAAAAAAAAAAAAAAAA".to_string()), + }, + tags: None, + }; + assert!(!document.build_tags().unwrap().is_empty()); + + let list = RadrootsList { + content: "private".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![TEST_PUBKEY_HEX.to_string()], + }], + }; + assert!(!list.build_tags().unwrap().is_empty()); + + let list_set = RadrootsListSet { + d_tag: "members.owners".to_string(), + content: "private".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![TEST_PUBKEY_HEX.to_string()], + }], + title: Some("owners".to_string()), + description: Some("team".to_string()), + image: Some("https://example.com/team.png".to_string()), + }; + assert!(!list_set.build_tags().unwrap().is_empty()); + + let plot = RadrootsPlot { + d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(), + farm: RadrootsFarmRef { + pubkey: TEST_PUBKEY_HEX.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + name: "Plot".to_string(), + about: None, + location: None, + tags: None, + }; + assert!(!plot.build_tags().unwrap().is_empty()); + + let job_request = RadrootsJobRequest { + kind: (KIND_JOB_REQUEST_MIN + 1) as u16, + inputs: vec![RadrootsJobInput { + data: "hello".to_string(), + input_type: JobInputType::Text, + relay: None, + marker: None, + }], + output: None, + params: vec![RadrootsJobParam { + key: "foo".to_string(), + value: "bar".to_string(), + }], + bid_sat: None, + relays: vec!["wss://relay.example.com".to_string()], + providers: vec![TEST_PUBKEY_HEX.to_string()], + topics: vec!["topic".to_string()], + encrypted: false, + }; + assert!(!job_request.build_tags().unwrap().is_empty()); + + let job_result = RadrootsJobResult { + kind: (KIND_JOB_RESULT_MIN + 1) as u16, + request_event: RadrootsNostrEventPtr { + id: "req".to_string(), + relays: Some("wss://relay.example.com".to_string()), + }, + request_json: None, + inputs: vec![RadrootsJobInput { + data: "hello".to_string(), + input_type: JobInputType::Text, + relay: None, + marker: None, + }], + customer_pubkey: Some(TEST_PUBKEY_HEX.to_string()), + payment: Some(JobPaymentRequest { + amount_sat: 1, + bolt11: None, + }), + content: Some("payload".to_string()), + encrypted: false, + }; + assert!(!job_result.build_tags().unwrap().is_empty()); + + let job_feedback = RadrootsJobFeedback { + kind: KIND_JOB_FEEDBACK as u16, + status: JobFeedbackStatus::Processing, + extra_info: Some("queued".to_string()), + request_event: RadrootsNostrEventPtr { + id: "req".to_string(), + relays: Some("wss://relay.example.com".to_string()), + }, + customer_pubkey: Some(TEST_PUBKEY_HEX.to_string()), + payment: Some(JobPaymentRequest { + amount_sat: 1, + bolt11: None, + }), + content: Some("payload".to_string()), + encrypted: false, + }; + assert!(!job_feedback.build_tags().unwrap().is_empty()); + + let seal = RadrootsSeal { + content: "sealed".to_string(), + }; + assert!(seal.build_tags().unwrap().is_empty()); + + let gift_wrap = RadrootsGiftWrap { + recipient: RadrootsGiftWrapRecipient { + public_key: TEST_PUBKEY_HEX.to_string(), + relay_url: Some("wss://relay.example.com".to_string()), + }, + content: "encrypted".to_string(), + expiration: Some(1700000000), + }; + assert!(!gift_wrap.build_tags().unwrap().is_empty()); + + let profile = RadrootsProfile { + name: "alice".to_string(), + display_name: None, + nip05: None, + about: None, + website: None, + picture: None, + banner: None, + lud06: None, + lud16: None, + bot: None, + }; + assert!(profile.build_tags().unwrap().is_empty()); + + let post = RadrootsPost { + content: "hello".to_string(), + }; + assert!(post.build_tags().unwrap().is_empty()); +} + +#[test] +fn job_request_tag_builder_rejects_encrypted_without_provider() { + let request = RadrootsJobRequest { + kind: (KIND_JOB_REQUEST_MIN + 1) as u16, + inputs: vec![RadrootsJobInput { + data: "hello".to_string(), + input_type: JobInputType::Text, + relay: None, + marker: None, + }], + output: None, + params: Vec::new(), + bid_sat: None, + relays: Vec::new(), + providers: Vec::new(), + topics: Vec::new(), + encrypted: true, + }; + let err = request.build_tags().unwrap_err(); + assert!(matches!(err, JobEncodeError::MissingProvidersForEncrypted)); +}