diff --git a/README.md b/README.md index 9e5c9a8..f6bab5f 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ A Swift client for the [Figma REST API](https://www.figma.com/developers/api) wi ## Features -- 8 Figma API endpoints (files, components, nodes, images, styles, variables) +- Full Figma REST API coverage (46 endpoints) — files, components, styles, variables, comments, webhooks, dev resources, analytics, and more - Token-bucket rate limiting with fair round-robin scheduling - Exponential backoff retry with jitter and `Retry-After` support -- Figma Variables API (read + write codeSyntax) +- Figma Variables API (read local, read published, write codeSyntax) +- Support for both API v1 and v2 (webhooks) - GitHub Releases endpoint (for version checking) - Swift 6 strict concurrency @@ -47,12 +48,18 @@ Then add `FigmaAPI` to your target dependencies: ```swift import FigmaAPI -// Create a client with your Figma personal access token +// Create a client +let figma = FigmaClient(accessToken: "your-figma-token", timeout: nil) + +// Wrap with rate limiting and retry +let rateLimiter = SharedRateLimiter() let client = RateLimitedClient( - inner: FigmaClient(accessToken: "your-figma-token") + client: figma, + rateLimiter: rateLimiter, + configID: "default" ) -// Fetch components +// Fetch components from a file let components = try await client.request( ComponentsEndpoint(fileId: "your-file-id") ) @@ -64,8 +71,22 @@ let variables = try await client.request( // Export images as SVG let images = try await client.request( - ImageEndpoint(fileId: "your-file-id", nodeIds: ["1:2", "3:4"], format: .svg) + ImageEndpoint(fileId: "your-file-id", nodeIds: ["1:2", "3:4"], params: SVGParams()) ) + +// Get current user +let me = try await client.request(GetMeEndpoint()) + +// Post a comment +let comment = try await client.request( + PostCommentEndpoint( + fileId: "your-file-id", + body: PostCommentBody(message: "Looks great!") + ) +) + +// List webhooks (v2 API) +let webhooks = try await client.request(GetWebhooksEndpoint()) ``` ## License diff --git a/Sources/FigmaAPI/Endpoint/BaseEndpoint.swift b/Sources/FigmaAPI/Endpoint/BaseEndpoint.swift index 25889f8..1fffadd 100644 --- a/Sources/FigmaAPI/Endpoint/BaseEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/BaseEndpoint.swift @@ -37,3 +37,4 @@ extension BaseEndpoint { } } } + diff --git a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift index b7f9ce1..34736c4 100644 --- a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift @@ -18,6 +18,7 @@ public struct ComponentsEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) .appendingPathComponent("components") diff --git a/Sources/FigmaAPI/Endpoint/DeleteCommentEndpoint.swift b/Sources/FigmaAPI/Endpoint/DeleteCommentEndpoint.swift new file mode 100644 index 0000000..52815eb --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/DeleteCommentEndpoint.swift @@ -0,0 +1,34 @@ +import Foundation +import YYJSON +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct DeleteCommentEndpoint: BaseEndpoint { + public typealias Content = EmptyResponse + + private let fileId: String + private let commentId: String + + public init(fileId: String, commentId: String) { + self.fileId = fileId + self.commentId = commentId + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + .appendingPathComponent(commentId) + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + return request + } + + public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse { + if body.isEmpty { return EmptyResponse() } + return try YYJSONDecoder().decode(EmptyResponse.self, from: body) + } +} diff --git a/Sources/FigmaAPI/Endpoint/DeleteDevResourceEndpoint.swift b/Sources/FigmaAPI/Endpoint/DeleteDevResourceEndpoint.swift new file mode 100644 index 0000000..476cc80 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/DeleteDevResourceEndpoint.swift @@ -0,0 +1,34 @@ +import Foundation +import YYJSON +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct DeleteDevResourceEndpoint: BaseEndpoint { + public typealias Content = EmptyResponse + + private let fileId: String + private let resourceId: String + + public init(fileId: String, resourceId: String) { + self.fileId = fileId + self.resourceId = resourceId + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("dev_resources") + .appendingPathComponent(resourceId) + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + return request + } + + public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse { + if body.isEmpty { return EmptyResponse() } + return try YYJSONDecoder().decode(EmptyResponse.self, from: body) + } +} diff --git a/Sources/FigmaAPI/Endpoint/DeleteReactionEndpoint.swift b/Sources/FigmaAPI/Endpoint/DeleteReactionEndpoint.swift new file mode 100644 index 0000000..a9bb920 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/DeleteReactionEndpoint.swift @@ -0,0 +1,44 @@ +import Foundation +import YYJSON +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct DeleteReactionEndpoint: BaseEndpoint { + public typealias Content = EmptyResponse + + private let fileId: String + private let commentId: String + private let emoji: String + + public init(fileId: String, commentId: String, emoji: String) { + self.fileId = fileId + self.commentId = commentId + self.emoji = emoji + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + .appendingPathComponent(commentId) + .appendingPathComponent("reactions") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + comps.queryItems = [URLQueryItem(name: "emoji", value: emoji)] + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + var request = URLRequest(url: finalURL) + request.httpMethod = "DELETE" + return request + } + + public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse { + if body.isEmpty { return EmptyResponse() } + return try YYJSONDecoder().decode(EmptyResponse.self, from: body) + } +} diff --git a/Sources/FigmaAPI/Endpoint/DeleteWebhookEndpoint.swift b/Sources/FigmaAPI/Endpoint/DeleteWebhookEndpoint.swift new file mode 100644 index 0000000..8f000b7 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/DeleteWebhookEndpoint.swift @@ -0,0 +1,30 @@ +import Foundation +import YYJSON +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct DeleteWebhookEndpoint: BaseEndpoint { + public typealias Content = EmptyResponse + + private let webhookId: String + + public init(webhookId: String) { + self.webhookId = webhookId + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + .appendingPathComponent(webhookId) + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + return request + } + + public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse { + if body.isEmpty { return EmptyResponse() } + return try YYJSONDecoder().decode(EmptyResponse.self, from: body) + } +} diff --git a/Sources/FigmaAPI/Endpoint/FileMetadataEndpoint.swift b/Sources/FigmaAPI/Endpoint/FileMetadataEndpoint.swift index dc813e7..815ec6a 100644 --- a/Sources/FigmaAPI/Endpoint/FileMetadataEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/FileMetadataEndpoint.swift @@ -16,6 +16,7 @@ public struct FileMetadataEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) throws -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) diff --git a/Sources/FigmaAPI/Endpoint/GetActivityLogsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetActivityLogsEndpoint.swift new file mode 100644 index 0000000..6ef63e9 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetActivityLogsEndpoint.swift @@ -0,0 +1,60 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetActivityLogsEndpoint: BaseEndpoint { + public typealias Content = [ActivityLog] + + private let events: String? + private let startTime: Double? + private let endTime: Double? + private let limit: Int? + private let order: String? + + public init( + events: String? = nil, + startTime: Double? = nil, + endTime: Double? = nil, + limit: Int? = nil, + order: String? = nil + ) { + self.events = events + self.startTime = startTime + self.endTime = endTime + self.limit = limit + self.order = order + } + + func content(from root: ActivityLogsResponse) -> [ActivityLog] { + root.activityLogs + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("activity_logs") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + var items: [URLQueryItem] = [] + if let events { items.append(URLQueryItem(name: "events", value: events)) } + if let startTime { items.append(URLQueryItem(name: "start_time", value: "\(startTime)")) } + if let endTime { items.append(URLQueryItem(name: "end_time", value: "\(endTime)")) } + if let limit { items.append(URLQueryItem(name: "limit", value: "\(limit)")) } + if let order { items.append(URLQueryItem(name: "order", value: order)) } + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} + +struct ActivityLogsResponse: Decodable { + let activityLogs: [ActivityLog] + + private enum CodingKeys: String, CodingKey { + case activityLogs = "activity_logs" + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetCommentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetCommentsEndpoint.swift new file mode 100644 index 0000000..81b901d --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetCommentsEndpoint.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetCommentsEndpoint: BaseEndpoint { + public typealias Content = [Comment] + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: CommentsResponse) -> [Comment] { + root.comments + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + return URLRequest(url: url) + } +} + +struct CommentsResponse: Decodable { + let comments: [Comment] +} diff --git a/Sources/FigmaAPI/Endpoint/GetComponentActionsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetComponentActionsEndpoint.swift new file mode 100644 index 0000000..841d1bf --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetComponentActionsEndpoint.swift @@ -0,0 +1,33 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetComponentActionsEndpoint: BaseEndpoint { + public typealias Content = [LibraryAnalyticsAction] + + private let fileKey: String + + public init(fileKey: String) { + self.fileKey = fileKey + } + + func content(from root: LibraryActionsResponse) -> [LibraryAnalyticsAction] { + root.actions + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("analytics") + .appendingPathComponent("libraries") + .appendingPathComponent(fileKey) + .appendingPathComponent("component") + .appendingPathComponent("actions") + return URLRequest(url: url) + } +} + +struct LibraryActionsResponse: Decodable { + let actions: [LibraryAnalyticsAction] +} diff --git a/Sources/FigmaAPI/Endpoint/GetComponentEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetComponentEndpoint.swift new file mode 100644 index 0000000..b20d3e8 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetComponentEndpoint.swift @@ -0,0 +1,30 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetComponentEndpoint: BaseEndpoint { + public typealias Content = Component + + private let key: String + + public init(key: String) { + self.key = key + } + + func content(from root: GetComponentResponse) -> Component { + root.meta + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("components") + .appendingPathComponent(key) + return URLRequest(url: url) + } +} + +struct GetComponentResponse: Decodable { + let meta: Component +} diff --git a/Sources/FigmaAPI/Endpoint/GetComponentSetEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetComponentSetEndpoint.swift new file mode 100644 index 0000000..6df7bfb --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetComponentSetEndpoint.swift @@ -0,0 +1,30 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetComponentSetEndpoint: BaseEndpoint { + public typealias Content = ComponentSet + + private let key: String + + public init(key: String) { + self.key = key + } + + func content(from root: GetComponentSetResponse) -> ComponentSet { + root.meta + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("component_sets") + .appendingPathComponent(key) + return URLRequest(url: url) + } +} + +struct GetComponentSetResponse: Decodable { + let meta: ComponentSet +} diff --git a/Sources/FigmaAPI/Endpoint/GetComponentUsagesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetComponentUsagesEndpoint.swift new file mode 100644 index 0000000..175556a --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetComponentUsagesEndpoint.swift @@ -0,0 +1,33 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetComponentUsagesEndpoint: BaseEndpoint { + public typealias Content = [LibraryAnalyticsUsage] + + private let fileKey: String + + public init(fileKey: String) { + self.fileKey = fileKey + } + + func content(from root: LibraryUsagesResponse) -> [LibraryAnalyticsUsage] { + root.usages + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("analytics") + .appendingPathComponent("libraries") + .appendingPathComponent(fileKey) + .appendingPathComponent("component") + .appendingPathComponent("usages") + return URLRequest(url: url) + } +} + +struct LibraryUsagesResponse: Decodable { + let usages: [LibraryAnalyticsUsage] +} diff --git a/Sources/FigmaAPI/Endpoint/GetDevResourcesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetDevResourcesEndpoint.swift new file mode 100644 index 0000000..f0a6e6e --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetDevResourcesEndpoint.swift @@ -0,0 +1,35 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetDevResourcesEndpoint: BaseEndpoint { + public typealias Content = [DevResource] + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: DevResourcesResponse) -> [DevResource] { + root.devResources + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("dev_resources") + return URLRequest(url: url) + } +} + +struct DevResourcesResponse: Decodable { + let devResources: [DevResource] + + private enum CodingKeys: String, CodingKey { + case devResources = "dev_resources" + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetFileComponentSetsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetFileComponentSetsEndpoint.swift new file mode 100644 index 0000000..613eb87 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetFileComponentSetsEndpoint.swift @@ -0,0 +1,39 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetFileComponentSetsEndpoint: BaseEndpoint { + public typealias Content = [ComponentSet] + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: FileComponentSetsResponse) -> [ComponentSet] { + root.meta.componentSets + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("component_sets") + return URLRequest(url: url) + } +} + +struct FileComponentSetsResponse: Decodable { + let meta: FileComponentSetsMeta +} + +struct FileComponentSetsMeta: Decodable { + let componentSets: [ComponentSet] + + private enum CodingKeys: String, CodingKey { + case componentSets = "component_sets" + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetFileMetaEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetFileMetaEndpoint.swift new file mode 100644 index 0000000..2b8e817 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetFileMetaEndpoint.swift @@ -0,0 +1,34 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetFileMetaEndpoint: Endpoint { + public typealias Content = FileMeta + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + return request + } + + public func content(from response: URLResponse?, with body: Data) throws -> FileMeta { + guard let http = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + return FileMeta( + lastModified: http.value(forHTTPHeaderField: "Last-Modified"), + version: http.value(forHTTPHeaderField: "X-Figma-Version") + ) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetFileVersionsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetFileVersionsEndpoint.swift new file mode 100644 index 0000000..9550767 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetFileVersionsEndpoint.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetFileVersionsEndpoint: BaseEndpoint { + public typealias Content = [FileVersion] + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: FileVersionsResponse) -> [FileVersion] { + root.versions + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("versions") + return URLRequest(url: url) + } +} + +struct FileVersionsResponse: Decodable { + let versions: [FileVersion] +} diff --git a/Sources/FigmaAPI/Endpoint/GetImageFillsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetImageFillsEndpoint.swift new file mode 100644 index 0000000..9f13f44 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetImageFillsEndpoint.swift @@ -0,0 +1,35 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetImageFillsEndpoint: BaseEndpoint { + public typealias Content = [String: String] + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: ImageFillsResponse) -> [String: String] { + root.meta.images + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("images") + return URLRequest(url: url) + } +} + +struct ImageFillsResponse: Decodable { + let meta: ImageFillsMeta +} + +struct ImageFillsMeta: Decodable { + let images: [String: String] +} diff --git a/Sources/FigmaAPI/Endpoint/GetMeEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetMeEndpoint.swift new file mode 100644 index 0000000..d0ab972 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetMeEndpoint.swift @@ -0,0 +1,15 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetMeEndpoint: BaseEndpoint { + public typealias Content = User + + public init() {} + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL.appendingPathComponent("v1").appendingPathComponent("me") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetPaymentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetPaymentsEndpoint.swift new file mode 100644 index 0000000..18edeb8 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetPaymentsEndpoint.swift @@ -0,0 +1,56 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetPaymentsEndpoint: BaseEndpoint { + public typealias Content = PaymentInfo + + private let pluginPaymentToken: String? + private let userId: String? + private let pluginId: String? + private let widgetId: String? + private let communityFileId: String? + + public init( + pluginPaymentToken: String? = nil, + userId: String? = nil, + pluginId: String? = nil, + widgetId: String? = nil, + communityFileId: String? = nil + ) { + self.pluginPaymentToken = pluginPaymentToken + self.userId = userId + self.pluginId = pluginId + self.widgetId = widgetId + self.communityFileId = communityFileId + } + + func content(from root: PaymentsResponse) -> PaymentInfo { + root.meta + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("payments") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + var items: [URLQueryItem] = [] + if let pluginPaymentToken { items.append(URLQueryItem(name: "plugin_payment_token", value: pluginPaymentToken)) } + if let userId { items.append(URLQueryItem(name: "user_id", value: userId)) } + if let pluginId { items.append(URLQueryItem(name: "plugin_id", value: pluginId)) } + if let widgetId { items.append(URLQueryItem(name: "widget_id", value: widgetId)) } + if let communityFileId { items.append(URLQueryItem(name: "community_file_id", value: communityFileId)) } + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} + +struct PaymentsResponse: Decodable { + let meta: PaymentInfo +} diff --git a/Sources/FigmaAPI/Endpoint/GetProjectFilesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetProjectFilesEndpoint.swift new file mode 100644 index 0000000..4e2e695 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetProjectFilesEndpoint.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetProjectFilesEndpoint: BaseEndpoint { + public typealias Content = [ProjectFile] + + private let projectId: String + + public init(projectId: String) { + self.projectId = projectId + } + + func content(from root: ProjectFilesResponse) -> [ProjectFile] { + root.files + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("projects") + .appendingPathComponent(projectId) + .appendingPathComponent("files") + return URLRequest(url: url) + } +} + +struct ProjectFilesResponse: Decodable { + let files: [ProjectFile] +} diff --git a/Sources/FigmaAPI/Endpoint/GetPublishedVariablesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetPublishedVariablesEndpoint.swift new file mode 100644 index 0000000..8ce8d3c --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetPublishedVariablesEndpoint.swift @@ -0,0 +1,28 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetPublishedVariablesEndpoint: BaseEndpoint { + public typealias Content = VariablesMeta + + private let fileId: String + + public init(fileId: String) { + self.fileId = fileId + } + + func content(from root: VariablesResponse) -> VariablesMeta { + root.meta + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("variables") + .appendingPathComponent("published") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetReactionsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetReactionsEndpoint.swift new file mode 100644 index 0000000..79cc9ee --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetReactionsEndpoint.swift @@ -0,0 +1,35 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetReactionsEndpoint: BaseEndpoint { + public typealias Content = [Reaction] + + private let fileId: String + private let commentId: String + + public init(fileId: String, commentId: String) { + self.fileId = fileId + self.commentId = commentId + } + + func content(from root: ReactionsResponse) -> [Reaction] { + root.reactions + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + .appendingPathComponent(commentId) + .appendingPathComponent("reactions") + return URLRequest(url: url) + } +} + +struct ReactionsResponse: Decodable { + let reactions: [Reaction] +} diff --git a/Sources/FigmaAPI/Endpoint/GetStyleActionsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetStyleActionsEndpoint.swift new file mode 100644 index 0000000..6d03829 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetStyleActionsEndpoint.swift @@ -0,0 +1,29 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetStyleActionsEndpoint: BaseEndpoint { + public typealias Content = [LibraryAnalyticsAction] + + private let fileKey: String + + public init(fileKey: String) { + self.fileKey = fileKey + } + + func content(from root: LibraryActionsResponse) -> [LibraryAnalyticsAction] { + root.actions + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("analytics") + .appendingPathComponent("libraries") + .appendingPathComponent(fileKey) + .appendingPathComponent("style") + .appendingPathComponent("actions") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetStyleEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetStyleEndpoint.swift new file mode 100644 index 0000000..d72785e --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetStyleEndpoint.swift @@ -0,0 +1,30 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetStyleEndpoint: BaseEndpoint { + public typealias Content = Style + + private let key: String + + public init(key: String) { + self.key = key + } + + func content(from root: GetStyleResponse) -> Style { + root.meta + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("styles") + .appendingPathComponent(key) + return URLRequest(url: url) + } +} + +struct GetStyleResponse: Decodable { + let meta: Style +} diff --git a/Sources/FigmaAPI/Endpoint/GetStyleUsagesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetStyleUsagesEndpoint.swift new file mode 100644 index 0000000..ceb8a23 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetStyleUsagesEndpoint.swift @@ -0,0 +1,29 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetStyleUsagesEndpoint: BaseEndpoint { + public typealias Content = [LibraryAnalyticsUsage] + + private let fileKey: String + + public init(fileKey: String) { + self.fileKey = fileKey + } + + func content(from root: LibraryUsagesResponse) -> [LibraryAnalyticsUsage] { + root.usages + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("analytics") + .appendingPathComponent("libraries") + .appendingPathComponent(fileKey) + .appendingPathComponent("style") + .appendingPathComponent("usages") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetTeamComponentSetsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetTeamComponentSetsEndpoint.swift new file mode 100644 index 0000000..7f2b26c --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetTeamComponentSetsEndpoint.swift @@ -0,0 +1,37 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetTeamComponentSetsEndpoint: BaseEndpoint { + public typealias Content = [ComponentSet] + + private let teamId: String + private let pagination: PaginationParams? + + public init(teamId: String, pagination: PaginationParams? = nil) { + self.teamId = teamId + self.pagination = pagination + } + + func content(from root: FileComponentSetsResponse) -> [ComponentSet] { + root.meta.componentSets + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("teams") + .appendingPathComponent(teamId) + .appendingPathComponent("component_sets") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + let items = pagination?.queryItems ?? [] + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetTeamComponentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetTeamComponentsEndpoint.swift new file mode 100644 index 0000000..c496d20 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetTeamComponentsEndpoint.swift @@ -0,0 +1,37 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetTeamComponentsEndpoint: BaseEndpoint { + public typealias Content = [Component] + + private let teamId: String + private let pagination: PaginationParams? + + public init(teamId: String, pagination: PaginationParams? = nil) { + self.teamId = teamId + self.pagination = pagination + } + + func content(from root: ComponentsResponse) -> [Component] { + root.meta.components + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("teams") + .appendingPathComponent(teamId) + .appendingPathComponent("components") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + let items = pagination?.queryItems ?? [] + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetTeamProjectsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetTeamProjectsEndpoint.swift new file mode 100644 index 0000000..8fbcf34 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetTeamProjectsEndpoint.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetTeamProjectsEndpoint: BaseEndpoint { + public typealias Content = [Project] + + private let teamId: String + + public init(teamId: String) { + self.teamId = teamId + } + + func content(from root: TeamProjectsResponse) -> [Project] { + root.projects + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("teams") + .appendingPathComponent(teamId) + .appendingPathComponent("projects") + return URLRequest(url: url) + } +} + +struct TeamProjectsResponse: Decodable { + let projects: [Project] +} diff --git a/Sources/FigmaAPI/Endpoint/GetTeamStylesEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetTeamStylesEndpoint.swift new file mode 100644 index 0000000..965db09 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetTeamStylesEndpoint.swift @@ -0,0 +1,37 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetTeamStylesEndpoint: BaseEndpoint { + public typealias Content = [Style] + + private let teamId: String + private let pagination: PaginationParams? + + public init(teamId: String, pagination: PaginationParams? = nil) { + self.teamId = teamId + self.pagination = pagination + } + + func content(from root: StylesResponse) -> [Style] { + root.meta.styles + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("teams") + .appendingPathComponent(teamId) + .appendingPathComponent("styles") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + let items = pagination?.queryItems ?? [] + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetTeamWebhooksEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetTeamWebhooksEndpoint.swift new file mode 100644 index 0000000..859ef30 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetTeamWebhooksEndpoint.swift @@ -0,0 +1,27 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetTeamWebhooksEndpoint: BaseEndpoint { + public typealias Content = [Webhook] + + private let teamId: String + + public init(teamId: String) { + self.teamId = teamId + } + + func content(from root: WebhooksResponse) -> [Webhook] { + root.webhooks + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("teams") + .appendingPathComponent(teamId) + .appendingPathComponent("webhooks") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetVariableActionsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetVariableActionsEndpoint.swift new file mode 100644 index 0000000..8d785fa --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetVariableActionsEndpoint.swift @@ -0,0 +1,29 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetVariableActionsEndpoint: BaseEndpoint { + public typealias Content = [LibraryAnalyticsAction] + + private let fileKey: String + + public init(fileKey: String) { + self.fileKey = fileKey + } + + func content(from root: LibraryActionsResponse) -> [LibraryAnalyticsAction] { + root.actions + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("analytics") + .appendingPathComponent("libraries") + .appendingPathComponent(fileKey) + .appendingPathComponent("variable") + .appendingPathComponent("actions") + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetWebhookEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetWebhookEndpoint.swift new file mode 100644 index 0000000..b3b93b3 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetWebhookEndpoint.swift @@ -0,0 +1,22 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetWebhookEndpoint: BaseEndpoint { + public typealias Content = Webhook + + private let webhookId: String + + public init(webhookId: String) { + self.webhookId = webhookId + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + .appendingPathComponent(webhookId) + return URLRequest(url: url) + } +} diff --git a/Sources/FigmaAPI/Endpoint/GetWebhookRequestsEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetWebhookRequestsEndpoint.swift new file mode 100644 index 0000000..f668b92 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetWebhookRequestsEndpoint.swift @@ -0,0 +1,31 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetWebhookRequestsEndpoint: BaseEndpoint { + public typealias Content = [WebhookRequest] + + private let webhookId: String + + public init(webhookId: String) { + self.webhookId = webhookId + } + + func content(from root: WebhookRequestsResponse) -> [WebhookRequest] { + root.requests + } + + public func makeRequest(baseURL: URL) -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + .appendingPathComponent(webhookId) + .appendingPathComponent("requests") + return URLRequest(url: url) + } +} + +struct WebhookRequestsResponse: Decodable { + let requests: [WebhookRequest] +} diff --git a/Sources/FigmaAPI/Endpoint/GetWebhooksEndpoint.swift b/Sources/FigmaAPI/Endpoint/GetWebhooksEndpoint.swift new file mode 100644 index 0000000..ca3f59f --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/GetWebhooksEndpoint.swift @@ -0,0 +1,41 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct GetWebhooksEndpoint: BaseEndpoint { + public typealias Content = [Webhook] + + private let context: String? + private let contextId: String? + + public init(context: String? = nil, contextId: String? = nil) { + self.context = context + self.contextId = contextId + } + + func content(from root: WebhooksResponse) -> [Webhook] { + root.webhooks + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw URLError(.badURL) + } + var items: [URLQueryItem] = [] + if let context { items.append(URLQueryItem(name: "context", value: context)) } + if let contextId { items.append(URLQueryItem(name: "context_id", value: contextId)) } + if !items.isEmpty { comps.queryItems = items } + guard let finalURL = comps.url else { + throw URLError(.badURL) + } + return URLRequest(url: finalURL) + } +} + +struct WebhooksResponse: Decodable { + let webhooks: [Webhook] +} diff --git a/Sources/FigmaAPI/Endpoint/ImageEndpoint.swift b/Sources/FigmaAPI/Endpoint/ImageEndpoint.swift index 7eff782..bbd0b0b 100644 --- a/Sources/FigmaAPI/Endpoint/ImageEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/ImageEndpoint.swift @@ -81,6 +81,7 @@ public struct ImageEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) throws -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("images") .appendingPathComponent(fileId) diff --git a/Sources/FigmaAPI/Endpoint/NodesEndpoint.swift b/Sources/FigmaAPI/Endpoint/NodesEndpoint.swift index a04f0ed..052de4e 100644 --- a/Sources/FigmaAPI/Endpoint/NodesEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/NodesEndpoint.swift @@ -20,6 +20,7 @@ public struct NodesEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) throws -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) .appendingPathComponent("nodes") diff --git a/Sources/FigmaAPI/Endpoint/PaginationParams.swift b/Sources/FigmaAPI/Endpoint/PaginationParams.swift new file mode 100644 index 0000000..741b9c8 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PaginationParams.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct PaginationParams: Sendable { + public let pageSize: Int? + public let cursor: String? + + public init(pageSize: Int? = nil, cursor: String? = nil) { + if let pageSize { + precondition(pageSize > 0, "pageSize must be positive") + } + self.pageSize = pageSize + self.cursor = cursor + } + + public var queryItems: [URLQueryItem] { + var items: [URLQueryItem] = [] + if let pageSize { items.append(.init(name: "page_size", value: "\(pageSize)")) } + if let cursor { items.append(.init(name: "cursor", value: cursor)) } + return items + } +} diff --git a/Sources/FigmaAPI/Endpoint/PostCommentEndpoint.swift b/Sources/FigmaAPI/Endpoint/PostCommentEndpoint.swift new file mode 100644 index 0000000..3726bf7 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PostCommentEndpoint.swift @@ -0,0 +1,33 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PostCommentEndpoint: BaseEndpoint { + public typealias Content = Comment + + private let fileId: String + private let body: PostCommentBody + + public init(fileId: String, body: PostCommentBody) { + self.fileId = fileId + self.body = body + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(body) + + return request + } +} diff --git a/Sources/FigmaAPI/Endpoint/PostDevResourceEndpoint.swift b/Sources/FigmaAPI/Endpoint/PostDevResourceEndpoint.swift new file mode 100644 index 0000000..82766ca --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PostDevResourceEndpoint.swift @@ -0,0 +1,49 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PostDevResourceEndpoint: BaseEndpoint { + public typealias Content = [DevResource] + + private let body: PostDevResourceBody + + public init(body: PostDevResourceBody) { + self.body = body + } + + func content(from root: LinksCreatedResponse) -> [DevResource] { + root.linksCreated + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("dev_resources") + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(DevResourcesWrapper(devResources: [body])) + + return request + } +} + +struct DevResourcesWrapper: Encodable { + let devResources: [PostDevResourceBody] + + private enum CodingKeys: String, CodingKey { + case devResources = "dev_resources" + } +} + +struct LinksCreatedResponse: Decodable { + let linksCreated: [DevResource] + + private enum CodingKeys: String, CodingKey { + case linksCreated = "links_created" + } +} diff --git a/Sources/FigmaAPI/Endpoint/PostReactionEndpoint.swift b/Sources/FigmaAPI/Endpoint/PostReactionEndpoint.swift new file mode 100644 index 0000000..992f882 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PostReactionEndpoint.swift @@ -0,0 +1,37 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PostReactionEndpoint: BaseEndpoint { + public typealias Content = EmptyResponse + + private let fileId: String + private let commentId: String + private let body: PostReactionBody + + public init(fileId: String, commentId: String, body: PostReactionBody) { + self.fileId = fileId + self.commentId = commentId + self.body = body + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("files") + .appendingPathComponent(fileId) + .appendingPathComponent("comments") + .appendingPathComponent(commentId) + .appendingPathComponent("reactions") + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(body) + + return request + } +} diff --git a/Sources/FigmaAPI/Endpoint/PostWebhookEndpoint.swift b/Sources/FigmaAPI/Endpoint/PostWebhookEndpoint.swift new file mode 100644 index 0000000..0a4bda0 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PostWebhookEndpoint.swift @@ -0,0 +1,27 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PostWebhookEndpoint: BaseEndpoint { + public typealias Content = Webhook + + private let body: PostWebhookBody + + public init(body: PostWebhookBody) { + self.body = body + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(body) + return request + } +} diff --git a/Sources/FigmaAPI/Endpoint/PutDevResourceEndpoint.swift b/Sources/FigmaAPI/Endpoint/PutDevResourceEndpoint.swift new file mode 100644 index 0000000..64a7092 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PutDevResourceEndpoint.swift @@ -0,0 +1,49 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PutDevResourceEndpoint: BaseEndpoint { + public typealias Content = [DevResource] + + private let body: PutDevResourceBody + + public init(body: PutDevResourceBody) { + self.body = body + } + + func content(from root: LinksUpdatedResponse) -> [DevResource] { + root.linksUpdated + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v1") + .appendingPathComponent("dev_resources") + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(PutDevResourcesWrapper(devResources: [body])) + + return request + } +} + +struct PutDevResourcesWrapper: Encodable { + let devResources: [PutDevResourceBody] + + private enum CodingKeys: String, CodingKey { + case devResources = "dev_resources" + } +} + +struct LinksUpdatedResponse: Decodable { + let linksUpdated: [DevResource] + + private enum CodingKeys: String, CodingKey { + case linksUpdated = "links_updated" + } +} diff --git a/Sources/FigmaAPI/Endpoint/PutWebhookEndpoint.swift b/Sources/FigmaAPI/Endpoint/PutWebhookEndpoint.swift new file mode 100644 index 0000000..a6d4795 --- /dev/null +++ b/Sources/FigmaAPI/Endpoint/PutWebhookEndpoint.swift @@ -0,0 +1,30 @@ +import Foundation +import YYJSON + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct PutWebhookEndpoint: BaseEndpoint { + public typealias Content = Webhook + + private let webhookId: String + private let body: PutWebhookBody + + public init(webhookId: String, body: PutWebhookBody) { + self.webhookId = webhookId + self.body = body + } + + public func makeRequest(baseURL: URL) throws -> URLRequest { + let url = baseURL + .appendingPathComponent("v2") + .appendingPathComponent("webhooks") + .appendingPathComponent(webhookId) + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try YYJSONEncoder().encode(body) + return request + } +} diff --git a/Sources/FigmaAPI/Endpoint/StylesEndpoint.swift b/Sources/FigmaAPI/Endpoint/StylesEndpoint.swift index 032e892..6770c78 100644 --- a/Sources/FigmaAPI/Endpoint/StylesEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/StylesEndpoint.swift @@ -18,6 +18,7 @@ public struct StylesEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) .appendingPathComponent("styles") diff --git a/Sources/FigmaAPI/Endpoint/UpdateVariablesEndpoint.swift b/Sources/FigmaAPI/Endpoint/UpdateVariablesEndpoint.swift index f120295..45bd42b 100644 --- a/Sources/FigmaAPI/Endpoint/UpdateVariablesEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/UpdateVariablesEndpoint.swift @@ -20,6 +20,7 @@ public struct UpdateVariablesEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) throws -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) .appendingPathComponent("variables") diff --git a/Sources/FigmaAPI/Endpoint/VariablesEndpoint.swift b/Sources/FigmaAPI/Endpoint/VariablesEndpoint.swift index a602526..bea222a 100644 --- a/Sources/FigmaAPI/Endpoint/VariablesEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/VariablesEndpoint.swift @@ -18,6 +18,7 @@ public struct VariablesEndpoint: BaseEndpoint { public func makeRequest(baseURL: URL) -> URLRequest { let url = baseURL + .appendingPathComponent("v1") .appendingPathComponent("files") .appendingPathComponent(fileId) .appendingPathComponent("variables") diff --git a/Sources/FigmaAPI/FigmaClient.swift b/Sources/FigmaAPI/FigmaClient.swift index 142d01b..d072b67 100644 --- a/Sources/FigmaAPI/FigmaClient.swift +++ b/Sources/FigmaAPI/FigmaClient.swift @@ -5,12 +5,12 @@ import Foundation public final class FigmaClient: BaseClient, @unchecked Sendable { // swiftlint:disable:next force_unwrapping - private static let figmaBaseURL = URL(string: "https://api.figma.com/v1/")! + public static let baseURL = URL(string: "https://api.figma.com/")! public init(accessToken: String, timeout: TimeInterval?) { let config = URLSessionConfiguration.ephemeral config.httpAdditionalHeaders = ["X-Figma-Token": accessToken] config.timeoutIntervalForRequest = timeout ?? 30 - super.init(baseURL: Self.figmaBaseURL, config: config) + super.init(baseURL: Self.baseURL, config: config) } } diff --git a/Sources/FigmaAPI/Model/ActivityLog.swift b/Sources/FigmaAPI/Model/ActivityLog.swift new file mode 100644 index 0000000..d1dcbf8 --- /dev/null +++ b/Sources/FigmaAPI/Model/ActivityLog.swift @@ -0,0 +1,16 @@ +public struct ActivityLog: Decodable, Sendable { + public let id: String + public let timestamp: String + public let actorId: String? + public let actionType: String + public let entityType: String + public let details: [String: String]? + + private enum CodingKeys: String, CodingKey { + case id, timestamp + case actorId = "actor_id" + case actionType = "action_type" + case entityType = "entity_type" + case details + } +} diff --git a/Sources/FigmaAPI/Model/Comment.swift b/Sources/FigmaAPI/Model/Comment.swift new file mode 100644 index 0000000..dc12135 --- /dev/null +++ b/Sources/FigmaAPI/Model/Comment.swift @@ -0,0 +1,29 @@ +public struct Comment: Decodable, Sendable { + public let id: String + public let message: String + public let createdAt: String + public let resolvedAt: String? + public let user: User + public let orderId: String? + public let parentId: String? + public let clientMeta: ClientMeta? + + private enum CodingKeys: String, CodingKey { + case id, message, user + case createdAt = "created_at" + case resolvedAt = "resolved_at" + case orderId = "order_id" + case parentId = "parent_id" + case clientMeta = "client_meta" + } +} + +public struct ClientMeta: Decodable, Sendable { + public let nodeId: String? + public let nodeOffset: Vector? + + private enum CodingKeys: String, CodingKey { + case nodeId = "node_id" + case nodeOffset = "node_offset" + } +} diff --git a/Sources/FigmaAPI/Model/ComponentSet.swift b/Sources/FigmaAPI/Model/ComponentSet.swift new file mode 100644 index 0000000..4cef3f6 --- /dev/null +++ b/Sources/FigmaAPI/Model/ComponentSet.swift @@ -0,0 +1,14 @@ +public struct ComponentSet: Decodable, Sendable { + public let key: String + public let nodeId: String + public let name: String + public let description: String? + public let containingFrame: ContainingFrame + + private enum CodingKeys: String, CodingKey { + case key + case nodeId = "node_id" + case name, description + case containingFrame = "containing_frame" + } +} diff --git a/Sources/FigmaAPI/Model/DevResource.swift b/Sources/FigmaAPI/Model/DevResource.swift new file mode 100644 index 0000000..d7bd51a --- /dev/null +++ b/Sources/FigmaAPI/Model/DevResource.swift @@ -0,0 +1,13 @@ +public struct DevResource: Decodable, Sendable { + public let id: String + public let name: String + public let url: String + public let nodeId: String + public let devStatus: String? + + private enum CodingKeys: String, CodingKey { + case id, name, url + case nodeId = "node_id" + case devStatus = "dev_status" + } +} diff --git a/Sources/FigmaAPI/Model/EmptyResponse.swift b/Sources/FigmaAPI/Model/EmptyResponse.swift new file mode 100644 index 0000000..b9b87ce --- /dev/null +++ b/Sources/FigmaAPI/Model/EmptyResponse.swift @@ -0,0 +1 @@ +public struct EmptyResponse: Decodable, Sendable {} diff --git a/Sources/FigmaAPI/Model/FileMeta.swift b/Sources/FigmaAPI/Model/FileMeta.swift new file mode 100644 index 0000000..75529e1 --- /dev/null +++ b/Sources/FigmaAPI/Model/FileMeta.swift @@ -0,0 +1,4 @@ +public struct FileMeta: Sendable { + public let lastModified: String? + public let version: String? +} diff --git a/Sources/FigmaAPI/Model/FileVersion.swift b/Sources/FigmaAPI/Model/FileVersion.swift new file mode 100644 index 0000000..af5b63e --- /dev/null +++ b/Sources/FigmaAPI/Model/FileVersion.swift @@ -0,0 +1,13 @@ +public struct FileVersion: Decodable, Sendable { + public let id: String + public let createdAt: String + public let label: String? + public let description: String? + public let user: User + + private enum CodingKeys: String, CodingKey { + case id + case createdAt = "created_at" + case label, description, user + } +} diff --git a/Sources/FigmaAPI/Model/LibraryAnalytics.swift b/Sources/FigmaAPI/Model/LibraryAnalytics.swift new file mode 100644 index 0000000..e9e6d08 --- /dev/null +++ b/Sources/FigmaAPI/Model/LibraryAnalytics.swift @@ -0,0 +1,41 @@ +public struct LibraryAnalyticsAction: Decodable, Sendable { + public let componentKey: String? + public let componentName: String? + public let styleKey: String? + public let styleName: String? + public let variableKey: String? + public let variableName: String? + public let actionType: String + public let actionCount: Int + + private enum CodingKeys: String, CodingKey { + case componentKey = "component_key" + case componentName = "component_name" + case styleKey = "style_key" + case styleName = "style_name" + case variableKey = "variable_key" + case variableName = "variable_name" + case actionType = "action_type" + case actionCount = "action_count" + } +} + +public struct LibraryAnalyticsUsage: Decodable, Sendable { + public let componentKey: String? + public let componentName: String? + public let styleKey: String? + public let styleName: String? + public let variableKey: String? + public let variableName: String? + public let usageCount: Int + + private enum CodingKeys: String, CodingKey { + case componentKey = "component_key" + case componentName = "component_name" + case styleKey = "style_key" + case styleName = "style_name" + case variableKey = "variable_key" + case variableName = "variable_name" + case usageCount = "usage_count" + } +} diff --git a/Sources/FigmaAPI/Model/PaymentInfo.swift b/Sources/FigmaAPI/Model/PaymentInfo.swift new file mode 100644 index 0000000..bddf816 --- /dev/null +++ b/Sources/FigmaAPI/Model/PaymentInfo.swift @@ -0,0 +1,14 @@ +public struct PaymentInfo: Decodable, Sendable { + public let status: Int + public let users: [PaymentUser]? +} + +public struct PaymentUser: Decodable, Sendable { + public let userId: String + public let status: String + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case status + } +} diff --git a/Sources/FigmaAPI/Model/PostCommentBody.swift b/Sources/FigmaAPI/Model/PostCommentBody.swift new file mode 100644 index 0000000..fa2b501 --- /dev/null +++ b/Sources/FigmaAPI/Model/PostCommentBody.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct PostCommentBody: Encodable, Sendable { + public let message: String + public let clientMeta: PostClientMeta? + public let commentId: String? + + public init(message: String, clientMeta: PostClientMeta? = nil, commentId: String? = nil) { + self.message = message + self.clientMeta = clientMeta + self.commentId = commentId + } + + private enum CodingKeys: String, CodingKey { + case message + case clientMeta = "client_meta" + case commentId = "comment_id" + } +} + +public struct PostClientMeta: Encodable, Sendable { + public let nodeId: String? + public let nodeOffset: PostVector? + + public init(nodeId: String? = nil, nodeOffset: PostVector? = nil) { + self.nodeId = nodeId + self.nodeOffset = nodeOffset + } + + private enum CodingKeys: String, CodingKey { + case nodeId = "node_id" + case nodeOffset = "node_offset" + } +} + +public struct PostVector: Encodable, Sendable { + public let x: Double + public let y: Double + + public init(x: Double, y: Double) { + self.x = x + self.y = y + } +} diff --git a/Sources/FigmaAPI/Model/PostDevResourceBody.swift b/Sources/FigmaAPI/Model/PostDevResourceBody.swift new file mode 100644 index 0000000..5c68fe7 --- /dev/null +++ b/Sources/FigmaAPI/Model/PostDevResourceBody.swift @@ -0,0 +1,22 @@ +public struct PostDevResourceBody: Encodable, Sendable { + public let name: String + public let url: String + public let fileKey: String + public let nodeId: String + public let devStatus: String? + + public init(name: String, url: String, fileKey: String, nodeId: String, devStatus: String? = nil) { + self.name = name + self.url = url + self.fileKey = fileKey + self.nodeId = nodeId + self.devStatus = devStatus + } + + private enum CodingKeys: String, CodingKey { + case name, url + case fileKey = "file_key" + case nodeId = "node_id" + case devStatus = "dev_status" + } +} diff --git a/Sources/FigmaAPI/Model/PostReactionBody.swift b/Sources/FigmaAPI/Model/PostReactionBody.swift new file mode 100644 index 0000000..90ee6d4 --- /dev/null +++ b/Sources/FigmaAPI/Model/PostReactionBody.swift @@ -0,0 +1,7 @@ +public struct PostReactionBody: Encodable, Sendable { + public let emoji: String + + public init(emoji: String) { + self.emoji = emoji + } +} diff --git a/Sources/FigmaAPI/Model/PostWebhookBody.swift b/Sources/FigmaAPI/Model/PostWebhookBody.swift new file mode 100644 index 0000000..a17f216 --- /dev/null +++ b/Sources/FigmaAPI/Model/PostWebhookBody.swift @@ -0,0 +1,21 @@ +public struct PostWebhookBody: Encodable, Sendable { + public let eventType: String + public let teamId: String + public let endpoint: String + public let passcode: String + public let description: String? + + public init(eventType: String, teamId: String, endpoint: String, passcode: String, description: String? = nil) { + self.eventType = eventType + self.teamId = teamId + self.endpoint = endpoint + self.passcode = passcode + self.description = description + } + + private enum CodingKeys: String, CodingKey { + case eventType = "event_type" + case teamId = "team_id" + case endpoint, passcode, description + } +} diff --git a/Sources/FigmaAPI/Model/Project.swift b/Sources/FigmaAPI/Model/Project.swift new file mode 100644 index 0000000..85d9710 --- /dev/null +++ b/Sources/FigmaAPI/Model/Project.swift @@ -0,0 +1,17 @@ +public struct Project: Decodable, Sendable { + public let id: String + public let name: String +} + +public struct ProjectFile: Decodable, Sendable { + public let key: String + public let name: String + public let thumbnailUrl: String? + public let lastModified: String + + private enum CodingKeys: String, CodingKey { + case key, name + case thumbnailUrl = "thumbnail_url" + case lastModified = "last_modified" + } +} diff --git a/Sources/FigmaAPI/Model/PutDevResourceBody.swift b/Sources/FigmaAPI/Model/PutDevResourceBody.swift new file mode 100644 index 0000000..d152f7a --- /dev/null +++ b/Sources/FigmaAPI/Model/PutDevResourceBody.swift @@ -0,0 +1,11 @@ +public struct PutDevResourceBody: Encodable, Sendable { + public let id: String + public let name: String? + public let url: String? + + public init(id: String, name: String? = nil, url: String? = nil) { + self.id = id + self.name = name + self.url = url + } +} diff --git a/Sources/FigmaAPI/Model/PutWebhookBody.swift b/Sources/FigmaAPI/Model/PutWebhookBody.swift new file mode 100644 index 0000000..ef77442 --- /dev/null +++ b/Sources/FigmaAPI/Model/PutWebhookBody.swift @@ -0,0 +1,18 @@ +public struct PutWebhookBody: Encodable, Sendable { + public let eventType: String? + public let endpoint: String? + public let passcode: String? + public let description: String? + + public init(eventType: String? = nil, endpoint: String? = nil, passcode: String? = nil, description: String? = nil) { + self.eventType = eventType + self.endpoint = endpoint + self.passcode = passcode + self.description = description + } + + private enum CodingKeys: String, CodingKey { + case eventType = "event_type" + case endpoint, passcode, description + } +} diff --git a/Sources/FigmaAPI/Model/Reaction.swift b/Sources/FigmaAPI/Model/Reaction.swift new file mode 100644 index 0000000..bde2bfc --- /dev/null +++ b/Sources/FigmaAPI/Model/Reaction.swift @@ -0,0 +1,10 @@ +public struct Reaction: Decodable, Sendable { + public let emoji: String + public let user: User + public let createdAt: String + + private enum CodingKeys: String, CodingKey { + case emoji, user + case createdAt = "created_at" + } +} diff --git a/Sources/FigmaAPI/Model/User.swift b/Sources/FigmaAPI/Model/User.swift new file mode 100644 index 0000000..7113d84 --- /dev/null +++ b/Sources/FigmaAPI/Model/User.swift @@ -0,0 +1,11 @@ +public struct User: Decodable, Sendable { + public let id: String + public let handle: String + public let email: String? + public let imgUrl: String? + + private enum CodingKeys: String, CodingKey { + case id, handle, email + case imgUrl = "img_url" + } +} diff --git a/Sources/FigmaAPI/Model/Webhook.swift b/Sources/FigmaAPI/Model/Webhook.swift new file mode 100644 index 0000000..ecbc57b --- /dev/null +++ b/Sources/FigmaAPI/Model/Webhook.swift @@ -0,0 +1,20 @@ +public struct Webhook: Decodable, Sendable { + public let id: String + public let teamId: String + public let eventType: String + public let clientId: String? + public let endpoint: String + public let passcode: String? + public let status: String + public let description: String? + public let protocolVersion: String? + + private enum CodingKeys: String, CodingKey { + case id + case teamId = "team_id" + case eventType = "event_type" + case clientId = "client_id" + case endpoint, passcode, status, description + case protocolVersion = "protocol_version" + } +} diff --git a/Sources/FigmaAPI/Model/WebhookRequest.swift b/Sources/FigmaAPI/Model/WebhookRequest.swift new file mode 100644 index 0000000..9c96a0c --- /dev/null +++ b/Sources/FigmaAPI/Model/WebhookRequest.swift @@ -0,0 +1,22 @@ +public struct WebhookRequest: Decodable, Sendable { + public let id: String + public let endpoint: String + public let payload: WebhookPayload? + public let error: String? + public let createdAt: String + + private enum CodingKeys: String, CodingKey { + case id, endpoint, payload, error + case createdAt = "created_at" + } +} + +public struct WebhookPayload: Decodable, Sendable { + public let eventType: String? + public let timestamp: String? + + private enum CodingKeys: String, CodingKey { + case eventType = "event_type" + case timestamp + } +} diff --git a/Tests/FigmaAPITests/ComponentsEndpointTests.swift b/Tests/FigmaAPITests/ComponentsEndpointTests.swift index 489cc9a..2a764c4 100644 --- a/Tests/FigmaAPITests/ComponentsEndpointTests.swift +++ b/Tests/FigmaAPITests/ComponentsEndpointTests.swift @@ -7,7 +7,7 @@ final class ComponentsEndpointTests: XCTestCase { func testMakeRequestConstructsCorrectURL() throws { let endpoint = ComponentsEndpoint(fileId: "abc123") - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/DeleteCommentEndpointTests.swift b/Tests/FigmaAPITests/DeleteCommentEndpointTests.swift new file mode 100644 index 0000000..c0e0625 --- /dev/null +++ b/Tests/FigmaAPITests/DeleteCommentEndpointTests.swift @@ -0,0 +1,45 @@ +@testable import FigmaAPI +import XCTest + +final class DeleteCommentEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = DeleteCommentEndpoint(fileId: "abc123", commentId: "comment1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/comments/comment1" + ) + } + + func testMakeRequestUsesDELETEMethod() throws { + let endpoint = DeleteCommentEndpoint(fileId: "abc123", commentId: "comment1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "DELETE") + } + + // MARK: - Response Parsing + + func testContentParsesEmptyBody() throws { + let data = Data() + let endpoint = DeleteCommentEndpoint(fileId: "test", commentId: "c1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } + + func testContentParsesEmptyJSONBody() throws { + let data = Data("{}".utf8) + let endpoint = DeleteCommentEndpoint(fileId: "test", commentId: "c1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } +} diff --git a/Tests/FigmaAPITests/DeleteDevResourceEndpointTests.swift b/Tests/FigmaAPITests/DeleteDevResourceEndpointTests.swift new file mode 100644 index 0000000..154a54f --- /dev/null +++ b/Tests/FigmaAPITests/DeleteDevResourceEndpointTests.swift @@ -0,0 +1,45 @@ +@testable import FigmaAPI +import XCTest + +final class DeleteDevResourceEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = DeleteDevResourceEndpoint(fileId: "abc123", resourceId: "res1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/dev_resources/res1" + ) + } + + func testMakeRequestUsesDELETEMethod() throws { + let endpoint = DeleteDevResourceEndpoint(fileId: "abc123", resourceId: "res1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "DELETE") + } + + // MARK: - Response Parsing + + func testContentParsesEmptyBody() throws { + let data = Data() + let endpoint = DeleteDevResourceEndpoint(fileId: "test", resourceId: "res1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } + + func testContentParsesEmptyJSONBody() throws { + let data = Data("{}".utf8) + let endpoint = DeleteDevResourceEndpoint(fileId: "test", resourceId: "res1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } +} diff --git a/Tests/FigmaAPITests/DeleteReactionEndpointTests.swift b/Tests/FigmaAPITests/DeleteReactionEndpointTests.swift new file mode 100644 index 0000000..1623df8 --- /dev/null +++ b/Tests/FigmaAPITests/DeleteReactionEndpointTests.swift @@ -0,0 +1,56 @@ +@testable import FigmaAPI +import XCTest + +final class DeleteReactionEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = DeleteReactionEndpoint(fileId: "abc123", commentId: "comment1", emoji: "👍") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertTrue( + request.url?.absoluteString.starts(with: + "https://api.figma.com/v1/files/abc123/comments/comment1/reactions") ?? false + ) + } + + func testMakeRequestIncludesEmojiQueryParameter() throws { + let endpoint = DeleteReactionEndpoint(fileId: "abc123", commentId: "comment1", emoji: "👍") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + let emojiItem = components.queryItems?.first(where: { $0.name == "emoji" }) + XCTAssertEqual(emojiItem?.value, "\u{1F44D}") + } + + func testMakeRequestUsesDELETEMethod() throws { + let endpoint = DeleteReactionEndpoint(fileId: "abc123", commentId: "comment1", emoji: "👍") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "DELETE") + } + + // MARK: - Response Parsing + + func testContentParsesEmptyBody() throws { + let data = Data() + let endpoint = DeleteReactionEndpoint(fileId: "test", commentId: "c1", emoji: "👍") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } + + func testContentParsesEmptyJSONBody() throws { + let data = Data("{}".utf8) + let endpoint = DeleteReactionEndpoint(fileId: "test", commentId: "c1", emoji: "👍") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } +} diff --git a/Tests/FigmaAPITests/DeleteWebhookEndpointTests.swift b/Tests/FigmaAPITests/DeleteWebhookEndpointTests.swift new file mode 100644 index 0000000..09ab2b0 --- /dev/null +++ b/Tests/FigmaAPITests/DeleteWebhookEndpointTests.swift @@ -0,0 +1,45 @@ +@testable import FigmaAPI +import XCTest + +final class DeleteWebhookEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = DeleteWebhookEndpoint(webhookId: "wh1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks/wh1" + ) + } + + func testMakeRequestUsesDELETEMethod() throws { + let endpoint = DeleteWebhookEndpoint(webhookId: "wh1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "DELETE") + } + + // MARK: - Response Parsing + + func testContentParsesEmptyBody() throws { + let data = Data() + let endpoint = DeleteWebhookEndpoint(webhookId: "wh1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } + + func testContentParsesEmptyJSONBody() throws { + let data = Data("{}".utf8) + let endpoint = DeleteWebhookEndpoint(webhookId: "wh1") + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } +} diff --git a/Tests/FigmaAPITests/EndpointMakeRequestTests.swift b/Tests/FigmaAPITests/EndpointMakeRequestTests.swift index e288906..d9c672f 100644 --- a/Tests/FigmaAPITests/EndpointMakeRequestTests.swift +++ b/Tests/FigmaAPITests/EndpointMakeRequestTests.swift @@ -11,8 +11,7 @@ final class EndpointMakeRequestTests: XCTestCase { // MARK: - Valid Base URL (happy path) func testImageEndpointMakeRequestSucceeds() throws { - // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1")) + let baseURL = FigmaClient.baseURL let endpoint = ImageEndpoint(fileId: "abc123", nodeIds: ["1:2"], params: SVGParams()) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -22,8 +21,7 @@ final class EndpointMakeRequestTests: XCTestCase { } func testNodesEndpointMakeRequestSucceeds() throws { - // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1")) + let baseURL = FigmaClient.baseURL let endpoint = NodesEndpoint(fileId: "abc123", nodeIds: ["1:2", "3:4"]) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -33,8 +31,7 @@ final class EndpointMakeRequestTests: XCTestCase { } func testFileMetadataEndpointMakeRequestSucceeds() throws { - // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1")) + let baseURL = FigmaClient.baseURL let endpoint = FileMetadataEndpoint(fileId: "abc123") let request = try endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/FileMetadataEndpointTests.swift b/Tests/FigmaAPITests/FileMetadataEndpointTests.swift index fb6facd..e32e8df 100644 --- a/Tests/FigmaAPITests/FileMetadataEndpointTests.swift +++ b/Tests/FigmaAPITests/FileMetadataEndpointTests.swift @@ -8,7 +8,7 @@ final class FileMetadataEndpointTests: XCTestCase { func testMakeRequestConstructsCorrectURL() throws { let endpoint = FileMetadataEndpoint(fileId: "abc123") // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -21,7 +21,7 @@ final class FileMetadataEndpointTests: XCTestCase { func testMakeRequestIncludesDepthParameter() throws { let endpoint = FileMetadataEndpoint(fileId: "test") // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/Fixtures/ActivityLogsResponse.json b/Tests/FigmaAPITests/Fixtures/ActivityLogsResponse.json new file mode 100644 index 0000000..838597b --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/ActivityLogsResponse.json @@ -0,0 +1,20 @@ +{ + "activity_logs": [ + { + "id": "log1", + "timestamp": "2024-01-15T10:30:00Z", + "actor_id": "user1", + "action_type": "FILE_OPEN", + "entity_type": "FILE", + "details": {"file_key": "abc123"} + }, + { + "id": "log2", + "timestamp": "2024-01-15T11:00:00Z", + "actor_id": null, + "action_type": "FILE_DELETE", + "entity_type": "FILE", + "details": null + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/CommentsResponse.json b/Tests/FigmaAPITests/Fixtures/CommentsResponse.json new file mode 100644 index 0000000..bc44818 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/CommentsResponse.json @@ -0,0 +1,40 @@ +{ + "comments": [ + { + "id": "comment1", + "message": "Great design!", + "created_at": "2024-01-15T10:30:00Z", + "resolved_at": null, + "user": { + "id": "user1", + "handle": "Designer", + "email": "designer@example.com", + "img_url": "https://example.com/avatar1.png" + }, + "order_id": "1", + "parent_id": null, + "client_meta": { + "node_id": "10:1", + "node_offset": { + "x": 100.0, + "y": 200.0 + } + } + }, + { + "id": "comment2", + "message": "I agree!", + "created_at": "2024-01-15T11:00:00Z", + "resolved_at": "2024-01-16T09:00:00Z", + "user": { + "id": "user2", + "handle": "Developer", + "email": null, + "img_url": null + }, + "order_id": "2", + "parent_id": "comment1", + "client_meta": null + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/DevResourcesResponse.json b/Tests/FigmaAPITests/Fixtures/DevResourcesResponse.json new file mode 100644 index 0000000..7dc3d0e --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/DevResourcesResponse.json @@ -0,0 +1,18 @@ +{ + "dev_resources": [ + { + "id": "res1", + "name": "Storybook", + "url": "https://storybook.example.com/button", + "node_id": "10:1", + "dev_status": "ready_for_dev" + }, + { + "id": "res2", + "name": "GitHub PR", + "url": "https://github.com/example/repo/pull/123", + "node_id": "10:2", + "dev_status": null + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/FileComponentSetsResponse.json b/Tests/FigmaAPITests/Fixtures/FileComponentSetsResponse.json new file mode 100644 index 0000000..33af6d9 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/FileComponentSetsResponse.json @@ -0,0 +1,20 @@ +{ + "meta": { + "component_sets": [ + { + "key": "cs1", + "node_id": "20:1", + "name": "Button", + "description": "Buttons", + "containing_frame": { + "node_id": "1:1", + "name": "Buttons", + "page_id": "0:1", + "page_name": "Components", + "background_color": null, + "containing_component_set": null + } + } + ] + } +} diff --git a/Tests/FigmaAPITests/Fixtures/FileVersionsResponse.json b/Tests/FigmaAPITests/Fixtures/FileVersionsResponse.json new file mode 100644 index 0000000..cae7b16 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/FileVersionsResponse.json @@ -0,0 +1,28 @@ +{ + "versions": [ + { + "id": "ver123", + "created_at": "2024-01-15T10:30:00Z", + "label": "v1.0", + "description": "Initial release", + "user": { + "id": "user1", + "handle": "Designer", + "email": "designer@example.com", + "img_url": "https://example.com/avatar1.png" + } + }, + { + "id": "ver456", + "created_at": "2024-02-20T14:00:00Z", + "label": null, + "description": null, + "user": { + "id": "user2", + "handle": "Developer", + "email": null, + "img_url": null + } + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/GetComponentResponse.json b/Tests/FigmaAPITests/Fixtures/GetComponentResponse.json new file mode 100644 index 0000000..71aba9a --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/GetComponentResponse.json @@ -0,0 +1,16 @@ +{ + "meta": { + "key": "comp_key_123", + "node_id": "10:1", + "name": "Button/Primary", + "description": "Primary action button", + "containing_frame": { + "node_id": "1:1", + "name": "Buttons", + "page_id": "0:1", + "page_name": "Components", + "background_color": null, + "containing_component_set": null + } + } +} diff --git a/Tests/FigmaAPITests/Fixtures/GetComponentSetResponse.json b/Tests/FigmaAPITests/Fixtures/GetComponentSetResponse.json new file mode 100644 index 0000000..f6e94de --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/GetComponentSetResponse.json @@ -0,0 +1,16 @@ +{ + "meta": { + "key": "cs_key_456", + "node_id": "20:1", + "name": "Button", + "description": "Button component set with variants", + "containing_frame": { + "node_id": "1:1", + "name": "Buttons", + "page_id": "0:1", + "page_name": "Components", + "background_color": null, + "containing_component_set": null + } + } +} diff --git a/Tests/FigmaAPITests/Fixtures/GetMeResponse.json b/Tests/FigmaAPITests/Fixtures/GetMeResponse.json new file mode 100644 index 0000000..404040b --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/GetMeResponse.json @@ -0,0 +1,6 @@ +{ + "id": "user123", + "handle": "John Doe", + "email": "john@example.com", + "img_url": "https://example.com/avatar.png" +} diff --git a/Tests/FigmaAPITests/Fixtures/GetStyleResponse.json b/Tests/FigmaAPITests/Fixtures/GetStyleResponse.json new file mode 100644 index 0000000..592c0b7 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/GetStyleResponse.json @@ -0,0 +1,8 @@ +{ + "meta": { + "style_type": "FILL", + "node_id": "5:1", + "name": "Colors/Primary", + "description": "Primary brand color" + } +} diff --git a/Tests/FigmaAPITests/Fixtures/ImageFillsResponse.json b/Tests/FigmaAPITests/Fixtures/ImageFillsResponse.json new file mode 100644 index 0000000..a46185b --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/ImageFillsResponse.json @@ -0,0 +1,8 @@ +{ + "meta": { + "images": { + "0:1": "https://s3-alpha.figma.com/img/abc/123/image1.png", + "0:2": "https://s3-alpha.figma.com/img/def/456/image2.png" + } + } +} diff --git a/Tests/FigmaAPITests/Fixtures/LibraryActionsResponse.json b/Tests/FigmaAPITests/Fixtures/LibraryActionsResponse.json new file mode 100644 index 0000000..a5b3dcf --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/LibraryActionsResponse.json @@ -0,0 +1,14 @@ +{ + "actions": [ + { + "component_key": "comp1", + "component_name": "Button", + "style_key": null, + "style_name": null, + "variable_key": null, + "variable_name": null, + "action_type": "INSERT", + "action_count": 42 + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/LibraryUsagesResponse.json b/Tests/FigmaAPITests/Fixtures/LibraryUsagesResponse.json new file mode 100644 index 0000000..35640c1 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/LibraryUsagesResponse.json @@ -0,0 +1,13 @@ +{ + "usages": [ + { + "component_key": "comp1", + "component_name": "Button", + "style_key": null, + "style_name": null, + "variable_key": null, + "variable_name": null, + "usage_count": 150 + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/PaymentsResponse.json b/Tests/FigmaAPITests/Fixtures/PaymentsResponse.json new file mode 100644 index 0000000..bb1f54a --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PaymentsResponse.json @@ -0,0 +1,17 @@ +{ + "status": 200, + "error": false, + "meta": { + "status": 200, + "users": [ + { + "user_id": "user1", + "status": "active" + }, + { + "user_id": "user2", + "status": "inactive" + } + ] + } +} diff --git a/Tests/FigmaAPITests/Fixtures/PostCommentResponse.json b/Tests/FigmaAPITests/Fixtures/PostCommentResponse.json new file mode 100644 index 0000000..8630510 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PostCommentResponse.json @@ -0,0 +1,21 @@ +{ + "id": "comment3", + "message": "New comment", + "created_at": "2024-03-01T12:00:00Z", + "resolved_at": null, + "user": { + "id": "user1", + "handle": "Designer", + "email": "designer@example.com", + "img_url": "https://example.com/avatar1.png" + }, + "order_id": "3", + "parent_id": null, + "client_meta": { + "node_id": "5:1", + "node_offset": { + "x": 50.0, + "y": 75.0 + } + } +} diff --git a/Tests/FigmaAPITests/Fixtures/PostDevResourceResponse.json b/Tests/FigmaAPITests/Fixtures/PostDevResourceResponse.json new file mode 100644 index 0000000..dc5f2de --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PostDevResourceResponse.json @@ -0,0 +1,11 @@ +{ + "links_created": [ + { + "id": "res3", + "name": "New Resource", + "url": "https://example.com/resource", + "node_id": "15:1", + "dev_status": "in_progress" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/PostReactionResponse.json b/Tests/FigmaAPITests/Fixtures/PostReactionResponse.json new file mode 100644 index 0000000..e732d22 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PostReactionResponse.json @@ -0,0 +1,10 @@ +{ + "emoji": "👍", + "user": { + "id": "user1", + "handle": "Designer", + "email": "designer@example.com", + "img_url": "https://example.com/avatar1.png" + }, + "created_at": "2024-01-15T12:00:00Z" +} diff --git a/Tests/FigmaAPITests/Fixtures/ProjectFilesResponse.json b/Tests/FigmaAPITests/Fixtures/ProjectFilesResponse.json new file mode 100644 index 0000000..f792de6 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/ProjectFilesResponse.json @@ -0,0 +1,16 @@ +{ + "files": [ + { + "key": "file1", + "name": "Components", + "thumbnail_url": "https://example.com/thumb1.png", + "last_modified": "2024-01-15T10:30:00Z" + }, + { + "key": "file2", + "name": "Icons", + "thumbnail_url": null, + "last_modified": "2024-02-20T14:00:00Z" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/PublishedVariablesResponse.json b/Tests/FigmaAPITests/Fixtures/PublishedVariablesResponse.json new file mode 100644 index 0000000..74bac0a --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PublishedVariablesResponse.json @@ -0,0 +1,28 @@ +{ + "meta": { + "variableCollections": { + "VariableCollectionId:2:1": { + "defaultModeId": "2:0", + "id": "VariableCollectionId:2:1", + "name": "Published Colors", + "modes": [ + { "modeId": "2:0", "name": "Default" } + ], + "variableIds": [ + "VariableID:2:2" + ] + } + }, + "variables": { + "VariableID:2:2": { + "id": "VariableID:2:2", + "name": "brand/primary", + "variableCollectionId": "VariableCollectionId:2:1", + "valuesByMode": { + "2:0": { "r": 0.0, "g": 0.5, "b": 1.0, "a": 1.0 } + }, + "description": "Primary brand color" + } + } + } +} diff --git a/Tests/FigmaAPITests/Fixtures/PutDevResourceResponse.json b/Tests/FigmaAPITests/Fixtures/PutDevResourceResponse.json new file mode 100644 index 0000000..c929806 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/PutDevResourceResponse.json @@ -0,0 +1,11 @@ +{ + "links_updated": [ + { + "id": "res1", + "name": "Updated Name", + "url": "https://new.example.com", + "node_id": "10:1", + "dev_status": "completed" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/ReactionsResponse.json b/Tests/FigmaAPITests/Fixtures/ReactionsResponse.json new file mode 100644 index 0000000..e100d07 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/ReactionsResponse.json @@ -0,0 +1,24 @@ +{ + "reactions": [ + { + "emoji": "👍", + "user": { + "id": "user1", + "handle": "Designer", + "email": "designer@example.com", + "img_url": "https://example.com/avatar1.png" + }, + "created_at": "2024-01-15T10:30:00Z" + }, + { + "emoji": "🎉", + "user": { + "id": "user2", + "handle": "Developer", + "email": null, + "img_url": null + }, + "created_at": "2024-01-15T11:00:00Z" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/TeamProjectsResponse.json b/Tests/FigmaAPITests/Fixtures/TeamProjectsResponse.json new file mode 100644 index 0000000..1edbbe9 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/TeamProjectsResponse.json @@ -0,0 +1,12 @@ +{ + "projects": [ + { + "id": "proj1", + "name": "Design System" + }, + { + "id": "proj2", + "name": "Marketing" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/WebhookRequestsResponse.json b/Tests/FigmaAPITests/Fixtures/WebhookRequestsResponse.json new file mode 100644 index 0000000..b0a73e9 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/WebhookRequestsResponse.json @@ -0,0 +1,21 @@ +{ + "requests": [ + { + "id": "req1", + "endpoint": "https://example.com/webhook", + "payload": { + "event_type": "FILE_UPDATE", + "timestamp": "2024-01-15T10:30:00Z" + }, + "error": null, + "created_at": "2024-01-15T10:30:01Z" + }, + { + "id": "req2", + "endpoint": "https://example.com/webhook", + "payload": null, + "error": "Connection timeout", + "created_at": "2024-01-15T11:00:00Z" + } + ] +} diff --git a/Tests/FigmaAPITests/Fixtures/WebhookResponse.json b/Tests/FigmaAPITests/Fixtures/WebhookResponse.json new file mode 100644 index 0000000..f0439b8 --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/WebhookResponse.json @@ -0,0 +1,11 @@ +{ + "id": "wh1", + "team_id": "team123", + "event_type": "FILE_UPDATE", + "client_id": null, + "endpoint": "https://example.com/webhook", + "passcode": "secret123", + "status": "ACTIVE", + "description": "File update webhook", + "protocol_version": "2" +} diff --git a/Tests/FigmaAPITests/Fixtures/WebhooksResponse.json b/Tests/FigmaAPITests/Fixtures/WebhooksResponse.json new file mode 100644 index 0000000..abd344f --- /dev/null +++ b/Tests/FigmaAPITests/Fixtures/WebhooksResponse.json @@ -0,0 +1,26 @@ +{ + "webhooks": [ + { + "id": "wh1", + "team_id": "team123", + "event_type": "FILE_UPDATE", + "client_id": null, + "endpoint": "https://example.com/webhook", + "passcode": "secret123", + "status": "ACTIVE", + "description": "File update webhook", + "protocol_version": "2" + }, + { + "id": "wh2", + "team_id": "team123", + "event_type": "FILE_DELETE", + "client_id": "client456", + "endpoint": "https://example.com/webhook2", + "passcode": null, + "status": "PAUSED", + "description": null, + "protocol_version": "1" + } + ] +} diff --git a/Tests/FigmaAPITests/GetActivityLogsEndpointTests.swift b/Tests/FigmaAPITests/GetActivityLogsEndpointTests.swift new file mode 100644 index 0000000..f19da7b --- /dev/null +++ b/Tests/FigmaAPITests/GetActivityLogsEndpointTests.swift @@ -0,0 +1,81 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetActivityLogsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetActivityLogsEndpoint() + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/activity_logs" + ) + } + + func testMakeRequestWithFilters() throws { + let endpoint = GetActivityLogsEndpoint( + events: "FILE_OPEN,FILE_DELETE", + startTime: 1704067200.0, + endTime: 1706745599.0, + limit: 50, + order: "desc" + ) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + let queryItems = try XCTUnwrap(components.queryItems) + XCTAssertEqual(queryItems.first(where: { $0.name == "events" })?.value, "FILE_OPEN,FILE_DELETE") + XCTAssertNotNil(queryItems.first(where: { $0.name == "start_time" })) + XCTAssertNotNil(queryItems.first(where: { $0.name == "end_time" })) + XCTAssertEqual(queryItems.first(where: { $0.name == "limit" })?.value, "50") + XCTAssertEqual(queryItems.first(where: { $0.name == "order" })?.value, "desc") + } + + // MARK: - Response Parsing + + func testContentParsesActivityLogsResponse() throws { + let response: ActivityLogsResponse = try FixtureLoader.load("ActivityLogsResponse") + + let endpoint = GetActivityLogsEndpoint() + let logs = endpoint.content(from: response) + + XCTAssertEqual(logs.count, 2) + + let firstLog = logs[0] + XCTAssertEqual(firstLog.id, "log1") + XCTAssertEqual(firstLog.timestamp, "2024-01-15T10:30:00Z") + XCTAssertEqual(firstLog.actorId, "user1") + XCTAssertEqual(firstLog.actionType, "FILE_OPEN") + XCTAssertEqual(firstLog.entityType, "FILE") + XCTAssertEqual(firstLog.details?["file_key"], "abc123") + + let secondLog = logs[1] + XCTAssertNil(secondLog.actorId) + XCTAssertNil(secondLog.details) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("ActivityLogsResponse") + + let endpoint = GetActivityLogsEndpoint() + let logs = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(logs.count, 2) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetActivityLogsEndpoint() + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetCommentsEndpointTests.swift b/Tests/FigmaAPITests/GetCommentsEndpointTests.swift new file mode 100644 index 0000000..e60cbca --- /dev/null +++ b/Tests/FigmaAPITests/GetCommentsEndpointTests.swift @@ -0,0 +1,55 @@ +@testable import FigmaAPI +import XCTest + +final class GetCommentsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetCommentsEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/comments" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: CommentsResponse = try FixtureLoader.load("CommentsResponse") + + let endpoint = GetCommentsEndpoint(fileId: "test") + let comments = endpoint.content(from: response) + + XCTAssertEqual(comments.count, 2) + } + + func testContentParsesCommentWithClientMeta() throws { + let response: CommentsResponse = try FixtureLoader.load("CommentsResponse") + + let endpoint = GetCommentsEndpoint(fileId: "test") + let comments = endpoint.content(from: response) + + let first = comments[0] + XCTAssertEqual(first.id, "comment1") + XCTAssertEqual(first.message, "Great design!") + XCTAssertEqual(first.clientMeta?.nodeId, "10:1") + XCTAssertEqual(first.clientMeta?.nodeOffset?.x, 100.0) + XCTAssertEqual(first.clientMeta?.nodeOffset?.y, 200.0) + } + + func testContentParsesResolvedComment() throws { + let response: CommentsResponse = try FixtureLoader.load("CommentsResponse") + + let endpoint = GetCommentsEndpoint(fileId: "test") + let comments = endpoint.content(from: response) + + let second = comments[1] + XCTAssertEqual(second.id, "comment2") + XCTAssertEqual(second.resolvedAt, "2024-01-16T09:00:00Z") + XCTAssertEqual(second.parentId, "comment1") + } +} diff --git a/Tests/FigmaAPITests/GetComponentActionsEndpointTests.swift b/Tests/FigmaAPITests/GetComponentActionsEndpointTests.swift new file mode 100644 index 0000000..c110425 --- /dev/null +++ b/Tests/FigmaAPITests/GetComponentActionsEndpointTests.swift @@ -0,0 +1,56 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetComponentActionsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetComponentActionsEndpoint(fileKey: "file123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/analytics/libraries/file123/component/actions" + ) + } + + // MARK: - Response Parsing + + func testContentParsesLibraryActionsResponse() throws { + let response: LibraryActionsResponse = try FixtureLoader.load("LibraryActionsResponse") + + let endpoint = GetComponentActionsEndpoint(fileKey: "test") + let actions = endpoint.content(from: response) + + XCTAssertEqual(actions.count, 1) + + let action = actions[0] + XCTAssertEqual(action.componentKey, "comp1") + XCTAssertEqual(action.componentName, "Button") + XCTAssertNil(action.styleKey) + XCTAssertNil(action.variableKey) + XCTAssertEqual(action.actionType, "INSERT") + XCTAssertEqual(action.actionCount, 42) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("LibraryActionsResponse") + + let endpoint = GetComponentActionsEndpoint(fileKey: "test") + let actions = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(actions.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetComponentActionsEndpoint(fileKey: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetComponentEndpointTests.swift b/Tests/FigmaAPITests/GetComponentEndpointTests.swift new file mode 100644 index 0000000..a2cc382 --- /dev/null +++ b/Tests/FigmaAPITests/GetComponentEndpointTests.swift @@ -0,0 +1,52 @@ +@testable import FigmaAPI +import XCTest + +final class GetComponentEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetComponentEndpoint(key: "comp_key_123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/components/comp_key_123" + ) + } + + // MARK: - Response Parsing + + func testContentParsesGetComponentResponse() throws { + let response: GetComponentResponse = try FixtureLoader.load("GetComponentResponse") + + let endpoint = GetComponentEndpoint(key: "comp_key_123") + let component = endpoint.content(from: response) + + XCTAssertEqual(component.key, "comp_key_123") + XCTAssertEqual(component.nodeId, "10:1") + XCTAssertEqual(component.name, "Button/Primary") + XCTAssertEqual(component.description, "Primary action button") + XCTAssertEqual(component.containingFrame.name, "Buttons") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("GetComponentResponse") + + let endpoint = GetComponentEndpoint(key: "test") + let component = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(component.key, "comp_key_123") + XCTAssertEqual(component.name, "Button/Primary") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetComponentEndpoint(key: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetComponentSetEndpointTests.swift b/Tests/FigmaAPITests/GetComponentSetEndpointTests.swift new file mode 100644 index 0000000..4df890d --- /dev/null +++ b/Tests/FigmaAPITests/GetComponentSetEndpointTests.swift @@ -0,0 +1,52 @@ +@testable import FigmaAPI +import XCTest + +final class GetComponentSetEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetComponentSetEndpoint(key: "cs_key_456") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/component_sets/cs_key_456" + ) + } + + // MARK: - Response Parsing + + func testContentParsesGetComponentSetResponse() throws { + let response: GetComponentSetResponse = try FixtureLoader.load("GetComponentSetResponse") + + let endpoint = GetComponentSetEndpoint(key: "cs_key_456") + let componentSet = endpoint.content(from: response) + + XCTAssertEqual(componentSet.key, "cs_key_456") + XCTAssertEqual(componentSet.nodeId, "20:1") + XCTAssertEqual(componentSet.name, "Button") + XCTAssertEqual(componentSet.description, "Button component set with variants") + XCTAssertEqual(componentSet.containingFrame.name, "Buttons") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("GetComponentSetResponse") + + let endpoint = GetComponentSetEndpoint(key: "test") + let componentSet = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(componentSet.key, "cs_key_456") + XCTAssertEqual(componentSet.name, "Button") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetComponentSetEndpoint(key: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetComponentUsagesEndpointTests.swift b/Tests/FigmaAPITests/GetComponentUsagesEndpointTests.swift new file mode 100644 index 0000000..510d737 --- /dev/null +++ b/Tests/FigmaAPITests/GetComponentUsagesEndpointTests.swift @@ -0,0 +1,55 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetComponentUsagesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetComponentUsagesEndpoint(fileKey: "file123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/analytics/libraries/file123/component/usages" + ) + } + + // MARK: - Response Parsing + + func testContentParsesLibraryUsagesResponse() throws { + let response: LibraryUsagesResponse = try FixtureLoader.load("LibraryUsagesResponse") + + let endpoint = GetComponentUsagesEndpoint(fileKey: "test") + let usages = endpoint.content(from: response) + + XCTAssertEqual(usages.count, 1) + + let usage = usages[0] + XCTAssertEqual(usage.componentKey, "comp1") + XCTAssertEqual(usage.componentName, "Button") + XCTAssertNil(usage.styleKey) + XCTAssertNil(usage.variableKey) + XCTAssertEqual(usage.usageCount, 150) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("LibraryUsagesResponse") + + let endpoint = GetComponentUsagesEndpoint(fileKey: "test") + let usages = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(usages.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetComponentUsagesEndpoint(fileKey: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetDevResourcesEndpointTests.swift b/Tests/FigmaAPITests/GetDevResourcesEndpointTests.swift new file mode 100644 index 0000000..e806767 --- /dev/null +++ b/Tests/FigmaAPITests/GetDevResourcesEndpointTests.swift @@ -0,0 +1,54 @@ +@testable import FigmaAPI +import XCTest + +final class GetDevResourcesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetDevResourcesEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/dev_resources" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: DevResourcesResponse = try FixtureLoader.load("DevResourcesResponse") + + let endpoint = GetDevResourcesEndpoint(fileId: "test") + let resources = endpoint.content(from: response) + + XCTAssertEqual(resources.count, 2) + } + + func testContentParsesFirstResource() throws { + let response: DevResourcesResponse = try FixtureLoader.load("DevResourcesResponse") + + let endpoint = GetDevResourcesEndpoint(fileId: "test") + let resources = endpoint.content(from: response) + + let first = resources[0] + XCTAssertEqual(first.id, "res1") + XCTAssertEqual(first.name, "Storybook") + XCTAssertEqual(first.url, "https://storybook.example.com/button") + XCTAssertEqual(first.nodeId, "10:1") + XCTAssertEqual(first.devStatus, "ready_for_dev") + } + + func testContentParsesResourceWithNullDevStatus() throws { + let response: DevResourcesResponse = try FixtureLoader.load("DevResourcesResponse") + + let endpoint = GetDevResourcesEndpoint(fileId: "test") + let resources = endpoint.content(from: response) + + let second = resources[1] + XCTAssertEqual(second.id, "res2") + XCTAssertNil(second.devStatus) + } +} diff --git a/Tests/FigmaAPITests/GetFileComponentSetsEndpointTests.swift b/Tests/FigmaAPITests/GetFileComponentSetsEndpointTests.swift new file mode 100644 index 0000000..ed7c8ff --- /dev/null +++ b/Tests/FigmaAPITests/GetFileComponentSetsEndpointTests.swift @@ -0,0 +1,51 @@ +@testable import FigmaAPI +import XCTest + +final class GetFileComponentSetsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetFileComponentSetsEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/component_sets" + ) + } + + // MARK: - Response Parsing + + func testContentParsesFileComponentSetsResponse() throws { + let response: FileComponentSetsResponse = try FixtureLoader.load("FileComponentSetsResponse") + + let endpoint = GetFileComponentSetsEndpoint(fileId: "test") + let componentSets = endpoint.content(from: response) + + XCTAssertEqual(componentSets.count, 1) + XCTAssertEqual(componentSets[0].key, "cs1") + XCTAssertEqual(componentSets[0].name, "Button") + XCTAssertEqual(componentSets[0].description, "Buttons") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("FileComponentSetsResponse") + + let endpoint = GetFileComponentSetsEndpoint(fileId: "test") + let componentSets = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(componentSets.count, 1) + XCTAssertEqual(componentSets[0].key, "cs1") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetFileComponentSetsEndpoint(fileId: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetFileMetaEndpointTests.swift b/Tests/FigmaAPITests/GetFileMetaEndpointTests.swift new file mode 100644 index 0000000..024730e --- /dev/null +++ b/Tests/FigmaAPITests/GetFileMetaEndpointTests.swift @@ -0,0 +1,62 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +final class GetFileMetaEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetFileMetaEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123" + ) + } + + func testMakeRequestUsesHEADMethod() throws { + let endpoint = GetFileMetaEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "HEAD") + } + + // MARK: - Response Parsing + + #if !canImport(FoundationNetworking) + func testContentExtractsHeaderValues() throws { + let endpoint = GetFileMetaEndpoint(fileId: "abc123") + let url = try XCTUnwrap(URL(string: "https://api.figma.com/v1/files/abc123")) + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: [ + "Last-Modified": "Wed, 15 Jan 2024 10:30:00 GMT", + "X-Figma-Version": "123456789", + ] + ) + + let meta = try endpoint.content(from: response, with: Data()) + + XCTAssertEqual(meta.lastModified, "Wed, 15 Jan 2024 10:30:00 GMT") + XCTAssertEqual(meta.version, "123456789") + } + + func testContentThrowsOnNonHTTPResponse() { + let endpoint = GetFileMetaEndpoint(fileId: "abc123") + let response = URLResponse() + + XCTAssertThrowsError(try endpoint.content(from: response, with: Data())) + } + #endif +} diff --git a/Tests/FigmaAPITests/GetFileVersionsEndpointTests.swift b/Tests/FigmaAPITests/GetFileVersionsEndpointTests.swift new file mode 100644 index 0000000..7e83307 --- /dev/null +++ b/Tests/FigmaAPITests/GetFileVersionsEndpointTests.swift @@ -0,0 +1,51 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetFileVersionsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetFileVersionsEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/versions" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("FileVersionsResponse") + + let endpoint = GetFileVersionsEndpoint(fileId: "test") + let versions = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(versions.count, 2) + + let first = versions[0] + XCTAssertEqual(first.id, "ver123") + XCTAssertEqual(first.createdAt, "2024-01-15T10:30:00Z") + XCTAssertEqual(first.label, "v1.0") + XCTAssertEqual(first.description, "Initial release") + XCTAssertEqual(first.user.handle, "Designer") + } + + func testContentParsesVersionWithNullFields() throws { + let data = try FixtureLoader.loadData("FileVersionsResponse") + + let endpoint = GetFileVersionsEndpoint(fileId: "test") + let versions = try endpoint.content(from: nil, with: data) + + let second = versions[1] + XCTAssertEqual(second.id, "ver456") + XCTAssertNil(second.label) + XCTAssertNil(second.description) + XCTAssertNil(second.user.email) + XCTAssertNil(second.user.imgUrl) + } +} diff --git a/Tests/FigmaAPITests/GetImageFillsEndpointTests.swift b/Tests/FigmaAPITests/GetImageFillsEndpointTests.swift new file mode 100644 index 0000000..e455e55 --- /dev/null +++ b/Tests/FigmaAPITests/GetImageFillsEndpointTests.swift @@ -0,0 +1,32 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetImageFillsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetImageFillsEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/images" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("ImageFillsResponse") + + let endpoint = GetImageFillsEndpoint(fileId: "test") + let images = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(images.count, 2) + XCTAssertEqual(images["0:1"], "https://s3-alpha.figma.com/img/abc/123/image1.png") + XCTAssertEqual(images["0:2"], "https://s3-alpha.figma.com/img/def/456/image2.png") + } +} diff --git a/Tests/FigmaAPITests/GetMeEndpointTests.swift b/Tests/FigmaAPITests/GetMeEndpointTests.swift new file mode 100644 index 0000000..9613828 --- /dev/null +++ b/Tests/FigmaAPITests/GetMeEndpointTests.swift @@ -0,0 +1,33 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetMeEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetMeEndpoint() + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/me" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("GetMeResponse") + + let endpoint = GetMeEndpoint() + let user = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(user.id, "user123") + XCTAssertEqual(user.handle, "John Doe") + XCTAssertEqual(user.email, "john@example.com") + XCTAssertEqual(user.imgUrl, "https://example.com/avatar.png") + } +} diff --git a/Tests/FigmaAPITests/GetPaymentsEndpointTests.swift b/Tests/FigmaAPITests/GetPaymentsEndpointTests.swift new file mode 100644 index 0000000..7f57c8a --- /dev/null +++ b/Tests/FigmaAPITests/GetPaymentsEndpointTests.swift @@ -0,0 +1,64 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetPaymentsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetPaymentsEndpoint(pluginId: "plugin123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + XCTAssertEqual(components.path, "/v1/payments") + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "plugin_id" })?.value, "plugin123") + } + + func testMakeRequestWithTokenAuth() throws { + let endpoint = GetPaymentsEndpoint(pluginPaymentToken: "token123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "plugin_payment_token" })?.value, "token123") + } + + func testMakeRequestWithUserAndWidget() throws { + let endpoint = GetPaymentsEndpoint(userId: "user1", widgetId: "widget1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "user_id" })?.value, "user1") + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "widget_id" })?.value, "widget1") + } + + // MARK: - Response Parsing + + func testContentParsesPaymentsResponse() throws { + let data = try FixtureLoader.loadData("PaymentsResponse") + + let endpoint = GetPaymentsEndpoint(pluginId: "plugin123") + let paymentInfo = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(paymentInfo.status, 200) + XCTAssertEqual(paymentInfo.users?.count, 2) + XCTAssertEqual(paymentInfo.users?[0].userId, "user1") + XCTAssertEqual(paymentInfo.users?[0].status, "active") + XCTAssertEqual(paymentInfo.users?[1].userId, "user2") + XCTAssertEqual(paymentInfo.users?[1].status, "inactive") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetPaymentsEndpoint(pluginId: "plugin123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetProjectFilesEndpointTests.swift b/Tests/FigmaAPITests/GetProjectFilesEndpointTests.swift new file mode 100644 index 0000000..d604884 --- /dev/null +++ b/Tests/FigmaAPITests/GetProjectFilesEndpointTests.swift @@ -0,0 +1,54 @@ +@testable import FigmaAPI +import XCTest + +final class GetProjectFilesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetProjectFilesEndpoint(projectId: "proj123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/projects/proj123/files" + ) + } + + // MARK: - Response Parsing + + func testContentParsesProjectFilesResponse() throws { + let response: ProjectFilesResponse = try FixtureLoader.load("ProjectFilesResponse") + + let endpoint = GetProjectFilesEndpoint(projectId: "proj123") + let files = endpoint.content(from: response) + + XCTAssertEqual(files.count, 2) + XCTAssertEqual(files[0].key, "file1") + XCTAssertEqual(files[0].name, "Components") + XCTAssertEqual(files[0].thumbnailUrl, "https://example.com/thumb1.png") + XCTAssertEqual(files[0].lastModified, "2024-01-15T10:30:00Z") + XCTAssertEqual(files[1].key, "file2") + XCTAssertNil(files[1].thumbnailUrl) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("ProjectFilesResponse") + + let endpoint = GetProjectFilesEndpoint(projectId: "proj123") + let files = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(files.count, 2) + XCTAssertEqual(files[0].key, "file1") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetProjectFilesEndpoint(projectId: "proj123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetPublishedVariablesEndpointTests.swift b/Tests/FigmaAPITests/GetPublishedVariablesEndpointTests.swift new file mode 100644 index 0000000..0546e4b --- /dev/null +++ b/Tests/FigmaAPITests/GetPublishedVariablesEndpointTests.swift @@ -0,0 +1,38 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetPublishedVariablesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetPublishedVariablesEndpoint(fileId: "abc123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/variables/published" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("PublishedVariablesResponse") + + let endpoint = GetPublishedVariablesEndpoint(fileId: "test") + let meta = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(meta.variableCollections.count, 1) + XCTAssertEqual(meta.variables.count, 1) + + let collection = meta.variableCollections["VariableCollectionId:2:1"] + XCTAssertEqual(collection?.name, "Published Colors") + + let variable = meta.variables["VariableID:2:2"] + XCTAssertEqual(variable?.name, "brand/primary") + XCTAssertEqual(variable?.description, "Primary brand color") + } +} diff --git a/Tests/FigmaAPITests/GetReactionsEndpointTests.swift b/Tests/FigmaAPITests/GetReactionsEndpointTests.swift new file mode 100644 index 0000000..73e746f --- /dev/null +++ b/Tests/FigmaAPITests/GetReactionsEndpointTests.swift @@ -0,0 +1,54 @@ +@testable import FigmaAPI +import XCTest + +final class GetReactionsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetReactionsEndpoint(fileId: "abc123", commentId: "comment1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/comments/comment1/reactions" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: ReactionsResponse = try FixtureLoader.load("ReactionsResponse") + + let endpoint = GetReactionsEndpoint(fileId: "test", commentId: "c1") + let reactions = endpoint.content(from: response) + + XCTAssertEqual(reactions.count, 2) + } + + func testContentParsesFirstReaction() throws { + let response: ReactionsResponse = try FixtureLoader.load("ReactionsResponse") + + let endpoint = GetReactionsEndpoint(fileId: "test", commentId: "c1") + let reactions = endpoint.content(from: response) + + let first = reactions[0] + XCTAssertEqual(first.emoji, "\u{1F44D}") + XCTAssertEqual(first.user.id, "user1") + XCTAssertEqual(first.user.handle, "Designer") + XCTAssertEqual(first.createdAt, "2024-01-15T10:30:00Z") + } + + func testContentParsesReactionWithNullUserFields() throws { + let response: ReactionsResponse = try FixtureLoader.load("ReactionsResponse") + + let endpoint = GetReactionsEndpoint(fileId: "test", commentId: "c1") + let reactions = endpoint.content(from: response) + + let second = reactions[1] + XCTAssertEqual(second.emoji, "\u{1F389}") + XCTAssertNil(second.user.email) + XCTAssertNil(second.user.imgUrl) + } +} diff --git a/Tests/FigmaAPITests/GetStyleActionsEndpointTests.swift b/Tests/FigmaAPITests/GetStyleActionsEndpointTests.swift new file mode 100644 index 0000000..7e95fee --- /dev/null +++ b/Tests/FigmaAPITests/GetStyleActionsEndpointTests.swift @@ -0,0 +1,50 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetStyleActionsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetStyleActionsEndpoint(fileKey: "file123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/analytics/libraries/file123/style/actions" + ) + } + + // MARK: - Response Parsing + + func testContentParsesLibraryActionsResponse() throws { + let response: LibraryActionsResponse = try FixtureLoader.load("LibraryActionsResponse") + + let endpoint = GetStyleActionsEndpoint(fileKey: "test") + let actions = endpoint.content(from: response) + + XCTAssertEqual(actions.count, 1) + XCTAssertEqual(actions[0].actionType, "INSERT") + XCTAssertEqual(actions[0].actionCount, 42) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("LibraryActionsResponse") + + let endpoint = GetStyleActionsEndpoint(fileKey: "test") + let actions = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(actions.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetStyleActionsEndpoint(fileKey: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetStyleEndpointTests.swift b/Tests/FigmaAPITests/GetStyleEndpointTests.swift new file mode 100644 index 0000000..b418a50 --- /dev/null +++ b/Tests/FigmaAPITests/GetStyleEndpointTests.swift @@ -0,0 +1,51 @@ +@testable import FigmaAPI +import XCTest + +final class GetStyleEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetStyleEndpoint(key: "style_key_789") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/styles/style_key_789" + ) + } + + // MARK: - Response Parsing + + func testContentParsesGetStyleResponse() throws { + let response: GetStyleResponse = try FixtureLoader.load("GetStyleResponse") + + let endpoint = GetStyleEndpoint(key: "test") + let style = endpoint.content(from: response) + + XCTAssertEqual(style.nodeId, "5:1") + XCTAssertEqual(style.name, "Colors/Primary") + XCTAssertEqual(style.styleType, .fill) + XCTAssertEqual(style.description, "Primary brand color") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("GetStyleResponse") + + let endpoint = GetStyleEndpoint(key: "test") + let style = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(style.name, "Colors/Primary") + XCTAssertEqual(style.styleType, .fill) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetStyleEndpoint(key: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetStyleUsagesEndpointTests.swift b/Tests/FigmaAPITests/GetStyleUsagesEndpointTests.swift new file mode 100644 index 0000000..4f2d682 --- /dev/null +++ b/Tests/FigmaAPITests/GetStyleUsagesEndpointTests.swift @@ -0,0 +1,49 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetStyleUsagesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetStyleUsagesEndpoint(fileKey: "file123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/analytics/libraries/file123/style/usages" + ) + } + + // MARK: - Response Parsing + + func testContentParsesLibraryUsagesResponse() throws { + let response: LibraryUsagesResponse = try FixtureLoader.load("LibraryUsagesResponse") + + let endpoint = GetStyleUsagesEndpoint(fileKey: "test") + let usages = endpoint.content(from: response) + + XCTAssertEqual(usages.count, 1) + XCTAssertEqual(usages[0].usageCount, 150) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("LibraryUsagesResponse") + + let endpoint = GetStyleUsagesEndpoint(fileKey: "test") + let usages = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(usages.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetStyleUsagesEndpoint(fileKey: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetTeamComponentSetsEndpointTests.swift b/Tests/FigmaAPITests/GetTeamComponentSetsEndpointTests.swift new file mode 100644 index 0000000..6adaa5b --- /dev/null +++ b/Tests/FigmaAPITests/GetTeamComponentSetsEndpointTests.swift @@ -0,0 +1,70 @@ +@testable import FigmaAPI +import XCTest + +final class GetTeamComponentSetsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/teams/team123/component_sets" + ) + } + + func testMakeRequestWithPagination() throws { + let pagination = PaginationParams(pageSize: 25, cursor: "next_page") + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123", pagination: pagination) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + let url = request.url?.absoluteString ?? "" + + XCTAssertTrue(url.contains("teams/team123/component_sets")) + XCTAssertTrue(url.contains("page_size=25")) + XCTAssertTrue(url.contains("cursor=next_page")) + } + + func testMakeRequestWithoutPaginationHasNoQueryItems() throws { + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertNil(request.url?.query) + } + + // MARK: - Response Parsing + + func testContentParsesFileComponentSetsResponse() throws { + let response: FileComponentSetsResponse = try FixtureLoader.load("FileComponentSetsResponse") + + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123") + let componentSets = endpoint.content(from: response) + + XCTAssertEqual(componentSets.count, 1) + XCTAssertEqual(componentSets[0].key, "cs1") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("FileComponentSetsResponse") + + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123") + let componentSets = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(componentSets.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetTeamComponentSetsEndpoint(teamId: "team123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetTeamComponentsEndpointTests.swift b/Tests/FigmaAPITests/GetTeamComponentsEndpointTests.swift new file mode 100644 index 0000000..b837e7c --- /dev/null +++ b/Tests/FigmaAPITests/GetTeamComponentsEndpointTests.swift @@ -0,0 +1,70 @@ +@testable import FigmaAPI +import XCTest + +final class GetTeamComponentsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetTeamComponentsEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/teams/team123/components" + ) + } + + func testMakeRequestWithPagination() throws { + let pagination = PaginationParams(pageSize: 10, cursor: "abc") + let endpoint = GetTeamComponentsEndpoint(teamId: "team123", pagination: pagination) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + let url = request.url?.absoluteString ?? "" + + XCTAssertTrue(url.contains("teams/team123/components")) + XCTAssertTrue(url.contains("page_size=10")) + XCTAssertTrue(url.contains("cursor=abc")) + } + + func testMakeRequestWithoutPaginationHasNoQueryItems() throws { + let endpoint = GetTeamComponentsEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertNil(request.url?.query) + } + + // MARK: - Response Parsing + + func testContentParsesComponentsResponse() throws { + let response: ComponentsResponse = try FixtureLoader.load("ComponentsResponse") + + let endpoint = GetTeamComponentsEndpoint(teamId: "team123") + let components = endpoint.content(from: response) + + XCTAssertEqual(components.count, 4) + XCTAssertEqual(components[0].name, "Icons/24/arrow_right") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("ComponentsResponse") + + let endpoint = GetTeamComponentsEndpoint(teamId: "team123") + let components = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(components.count, 4) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetTeamComponentsEndpoint(teamId: "team123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetTeamProjectsEndpointTests.swift b/Tests/FigmaAPITests/GetTeamProjectsEndpointTests.swift new file mode 100644 index 0000000..fd4559c --- /dev/null +++ b/Tests/FigmaAPITests/GetTeamProjectsEndpointTests.swift @@ -0,0 +1,52 @@ +@testable import FigmaAPI +import XCTest + +final class GetTeamProjectsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetTeamProjectsEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/teams/team123/projects" + ) + } + + // MARK: - Response Parsing + + func testContentParsesTeamProjectsResponse() throws { + let response: TeamProjectsResponse = try FixtureLoader.load("TeamProjectsResponse") + + let endpoint = GetTeamProjectsEndpoint(teamId: "team123") + let projects = endpoint.content(from: response) + + XCTAssertEqual(projects.count, 2) + XCTAssertEqual(projects[0].id, "proj1") + XCTAssertEqual(projects[0].name, "Design System") + XCTAssertEqual(projects[1].id, "proj2") + XCTAssertEqual(projects[1].name, "Marketing") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("TeamProjectsResponse") + + let endpoint = GetTeamProjectsEndpoint(teamId: "team123") + let projects = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(projects.count, 2) + XCTAssertEqual(projects[0].id, "proj1") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetTeamProjectsEndpoint(teamId: "team123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetTeamStylesEndpointTests.swift b/Tests/FigmaAPITests/GetTeamStylesEndpointTests.swift new file mode 100644 index 0000000..9d8e9ed --- /dev/null +++ b/Tests/FigmaAPITests/GetTeamStylesEndpointTests.swift @@ -0,0 +1,70 @@ +@testable import FigmaAPI +import XCTest + +final class GetTeamStylesEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetTeamStylesEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/teams/team123/styles" + ) + } + + func testMakeRequestWithPagination() throws { + let pagination = PaginationParams(pageSize: 50, cursor: "cursor_token") + let endpoint = GetTeamStylesEndpoint(teamId: "team123", pagination: pagination) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + let url = request.url?.absoluteString ?? "" + + XCTAssertTrue(url.contains("teams/team123/styles")) + XCTAssertTrue(url.contains("page_size=50")) + XCTAssertTrue(url.contains("cursor=cursor_token")) + } + + func testMakeRequestWithoutPaginationHasNoQueryItems() throws { + let endpoint = GetTeamStylesEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertNil(request.url?.query) + } + + // MARK: - Response Parsing + + func testContentParsesStylesResponse() throws { + let response: StylesResponse = try FixtureLoader.load("StylesResponse") + + let endpoint = GetTeamStylesEndpoint(teamId: "team123") + let styles = endpoint.content(from: response) + + XCTAssertEqual(styles.count, 5) + XCTAssertEqual(styles[0].name, "primary/background") + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("StylesResponse") + + let endpoint = GetTeamStylesEndpoint(teamId: "team123") + let styles = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(styles.count, 5) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetTeamStylesEndpoint(teamId: "team123") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetTeamWebhooksEndpointTests.swift b/Tests/FigmaAPITests/GetTeamWebhooksEndpointTests.swift new file mode 100644 index 0000000..33b56d3 --- /dev/null +++ b/Tests/FigmaAPITests/GetTeamWebhooksEndpointTests.swift @@ -0,0 +1,31 @@ +@testable import FigmaAPI +import XCTest + +final class GetTeamWebhooksEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetTeamWebhooksEndpoint(teamId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/teams/team123/webhooks" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: WebhooksResponse = try FixtureLoader.load("WebhooksResponse") + + let endpoint = GetTeamWebhooksEndpoint(teamId: "team123") + let webhooks = endpoint.content(from: response) + + XCTAssertEqual(webhooks.count, 2) + XCTAssertEqual(webhooks[0].id, "wh1") + XCTAssertEqual(webhooks[1].id, "wh2") + } +} diff --git a/Tests/FigmaAPITests/GetVariableActionsEndpointTests.swift b/Tests/FigmaAPITests/GetVariableActionsEndpointTests.swift new file mode 100644 index 0000000..3ed9da7 --- /dev/null +++ b/Tests/FigmaAPITests/GetVariableActionsEndpointTests.swift @@ -0,0 +1,50 @@ +import CustomDump +@testable import FigmaAPI +import XCTest + +final class GetVariableActionsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetVariableActionsEndpoint(fileKey: "file123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/analytics/libraries/file123/variable/actions" + ) + } + + // MARK: - Response Parsing + + func testContentParsesLibraryActionsResponse() throws { + let response: LibraryActionsResponse = try FixtureLoader.load("LibraryActionsResponse") + + let endpoint = GetVariableActionsEndpoint(fileKey: "test") + let actions = endpoint.content(from: response) + + XCTAssertEqual(actions.count, 1) + XCTAssertEqual(actions[0].actionType, "INSERT") + XCTAssertEqual(actions[0].actionCount, 42) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("LibraryActionsResponse") + + let endpoint = GetVariableActionsEndpoint(fileKey: "test") + let actions = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(actions.count, 1) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetVariableActionsEndpoint(fileKey: "test") + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/GetWebhookEndpointTests.swift b/Tests/FigmaAPITests/GetWebhookEndpointTests.swift new file mode 100644 index 0000000..530fc26 --- /dev/null +++ b/Tests/FigmaAPITests/GetWebhookEndpointTests.swift @@ -0,0 +1,37 @@ +@testable import FigmaAPI +import XCTest + +final class GetWebhookEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetWebhookEndpoint(webhookId: "wh1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks/wh1" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("WebhookResponse") + + let endpoint = GetWebhookEndpoint(webhookId: "wh1") + let webhook = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(webhook.id, "wh1") + XCTAssertEqual(webhook.teamId, "team123") + XCTAssertEqual(webhook.eventType, "FILE_UPDATE") + XCTAssertNil(webhook.clientId) + XCTAssertEqual(webhook.endpoint, "https://example.com/webhook") + XCTAssertEqual(webhook.passcode, "secret123") + XCTAssertEqual(webhook.status, "ACTIVE") + XCTAssertEqual(webhook.description, "File update webhook") + XCTAssertEqual(webhook.protocolVersion, "2") + } +} diff --git a/Tests/FigmaAPITests/GetWebhookRequestsEndpointTests.swift b/Tests/FigmaAPITests/GetWebhookRequestsEndpointTests.swift new file mode 100644 index 0000000..3b3c629 --- /dev/null +++ b/Tests/FigmaAPITests/GetWebhookRequestsEndpointTests.swift @@ -0,0 +1,56 @@ +@testable import FigmaAPI +import XCTest + +final class GetWebhookRequestsEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetWebhookRequestsEndpoint(webhookId: "wh1") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks/wh1/requests" + ) + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: WebhookRequestsResponse = try FixtureLoader.load("WebhookRequestsResponse") + + let endpoint = GetWebhookRequestsEndpoint(webhookId: "wh1") + let requests = endpoint.content(from: response) + + XCTAssertEqual(requests.count, 2) + } + + func testContentParsesRequestWithPayload() throws { + let response: WebhookRequestsResponse = try FixtureLoader.load("WebhookRequestsResponse") + + let endpoint = GetWebhookRequestsEndpoint(webhookId: "wh1") + let requests = endpoint.content(from: response) + + let first = requests[0] + XCTAssertEqual(first.id, "req1") + XCTAssertEqual(first.endpoint, "https://example.com/webhook") + XCTAssertEqual(first.payload?.eventType, "FILE_UPDATE") + XCTAssertEqual(first.payload?.timestamp, "2024-01-15T10:30:00Z") + XCTAssertNil(first.error) + XCTAssertEqual(first.createdAt, "2024-01-15T10:30:01Z") + } + + func testContentParsesRequestWithError() throws { + let response: WebhookRequestsResponse = try FixtureLoader.load("WebhookRequestsResponse") + + let endpoint = GetWebhookRequestsEndpoint(webhookId: "wh1") + let requests = endpoint.content(from: response) + + let second = requests[1] + XCTAssertEqual(second.id, "req2") + XCTAssertNil(second.payload) + XCTAssertEqual(second.error, "Connection timeout") + } +} diff --git a/Tests/FigmaAPITests/GetWebhooksEndpointTests.swift b/Tests/FigmaAPITests/GetWebhooksEndpointTests.swift new file mode 100644 index 0000000..d89e7b3 --- /dev/null +++ b/Tests/FigmaAPITests/GetWebhooksEndpointTests.swift @@ -0,0 +1,88 @@ +@testable import FigmaAPI +import XCTest + +final class GetWebhooksEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let endpoint = GetWebhooksEndpoint() + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks" + ) + } + + func testMakeRequestWithContextFilter() throws { + let endpoint = GetWebhooksEndpoint(context: "team", contextId: "team123") + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let components = try XCTUnwrap(URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)) + XCTAssertEqual(components.path, "/v2/webhooks") + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "context" })?.value, "team") + XCTAssertEqual(components.queryItems?.first(where: { $0.name == "context_id" })?.value, "team123") + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let response: WebhooksResponse = try FixtureLoader.load("WebhooksResponse") + + let endpoint = GetWebhooksEndpoint() + let webhooks = endpoint.content(from: response) + + XCTAssertEqual(webhooks.count, 2) + } + + func testContentParsesFirstWebhook() throws { + let response: WebhooksResponse = try FixtureLoader.load("WebhooksResponse") + + let endpoint = GetWebhooksEndpoint() + let webhooks = endpoint.content(from: response) + + let first = webhooks[0] + XCTAssertEqual(first.id, "wh1") + XCTAssertEqual(first.teamId, "team123") + XCTAssertEqual(first.eventType, "FILE_UPDATE") + XCTAssertNil(first.clientId) + XCTAssertEqual(first.status, "ACTIVE") + } + + func testContentParsesSecondWebhook() throws { + let response: WebhooksResponse = try FixtureLoader.load("WebhooksResponse") + + let endpoint = GetWebhooksEndpoint() + let webhooks = endpoint.content(from: response) + + let second = webhooks[1] + XCTAssertEqual(second.id, "wh2") + XCTAssertEqual(second.eventType, "FILE_DELETE") + XCTAssertEqual(second.clientId, "client456") + XCTAssertNil(second.passcode) + XCTAssertEqual(second.status, "PAUSED") + XCTAssertNil(second.description) + } + + func testContentFromResponseWithBody() throws { + let data = try FixtureLoader.loadData("WebhooksResponse") + + let endpoint = GetWebhooksEndpoint() + let webhooks = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(webhooks.count, 2) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let endpoint = GetWebhooksEndpoint() + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/ImageEndpointTests.swift b/Tests/FigmaAPITests/ImageEndpointTests.swift index 569f603..0a9a598 100644 --- a/Tests/FigmaAPITests/ImageEndpointTests.swift +++ b/Tests/FigmaAPITests/ImageEndpointTests.swift @@ -8,7 +8,7 @@ final class ImageEndpointTests: XCTestCase { func testMakeRequestWithPNGParams() throws { let params = PNGParams(scale: 2.0) let endpoint = ImageEndpoint(fileId: "abc123", nodeIds: ["1:2", "1:3"], params: params) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) let url = request.url?.absoluteString ?? "" @@ -25,7 +25,7 @@ final class ImageEndpointTests: XCTestCase { params.svgSimplifyStroke = true let endpoint = ImageEndpoint(fileId: "file123", nodeIds: ["10:1"], params: params) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) let url = request.url?.absoluteString ?? "" @@ -38,7 +38,7 @@ final class ImageEndpointTests: XCTestCase { func testMakeRequestWithPDFParams() throws { let params = PDFParams() let endpoint = ImageEndpoint(fileId: "file123", nodeIds: ["5:1"], params: params) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) let url = request.url?.absoluteString ?? "" @@ -50,7 +50,7 @@ final class ImageEndpointTests: XCTestCase { func testMakeRequestIncludesUseAbsoluteBounds() throws { let params = PNGParams(scale: 1.0) let endpoint = ImageEndpoint(fileId: "file123", nodeIds: ["1:1"], params: params) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) let url = request.url?.absoluteString ?? "" diff --git a/Tests/FigmaAPITests/Mocks/MockClient.swift b/Tests/FigmaAPITests/Mocks/MockClient.swift index cde71f8..fb4d37f 100644 --- a/Tests/FigmaAPITests/Mocks/MockClient.swift +++ b/Tests/FigmaAPITests/Mocks/MockClient.swift @@ -57,8 +57,7 @@ public final class MockClient: Client, @unchecked Sendable { public func request(_ endpoint: T) async throws -> T.Content { let key = String(describing: type(of: endpoint)) - // swiftlint:disable:next force_unwrapping - let baseURL = URL(string: "https://api.figma.com/v1/")! + let baseURL = FigmaClient.baseURL let request = try endpoint.makeRequest(baseURL: baseURL) // Record timestamp and get delay (thread-safe) diff --git a/Tests/FigmaAPITests/NodesEndpointTests.swift b/Tests/FigmaAPITests/NodesEndpointTests.swift index 1aa3c42..8faa9e8 100644 --- a/Tests/FigmaAPITests/NodesEndpointTests.swift +++ b/Tests/FigmaAPITests/NodesEndpointTests.swift @@ -7,7 +7,7 @@ final class NodesEndpointTests: XCTestCase { func testMakeRequestConstructsCorrectURL() throws { let endpoint = NodesEndpoint(fileId: "abc123", nodeIds: ["1:2", "1:3"]) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -17,7 +17,7 @@ final class NodesEndpointTests: XCTestCase { func testMakeRequestWithSingleNodeId() throws { let endpoint = NodesEndpoint(fileId: "file123", nodeIds: ["10:5"]) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -27,7 +27,7 @@ final class NodesEndpointTests: XCTestCase { func testMakeRequestWithManyNodeIds() throws { let nodeIds = (1 ... 100).map { "1:\($0)" } let endpoint = NodesEndpoint(fileId: "file123", nodeIds: nodeIds) - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/PostCommentEndpointTests.swift b/Tests/FigmaAPITests/PostCommentEndpointTests.swift new file mode 100644 index 0000000..46976a8 --- /dev/null +++ b/Tests/FigmaAPITests/PostCommentEndpointTests.swift @@ -0,0 +1,66 @@ +@testable import FigmaAPI +import XCTest + +final class PostCommentEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PostCommentBody(message: "Hello") + let endpoint = PostCommentEndpoint(fileId: "abc123", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/comments" + ) + } + + func testMakeRequestUsesPOSTMethod() throws { + let body = PostCommentBody(message: "Hello") + let endpoint = PostCommentEndpoint(fileId: "test", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "POST") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PostCommentBody(message: "Hello") + let endpoint = PostCommentEndpoint(fileId: "test", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestIncludesBody() throws { + let body = PostCommentBody(message: "Test comment", commentId: "parent1") + let endpoint = PostCommentEndpoint(fileId: "test", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + XCTAssertEqual(json["message"] as? String, "Test comment") + XCTAssertEqual(json["comment_id"] as? String, "parent1") + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("PostCommentResponse") + + let body = PostCommentBody(message: "New comment") + let endpoint = PostCommentEndpoint(fileId: "test", body: body) + let comment = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(comment.id, "comment3") + XCTAssertEqual(comment.message, "New comment") + XCTAssertEqual(comment.clientMeta?.nodeId, "5:1") + } +} diff --git a/Tests/FigmaAPITests/PostDevResourceEndpointTests.swift b/Tests/FigmaAPITests/PostDevResourceEndpointTests.swift new file mode 100644 index 0000000..706660f --- /dev/null +++ b/Tests/FigmaAPITests/PostDevResourceEndpointTests.swift @@ -0,0 +1,82 @@ +@testable import FigmaAPI +import XCTest + +final class PostDevResourceEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PostDevResourceBody(name: "Test", url: "https://example.com", fileKey: "abc123", nodeId: "10:1") + let endpoint = PostDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/dev_resources" + ) + } + + func testMakeRequestUsesPOSTMethod() throws { + let body = PostDevResourceBody(name: "Test", url: "https://example.com", fileKey: "abc123", nodeId: "10:1") + let endpoint = PostDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "POST") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PostDevResourceBody(name: "Test", url: "https://example.com", fileKey: "abc123", nodeId: "10:1") + let endpoint = PostDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestWrapsBodyInDevResourcesArray() throws { + let body = PostDevResourceBody(name: "Storybook", url: "https://storybook.example.com", fileKey: "file1", nodeId: "10:1", devStatus: "ready_for_dev") + let endpoint = PostDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + let devResources = try XCTUnwrap(json["dev_resources"] as? [[String: Any]]) + XCTAssertEqual(devResources.count, 1) + XCTAssertEqual(devResources[0]["name"] as? String, "Storybook") + XCTAssertEqual(devResources[0]["url"] as? String, "https://storybook.example.com") + XCTAssertEqual(devResources[0]["file_key"] as? String, "file1") + XCTAssertEqual(devResources[0]["node_id"] as? String, "10:1") + XCTAssertEqual(devResources[0]["dev_status"] as? String, "ready_for_dev") + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("PostDevResourceResponse") + + let body = PostDevResourceBody(name: "New Resource", url: "https://example.com/resource", fileKey: "file1", nodeId: "15:1") + let endpoint = PostDevResourceEndpoint(body: body) + let resources = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(resources.count, 1) + XCTAssertEqual(resources[0].id, "res3") + XCTAssertEqual(resources[0].name, "New Resource") + XCTAssertEqual(resources[0].nodeId, "15:1") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let body = PostDevResourceBody(name: "Test", url: "https://example.com", fileKey: "file1", nodeId: "10:1") + let endpoint = PostDevResourceEndpoint(body: body) + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/PostReactionEndpointTests.swift b/Tests/FigmaAPITests/PostReactionEndpointTests.swift new file mode 100644 index 0000000..99d0c85 --- /dev/null +++ b/Tests/FigmaAPITests/PostReactionEndpointTests.swift @@ -0,0 +1,72 @@ +@testable import FigmaAPI +import XCTest + +final class PostReactionEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PostReactionBody(emoji: "👍") + let endpoint = PostReactionEndpoint(fileId: "abc123", commentId: "comment1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/files/abc123/comments/comment1/reactions" + ) + } + + func testMakeRequestUsesPOSTMethod() throws { + let body = PostReactionBody(emoji: "👍") + let endpoint = PostReactionEndpoint(fileId: "test", commentId: "c1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "POST") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PostReactionBody(emoji: "👍") + let endpoint = PostReactionEndpoint(fileId: "test", commentId: "c1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestIncludesBody() throws { + let body = PostReactionBody(emoji: "🎉") + let endpoint = PostReactionEndpoint(fileId: "test", commentId: "c1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + XCTAssertEqual(json["emoji"] as? String, "\u{1F389}") + } + + // MARK: - Response Parsing + + func testContentParsesEmptyResponse() throws { + let data = Data("{\"status\": 200, \"error\": false}".utf8) + let body = PostReactionBody(emoji: "👍") + let endpoint = PostReactionEndpoint(fileId: "test", commentId: "c1", body: body) + let response = try endpoint.content(from: nil, with: data) + + XCTAssertNotNil(response) + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let body = PostReactionBody(emoji: "👍") + let endpoint = PostReactionEndpoint(fileId: "test", commentId: "c1", body: body) + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/PostWebhookEndpointTests.swift b/Tests/FigmaAPITests/PostWebhookEndpointTests.swift new file mode 100644 index 0000000..f72fe2c --- /dev/null +++ b/Tests/FigmaAPITests/PostWebhookEndpointTests.swift @@ -0,0 +1,55 @@ +@testable import FigmaAPI +import XCTest + +final class PostWebhookEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PostWebhookBody(eventType: "FILE_UPDATE", teamId: "team123", endpoint: "https://example.com/webhook", passcode: "secret") + let endpoint = PostWebhookEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks" + ) + } + + func testMakeRequestUsesPOSTMethod() throws { + let body = PostWebhookBody(eventType: "FILE_UPDATE", teamId: "team123", endpoint: "https://example.com/webhook", passcode: "secret") + let endpoint = PostWebhookEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "POST") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PostWebhookBody(eventType: "FILE_UPDATE", teamId: "team123", endpoint: "https://example.com/webhook", passcode: "secret") + let endpoint = PostWebhookEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestIncludesBody() throws { + let body = PostWebhookBody(eventType: "FILE_UPDATE", teamId: "team123", endpoint: "https://example.com/webhook", passcode: "secret", description: "My webhook") + let endpoint = PostWebhookEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + XCTAssertEqual(json["event_type"] as? String, "FILE_UPDATE") + XCTAssertEqual(json["team_id"] as? String, "team123") + XCTAssertEqual(json["endpoint"] as? String, "https://example.com/webhook") + XCTAssertEqual(json["passcode"] as? String, "secret") + XCTAssertEqual(json["description"] as? String, "My webhook") + } +} diff --git a/Tests/FigmaAPITests/PutDevResourceEndpointTests.swift b/Tests/FigmaAPITests/PutDevResourceEndpointTests.swift new file mode 100644 index 0000000..c445c37 --- /dev/null +++ b/Tests/FigmaAPITests/PutDevResourceEndpointTests.swift @@ -0,0 +1,79 @@ +@testable import FigmaAPI +import XCTest + +final class PutDevResourceEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PutDevResourceBody(id: "res1", name: "Updated") + let endpoint = PutDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v1/dev_resources" + ) + } + + func testMakeRequestUsesPUTMethod() throws { + let body = PutDevResourceBody(id: "res1", name: "Updated") + let endpoint = PutDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "PUT") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PutDevResourceBody(id: "res1", name: "Updated") + let endpoint = PutDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestWrapsBodyInDevResourcesArray() throws { + let body = PutDevResourceBody(id: "res1", name: "Updated Name", url: "https://new.example.com") + let endpoint = PutDevResourceEndpoint(body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + let devResources = try XCTUnwrap(json["dev_resources"] as? [[String: Any]]) + XCTAssertEqual(devResources.count, 1) + XCTAssertEqual(devResources[0]["id"] as? String, "res1") + XCTAssertEqual(devResources[0]["name"] as? String, "Updated Name") + XCTAssertEqual(devResources[0]["url"] as? String, "https://new.example.com") + } + + // MARK: - Response Parsing + + func testContentParsesResponse() throws { + let data = try FixtureLoader.loadData("PutDevResourceResponse") + + let body = PutDevResourceBody(id: "res1", name: "Updated Name") + let endpoint = PutDevResourceEndpoint(body: body) + let resources = try endpoint.content(from: nil, with: data) + + XCTAssertEqual(resources.count, 1) + XCTAssertEqual(resources[0].id, "res1") + XCTAssertEqual(resources[0].name, "Updated Name") + } + + // MARK: - Error Handling + + func testContentThrowsOnInvalidJSON() { + let invalidData = Data("invalid".utf8) + let body = PutDevResourceBody(id: "res1", name: "Test") + let endpoint = PutDevResourceEndpoint(body: body) + + XCTAssertThrowsError(try endpoint.content(from: nil, with: invalidData)) + } +} diff --git a/Tests/FigmaAPITests/PutWebhookEndpointTests.swift b/Tests/FigmaAPITests/PutWebhookEndpointTests.swift new file mode 100644 index 0000000..c5d44db --- /dev/null +++ b/Tests/FigmaAPITests/PutWebhookEndpointTests.swift @@ -0,0 +1,52 @@ +@testable import FigmaAPI +import XCTest + +final class PutWebhookEndpointTests: XCTestCase { + // MARK: - URL Construction + + func testMakeRequestConstructsCorrectURL() throws { + let body = PutWebhookBody(eventType: "FILE_DELETE") + let endpoint = PutWebhookEndpoint(webhookId: "wh1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual( + request.url?.absoluteString, + "https://api.figma.com/v2/webhooks/wh1" + ) + } + + func testMakeRequestUsesPUTMethod() throws { + let body = PutWebhookBody(endpoint: "https://example.com/new") + let endpoint = PutWebhookEndpoint(webhookId: "wh1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.httpMethod, "PUT") + } + + func testMakeRequestSetsContentTypeHeader() throws { + let body = PutWebhookBody(passcode: "newpass") + let endpoint = PutWebhookEndpoint(webhookId: "wh1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json") + } + + func testMakeRequestIncludesBody() throws { + let body = PutWebhookBody(eventType: "FILE_DELETE", description: "Updated webhook") + let endpoint = PutWebhookEndpoint(webhookId: "wh1", body: body) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) + + let request = try endpoint.makeRequest(baseURL: baseURL) + + let httpBody = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: httpBody) as? [String: Any]) + XCTAssertEqual(json["event_type"] as? String, "FILE_DELETE") + XCTAssertEqual(json["description"] as? String, "Updated webhook") + } +} diff --git a/Tests/FigmaAPITests/StylesEndpointTests.swift b/Tests/FigmaAPITests/StylesEndpointTests.swift index 2e3aa96..43a08df 100644 --- a/Tests/FigmaAPITests/StylesEndpointTests.swift +++ b/Tests/FigmaAPITests/StylesEndpointTests.swift @@ -7,7 +7,7 @@ final class StylesEndpointTests: XCTestCase { func testMakeRequestConstructsCorrectURL() throws { let endpoint = StylesEndpoint(fileId: "abc123") - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = endpoint.makeRequest(baseURL: baseURL) @@ -19,7 +19,7 @@ final class StylesEndpointTests: XCTestCase { func testMakeRequestWithSpecialCharactersInFileId() throws { let endpoint = StylesEndpoint(fileId: "file-with-dashes") - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/UpdateVariablesEndpointTests.swift b/Tests/FigmaAPITests/UpdateVariablesEndpointTests.swift index b630d8b..ae0318e 100644 --- a/Tests/FigmaAPITests/UpdateVariablesEndpointTests.swift +++ b/Tests/FigmaAPITests/UpdateVariablesEndpointTests.swift @@ -8,7 +8,7 @@ final class UpdateVariablesEndpointTests: XCTestCase { let body = VariablesUpdateRequest(variables: []) let endpoint = UpdateVariablesEndpoint(fileId: "abc123", body: body) // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -22,7 +22,7 @@ final class UpdateVariablesEndpointTests: XCTestCase { let body = VariablesUpdateRequest(variables: []) let endpoint = UpdateVariablesEndpoint(fileId: "test", body: body) // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -33,7 +33,7 @@ final class UpdateVariablesEndpointTests: XCTestCase { let body = VariablesUpdateRequest(variables: []) let endpoint = UpdateVariablesEndpoint(fileId: "test", body: body) // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) @@ -48,7 +48,7 @@ final class UpdateVariablesEndpointTests: XCTestCase { let body = VariablesUpdateRequest(variables: [update]) let endpoint = UpdateVariablesEndpoint(fileId: "test", body: body) // swiftlint:disable:next force_unwrapping - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = try endpoint.makeRequest(baseURL: baseURL) diff --git a/Tests/FigmaAPITests/VariablesEndpointTests.swift b/Tests/FigmaAPITests/VariablesEndpointTests.swift index 320efb7..3f2b49b 100644 --- a/Tests/FigmaAPITests/VariablesEndpointTests.swift +++ b/Tests/FigmaAPITests/VariablesEndpointTests.swift @@ -8,7 +8,7 @@ final class VariablesEndpointTests: XCTestCase { func testMakeRequestConstructsCorrectURL() throws { let endpoint = VariablesEndpoint(fileId: "abc123") - let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/v1/")) + let baseURL = try XCTUnwrap(URL(string: "https://api.figma.com/")) let request = endpoint.makeRequest(baseURL: baseURL)