Files
container/Tests/ContainerClientTests/ParserTest.swift
T
Danish Singh Sethi c909cb3a27 Add --mac-address flag to set custom MAC addresses for containers (#753)
- Closes #752.
- Currently, there is no way to specify a custom MAC address for a
container's network interface and the MAC address is auto-generated by
the system.
- Use Cases
  - **Network Testing**: Developers testing network-dependent applications
that need predictable MAC addresses
  - **License Management**: Running containerized software with MAC-based
license keys
  - **Network Automation**: Scripts and tools that expect specific MAC
addresses for configuration
  - **Debugging**: Consistent MAC addresses across container restarts for
easier troubleshooting
2025-10-30 16:26:06 -07:00

507 lines
16 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import Foundation
import Testing
@testable import ContainerClient
struct ParserTest {
@Test
func testPublishPortParserTcp() throws {
let result = try Parser.publishPorts(["127.0.0.1:8080:8000/tcp"])
#expect(result.count == 1)
#expect(result[0].hostAddress == "127.0.0.1")
#expect(result[0].hostPort == UInt16(8080))
#expect(result[0].containerPort == UInt16(8000))
#expect(result[0].proto == .tcp)
}
@Test
func testPublishPortParserUdp() throws {
let result = try Parser.publishPorts(["192.168.32.36:8000:8080/UDP"])
#expect(result.count == 1)
#expect(result[0].hostAddress == "192.168.32.36")
#expect(result[0].hostPort == UInt16(8000))
#expect(result[0].containerPort == UInt16(8080))
#expect(result[0].proto == .udp)
}
@Test
func testPublishPortNoHostAddress() throws {
let result = try Parser.publishPorts(["8080:8000/tcp"])
#expect(result.count == 1)
#expect(result[0].hostAddress == "0.0.0.0")
#expect(result[0].hostPort == UInt16(8080))
#expect(result[0].containerPort == UInt16(8000))
#expect(result[0].proto == .tcp)
}
@Test
func testPublishPortNoProtocol() throws {
let result = try Parser.publishPorts(["8080:8000"])
#expect(result.count == 1)
#expect(result[0].hostAddress == "0.0.0.0")
#expect(result[0].hostPort == UInt16(8080))
#expect(result[0].containerPort == UInt16(8000))
#expect(result[0].proto == .tcp)
}
@Test
func testPublishPortInvalidProtocol() throws {
#expect {
_ = try Parser.publishPorts(["8080:8000/sctp"])
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish protocol")
}
}
@Test
func testPublishPortInvalidValue() throws {
#expect {
_ = try Parser.publishPorts([""])
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish value")
}
}
@Test
func testPublishPortInvalidAddress() throws {
#expect {
_ = try Parser.publishPorts(["1234"])
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish address")
}
}
@Test
func testPublishPortInvalidHostPort() throws {
#expect {
_ = try Parser.publishPorts(["foo:1234"])
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish host port")
}
}
@Test
func testPublishPortInvalidContainerPort() throws {
#expect {
_ = try Parser.publishPorts(["1234:foo"])
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish container port")
}
}
@Test
func testMountBindRelativePath() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: tempDir)
}
let originalDir = FileManager.default.currentDirectoryPath
FileManager.default.changeCurrentDirectoryPath(tempDir.path)
defer {
FileManager.default.changeCurrentDirectoryPath(originalDir)
}
let result = try Parser.mount("type=bind,src=.,dst=/foo")
switch result {
case .filesystem(let fs):
let expectedPath = URL(filePath: ".").absoluteURL.path
#expect(fs.source == expectedPath)
#expect(fs.destination == "/foo")
#expect(!fs.isVolume)
case .volume:
#expect(Bool(false), "Expected filesystem mount, got volume")
}
}
@Test
func testMountBindAbsolutePath() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-abs-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: tempDir)
}
let result = try Parser.mount("type=bind,src=\(tempDir.path),dst=/foo")
switch result {
case .filesystem(let fs):
#expect(fs.source == tempDir.path)
#expect(fs.destination == "/foo")
#expect(!fs.isVolume)
case .volume:
#expect(Bool(false), "Expected filesystem mount, got volume")
}
}
@Test
func testMountVolumeValidName() throws {
let result = try Parser.mount("type=volume,src=myvolume,dst=/data")
switch result {
case .filesystem:
#expect(Bool(false), "Expected volume mount, got filesystem")
case .volume(let vol):
#expect(vol.name == "myvolume")
#expect(vol.destination == "/data")
}
}
@Test
func testMountVolumeInvalidName() throws {
#expect {
_ = try Parser.mount("type=volume,src=.,dst=/data")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid volume name")
}
}
@Test
func testMountBindNonExistentPath() throws {
#expect {
_ = try Parser.mount("type=bind,src=/nonexistent/path,dst=/foo")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("path") && error.description.contains("does not exist")
}
}
@Test
func testMountBindFileInsteadOfDirectory() throws {
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-file-\(UUID().uuidString)")
try "test content".write(to: tempFile, atomically: true, encoding: .utf8)
defer {
try? FileManager.default.removeItem(at: tempFile)
}
#expect {
_ = try Parser.mount("type=bind,src=\(tempFile.path),dst=/foo")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("path") && error.description.contains("is not a directory")
}
}
@Test
func testIsValidDomainNameOk() throws {
let names = [
"a",
"a.b",
"foo.bar",
"F-O.B-R",
[
String(repeating: "0", count: 63),
String(repeating: "1", count: 63),
String(repeating: "2", count: 63),
String(repeating: "3", count: 63),
].joined(separator: "."),
]
for name in names {
#expect(Parser.isValidDomainName(name))
}
}
@Test
func testIsValidDomainNameBad() throws {
let names = [
".foo",
"foo.",
".foo.bar",
"foo.bar.",
"-foo.bar",
"foo.bar-",
[
String(repeating: "0", count: 63),
String(repeating: "1", count: 63),
String(repeating: "2", count: 63),
String(repeating: "3", count: 62),
"4",
].joined(separator: "."),
]
for name in names {
#expect(!Parser.isValidDomainName(name))
}
}
private func tmpFileWithContent(_ content: String) throws -> URL {
let tempDir = FileManager.default.temporaryDirectory
let tempFile = tempDir.appendingPathComponent("envfile-test-\(UUID().uuidString)")
try content.write(to: tempFile, atomically: true, encoding: .utf8)
return tempFile
}
// NOTE: A lot of these env-file tests are recreations of the docker cli's unit tests for their
// env-file support.
@Test
func testParseEnvFileGoodFile() throws {
var content = """
foo=bar
baz=quux
# comment
_foobar=foobaz
with.dots=working
and_underscore=working too
"""
content += "\n \t "
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
let lines = try Parser.envFile(path: tmpFile.path)
let expectedLines = [
"foo=bar",
"baz=quux",
"_foobar=foobaz",
"with.dots=working",
"and_underscore=working too",
]
#expect(lines == expectedLines)
}
@Test
func testParseEnvFileMultipleEqualsSigns() throws {
let content = """
URL=https://foo.bar?baz=woo
"""
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
let lines = try Parser.envFile(path: tmpFile.path)
let expectedLines = [
"URL=https://foo.bar?baz=woo"
]
#expect(lines == expectedLines)
}
@Test
func testParseEnvFileEmptyFile() throws {
let tmpFile = try tmpFileWithContent("")
defer { try? FileManager.default.removeItem(at: tmpFile) }
let lines = try Parser.envFile(path: tmpFile.path)
#expect(lines.isEmpty)
}
@Test
func testParseEnvFileNonExistentFile() throws {
#expect {
_ = try Parser.envFile(path: "/nonexistent/foo_bar_baz")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("not found")
}
}
@Test
func testParseEnvFileBadlyFormattedFile() throws {
let content = """
foo=bar
f =quux
"""
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
#expect {
_ = try Parser.envFile(path: tmpFile.path)
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("contains whitespaces")
}
}
@Test
func testParseEnvFileRandomFile() throws {
let content = """
first line
another invalid line
"""
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
#expect {
_ = try Parser.envFile(path: tmpFile.path)
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("first line") && error.description.contains("contains whitespaces")
}
}
@Test
func testParseEnvVariableDefinitionsFile() throws {
let content = """
# comment=
UNDEFINED_VAR
HOME
"""
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
let variables = try Parser.envFile(path: tmpFile.path)
// HOME should be imported from environment
guard let homeValue = ProcessInfo.processInfo.environment["HOME"] else {
Issue.record("HOME environment variable not set")
return
}
#expect(variables.count == 1)
#expect(variables[0] == "HOME=\(homeValue)")
}
@Test
func testParseEnvVariableWithNoNameFile() throws {
let content = """
# comment=
=blank variable names are an error case
"""
let tmpFile = try tmpFileWithContent(content)
defer { try? FileManager.default.removeItem(at: tmpFile) }
#expect {
_ = try Parser.envFile(path: tmpFile.path)
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("no variable name")
}
}
// MARK: Network Parser Tests
@Test
func testParseNetworkSimpleName() throws {
let result = try Parser.network("default")
#expect(result.name == "default")
#expect(result.macAddress == nil)
}
@Test
func testParseNetworkWithMACAddress() throws {
let result = try Parser.network("backend,mac=02:42:ac:11:00:02")
#expect(result.name == "backend")
#expect(result.macAddress == "02:42:ac:11:00:02")
}
@Test
func testParseNetworkWithMACAddressHyphenSeparator() throws {
let result = try Parser.network("backend,mac=02-42-ac-11-00-02")
#expect(result.name == "backend")
#expect(result.macAddress == "02-42-ac-11-00-02")
}
@Test
func testParseNetworkEmptyString() throws {
#expect {
_ = try Parser.network("")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("network specification cannot be empty")
}
}
@Test
func testParseNetworkEmptyName() throws {
#expect {
_ = try Parser.network(",mac=02:42:ac:11:00:02")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("network name cannot be empty")
}
}
@Test
func testParseNetworkEmptyMACAddress() throws {
#expect {
_ = try Parser.network("backend,mac=")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("mac address value cannot be empty")
}
}
@Test
func testParseNetworkUnknownProperty() throws {
#expect {
_ = try Parser.network("backend,unknown=value")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("unknown network property") && error.description.contains("unknown")
}
}
@Test
func testParseNetworkInvalidPropertyFormat() throws {
#expect {
_ = try Parser.network("backend,invalidproperty")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid property format")
}
}
}