-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7451e76
commit 9426345
Showing
7 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// swift-tools-version:5.5 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "PassGen", | ||
dependencies: [ | ||
// Dependencies declare other packages that this package depends on. | ||
// .package(url: /* package url */, from: "1.0.0"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package. A target can define a module or a test suite. | ||
// Targets can depend on other targets in this package, and on products in packages this package depends on. | ||
.executableTarget( | ||
name: "PassGen", | ||
dependencies: []), | ||
.testTarget( | ||
name: "PassGenTests", | ||
dependencies: ["PassGen"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# PassGen | ||
|
||
A description of this package. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// | ||
// PasswordGenerator.swift | ||
// | ||
// | ||
// Created by Andrej Kolar on 6/8/22. | ||
// | ||
|
||
import Foundation | ||
|
||
struct PasswordGenerator { | ||
public struct Defaults { | ||
static let length = 16 | ||
static let wordLength = 8 | ||
static let wordCount = 4 | ||
static let type: PasswordType = .normal(Defaults.length) | ||
static let characters: Set<CharacterType> = [ | ||
.uppercase, | ||
.lowercase, | ||
.numbers, | ||
.symbols | ||
] | ||
static let separator: SeparatorType = .space | ||
} | ||
|
||
enum PasswordType { | ||
case normal(Int) | ||
case separated(Int, Int, SeparatorType) | ||
} | ||
|
||
enum CharacterType: String { | ||
case uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
case lowercase = "abcdefghijklmnopqrstuvwxyz" | ||
case numbers = "0123456789" | ||
case symbols = "!@#$%^&*()+_-=}{[]|:;\"/?.><,`~" | ||
} | ||
|
||
enum SeparatorType: String { | ||
case hyphen = "-" | ||
case underscore = "_" | ||
case space = " " | ||
case slash = "/" | ||
case verticalBar = "|" | ||
} | ||
|
||
public func generate( | ||
type: PasswordType? = nil, | ||
characters: Set<CharacterType>? = nil | ||
) -> String { | ||
|
||
var passwordLength = Defaults.length | ||
|
||
var separatorIndices: [Int] = [] | ||
var separatorCharacter = Defaults.separator.rawValue | ||
|
||
let typeOption = type ?? Defaults.type | ||
switch typeOption { | ||
case .normal(let length): | ||
passwordLength = length | ||
|
||
case .separated(let wordLength, let wordCount, let separator): | ||
let separatorCount = wordCount - 1 | ||
passwordLength = (wordLength * wordCount) + (separatorCount) | ||
|
||
separatorIndices = (0..<separatorCount) | ||
.compactMap { index in | ||
((index + 1) * wordLength) + (index * 1) | ||
} | ||
|
||
separatorCharacter = separator.rawValue | ||
} | ||
|
||
let characterOptions = characters ?? Defaults.characters | ||
let charactersArray = characterOptions.map { $0.rawValue } | ||
|
||
let characterArrays: [String] = (0..<passwordLength) | ||
.compactMap { index in | ||
guard case .separated = typeOption else { | ||
return charactersArray.randomElement() | ||
} | ||
|
||
let isSeparator = separatorIndices.contains(index) | ||
|
||
return isSeparator ? separatorCharacter : charactersArray.randomElement() | ||
} | ||
|
||
let pass = characterArrays.compactMap { | ||
$0.randomElement() | ||
} | ||
|
||
return String(pass) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
let generator = PasswordGenerator() | ||
let password = generator.generate( | ||
type: .separated(8, 4, .space), | ||
characters: [.numbers, .lowercase, .uppercase] | ||
) | ||
|
||
print("Generated password:") | ||
print(password) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import XCTest | ||
import class Foundation.Bundle | ||
|
||
final class PassGenTests: XCTestCase { | ||
func testExample() throws { | ||
// This is an example of a functional test case. | ||
// Use XCTAssert and related functions to verify your tests produce the correct | ||
// results. | ||
|
||
// Some of the APIs that we use below are available in macOS 10.13 and above. | ||
guard #available(macOS 10.13, *) else { | ||
return | ||
} | ||
|
||
// Mac Catalyst won't have `Process`, but it is supported for executables. | ||
#if !targetEnvironment(macCatalyst) | ||
|
||
let fooBinary = productsDirectory.appendingPathComponent("PassGen") | ||
|
||
let process = Process() | ||
process.executableURL = fooBinary | ||
|
||
let pipe = Pipe() | ||
process.standardOutput = pipe | ||
|
||
try process.run() | ||
process.waitUntilExit() | ||
|
||
let data = pipe.fileHandleForReading.readDataToEndOfFile() | ||
let output = String(data: data, encoding: .utf8) | ||
|
||
XCTAssertEqual(output, "Hello, world!\n") | ||
#endif | ||
} | ||
|
||
/// Returns path to the built products directory. | ||
var productsDirectory: URL { | ||
#if os(macOS) | ||
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { | ||
return bundle.bundleURL.deletingLastPathComponent() | ||
} | ||
fatalError("couldn't find the products directory") | ||
#else | ||
return Bundle.main.bundleURL | ||
#endif | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// | ||
// PasswordGeneratorTests.swift | ||
// | ||
// | ||
// Created by Andrej Kolar on 6/8/22. | ||
// | ||
|
||
import XCTest | ||
|
||
@testable import PassGen | ||
|
||
class UnitTests_macOS: XCTestCase { | ||
|
||
var sut: PasswordGenerator! | ||
|
||
override func setUpWithError() throws { | ||
try super.setUpWithError() | ||
sut = PasswordGenerator() | ||
} | ||
|
||
override func tearDownWithError() throws { | ||
sut = nil | ||
try super.tearDownWithError() | ||
} | ||
|
||
|
||
func testGeneratePassword() { | ||
XCTAssertFalse(sut.generate().isEmpty, "Password is empty") | ||
} | ||
|
||
func testDefaultPasswordLength() { | ||
XCTAssertEqual( | ||
sut.generate().count, | ||
PasswordGenerator.Defaults.length, | ||
"Password is not the default length" | ||
) | ||
} | ||
|
||
func testCustomPasswordLength() { | ||
let length = 17 | ||
XCTAssertEqual( | ||
sut.generate(type: .normal(length)).count, | ||
length, | ||
"Password is not the passed in length" | ||
) | ||
} | ||
|
||
func testDefaultOptions() { | ||
let password = sut.generate() | ||
let isNumeric = checkCharacters(password, type: .numbers) | ||
let isLowercase = checkCharacters(password, type: .lowercase) | ||
let isUppercase = checkCharacters(password, type: .uppercase) | ||
let isSymbol = checkCharacters(password, type: .symbols) | ||
|
||
XCTAssertFalse(isNumeric, "Password should not contain only numbers") | ||
XCTAssertFalse(isLowercase, "Password should not contain only lowercase letters") | ||
XCTAssertFalse(isUppercase, "Password should not contain only uppercase letters") | ||
XCTAssertFalse(isSymbol, "Password should not contain only symbols") | ||
} | ||
|
||
func testOnlyNumbers() { | ||
let password = sut.generate(characters: [.numbers]) | ||
let isNumeric = checkCharacters(password, type: .numbers) | ||
XCTAssertTrue(isNumeric, "Password is not number only") | ||
} | ||
|
||
func testOnlyLowerCase() { | ||
let password = sut.generate(characters: [.lowercase]) | ||
let isLowercase = checkCharacters(password, type: .lowercase) | ||
XCTAssertTrue(isLowercase, "Password should contain only lowercase letters") | ||
} | ||
|
||
func testOnlyUppercase() { | ||
let password = sut.generate(characters: [.uppercase]) | ||
let isUppercase = checkCharacters(password, type: .uppercase) | ||
XCTAssertTrue(isUppercase, "Password should contain only uppercase letters") | ||
} | ||
|
||
func testOnlySymbols() { | ||
let password = sut.generate(characters: [.symbols]) | ||
let isSymbol = checkCharacters(password, type: .symbols) | ||
|
||
XCTAssertTrue(isSymbol, "Password should contain only symbols") | ||
} | ||
|
||
func testWithSeparatorsLength() { | ||
let wordLength = 4 | ||
let wordCount = 4 | ||
|
||
let length = lengthWithSeparator(wordLength: wordLength, wordCount: wordCount) | ||
|
||
let password = sut.generate( | ||
type: .separated(wordLength, wordCount, .verticalBar), | ||
characters: [.uppercase] | ||
) | ||
|
||
XCTAssertEqual( | ||
password.count, | ||
length, | ||
"Password is not the correct length" | ||
) | ||
} | ||
|
||
func testWithSeparatorsCharacters() { | ||
let wordLength = 4 | ||
let wordCount = 4 | ||
|
||
let password = sut.generate( | ||
type: .separated(wordLength, wordCount, .verticalBar), | ||
characters: [.uppercase] | ||
) | ||
|
||
XCTAssertTrue(checkIfSeparator(password, index: 4, separator: "|"), "Character is not a separator") | ||
XCTAssertTrue(checkIfSeparator(password, index: 9, separator: "|"), "Character is not a separator") | ||
XCTAssertTrue(checkIfSeparator(password, index: 14, separator: "|"), "Character is not a separator") | ||
|
||
XCTAssertFalse(checkIfSeparator(password, index: 1, separator: "|"), "Separator at wrong place") | ||
XCTAssertFalse(checkIfSeparator(password, index: 10, separator: "|"), "Separator at wrong place") | ||
XCTAssertFalse(checkIfSeparator(password, index: 13, separator: "|"), "Separator at wrong place") | ||
|
||
XCTAssertFalse(checkIfSeparator(password, index: 4, separator: "_"), "Wrong separator character") | ||
XCTAssertFalse(checkIfSeparator(password, index: 9, separator: " "), "Wrong separator character") | ||
XCTAssertFalse(checkIfSeparator(password, index: 14, separator: "/"), "Wrong separator character") | ||
} | ||
|
||
// Helpers | ||
|
||
private func checkCharacters(_ password: String, type: PasswordGenerator.CharacterType) -> Bool { | ||
let typeCharacterSet = CharacterSet(charactersIn: type.rawValue) | ||
let passwordCharacterSet = CharacterSet(charactersIn: password) | ||
return passwordCharacterSet.isSubset(of: typeCharacterSet) | ||
|
||
} | ||
|
||
private func lengthWithSeparator(wordLength: Int, wordCount: Int) -> Int { | ||
(wordLength * wordCount) + (wordCount - 1) | ||
} | ||
|
||
private func checkIfSeparator(_ password: String, index: Int, separator: Character) -> Bool { | ||
let char = password[password.index(password.startIndex, offsetBy: index)] | ||
return char == separator | ||
} | ||
} |