diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..ca036e3 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,17 @@ +name: Deployment + +on: + push: + tags: + - 'v*' + - '[0-9]*' + workflow_dispatch: + +jobs: + + create-docc-and-deploy: + uses: BinaryBirds/github-workflows/.github/workflows/docc_deploy.yml@main + permissions: + contents: read + pages: write + id-token: write diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml deleted file mode 100644 index b10006f..0000000 --- a/.github/workflows/run-checks.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Run checks - -on: - pull_request: - branches: - - main - -jobs: - - run-checks: - # runs-on: macOS-latest - runs-on: self-hosted - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Install Dependencies - run: | - brew install mint - mint install NickLockwood/SwiftFormat@0.53.4 --no-link - - - name: run script - run: ./scripts/run-checks.sh diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 174656d..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Run tests - -on: - push: - branches: - - main - paths: - - '**.swift' - pull_request: - branches: - - main - -jobs: - - macOS-tests: - runs-on: self-hosted - steps: - - - name: Checkout - uses: actions/checkout@v4 - - # - name: Cache - # uses: actions/cache@v3 - # with: - # path: server/.build - # key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - # restore-keys: ${{ runner.os }}-spm- - - - name: Test - run: swift test --parallel --enable-code-coverage - - # linux-tests: - # runs-on: ubuntu-latest - # strategy: - # matrix: - # image: - # - 'swift:5.9' - # - 'swift:5.10' - # container: - # image: ${{ matrix.image }} - # steps: - - # - name: Checkout - # uses: actions/checkout@v4 - - # # - name: Cache - # # uses: actions/cache@v3 - # # with: - # # path: server/.build - # # key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - # # restore-keys: ${{ runner.os }}-spm- - - # - uses: webfactory/ssh-agent@v0.9.0 - # with: - # ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - # - name: Test - # shell: bash - # run: | - # mkdir ~/.ssh - # echo $PRIVATE_KEY > ~/.ssh/key - # touch ~/.ssh/config - # touch ~/.ssh/known_hosts - # echo "Host github.com" >> ~/.ssh/config - # echo " HostName github.com" >> ~/.ssh/config - # echo " User git" >> ~/.ssh/config - # echo " IdentityFile ./.ssh/key" >> ~/.ssh/config - # echo " StrictHostKeyChecking no" >> ~/.ssh/config - # echo " UserKnownHostsFile ~/.ssh/known_hosts" >> ~/.ssh/config - # chmod 600 ~/.ssh/config - # ssh-keyscan github.com >> ~/.ssh/known_hosts - # eval "$(ssh-agent -s)" - # ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}" - # swift test --parallel --enable-code-coverage - # env: - # PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..539e860 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,36 @@ +name: Testing + +on: + pull_request: + branches: + - main + +jobs: + api_breakage: + name: Check API breakage + uses: BinaryBirds/github-workflows/.github/workflows/api_breakage.yml@main + + swiftlang_checks: + name: Swiftlang Checks + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "project" + format_check_enabled: true + broken_symlink_check_enabled: true + unacceptable_language_check_enabled: true + shell_check_enabled: false + docs_check_enabled: false + api_breakage_check_enabled: false + license_header_check_enabled: false + yamllint_check_enabled: false + python_lint_check_enabled: false + + bb_checks: + name: BB Checks + uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main + with: + local_swift_dependencies_check_enabled: true + run_tests_with_cache_enabled: true + headers_check_enabled: false + docc_warnings_check_enabled: true + run_tests_swift_versions: '["6.1", "6.2"]' diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 0000000..c73cf0c --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +Package.swift \ No newline at end of file diff --git a/LICENSE b/LICENSE index 9a07ee3..d48549a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2018-2022 Tibor Bödecs -Copyright (c) 2022-2024 Binary Birds Ltd. +Copyright (c) 2022-2026 Binary Birds Kft. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/Makefile b/Makefile index dc1a39a..9372543 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,48 @@ -build: - swift build +SHELL=/bin/bash -release: - swift build -c release - -test: - swift test --parallel +baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts + +check: symlinks language deps lint headers docc-warnings package + +breakage: + curl -s $(baseUrl)/check-api-breakage.sh | bash -test-with-coverage: - swift test --parallel --enable-code-coverage +package: + curl -s $(baseUrl)/check-swift-package.sh | bash -clean: - rm -rf .build +symlinks: + curl -s $(baseUrl)/check-broken-symlinks.sh | bash + +language: + curl -s $(baseUrl)/check-unacceptable-language.sh | bash + +deps: + curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash -check: - ./scripts/run-checks.sh +lint: + curl -s $(baseUrl)/run-swift-format.sh | bash format: - ./scripts/run-swift-format.sh --fix + curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix + +docc-local: + curl -s $(baseUrl)/generate-docc.sh | bash -s -- --local + +run-docc: + curl -s $(baseUrl)/run-docc-docker.sh | bash + +docc-warnings: + curl -s $(baseUrl)/check-docc-warnings.sh | bash + +headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash + +fix-headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix + +test: + swift test --parallel + +docker-test: + docker build -t tests . -f ./docker/tests/Dockerfile && docker run --rm tests + diff --git a/Package.resolved b/Package.resolved index d7f3d45..f17c4c2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,23 +1,4 @@ { - "pins" : [ - { - "identity" : "feather-component", - "kind" : "remoteSourceControl", - "location" : "https://github.com/feather-framework/feather-component.git", - "state" : { - "revision" : "c209d87352d27afc124422db11c9ca2bb0f7a376", - "version" : "0.5.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log", - "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" - } - } - ], + "pins" : [], "version" : 2 } diff --git a/Package.swift b/Package.swift index 203601a..8ae0415 100644 --- a/Package.swift +++ b/Package.swift @@ -1,50 +1,52 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 import PackageDescription +// NOTE: https://github.com/swift-server/swift-http-server/blob/main/Package.swift +var defaultSwiftSettings: [SwiftSetting] = [ + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0441-formalize-language-mode-terminology.md + .swiftLanguageMode(.v6), + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + .enableUpcomingFeature("MemberImportVisibility"), + // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableExperimentalFeature("Lifetimes"), + // https://github.com/swiftlang/swift/pull/65218 + .enableExperimentalFeature("AvailabilityMacro=featherPush:macOS 15, iOS 18, watchOS 11, tvOS 11, visionOS 2"), +] + +#if compiler(>=6.2) +defaultSwiftSettings.append( + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +) +#endif + let package = Package( name: "feather-push", platforms: [ - .macOS(.v13), - .iOS(.v16), - .tvOS(.v16), - .watchOS(.v9), - .visionOS(.v1), + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), ], products: [ .library(name: "FeatherPush", targets: ["FeatherPush"]), - .library(name: "XCTFeatherPush", targets: ["XCTFeatherPush"]), ], dependencies: [ - .package(url: "https://github.com/feather-framework/feather-component.git", .upToNextMinor(from: "0.5.0") - ), + // [docc-plugin-placeholder] ], targets: [ .target( name: "FeatherPush", - dependencies: [ - .product(name: "FeatherComponent", package: "feather-component") - ] - ), - .target( - name: "XCTFeatherPush", - dependencies: [ - .target(name: "FeatherPush"), - ], - resources: [ - .copy("Assets/feather.png") - ] + dependencies: [], + swiftSettings: defaultSwiftSettings ), .testTarget( name: "FeatherPushTests", dependencies: [ .target(name: "FeatherPush"), - ] - ), - .testTarget( - name: "XCTFeatherPushTests", - dependencies: [ - .target(name: "XCTFeatherPush"), - ] + ], + swiftSettings: defaultSwiftSettings ), ] ) diff --git a/README.md b/README.md index 03fe75e..26467d9 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,87 @@ # Feather Push -An abstract push notification component for Feather CMS. +An abstract push notification client for Feather CMS. -## Getting started +[![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138)](https://github.com/feather-framework/feather-push/releases/tag/1.0.0-beta.1) -⚠️ This repository is a work in progress, things can break until it reaches v1.0.0. +## Features -Use at your own risk. +- Topic-based push notification delivery +- Silent and normal delivery modes +- Deep-link and rich notification metadata +- Badge, sound, and notification collapsing support +- Provider-neutral client and error APIs -### Adding the dependency +## Requirements -To add a dependency on the package, declare it in your `Package.swift`: +![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) +![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) + +- Swift 6.1+ +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ + +## Installation + +Use Swift Package Manager; add the dependency to your `Package.swift` file: ```swift -.package(url: "https://github.com/feather-framework/feather-push.git", .upToNextMinor(from: "0.1.0")), +.package(url: "https://github.com/feather-framework/feather-push", exact: "1.0.0-beta.1"), ``` -and to your application target, add `FeatherPush` to your dependencies: +Then add `FeatherPush` to your target dependencies: ```swift -.product(name: "FeatherPush", package: "feather-push") +.product(name: "FeatherPush", package: "feather-push"), ``` -Example `Package.swift` file with `FeatherPush` as a dependency: +## Usage + +[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-push/) + +API documentation is available at the following link. ```swift -// swift-tools-version:5.9 -import PackageDescription - -let package = Package( - name: "my-application", - dependencies: [ - .package(url: "https://github.com/feather-framework/feather-push.git", .upToNextMinor(from: "0.1.0")), - ], - targets: [ - .target(name: "MyApplication", dependencies: [ - .product(name: "FeatherPush", package: "feather-push") - ]), - .testTarget(name: "MyApplicationTests", dependencies: [ - .target(name: "MyApplication"), - ]), - ] +let notification = PushNotification( + title: "New message", + body: "You have a new message.", + data: ["messageID": "123"], + deepLink: "myapp://messages/123", + badge: 1, + sound: .default +) + +try await client.send( + notification: notification, + to: "topic" ) ``` +> [!WARNING] +> This repository is a work in progress, things can break until it reaches v1.0.0. + +## Push implementations + +The following push client implementations are available for use: + +- [Feather Push APNS](https://github.com/feather-framework/feather-push-apns) +- [Feather Push FCM](https://github.com/feather-framework/feather-push-fcm) +- [Feather Push Ephemeral](https://github.com/feather-framework/feather-push-ephemeral) + +## Development + +- Build: `swift build` +- Test: + - local: `make test` + - using Docker: `make docker-test` +- Format: `make format` +- Check: `make check` + +## Contributing + +[Pull requests](https://github.com/feather-framework/feather-push/pulls) are welcome. Please keep changes focused and include tests for new logic. diff --git a/Sources/FeatherPush/Component+Push.swift b/Sources/FeatherPush/Component+Push.swift deleted file mode 100644 index 287b413..0000000 --- a/Sources/FeatherPush/Component+Push.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Component+Push.swift -// FeatherPush -// -// Created by Tibor Bodecs on 20/11/2023. -// - -import FeatherComponent -import Logging - -public enum PushComponentID: ComponentID { - - /// default push component identifier - case `default` - - /// custom push component identifier - case custom(String) - - /// rawId - public var rawId: String { - switch self { - case .default: - return "push-component-id" - case .custom(let value): - return "\(value)-push-component-id" - } - } -} - -public extension ComponentRegistry { - - /// add a new push component using a context - func addPush( - _ context: ComponentContext, - id: PushComponentID = .default - ) async throws { - try await add(context, id: id) - } - - /// returns a push component by a given id - func push( - _ id: PushComponentID = .default, - logger: Logger? = nil - ) throws -> PushComponent { - guard let push = try get(id, logger: logger) as? PushComponent else { - fatalError( - "Push component not found, call `addPush()` to register." - ) - } - return push - } -} diff --git a/Sources/FeatherPush/Models/Delivery.swift b/Sources/FeatherPush/Models/Delivery.swift new file mode 100644 index 0000000..f1527b4 --- /dev/null +++ b/Sources/FeatherPush/Models/Delivery.swift @@ -0,0 +1,14 @@ +// +// Delivery.swift +// feather-push +// +// Created by Binary Birds on 2026. 08. 13. +// + +/// The delivery priority of a push notification. +public enum Delivery: String, Sendable { + /// Delivers the notification normally. + case normal + /// Delivers the notification without presenting an alert. + case silent +} diff --git a/Sources/FeatherPush/Models/Platform.swift b/Sources/FeatherPush/Models/Platform.swift new file mode 100644 index 0000000..712bd15 --- /dev/null +++ b/Sources/FeatherPush/Models/Platform.swift @@ -0,0 +1,28 @@ +// +// Platform.swift +// feather-push +// +// Created by Binary Birds on 2026. 08. 13. +// + +/// A platform supported by a push provider. +public enum Platform: Sendable, Equatable { + /// Apple iPhone and iPad platform. + case iOS + /// Apple Mac platform. + case macOS + /// Apple TV platform. + case tvOS + /// Apple Watch platform. + case watchOS + /// Apple Vision Pro platform. + case visionOS + /// Safari web-push platform. + case safari + /// Android platform. + case android + /// Generic web-push platform. + case web + /// A provider-specific platform. + case custom(String) +} diff --git a/Sources/FeatherPush/Models/PushNotification.swift b/Sources/FeatherPush/Models/PushNotification.swift new file mode 100644 index 0000000..2f23bb2 --- /dev/null +++ b/Sources/FeatherPush/Models/PushNotification.swift @@ -0,0 +1,51 @@ +// +// PushNotification.swift +// feather-push +// +// Created by Binary Birds on 2026. 08. 13. +// + +/// A push notification payload. +public struct PushNotification: Sendable { + /// The notification title. + public let title: String + /// The notification body. + public let body: String + /// Provider-specific notification metadata. + public let data: [String: String] + /// The requested delivery priority. + public let delivery: Delivery + /// An optional deep link opened when the notification is selected. + public let deepLink: String? + /// An optional image URL supported by providers that render rich notifications. + public let imageURL: String? + /// An optional badge value for the application icon. + public let badge: Int? + /// An optional notification sound. + public let sound: Sound? + /// An optional identifier used to collapse equivalent notifications. + public let collapseID: String? + + /// Creates a push notification. + public init( + title: String, + body: String, + data: [String: String] = [:], + delivery: Delivery = .normal, + deepLink: String? = nil, + imageURL: String? = nil, + badge: Int? = nil, + sound: Sound? = nil, + collapseID: String? = nil + ) { + self.title = title + self.body = body + self.data = data + self.delivery = delivery + self.deepLink = deepLink + self.imageURL = imageURL + self.badge = badge + self.sound = sound + self.collapseID = collapseID + } +} diff --git a/Sources/FeatherPush/Models/Sound.swift b/Sources/FeatherPush/Models/Sound.swift new file mode 100644 index 0000000..98d78db --- /dev/null +++ b/Sources/FeatherPush/Models/Sound.swift @@ -0,0 +1,14 @@ +// +// Sound.swift +// feather-push +// +// Created by Binary Birds on 2026. 08. 13. +// + +/// The sound to play when a notification is presented. +public enum Sound: Sendable, Equatable { + /// Use the platform default notification sound. + case `default` + /// Use a bundled, provider-specific sound name. + case named(String) +} diff --git a/Sources/FeatherPush/Push.swift b/Sources/FeatherPush/Push.swift deleted file mode 100644 index dd3f709..0000000 --- a/Sources/FeatherPush/Push.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Push.swift -// FeatherPush -// -// Created by Tibor Bodecs on 2023. 01. 16.. -// - -public enum Delivery: String, Sendable { - case normal - case silent -} - -public enum Platform: Sendable { - case iOS - case macOS - case tvOS - case watchOS - case visionOS - case safari - case android - case web - case custom(String) -} - -/// push notification recipient -public struct Recipient: Sendable { - /// device token - public let token: String - /// device platform - public let platform: Platform - - /// init push notification recipient - public init( - token: String, - platform: Platform - ) { - self.token = token - self.platform = platform - } -} - -/// push notification -public struct Notification: Sendable { - /// title - public let title: String - /// body - public let body: String - /// userInfo - public let userInfo: [String: String] - /// delivery - public let delivery: Delivery - - /// init push notification - public init( - title: String, - body: String, - userInfo: [String: String] = [:], - delivery: Delivery = .normal - ) { - self.title = title - self.body = body - self.userInfo = userInfo - self.delivery = delivery - } -} diff --git a/Sources/FeatherPush/PushClient.swift b/Sources/FeatherPush/PushClient.swift new file mode 100644 index 0000000..2d4bc70 --- /dev/null +++ b/Sources/FeatherPush/PushClient.swift @@ -0,0 +1,21 @@ +// +// PushClient.swift +// feather-push +// +// Created by Tibor Bodecs on 2023. 01. 16. +// + +/// A client capable of delivering push notifications. +public protocol PushClient: Sendable { + + /// Sends a push notification to the given recipients. + /// + /// - Parameters: + /// - notification: The notification to deliver. + /// - topic: The topic that receives the notification. + /// - Throws: `PushClientError` when delivery fails. + func send( + notification: PushNotification, + to topic: String + ) async throws(PushClientError) +} diff --git a/Sources/FeatherPush/PushClientError.swift b/Sources/FeatherPush/PushClientError.swift new file mode 100644 index 0000000..9c4b298 --- /dev/null +++ b/Sources/FeatherPush/PushClientError.swift @@ -0,0 +1,25 @@ +// +// PushClientError.swift +// feather-push +// +// Created by Tibor Bodecs on 2023. 01. 16. +// + +/// Errors that can occur while delivering a push notification. +public enum PushClientError: Error { + + /// The topic is empty or otherwise invalid. + case invalidTopic + /// The notification payload is invalid or cannot be delivered. + case invalidNotification + /// The provider credentials or authorization are invalid. + case unauthorized + /// The provider temporarily rejected the request because it was rate-limited. + case rateLimited + /// The provider is temporarily unavailable. + case unavailable + /// The provider rejected the request with a descriptive reason. + case rejected(String) + /// An underlying provider error that does not have a more specific mapping. + case unknown(Error) +} diff --git a/Sources/FeatherPush/PushComponent.swift b/Sources/FeatherPush/PushComponent.swift deleted file mode 100644 index 55c9695..0000000 --- a/Sources/FeatherPush/PushComponent.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// PushComponent.swift -// FeatherPush -// -// Created by Tibor Bodecs on 2023. 01. 16.. -// - -import FeatherComponent - -/// the push component protocol -public protocol PushComponent: Component { - - /// send a push notification to a given recipients - func send(notification: Notification, to: [Recipient]) async throws -} diff --git a/Sources/FeatherPush/PushComponentError.swift b/Sources/FeatherPush/PushComponentError.swift deleted file mode 100644 index 8bb383f..0000000 --- a/Sources/FeatherPush/PushComponentError.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// PushComponentError.swift -// FeatherPush -// -// Created by Tibor Bodecs on 2023. 01. 16.. -// - -/// push component error -public enum PushComponentError: Error { - - case unknown(Error) -} diff --git a/Sources/XCTFeatherPush/Assets/feather.png b/Sources/XCTFeatherPush/Assets/feather.png deleted file mode 100644 index 500265b..0000000 Binary files a/Sources/XCTFeatherPush/Assets/feather.png and /dev/null differ diff --git a/Sources/XCTFeatherPush/PushTestSuite.swift b/Sources/XCTFeatherPush/PushTestSuite.swift deleted file mode 100644 index e7ffe06..0000000 --- a/Sources/XCTFeatherPush/PushTestSuite.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// PushTestSuite.swift -// XCTFeatherPush -// -// Created by Tibor Bodecs on 17/11/2023. -// - -import Foundation -import FeatherPush - -/// push test suit error -public struct PushTestSuiteError: Error { - - /// function - public let function: String - /// line - public let line: Int - /// error - public let error: Error? - - init( - function: String = #function, - line: Int = #line, - error: Error? = nil - ) { - self.function = function - self.line = line - self.error = error - } -} - -/// push test suite -public struct PushTestSuite { - - let push: PushComponent - - /// push test suite init - public init(_ push: PushComponent) { - self.push = push - } - - /// test all push sending - public func testAll( - from: String, - to: String - ) async throws { - async let tests: [Void] = [ - testPlainText(from: from, to: to) - ] - do { - _ = try await tests - } - catch let error as PushTestSuiteError { - throw error - } - catch { - throw PushTestSuiteError(error: error) - } - } -} - -public extension PushTestSuite { - - func getAttachmentUrl() -> URL? { - Bundle.module.url(forResource: "feather", withExtension: "png") - } - - // MARK: - tests - - func testPlainText( - from: String, - to: String - ) async throws { - - } - -} diff --git a/Tests/FeatherPushTests/FeatherPushTests.swift b/Tests/FeatherPushTests/FeatherPushTests.swift index f07db9e..01b88d3 100644 --- a/Tests/FeatherPushTests/FeatherPushTests.swift +++ b/Tests/FeatherPushTests/FeatherPushTests.swift @@ -1,14 +1,59 @@ // // FeatherPushTests.swift -// FeatherPushTests +// feather-push // -// Created by Tibor Bodecs on 2023. 01. 16.. +// Created by Tibor Bodecs on 2023. 01. 16. // -import XCTest import FeatherPush +import Testing -final class FeatherPushTests: XCTestCase { +@Suite +struct FeatherPushTests { - func testExample() async throws {} + @Test + func notificationDefaults() { + let notification = PushNotification( + title: "Title", + body: "Body" + ) + + #expect(notification.data.isEmpty) + #expect(notification.delivery == .normal) + #expect(notification.deepLink == nil) + #expect(notification.badge == nil) + } + + @Test + func notificationSupportsDeepLinksAndProviderMetadata() { + let notification = PushNotification( + title: "New item", + body: "Open the item", + data: ["itemID": "123"], + deepLink: "myapp://items/123", + imageURL: "https://example.com/image.png", + badge: 1, + sound: .default, + collapseID: "item-123" + ) + + #expect(notification.data["itemID"] == "123") + #expect(notification.deepLink == "myapp://items/123") + #expect(notification.sound == .default) + #expect(notification.collapseID == "item-123") + } + + @Test + func clientErrorsExposeProviderNeutralCases() { + let errors: [PushClientError] = [ + .invalidTopic, + .invalidNotification, + .unauthorized, + .rateLimited, + .unavailable, + .rejected("provider rejected the request"), + ] + + #expect(errors.count == 6) + } } diff --git a/Tests/XCTFeatherPushTests/XCTFeatherPushTests.swift b/Tests/XCTFeatherPushTests/XCTFeatherPushTests.swift deleted file mode 100644 index f1a7ec5..0000000 --- a/Tests/XCTFeatherPushTests/XCTFeatherPushTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// XCTFeatherPushTests.swift -// XCTFeatherPushTests -// -// Created by Tibor Bodecs on 2023. 01. 16.. -// - -import XCTest -import XCTFeatherPush - -final class XCTFeatherPushTests: XCTestCase { - - func testExample() async throws { - - } -} diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile new file mode 100644 index 0000000..73be175 --- /dev/null +++ b/docker/tests/Dockerfile @@ -0,0 +1,11 @@ +FROM swift:6.1 + +WORKDIR /app + +COPY . ./ + +RUN swift package resolve +RUN swift package clean +RUN swift package update + +CMD ["swift", "test", "--parallel", "--enable-code-coverage"] diff --git a/scripts/check-broken-symlinks.sh b/scripts/check-broken-symlinks.sh deleted file mode 100755 index 3d7e919..0000000 --- a/scripts/check-broken-symlinks.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -log "Checking for broken symlinks..." -NUM_BROKEN_SYMLINKS=0 -while read -r -d '' file; do - if ! test -e "${REPO_ROOT}/${file}"; then - error "Broken symlink: ${file}" - ((NUM_BROKEN_SYMLINKS++)) - fi -done < <(git -C "${REPO_ROOT}" ls-files -z) - -if [ "${NUM_BROKEN_SYMLINKS}" -gt 0 ]; then - fatal "❌ Found ${NUM_BROKEN_SYMLINKS} symlinks." -fi - -log "✅ Found 0 symlinks." diff --git a/scripts/check-local-swift-dependencies.sh b/scripts/check-local-swift-dependencies.sh deleted file mode 100755 index c5fcb58..0000000 --- a/scripts/check-local-swift-dependencies.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -read -ra PATHS_TO_CHECK <<< "$( \ - git -C "${REPO_ROOT}" ls-files -z \ - "Package.swift" \ - | xargs -0 \ -)" - -for FILE_PATH in "${PATHS_TO_CHECK[@]}"; do - if [[ $(grep ".package(path:" "${FILE_PATH}"|wc -l) -ne 0 ]] ; then - fatal "❌ The '${FILE_PATH}' file contains local Swift package reference(s)." - fi -done - -log "✅ Found 0 local Swift package dependency references." diff --git a/scripts/check-unacceptable-language.sh b/scripts/check-unacceptable-language.sh deleted file mode 100755 index 3d93707..0000000 --- a/scripts/check-unacceptable-language.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" -UNACCEPTABLE_LANGUAGE_PATTERNS_PATH="${CURRENT_SCRIPT_DIR}/unacceptable-language.txt" - -log "Checking for unacceptable language..." -PATHS_WITH_UNACCEPTABLE_LANGUAGE=$(git -C "${REPO_ROOT}" grep \ - -l -F -w \ - -f "${UNACCEPTABLE_LANGUAGE_PATTERNS_PATH}" \ - -- \ - ":(exclude)${UNACCEPTABLE_LANGUAGE_PATTERNS_PATH}" \ -) || true | /usr/bin/paste -s -d " " - - -if [ -n "${PATHS_WITH_UNACCEPTABLE_LANGUAGE}" ]; then - fatal "❌ Found unacceptable language in files: ${PATHS_WITH_UNACCEPTABLE_LANGUAGE}." -fi - -log "✅ Found no unacceptable language." diff --git a/scripts/generate-contributors-list.sh b/scripts/generate-contributors-list.sh deleted file mode 100755 index ec165b8..0000000 --- a/scripts/generate-contributors-list.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) - -cat > "$here/../CONTRIBUTORS.txt" <<- EOF - ### Contributors - - $contributors - - **Updating this list** - - Please do not edit this file manually. It is generated using \`bash ./scripts/generate-contributors-list.sh\`. - If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\`. -EOF diff --git a/scripts/install-swift-format.sh b/scripts/install-swift-format.sh deleted file mode 100755 index 3fb3333..0000000 --- a/scripts/install-swift-format.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -# https://github.com/apple/swift-format - -VERSION="509.0.0" - -curl -L -o "${VERSION}.tar.gz" "https://github.com/apple/swift-format/archive/refs/tags/${VERSION}.tar.gz" -tar -xf "${VERSION}.tar.gz" -cd "swift-format-${VERSION}" -swift build -c release -install .build/release/swift-format /usr/local/bin/swift-format -cd .. -rm -f "${VERSION}.tar.gz" -rm -rf "swift-format-${VERSION}" diff --git a/scripts/run-checks.sh b/scripts/run-checks.sh deleted file mode 100755 index b42e0f9..0000000 --- a/scripts/run-checks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -NUM_CHECKS_FAILED=0 - -FIX_FORMAT="" -for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FIX_FORMAT="--fix" - fi -done - -SCRIPT_PATHS=( - "${CURRENT_SCRIPT_DIR}/check-broken-symlinks.sh" - "${CURRENT_SCRIPT_DIR}/check-unacceptable-language.sh" - "${CURRENT_SCRIPT_DIR}/check-local-swift-dependencies.sh" -) - -for SCRIPT_PATH in "${SCRIPT_PATHS[@]}"; do - log "Running ${SCRIPT_PATH}..." - if ! bash "${SCRIPT_PATH}"; then - ((NUM_CHECKS_FAILED+=1)) - fi -done - -log "Running swift-format..." -bash "${CURRENT_SCRIPT_DIR}"/run-swift-format.sh $FIX_FORMAT > /dev/null -FORMAT_EXIT_CODE=$? -if [ $FORMAT_EXIT_CODE -ne 0 ]; then - ((NUM_CHECKS_FAILED+=1)) -fi - -if [ "${NUM_CHECKS_FAILED}" -gt 0 ]; then - fatal "❌ ${NUM_CHECKS_FAILED} check(s) failed." -fi - -log "✅ All check(s) passed." diff --git a/scripts/run-chmod.sh b/scripts/run-chmod.sh deleted file mode 100755 index 783e89d..0000000 --- a/scripts/run-chmod.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" -chmod -R oug+x "${REPO_ROOT}/scripts/" \ No newline at end of file diff --git a/scripts/run-clean.sh b/scripts/run-clean.sh deleted file mode 100755 index 737914f..0000000 --- a/scripts/run-clean.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - - -rm -rf ".build" -rm -rf ".swiftpm" -rm -f "openapi/openapi.yaml" -rm -f "db.sqlite" -rm -f "migration-entries.json" diff --git a/scripts/run-swift-format.sh b/scripts/run-swift-format.sh deleted file mode 100755 index 4f5d8ed..0000000 --- a/scripts/run-swift-format.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -FORMAT_COMMAND=(lint --strict) -for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FORMAT_COMMAND=(format --in-place) - fi -done - -SWIFTFORMAT_BIN=${SWIFTFORMAT_BIN:-$(command -v swift-format)} || fatal "❌ SWIFTFORMAT_BIN unset and no swift-format on PATH" - -git -C "${REPO_ROOT}" ls-files -z '*.swift' \ - | grep -z -v \ - -e 'Package.swift' \ - | xargs -0 "${SWIFTFORMAT_BIN}" "${FORMAT_COMMAND[@]}" --parallel \ - && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? - -if [ "${SWIFT_FORMAT_RC}" -ne 0 ]; then - fatal "❌ Running swift-format produced errors. - - To fix, run the following command: - - % ./scripts/run-swift-format.sh --fix - " - exit "${SWIFT_FORMAT_RC}" -fi - -log "✅ Ran swift-format with no errors." diff --git a/scripts/unacceptable-language.txt b/scripts/unacceptable-language.txt deleted file mode 100755 index 6ac4a98..0000000 --- a/scripts/unacceptable-language.txt +++ /dev/null @@ -1,15 +0,0 @@ -blacklist -whitelist -slave -master -sane -sanity -insane -insanity -kill -killed -killing -hang -hung -hanged -hanging