use super::*;
#[test]
fn round_trip_gtin_serial() {
let uri = "https://id.odal-node.io/01/09506000134352/21/ABC123";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.gtin.as_str(), "09506000134352");
assert_eq!(dl.serial.as_deref(), Some("ABC123"));
assert_eq!(dl.build(), uri);
}
#[test]
fn non_https_scheme_is_rejected() {
assert!(matches!(
DigitalLink::parse("http://id.odal-node.io/01/09506000134352"),
Err(DigitalLinkError::InvalidScheme(_))
));
}
#[test]
fn round_trips_all_qualifiers_in_canonical_order() {
let uri = "https://id.odal-node.io/01/09506000134352/22/VAR-1/10/LOT-9/21/SN-7/235/TPX-3";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.variant.as_deref(), Some("VAR-1"));
assert_eq!(dl.batch.as_deref(), Some("LOT-9"));
assert_eq!(dl.serial.as_deref(), Some("SN-7"));
assert_eq!(dl.tpcsn.as_deref(), Some("TPX-3"));
assert_eq!(dl.build(), uri);
}
#[test]
fn parse_with_batch() {
let uri = "https://id.odal-node.io/01/09506000134352/10/BATCH01/21/SN001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.batch.as_deref(), Some("BATCH01"));
assert_eq!(dl.serial.as_deref(), Some("SN001"));
}
#[test]
fn invalid_gtin_rejected() {
let uri = "https://id.odal-node.io/01/123/21/SN1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::InvalidGtin(_))
));
}
#[test]
fn missing_gtin_rejected() {
let uri = "https://id.odal-node.io/21/SN1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::MissingGtin)
));
}
mod gs1_official_worked_examples {
use super::*;
#[test]
fn section_5_1_gtin_canonical() {
let uri = "https://id.gs1.org/01/09520123456788";
let dl = DigitalLink::parse(uri).expect("GS1's own canonical example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.resolver_base, "https://id.gs1.org");
assert_eq!(dl.build(), uri, "must round-trip to the canonical form");
}
#[test]
fn section_5_1_gtin_custom_domain() {
let uri = "https://brand.example.com/01/09520123456788";
let dl = DigitalLink::parse(uri).expect("GS1's own custom-domain example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.build(), uri);
}
#[test]
fn section_5_1_gtin_with_path_prefix() {
let uri = "https://brand.example.com/some-extra/pathinfo/01/09520123456788";
let dl = DigitalLink::parse(uri).expect("GS1's own path-prefix example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(
dl.resolver_base,
"https://brand.example.com/some-extra/pathinfo"
);
assert_eq!(dl.build(), uri, "path prefix must round-trip exactly");
}
#[test]
fn section_5_2_gtin_plus_cpv() {
let uri = "https://id.gs1.org/01/09520123456788/22/2A";
let dl = DigitalLink::parse(uri).expect("GS1's own CPV example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.variant.as_deref(), Some("2A"));
assert_eq!(dl.build(), uri);
}
#[test]
fn section_5_3_gtin_plus_batch_lot() {
let uri = "https://id.gs1.org/01/09520123456788/10/ABC123";
let dl = DigitalLink::parse(uri).expect("GS1's own batch/lot example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.batch.as_deref(), Some("ABC123"));
assert_eq!(dl.build(), uri);
}
#[test]
fn section_5_4_gtin_plus_serial() {
let uri = "https://id.gs1.org/01/09520123456788/21/12345";
let dl = DigitalLink::parse(uri).expect("GS1's own serial-number example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.serial.as_deref(), Some("12345"));
assert_eq!(dl.build(), uri);
}
#[test]
fn section_5_5_gtin_plus_batch_plus_serial_query_string_ignored() {
let uri = "https://id.gs1.org/01/09520123456788/10/ABC1/21/12345?17=180426";
let dl = DigitalLink::parse(uri).expect("GS1's own batch+serial+expiry example must parse");
assert_eq!(dl.gtin.as_str(), "09520123456788");
assert_eq!(dl.batch.as_deref(), Some("ABC1"));
assert_eq!(dl.serial.as_deref(), Some("12345"));
assert_eq!(
dl.build(),
"https://id.gs1.org/01/09520123456788/10/ABC1/21/12345"
);
}
}
#[test]
fn validate_gtin_valid() {
assert!(validate_gtin("09506000134352").is_ok());
}
#[test]
fn validate_gtin_wrong_length() {
assert!(matches!(
validate_gtin("123456"),
Err(DigitalLinkError::InvalidGtin(_))
));
}
#[test]
fn validate_gtin_non_digits() {
assert!(matches!(
validate_gtin("0950600013435X"),
Err(DigitalLinkError::InvalidGtin(_))
));
}
#[test]
fn validate_gtin_bad_check_digit() {
assert!(matches!(
validate_gtin("09506000134351"),
Err(DigitalLinkError::InvalidGtinCheckDigit { .. })
));
}
#[test]
fn build_qr_url_with_batch() {
let url = build_qr_url(
"https://id.odal-node.io",
"09506000134352",
"passport-123",
Some("BATCH-01"),
);
assert_eq!(
url,
"https://id.odal-node.io/01/09506000134352/10/BATCH-01/21/passport-123"
);
}
#[test]
fn build_qr_url_without_batch() {
let url = build_qr_url(
"https://id.odal-node.io",
"09506000134352",
"passport-456",
None,
);
assert_eq!(
url,
"https://id.odal-node.io/01/09506000134352/21/passport-456"
);
}
#[test]
fn bad_check_digit_rejected_via_parse() {
let uri = "https://id.odal-node.io/01/09506000134351/21/SN1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::InvalidGtinCheckDigit { .. })
));
}
#[test]
fn query_string_does_not_corrupt_serial() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN001?linkType=gs1:pip";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.serial.as_deref(), Some("SN001"));
}
#[test]
fn query_only_uri_returns_gtin_cleanly() {
let uri = "https://id.odal-node.io/01/09506000134352?linkType=gs1:pip";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.gtin.as_str(), "09506000134352");
assert_eq!(dl.serial, None);
}
#[test]
fn gtin_13_normalised_to_14() {
let uri = "https://id.odal-node.io/01/5901234123457/21/SN1";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.gtin.as_str(), "05901234123457");
}
#[test]
fn gtin_12_normalised_to_14() {
let uri = "https://id.odal-node.io/01/012345678905/21/SN1";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.gtin.as_str(), "00012345678905");
}
#[test]
fn gtin_of_invalid_length_rejected() {
let uri = "https://id.odal-node.io/01/123456789/21/SN1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::InvalidGtin(_))
));
}
#[test]
fn unknown_ai_rejected() {
let uri = "https://id.odal-node.io/01/09506000134352/99/unknown/21/SN1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::UnknownApplicationIdentifier(_))
));
}
#[test]
fn misordered_qualifiers_rejected() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN1/10/BATCH1";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::QualifiersOutOfOrder { .. })
));
}
#[test]
fn trailing_unpaired_ai_rejected() {
let uri = "https://id.odal-node.io/01/09506000134352/21";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::TrailingUnpairedSegment(_))
));
}
#[test]
fn duplicate_primary_key_rejected() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN1/01/00000000000001";
assert!(matches!(
DigitalLink::parse(uri),
Err(DigitalLinkError::DuplicatePrimaryKey)
));
}
#[test]
fn oversized_ai_value_rejected() {
let long_serial = "X".repeat(21);
let uri = format!("https://id.odal-node.io/01/09506000134352/21/{long_serial}");
assert!(matches!(
DigitalLink::parse(&uri),
Err(DigitalLinkError::ValueTooLong { code, max_len, actual })
if code == "21" && max_len == 20 && actual == 21
));
}
#[test]
fn percent_encoded_serial_decoded_on_parse() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN%2F001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.serial.as_deref(), Some("SN/001"));
}
#[test]
fn slash_in_serial_encoded_on_build() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN1";
let mut dl = DigitalLink::parse(uri).unwrap();
dl.serial = Some("SN/001".to_owned());
assert!(dl.build().contains("/21/SN%2F001"));
}
#[test]
fn question_mark_in_serial_encoded_on_build() {
let uri = "https://id.odal-node.io/01/09506000134352/21/SN1";
let mut dl = DigitalLink::parse(uri).unwrap();
dl.serial = Some("SN?001".to_owned());
assert!(dl.build().contains("/21/SN%3F001"));
}
#[test]
fn path_prefixed_resolver_round_trip() {
let uri = "https://example.com/resolve/01/09506000134352/21/SN001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.resolver_base, "https://example.com/resolve");
assert_eq!(dl.build(), uri);
}
#[test]
fn multi_segment_path_prefix_preserved() {
let uri = "https://example.com/api/v1/dpp/01/09506000134352/21/SN001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.resolver_base, "https://example.com/api/v1/dpp");
assert_eq!(dl.build(), uri);
}
#[test]
fn port_bearing_resolver_round_trip() {
let uri = "https://localhost:8080/01/09506000134352/21/SN001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.resolver_base, "https://localhost:8080");
assert_eq!(dl.build(), uri);
}
#[test]
fn path_prefixed_resolver_with_batch_round_trip() {
let uri = "https://example.com/resolve/01/09506000134352/10/LOT1/21/SN001";
let dl = DigitalLink::parse(uri).unwrap();
assert_eq!(dl.resolver_base, "https://example.com/resolve");
assert_eq!(dl.build(), uri);
}