From 7f14fa98bfe1f80f3208cf9b750e7a58cc7e64fa Mon Sep 17 00:00:00 2001 From: Jonathon Date: Mon, 9 Nov 2020 18:31:32 -0400 Subject: [PATCH 1/2] adds User API --- README.md | 46 +++++- Sources/TestRailKit/Tests/TestResource.swift | 21 +-- Sources/TestRailKit/Users/User.swift | 8 + Sources/TestRailKit/Users/UserResource.swift | 37 +++++ Tests/TestRailKitTests/Routes/UserTests.swift | 152 ++++++++++++++++++ .../Utilities/Classes/UserUtilities.swift | 10 ++ .../Utilities/UtilityRequestResponse.swift | 11 ++ 7 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 Sources/TestRailKit/Users/User.swift create mode 100644 Sources/TestRailKit/Users/UserResource.swift create mode 100644 Tests/TestRailKitTests/Routes/UserTests.swift create mode 100644 Tests/TestRailKitTests/Utilities/Classes/UserUtilities.swift diff --git a/README.md b/README.md index 19ff939..a3389cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,48 @@ # TestRailKit ![](https://img.shields.io/badge/Swift-5.3-orange.svg?style=svg) [![codecov](https://codecov.io/gh/jonny7/testrail-kit/branch/master/graph/badge.svg)](https://codecov.io/gh/jonny7/testrail-kit) ![testrail-ci](https://github.com/jonny7/testrail-kit/workflows/testrail-ci/badge.svg) ![license](https://img.shields.io/github/license/jonny7/testrail-kit) [![Maintainability](https://api.codeclimate.com/v1/badges/58d6e1a7f9f8038f92c8/maintainability)](https://codeclimate.com/github/jonny7/testrail-kit/maintainability) -![wip](https://img.shields.io/badge/WIP-Work%20In%20Progress-orange) +## Overview + +**TestRailKit** is an asynchronous pure Swift wrapper around the [TestRail API](https://www.gurock.com/testrail/docs/api), written on top of Apple's [swift-nio](https://github.com/apple/swift-nio) and [AsyncHTTPClient](https://github.com/swift-server/async-http-client). Whereas other TestRail bindings generally provide some form of minimal client to send requests. This library provides the full type safe implementation of TestRail's API. Meaning that it will automatically encode and decode models to be sent and received and all the endpoints are already built in. These models were generally created by using our own unmodified TestRail instance and seeing what endpoints returned. But you can still make your own models easily. + +## Installing +Add the following entry in your Package.swift to start using TestRailKit: +```swift +.package(url: "https://github.com/jonny7/testrail-kit", from: "1.0.0-alpha.1") +``` +## Getting Started +```swift +import TestRailKit + +let httpClient = HTTPClient(...) +let client = TestRailClient(httpClient: httpClient, eventLoop: eventLoop, username: "your_username", apiKey: "your-key", testRailUrl: "https://my-testrail-domain", port: nil) // `use port` if you're on a non-standard port +``` +This gives you access to the TestRail client now. The library has extensive tests for all the endpoints, so you can always look there for example usage. At it's heart there are two main functions: +```swift +client.action(resource: ConfigurationRepresentable, body: TestRailPostable) -> TestRailModel // for posting models to TestRail +client.action(resource: ConfigurationRepresentable) -> TestRailModel // for retrieving models from TestRail +``` + +`ConfigurationRepresentation`: When calling either `action` method you will need to pass the resource argument, these all follow the same naming convention of TestRail resource, eg `Case`, `Suite`, `Plan` etc + "Resource". So the previously listed models all become `CaseResource`, `SuiteResource`, `PlanResource`. Each resource, then has various other enumerated options in order to provide an abstracted type-safe API, leaving the developer to pass simple typed arguments to manage TestRail. + +For example: +```swift +let tests: EventLoopFuture<[Test]> = client.action(resource: TestResource.all(runId: 89, statusIds: [1]))).wait() // return all tests for run 89 with a status of 1 +``` +> _**Note**: Use of `wait()` was used here for simplicity. Never call this method on an `eventLoop`!_ + +## Conventions +TestRail uses Unix timestamps when working with dates. TestRailKit will encode or decode all Swift `Date` objects into UNIX timestamps automatically. +TestRail also uses `snake_case` for property names, TestRailKit automatically encodes or decodes to `camelCase` as per Swift conventions + +## Customizing +If you wish to use a model that doesn't currently exist in the library because your own TestRail is mofidied you can simply make this model conform to `TestRailModel` if decoding it from TestRail or `TestRailPostable` if you wish to post this model to TestRail. + +### Partial Updates +TestRail supports partial updates, if you wish to use these, you will need to make this new object conform to `TestRailPostable`. You can see an example of this in the [tests](https://github.com/jonny7/testrail-kit/blob/master/Tests/TestRailKitTests/Utilities/Classes/SuiteUtilities.swift). + +## Vapor +For those who want to use this library with the most popular server side Swift framework Vapor, please see this ![repo](https://github.com/jonny7/testrail). + +## Contributing +All help is welcomed, please open a PR diff --git a/Sources/TestRailKit/Tests/TestResource.swift b/Sources/TestRailKit/Tests/TestResource.swift index 17f084c..7286aab 100644 --- a/Sources/TestRailKit/Tests/TestResource.swift +++ b/Sources/TestRailKit/Tests/TestResource.swift @@ -10,20 +10,15 @@ public enum TestResource: ConfigurationRepresentable { public var request: RequestDetails { switch self { - case .one(let testId): - return (uri: "get_test/\(testId)", method: .GET) - case .all(let runId, let statusIds): - guard let ids = statusIds else { - return (uri: "get_tests/\(runId)", method: .GET) - } - return (uri: "get_tests/\(runId)\(self.getIdList(name: "status_id", list: ids))", method: .GET) + case .one(let testId): + return (uri: "get_test/\(testId)", method: .GET) + case .all(let runId, let statusIds): + guard let ids = statusIds else { + return (uri: "get_tests/\(runId)", method: .GET) + } + return (uri: "get_tests/\(runId)\(self.getIdList(name: "status_id", list: ids))", method: .GET) } } } -extension TestResource: IDRepresentable { -// func getIdList(name: String, list: [Int]) -> String { -// let ids = list.map { String($0) }.joined(separator: ",") -// return "&\(name)=\(ids)" -// } -} +extension TestResource: IDRepresentable {} diff --git a/Sources/TestRailKit/Users/User.swift b/Sources/TestRailKit/Users/User.swift new file mode 100644 index 0000000..2c22bbd --- /dev/null +++ b/Sources/TestRailKit/Users/User.swift @@ -0,0 +1,8 @@ +public struct User: TestRailModel { + public var email: String + public var id: Int + public var isActive: Bool + public var name: String + public var roleId: Int + public var role: String +} diff --git a/Sources/TestRailKit/Users/UserResource.swift b/Sources/TestRailKit/Users/UserResource.swift new file mode 100644 index 0000000..ec24dbc --- /dev/null +++ b/Sources/TestRailKit/Users/UserResource.swift @@ -0,0 +1,37 @@ +public enum UserResource: ConfigurationRepresentable { + case get(type: GetAction) + + public var request: RequestDetails { + switch self { + case .get(.one(let userId)): + return (uri: "get_user/\(userId)", method: .GET) + case .get(type: .current(let userId)): + return (uri: "get_current_user/\(userId)", method: .GET) + case .get(type: .email(let email)): + return (uri: "get_user_by_email&email=\(email)", method: .GET) + case .get(type: .all(let projectId)): + guard let project = projectId else { + return (uri: "get_users", method: .GET) + } + return (uri: "get_users&project_id=\(project)", method: .GET) + } + } + + public enum GetAction { + /// Returns an existing user. + /// See https://www.gurock.com/testrail/docs/api/reference/users#get_user + case one(userId: Int) + + /// Returns user details for the TestRail user making the API request. + /// See https://www.gurock.com/testrail/docs/api/reference/users#get_current_user + case current(userId: Int) + + /// Returns an existing user by his/her email address. + /// See https://www.gurock.com/testrail/docs/api/reference/users#get_user_by_email + case email(email: String) + + /// Returns a list of users. + /// See https://www.gurock.com/testrail/docs/api/reference/users#get_users + case all(projectId: Int?) + } +} diff --git a/Tests/TestRailKitTests/Routes/UserTests.swift b/Tests/TestRailKitTests/Routes/UserTests.swift new file mode 100644 index 0000000..d156814 --- /dev/null +++ b/Tests/TestRailKitTests/Routes/UserTests.swift @@ -0,0 +1,152 @@ +import NIO +import NIOHTTP1 +import XCTest + +@testable import TestRailKit + +class UserTests: XCTestCase { + + static var utilities = UserUtilities() + + override class func tearDown() { + //XCTAssertNoThrow(try Self.utilities.testServer.stop()) //this is a nio problem and should remain. Omitting for GH Actions only + XCTAssertNoThrow(try Self.utilities.httpClient.syncShutdown()) + XCTAssertNoThrow(try Self.utilities.group.syncShutdownGracefully()) + } + + func testGetUser() { + var requestComplete: EventLoopFuture! + XCTAssertNoThrow(requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .one(userId: 1)))) + + XCTAssertNoThrow( + XCTAssertEqual( + .head( + .init( + version: .init(major: 1, minor: 1), + method: .GET, + uri: "/index.php?/api/v2/get_user/1", + headers: .init([ + ("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="), + ("content-type", "application/json; charset=utf-8"), + ("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"), + ("Content-Length", "0"), + ]))), + try Self.utilities.testServer.readInbound())) + + XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil)) + + var responseBuffer = Self.utilities.allocator.buffer(capacity: 0) + responseBuffer.writeString(Self.utilities.userResponse) + + XCTAssertNoThrow( + try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil))) + + let response = try! requestComplete.wait() + XCTAssertEqual(response.email, "jonny@github.com") + } + + func testGetCurrentUser() { + var requestComplete: EventLoopFuture! + XCTAssertNoThrow(requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .current(userId: 1)))) + + XCTAssertNoThrow( + XCTAssertEqual( + .head( + .init( + version: .init(major: 1, minor: 1), + method: .GET, + uri: "/index.php?/api/v2/get_current_user/1", + headers: .init([ + ("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="), + ("content-type", "application/json; charset=utf-8"), + ("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"), + ("Content-Length", "0"), + ]))), + try Self.utilities.testServer.readInbound())) + + XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil)) + + var responseBuffer = Self.utilities.allocator.buffer(capacity: 0) + responseBuffer.writeString(Self.utilities.userResponse) + + XCTAssertNoThrow( + try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil))) + + let response = try! requestComplete.wait() + XCTAssertEqual(response.email, "jonny@github.com") + } + + func testGetUserByEmail() { + var requestComplete: EventLoopFuture! + XCTAssertNoThrow( + requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .email(email: "jonny@github.com"))) + ) + + XCTAssertNoThrow( + XCTAssertEqual( + .head( + .init( + version: .init(major: 1, minor: 1), + method: .GET, + uri: "/index.php?/api/v2/get_user_by_email&email=jonny@github.com", + headers: .init([ + ("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="), + ("content-type", "application/json; charset=utf-8"), + ("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"), + ("Content-Length", "0"), + ]))), + try Self.utilities.testServer.readInbound())) + + XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil)) + + var responseBuffer = Self.utilities.allocator.buffer(capacity: 0) + responseBuffer.writeString(Self.utilities.userResponse) + + XCTAssertNoThrow( + try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil))) + + let response = try! requestComplete.wait() + XCTAssertEqual(response.email, "jonny@github.com") + } + + func testGetUsers() { + var requestComplete: EventLoopFuture<[User]>! + XCTAssertNoThrow( + requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .all(projectId: nil))) + ) + + XCTAssertNoThrow( + XCTAssertEqual( + .head( + .init( + version: .init(major: 1, minor: 1), + method: .GET, + uri: "/index.php?/api/v2/get_users", + headers: .init([ + ("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="), + ("content-type", "application/json; charset=utf-8"), + ("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"), + ("Content-Length", "0"), + ]))), + try Self.utilities.testServer.readInbound())) + + XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil)) + + var responseBuffer = Self.utilities.allocator.buffer(capacity: 0) + responseBuffer.writeString(Self.utilities.usersResponse) + + XCTAssertNoThrow( + try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil))) + + let response = try! requestComplete.wait() + XCTAssertEqual(response.first?.email, "jonny@github.com") + } +} diff --git a/Tests/TestRailKitTests/Utilities/Classes/UserUtilities.swift b/Tests/TestRailKitTests/Utilities/Classes/UserUtilities.swift new file mode 100644 index 0000000..332a2e4 --- /dev/null +++ b/Tests/TestRailKitTests/Utilities/Classes/UserUtilities.swift @@ -0,0 +1,10 @@ +import Foundation + +@testable import TestRailKit + +class UserUtilities: TestingUtilities { + let userResponse = userResponseString + let usersResponse = "[\(userResponseString)]" +} + + diff --git a/Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift b/Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift index 003099c..ca0b354 100644 --- a/Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift +++ b/Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift @@ -1757,3 +1757,14 @@ let testResponseString = """ "custom_goals": null } """ + +let userResponseString = """ +{ + "name": "Jonny", + "id": 1, + "email": "jonny@github.com", + "is_active": true, + "role_id": 6, + "role": "QA Lead" +} +""" From e2177b77a1f325b9ba919b2c5687813f4ec24a8e Mon Sep 17 00:00:00 2001 From: Jonathon Date: Mon, 9 Nov 2020 18:40:11 -0400 Subject: [PATCH 2/2] adds extra user test --- Tests/TestRailKitTests/Routes/UserTests.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Tests/TestRailKitTests/Routes/UserTests.swift b/Tests/TestRailKitTests/Routes/UserTests.swift index d156814..6d6dc67 100644 --- a/Tests/TestRailKitTests/Routes/UserTests.swift +++ b/Tests/TestRailKitTests/Routes/UserTests.swift @@ -149,4 +149,39 @@ class UserTests: XCTestCase { let response = try! requestComplete.wait() XCTAssertEqual(response.first?.email, "jonny@github.com") } + + func testGetUsersNonAdmin() { + var requestComplete: EventLoopFuture<[User]>! + XCTAssertNoThrow( + requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .all(projectId: 1))) + ) + + XCTAssertNoThrow( + XCTAssertEqual( + .head( + .init( + version: .init(major: 1, minor: 1), + method: .GET, + uri: "/index.php?/api/v2/get_users&project_id=1", + headers: .init([ + ("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="), + ("content-type", "application/json; charset=utf-8"), + ("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"), + ("Content-Length", "0"), + ]))), + try Self.utilities.testServer.readInbound())) + + XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil)) + + var responseBuffer = Self.utilities.allocator.buffer(capacity: 0) + responseBuffer.writeString(Self.utilities.usersResponse) + + XCTAssertNoThrow( + try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer)))) + XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil))) + + let response = try! requestComplete.wait() + XCTAssertEqual(response.first?.email, "jonny@github.com") + } }