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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

import Foundation

/// Validates a handle typed into the username field: lowercases the input, then
/// holds it to the `Username` contract.
/// Validates a handle typed into the username field: lowercases the input and
/// drops the `@` a handle is shown with, then holds it to the `Username` contract.
///
/// Lowercasing lives here rather than in `Username.init?` so the model stays
/// strict — a handle reaching it uppercase is still a caller error. This is the
/// only place a typed handle is rewritten before it reaches the model.
/// That normalising lives here rather than in `Username.init?` so the model stays
/// strict — a handle reaching it uppercase or prefixed is still a caller error.
/// This is the only place a typed handle is rewritten before it reaches the model.
public struct UsernameValidator: Validator {

/// Why an input was rejected. The claim screen raises a different dialog for
Expand Down Expand Up @@ -54,9 +54,15 @@ public struct UsernameValidator: Validator {
return .invalidCharacters
}

/// Lowercased and stripped of surrounding whitespace. A paste carries both,
/// and neither is worth rejecting someone over.
/// Lowercased, stripped of surrounding whitespace, and of one leading `@`.
/// A paste carries all three — a handle copied out of a profile arrives as
/// `@taylor` — and none is worth rejecting someone over. Only one `@` goes:
/// `@@taylor` is not a handle anyone has, and the field never adds one.
private func normalized(_ input: String) -> String {
input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
var candidate = input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if candidate.hasPrefix("@") {
candidate.removeFirst()
}
return candidate
}
}
22 changes: 22 additions & 0 deletions FlipcashCore/Tests/FlipcashCoreTests/UsernameValidatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ struct UsernameValidatorTests {
#expect(validator.validate(" taylor ")?.value == "taylor")
}

@Test("A leading @ is dropped, so a pasted handle is accepted as its username")
func validate_leadingAt_stripped() {
#expect(validator.validate("@taylor")?.value == "taylor")
#expect(validator.validate(" @Taylor ")?.value == "taylor")
}

@Test("Only one leading @ is dropped; a second is an illegal character")
func validate_doubleLeadingAt_rejected() {
#expect(validator.validate("@@taylor") == nil)
#expect(validator.failure(for: "@@taylor") == .invalidCharacters)
}

@Test("A lone @ is an empty handle, so it reports too short")
func failure_loneAt_tooShort() {
#expect(validator.failure(for: "@") == .tooShort)
}

@Test("An @ anywhere but the front is an illegal character")
func failure_embeddedAt_invalidCharacters() {
#expect(validator.failure(for: "tay@lor") == .invalidCharacters)
}

@Test("Two characters is the shortest accepted handle")
func validate_minimumLength_boundary() {
#expect(validator.validate("ab")?.value == "ab")
Expand Down
Loading