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
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Swift Test (Ubuntu 24.04)

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
container:
image: swift:6.2.3-noble

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Cache Swift dependencies
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-swift-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-swift-

- name: Install dependencies
run: |
apt-get update
apt-get install -y dbus-x11 gnome-keyring

- name: Test with DBus & Secret Service
run: |
# Use dbus-run-session to wrap the test execution
dbus-run-session -- sh -c '
echo "test" | gnome-keyring-daemon --unlock --components=secrets
RUN_INTEGRATION_TESTS=1 swift test --parallel -Xcc -I/usr/include -Xlinker -L/usr/lib
'
18 changes: 18 additions & 0 deletions Sources/SecretService/Helper/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ extension Array where Element == DBusValue {

return result
}

/// Tries to convert [DBusValue] to [String] as objectPaths
/// Returns nil if the array contains something not a byte
var asObjectPathArray: [String]? {
var result = [String]()

for element in self {
guard let byte = element.objectPath else {
print("found unexpected type")
return nil
}

result.append(byte)
}

return result
}

}

extension Array where Element == UInt8 {
Expand Down
12 changes: 12 additions & 0 deletions Sources/SecretService/Helper/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ extension Dictionary where Key == String, Value == DBusValue {
return new
}
}

extension Dictionary where Key == String, Value == String {
var asStringToString: [DBusValue: DBusValue] {
var new = [DBusValue: DBusValue]()

for (key, value) in self {
new[.string(key)] = .string(value)
}

return new
}
}
28 changes: 28 additions & 0 deletions Sources/SecretService/Helper/MessageDecoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,32 @@ extension DBusMessage {
throw .unexpectedResponse(for: "GetSecrets")
}
}

func decodeSearchItems() throws(SecSError) -> [String] {
if
case .methodReturn = self.messageType,
body.count >= 1,
let items = body[0].array?.asObjectPathArray
{
return items
} else if case .error = self.messageType {
throw .returnedError(body[0, nil]?.string)
} else {
throw .unexpectedResponse(for: "SearchItems")
}
}

func decodeDeleteItem() throws(SecSError) -> String? {
if
case .methodReturn = self.messageType,
body.count >= 1,
let prompt = body[0].objectPath
{
return prompt != "/" ? prompt: nil
} else if case .error = self.messageType {
throw .returnedError(body[0, nil]?.string)
} else {
throw .unexpectedResponse(for: "Items.Delete")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
39 changes: 38 additions & 1 deletion Sources/SecretService/SecretService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,43 @@ public final class SecretService: Sendable {
return try response.decodeGetSecrets(with: symmetricKey)
}

/// Search for items with certain attributes in the collection
public func searchItems(
for attributes: [String: String],
in collection: String
) async throws(SecSError) -> [String] {
let request = DBusRequest.createMethodCall(
destination: SecS.service,
path: collection,
interface: SecS.Iface.collection,
method: "SearchItems",
body: [
.dictionary(attributes.asStringToString)
]
)

guard let response = try await send(request) else { throw .noResponse }

return try response.decodeSearchItems()
}

/// Deletes an item
/// Returns prompt object or nil if no prompt is necessary
public func deleteItem(
item: String
) async throws(SecSError) -> String? {
let request = DBusRequest.createMethodCall(
destination: SecS.service,
path: item,
interface: SecS.Iface.item,
method: "Delete",
)

guard let response = try await send(request) else { throw .noResponse }

return try response.decodeDeleteItem()
}

public static func withDefaultConnection<R: Sendable>(
_ block: @escaping @Sendable (DBusServerConnection) async throws -> R
) async throws -> R {
Expand All @@ -143,7 +180,7 @@ public final class SecretService: Sendable {
}

/// Sends the request on the current connection and converts errors
private func send(_ request: DBusRequest) async throws(SecretServiceError) -> DBusMessage? {
private func send(_ request: DBusRequest) async throws(SecSError) -> DBusMessage? {
do {
return try await connection.send(request)
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import CryptoSwift
@testable import SecretService

class IntegrationTests {
/// Integration tests should set this attribute as string to true on temporarily created items
/// Integration tests should set this attribute as string to "1" on temporarily created items
/// They will be deleted in deinit in the future
static let teardownDeleteAttributeName = "swift-secret-service-delete-on-teardown"

Expand All @@ -29,7 +29,7 @@ class IntegrationTests {
}

@Test(.enabled(if: ProcessInfo.runIntegrationTests))
func testCreateReadItem() async throws {
func testCreateReadDeleteItem() async throws {
try await SecretService.withDefaultConnection { connection in
let service = SecretService(connection: connection)
try await service.connect()
Expand All @@ -45,11 +45,11 @@ class IntegrationTests {
"org.freedesktop.Secret.Item.Label": .string("test"),
"org.freedesktop.Secret.Item.Attributes": .dictionary([
.string("service"): .string("de.amethystsoft.swift-secret-service.tests"),
.string(Self.teardownDeleteAttributeName): .boolean(true)
.string(Self.teardownDeleteAttributeName): .string("1")
])
]

let (item, prompt) = try await service.createItem(
let (item, _) = try await service.createItem(
secret: Secret(value: secret.bytes),
collection: collection,
properties: properties
Expand Down Expand Up @@ -77,6 +77,26 @@ class IntegrationTests {

#expect(key == item)
#expect(value.value == secret.bytes)

try await Self.teardown(collection: collection, service: service)

let itemsAfterDelete = try await service.searchItems(
for: [Self.teardownDeleteAttributeName: "1"],
in: collection
)

#expect(itemsAfterDelete.isEmpty)
}
}

static func teardown(collection: String, service: SecretService) async throws {
let items = try await service.searchItems(
for: [Self.teardownDeleteAttributeName: "1"],
in: collection
)

for item in items {
_ = try await service.deleteItem(item: item)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Expand Down
Loading