Skip to content

Commit

Permalink
Merge pull request #29 from jonny7/users
Browse files Browse the repository at this point in the history
adds User API
  • Loading branch information
jonny7 authored Nov 9, 2020
2 parents 1c67435 + e2177b7 commit 1831e57
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 14 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 8 additions & 13 deletions Sources/TestRailKit/Tests/TestResource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
8 changes: 8 additions & 0 deletions Sources/TestRailKit/Users/User.swift
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions Sources/TestRailKit/Users/UserResource.swift
Original file line number Diff line number Diff line change
@@ -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?)
}
}
187 changes: 187 additions & 0 deletions Tests/TestRailKitTests/Routes/UserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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<User>!
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<User>!
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<User>!
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")
}

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")
}
}
10 changes: 10 additions & 0 deletions Tests/TestRailKitTests/Utilities/Classes/UserUtilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

@testable import TestRailKit

class UserUtilities: TestingUtilities {
let userResponse = userResponseString
let usersResponse = "[\(userResponseString)]"
}


11 changes: 11 additions & 0 deletions Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
"""

0 comments on commit 1831e57

Please sign in to comment.