|
1 | | -import yabase/core/error.{InvalidCharacter, InvalidLength, Overflow} |
| 1 | +import gleam/string |
| 2 | +import yabase/core/error.{ |
| 3 | + InvalidCharacter, InvalidChecksum, InvalidLength, Overflow, |
| 4 | +} |
2 | 5 | import yabase/intid |
3 | 6 |
|
4 | 7 | // === Base32 (RFC 4648) === |
@@ -359,6 +362,133 @@ pub fn decode_int_base32_crockford_bounded_above_cap_test() -> Nil { |
359 | 362 | == Error(Overflow) |
360 | 363 | } |
361 | 364 |
|
| 365 | +// === Issue #73: Crockford Base32 with check symbol === |
| 366 | + |
| 367 | +pub fn encode_int_base32_crockford_check_zero_test() -> Nil { |
| 368 | + // 0 encoded as Crockford "0" then check digit for 0 mod 37 == "0". |
| 369 | + assert intid.encode_int_base32_crockford_check(0) == "00" |
| 370 | +} |
| 371 | + |
| 372 | +pub fn decode_int_base32_crockford_check_roundtrip_test() -> Nil { |
| 373 | + let encoded = intid.encode_int_base32_crockford_check(987_654) |
| 374 | + assert intid.decode_int_base32_crockford_check(encoded) == Ok(987_654) |
| 375 | +} |
| 376 | + |
| 377 | +pub fn decode_int_base32_crockford_check_empty_test() -> Nil { |
| 378 | + assert intid.decode_int_base32_crockford_check("") == Error(InvalidLength(0)) |
| 379 | +} |
| 380 | + |
| 381 | +pub fn decode_int_base32_crockford_check_detects_typo_test() -> Nil { |
| 382 | + // Take a valid checksummed encoding and mutate one body character. |
| 383 | + // The decoder must reject the typo via InvalidChecksum, which is the |
| 384 | + // whole reason callers reach for the `_check` variant. |
| 385 | + let encoded = intid.encode_int_base32_crockford_check(123_456) |
| 386 | + let body = string_drop_last(encoded) |
| 387 | + let check = string_take_last(encoded) |
| 388 | + let mutated = mutate_first_body_char(body) <> check |
| 389 | + assert intid.decode_int_base32_crockford_check(mutated) |
| 390 | + == Error(InvalidChecksum) |
| 391 | +} |
| 392 | + |
| 393 | +pub fn decode_int_base32_crockford_check_bounded_within_test() -> Nil { |
| 394 | + let encoded = intid.encode_int_base32_crockford_check(42) |
| 395 | + assert intid.decode_int_base32_crockford_check_bounded( |
| 396 | + input: encoded, |
| 397 | + max: intid.int64_max, |
| 398 | + ) |
| 399 | + == Ok(42) |
| 400 | +} |
| 401 | + |
| 402 | +@target(erlang) |
| 403 | +pub fn decode_int_base32_crockford_check_bounded_above_cap_test() -> Nil { |
| 404 | + let encoded = intid.encode_int_base32_crockford_check(intid.int64_max + 1) |
| 405 | + assert intid.decode_int_base32_crockford_check_bounded( |
| 406 | + input: encoded, |
| 407 | + max: intid.int64_max, |
| 408 | + ) |
| 409 | + == Error(Overflow) |
| 410 | +} |
| 411 | + |
| 412 | +// === Issue #73: Base58Check === |
| 413 | +// |
| 414 | +// The Base58Check round-trip tests below are `@target(erlang)` because |
| 415 | +// `yabase/base58check`'s round-trip is itself only exercised on Erlang |
| 416 | +// in this repo (see `test/base58check_test.gleam`); the JS-side |
| 417 | +// SHA-256 divergence is pre-existing scope and tracked separately. The |
| 418 | +// `decode_int_base58check_empty_test` runs on both targets because |
| 419 | +// empty-input rejection short-circuits before any hashing. |
| 420 | + |
| 421 | +@target(erlang) |
| 422 | +pub fn encode_int_base58check_zero_roundtrips_test() -> Nil { |
| 423 | + let encoded = intid.encode_int_base58check(0) |
| 424 | + assert intid.decode_int_base58check(encoded) == Ok(0) |
| 425 | +} |
| 426 | + |
| 427 | +@target(erlang) |
| 428 | +pub fn decode_int_base58check_roundtrip_test() -> Nil { |
| 429 | + let encoded = intid.encode_int_base58check(9_999_999_999) |
| 430 | + assert intid.decode_int_base58check(encoded) == Ok(9_999_999_999) |
| 431 | +} |
| 432 | + |
| 433 | +pub fn decode_int_base58check_empty_test() -> Nil { |
| 434 | + assert intid.decode_int_base58check("") == Error(InvalidLength(0)) |
| 435 | +} |
| 436 | + |
| 437 | +@target(erlang) |
| 438 | +pub fn decode_int_base58check_detects_typo_test() -> Nil { |
| 439 | + // Mutate the first checksum-bearing position. Base58Check's 4-byte |
| 440 | + // SHA-256 suffix means this *must* fail — that is the property the |
| 441 | + // helper exists to guarantee for callers. |
| 442 | + let encoded = intid.encode_int_base58check(424_242) |
| 443 | + let mutated = mutate_first_body_char(encoded) |
| 444 | + assert intid.decode_int_base58check(mutated) == Error(InvalidChecksum) |
| 445 | +} |
| 446 | + |
| 447 | +@target(erlang) |
| 448 | +pub fn decode_int_base58check_bounded_within_test() -> Nil { |
| 449 | + let encoded = intid.encode_int_base58check(1234) |
| 450 | + assert intid.decode_int_base58check_bounded( |
| 451 | + input: encoded, |
| 452 | + max: intid.int64_max, |
| 453 | + ) |
| 454 | + == Ok(1234) |
| 455 | +} |
| 456 | + |
| 457 | +@target(erlang) |
| 458 | +pub fn decode_int_base58check_bounded_above_cap_test() -> Nil { |
| 459 | + let encoded = intid.encode_int_base58check(intid.int64_max + 1) |
| 460 | + assert intid.decode_int_base58check_bounded( |
| 461 | + input: encoded, |
| 462 | + max: intid.int64_max, |
| 463 | + ) |
| 464 | + == Error(Overflow) |
| 465 | +} |
| 466 | + |
| 467 | +// Helpers for the typo-detection tests above. Kept in the test module so |
| 468 | +// the production API stays focused on Int↔string. |
| 469 | +fn string_drop_last(s: String) -> String { |
| 470 | + let len = string.length(s) |
| 471 | + string.slice(s, 0, len - 1) |
| 472 | +} |
| 473 | + |
| 474 | +fn string_take_last(s: String) -> String { |
| 475 | + let len = string.length(s) |
| 476 | + string.slice(s, len - 1, 1) |
| 477 | +} |
| 478 | + |
| 479 | +fn mutate_first_body_char(s: String) -> String { |
| 480 | + // Flip the first character to a different valid Crockford / Base58 |
| 481 | + // character. Both alphabets contain "1" and "2", so swapping between |
| 482 | + // them produces a syntactically valid but checksum-invalid string. |
| 483 | + let head = string.slice(s, 0, 1) |
| 484 | + let tail = string.slice(s, 1, string.length(s) - 1) |
| 485 | + let replacement = case head { |
| 486 | + "1" -> "2" |
| 487 | + _ -> "1" |
| 488 | + } |
| 489 | + replacement <> tail |
| 490 | +} |
| 491 | + |
362 | 492 | // Issue #74: a wrapper that only `import yabase/intid` must be able to |
363 | 493 | // type-annotate the error returned by `decode_int_*` without reaching |
364 | 494 | // into `yabase/core/error`. This test pins down that the re-exported |
|
0 commit comments