Skip to content

Commit dad15d5

Browse files
authored
Merge pull request #29 from jonny7/users
adds User API
2 parents 039b6ec + eda8be3 commit dad15d5

File tree

7 files changed

+306
-14
lines changed

7 files changed

+306
-14
lines changed

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,48 @@
11
# TestRailKit
22
![](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)
33

4-
![wip](https://img.shields.io/badge/WIP-Work%20In%20Progress-orange)
4+
## Overview
5+
6+
**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.
7+
8+
## Installing
9+
Add the following entry in your Package.swift to start using TestRailKit:
10+
```swift
11+
.package(url: "https://github.com/jonny7/testrail-kit", from: "1.0.0-alpha.1")
12+
```
13+
## Getting Started
14+
```swift
15+
import TestRailKit
16+
17+
let httpClient = HTTPClient(...)
18+
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
19+
```
20+
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:
21+
```swift
22+
client.action(resource: ConfigurationRepresentable, body: TestRailPostable) -> TestRailModel // for posting models to TestRail
23+
client.action(resource: ConfigurationRepresentable) -> TestRailModel // for retrieving models from TestRail
24+
```
25+
26+
`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.
27+
28+
For example:
29+
```swift
30+
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
31+
```
32+
> _**Note**: Use of `wait()` was used here for simplicity. Never call this method on an `eventLoop`!_
33+
34+
## Conventions
35+
TestRail uses Unix timestamps when working with dates. TestRailKit will encode or decode all Swift `Date` objects into UNIX timestamps automatically.
36+
TestRail also uses `snake_case` for property names, TestRailKit automatically encodes or decodes to `camelCase` as per Swift conventions
37+
38+
## Customizing
39+
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.
40+
41+
### Partial Updates
42+
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).
43+
44+
## Vapor
45+
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).
46+
47+
## Contributing
48+
All help is welcomed, please open a PR

Sources/TestRailKit/Tests/TestResource.swift

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,15 @@ public enum TestResource: ConfigurationRepresentable {
1010

1111
public var request: RequestDetails {
1212
switch self {
13-
case .one(let testId):
14-
return (uri: "get_test/\(testId)", method: .GET)
15-
case .all(let runId, let statusIds):
16-
guard let ids = statusIds else {
17-
return (uri: "get_tests/\(runId)", method: .GET)
18-
}
19-
return (uri: "get_tests/\(runId)\(self.getIdList(name: "status_id", list: ids))", method: .GET)
13+
case .one(let testId):
14+
return (uri: "get_test/\(testId)", method: .GET)
15+
case .all(let runId, let statusIds):
16+
guard let ids = statusIds else {
17+
return (uri: "get_tests/\(runId)", method: .GET)
18+
}
19+
return (uri: "get_tests/\(runId)\(self.getIdList(name: "status_id", list: ids))", method: .GET)
2020
}
2121
}
2222
}
2323

24-
extension TestResource: IDRepresentable {
25-
// func getIdList(name: String, list: [Int]) -> String {
26-
// let ids = list.map { String($0) }.joined(separator: ",")
27-
// return "&\(name)=\(ids)"
28-
// }
29-
}
24+
extension TestResource: IDRepresentable {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public struct User: TestRailModel {
2+
public var email: String
3+
public var id: Int
4+
public var isActive: Bool
5+
public var name: String
6+
public var roleId: Int
7+
public var role: String
8+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
public enum UserResource: ConfigurationRepresentable {
2+
case get(type: GetAction)
3+
4+
public var request: RequestDetails {
5+
switch self {
6+
case .get(.one(let userId)):
7+
return (uri: "get_user/\(userId)", method: .GET)
8+
case .get(type: .current(let userId)):
9+
return (uri: "get_current_user/\(userId)", method: .GET)
10+
case .get(type: .email(let email)):
11+
return (uri: "get_user_by_email&email=\(email)", method: .GET)
12+
case .get(type: .all(let projectId)):
13+
guard let project = projectId else {
14+
return (uri: "get_users", method: .GET)
15+
}
16+
return (uri: "get_users&project_id=\(project)", method: .GET)
17+
}
18+
}
19+
20+
public enum GetAction {
21+
/// Returns an existing user.
22+
/// See https://www.gurock.com/testrail/docs/api/reference/users#get_user
23+
case one(userId: Int)
24+
25+
/// Returns user details for the TestRail user making the API request.
26+
/// See https://www.gurock.com/testrail/docs/api/reference/users#get_current_user
27+
case current(userId: Int)
28+
29+
/// Returns an existing user by his/her email address.
30+
/// See https://www.gurock.com/testrail/docs/api/reference/users#get_user_by_email
31+
case email(email: String)
32+
33+
/// Returns a list of users.
34+
/// See https://www.gurock.com/testrail/docs/api/reference/users#get_users
35+
case all(projectId: Int?)
36+
}
37+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import NIO
2+
import NIOHTTP1
3+
import XCTest
4+
5+
@testable import TestRailKit
6+
7+
class UserTests: XCTestCase {
8+
9+
static var utilities = UserUtilities()
10+
11+
override class func tearDown() {
12+
//XCTAssertNoThrow(try Self.utilities.testServer.stop()) //this is a nio problem and should remain. Omitting for GH Actions only
13+
XCTAssertNoThrow(try Self.utilities.httpClient.syncShutdown())
14+
XCTAssertNoThrow(try Self.utilities.group.syncShutdownGracefully())
15+
}
16+
17+
func testGetUser() {
18+
var requestComplete: EventLoopFuture<User>!
19+
XCTAssertNoThrow(requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .one(userId: 1))))
20+
21+
XCTAssertNoThrow(
22+
XCTAssertEqual(
23+
.head(
24+
.init(
25+
version: .init(major: 1, minor: 1),
26+
method: .GET,
27+
uri: "/index.php?/api/v2/get_user/1",
28+
headers: .init([
29+
("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="),
30+
("content-type", "application/json; charset=utf-8"),
31+
("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"),
32+
("Content-Length", "0"),
33+
]))),
34+
try Self.utilities.testServer.readInbound()))
35+
36+
XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil))
37+
38+
var responseBuffer = Self.utilities.allocator.buffer(capacity: 0)
39+
responseBuffer.writeString(Self.utilities.userResponse)
40+
41+
XCTAssertNoThrow(
42+
try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
43+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
44+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil)))
45+
46+
let response = try! requestComplete.wait()
47+
XCTAssertEqual(response.email, "[email protected]")
48+
}
49+
50+
func testGetCurrentUser() {
51+
var requestComplete: EventLoopFuture<User>!
52+
XCTAssertNoThrow(requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .current(userId: 1))))
53+
54+
XCTAssertNoThrow(
55+
XCTAssertEqual(
56+
.head(
57+
.init(
58+
version: .init(major: 1, minor: 1),
59+
method: .GET,
60+
uri: "/index.php?/api/v2/get_current_user/1",
61+
headers: .init([
62+
("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="),
63+
("content-type", "application/json; charset=utf-8"),
64+
("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"),
65+
("Content-Length", "0"),
66+
]))),
67+
try Self.utilities.testServer.readInbound()))
68+
69+
XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil))
70+
71+
var responseBuffer = Self.utilities.allocator.buffer(capacity: 0)
72+
responseBuffer.writeString(Self.utilities.userResponse)
73+
74+
XCTAssertNoThrow(
75+
try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
76+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
77+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil)))
78+
79+
let response = try! requestComplete.wait()
80+
XCTAssertEqual(response.email, "[email protected]")
81+
}
82+
83+
func testGetUserByEmail() {
84+
var requestComplete: EventLoopFuture<User>!
85+
XCTAssertNoThrow(
86+
requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .email(email: "[email protected]")))
87+
)
88+
89+
XCTAssertNoThrow(
90+
XCTAssertEqual(
91+
.head(
92+
.init(
93+
version: .init(major: 1, minor: 1),
94+
method: .GET,
95+
uri: "/index.php?/api/v2/get_user_by_email&[email protected]",
96+
headers: .init([
97+
("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="),
98+
("content-type", "application/json; charset=utf-8"),
99+
("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"),
100+
("Content-Length", "0"),
101+
]))),
102+
try Self.utilities.testServer.readInbound()))
103+
104+
XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil))
105+
106+
var responseBuffer = Self.utilities.allocator.buffer(capacity: 0)
107+
responseBuffer.writeString(Self.utilities.userResponse)
108+
109+
XCTAssertNoThrow(
110+
try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
111+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
112+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil)))
113+
114+
let response = try! requestComplete.wait()
115+
XCTAssertEqual(response.email, "[email protected]")
116+
}
117+
118+
func testGetUsers() {
119+
var requestComplete: EventLoopFuture<[User]>!
120+
XCTAssertNoThrow(
121+
requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .all(projectId: nil)))
122+
)
123+
124+
XCTAssertNoThrow(
125+
XCTAssertEqual(
126+
.head(
127+
.init(
128+
version: .init(major: 1, minor: 1),
129+
method: .GET,
130+
uri: "/index.php?/api/v2/get_users",
131+
headers: .init([
132+
("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="),
133+
("content-type", "application/json; charset=utf-8"),
134+
("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"),
135+
("Content-Length", "0"),
136+
]))),
137+
try Self.utilities.testServer.readInbound()))
138+
139+
XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil))
140+
141+
var responseBuffer = Self.utilities.allocator.buffer(capacity: 0)
142+
responseBuffer.writeString(Self.utilities.usersResponse)
143+
144+
XCTAssertNoThrow(
145+
try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
146+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
147+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil)))
148+
149+
let response = try! requestComplete.wait()
150+
XCTAssertEqual(response.first?.email, "[email protected]")
151+
}
152+
153+
func testGetUsersNonAdmin() {
154+
var requestComplete: EventLoopFuture<[User]>!
155+
XCTAssertNoThrow(
156+
requestComplete = try Self.utilities.client.action(resource: UserResource.get(type: .all(projectId: 1)))
157+
)
158+
159+
XCTAssertNoThrow(
160+
XCTAssertEqual(
161+
.head(
162+
.init(
163+
version: .init(major: 1, minor: 1),
164+
method: .GET,
165+
uri: "/index.php?/api/v2/get_users&project_id=1",
166+
headers: .init([
167+
("authorization", "Basic dXNlckB0ZXN0cmFpbC5pbzoxMjM0YWJjZA=="),
168+
("content-type", "application/json; charset=utf-8"),
169+
("Host", "127.0.0.1:\(Self.utilities.testServer.serverPort)"),
170+
("Content-Length", "0"),
171+
]))),
172+
try Self.utilities.testServer.readInbound()))
173+
174+
XCTAssertEqual(try Self.utilities.testServer.readInbound(), .end(nil))
175+
176+
var responseBuffer = Self.utilities.allocator.buffer(capacity: 0)
177+
responseBuffer.writeString(Self.utilities.usersResponse)
178+
179+
XCTAssertNoThrow(
180+
try Self.utilities.testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
181+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
182+
XCTAssertNoThrow(try Self.utilities.testServer.writeOutbound(.end(nil)))
183+
184+
let response = try! requestComplete.wait()
185+
XCTAssertEqual(response.first?.email, "[email protected]")
186+
}
187+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
@testable import TestRailKit
4+
5+
class UserUtilities: TestingUtilities {
6+
let userResponse = userResponseString
7+
let usersResponse = "[\(userResponseString)]"
8+
}
9+
10+

Tests/TestRailKitTests/Utilities/UtilityRequestResponse.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,3 +1757,14 @@ let testResponseString = """
17571757
"custom_goals": null
17581758
}
17591759
"""
1760+
1761+
let userResponseString = """
1762+
{
1763+
"name": "Jonny",
1764+
"id": 1,
1765+
"email": "[email protected]",
1766+
"is_active": true,
1767+
"role_id": 6,
1768+
"role": "QA Lead"
1769+
}
1770+
"""

0 commit comments

Comments
 (0)