Skip to content

Commit

Permalink
Fix for #9.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikdoe committed Jan 12, 2025
1 parent 594f45f commit 33746ee
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 10 deletions.
8 changes: 8 additions & 0 deletions CCMenu.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
038FF7162BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */; };
038FF7182BB631CB0017CD4C /* URLComponentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038FF7172BB631CB0017CD4C /* URLComponentsExtension.swift */; };
039B524629676D0700994910 /* Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039B524529676D0700994910 /* Build.swift */; };
03A3EF932D3459D400407A6F /* GitHubUserResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 03A3EF922D3459D400407A6F /* GitHubUserResponse.json */; };
03A3EF942D3459D400407A6F /* GitHubUserOrgResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */; };
03B021252A5D9F9D00B889BE /* MenuExtraModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B021242A5D9F9D00B889BE /* MenuExtraModelTests.swift */; };
03B1D75F2A6079CD007BCB8A /* MenuItemModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B1D75E2A6079CD007BCB8A /* MenuItemModelTests.swift */; };
03B1D7612A607DD0007BCB8A /* PipelineRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B1D7602A607DD0007BCB8A /* PipelineRowViewModel.swift */; };
Expand Down Expand Up @@ -178,6 +180,8 @@
038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = GitHubReposByUserCCM2OnlyResponse.json; sourceTree = "<group>"; };
038FF7172BB631CB0017CD4C /* URLComponentsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtension.swift; sourceTree = "<group>"; };
039B524529676D0700994910 /* Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Build.swift; sourceTree = "<group>"; };
03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = GitHubUserOrgResponse.json; sourceTree = "<group>"; };
03A3EF922D3459D400407A6F /* GitHubUserResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = GitHubUserResponse.json; sourceTree = "<group>"; };
03B021242A5D9F9D00B889BE /* MenuExtraModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuExtraModelTests.swift; sourceTree = "<group>"; };
03B1D75E2A6079CD007BCB8A /* MenuItemModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemModelTests.swift; sourceTree = "<group>"; };
03B1D7602A607DD0007BCB8A /* PipelineRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineRowViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -294,6 +298,8 @@
0322051F2B913BC900205DC6 /* Responses */ = {
isa = PBXGroup;
children = (
03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */,
03A3EF922D3459D400407A6F /* GitHubUserResponse.json */,
032205222B913CC000205DC6 /* GitHubWorkflowRunsResponse.json */,
032205242B913D4000205DC6 /* GitHubReposByUserResponse.json */,
038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */,
Expand Down Expand Up @@ -711,6 +717,8 @@
032205212B913C4E00205DC6 /* GitHubWorkflowsResponse.json in Resources */,
032205292B96677200205DC6 /* GitHubUserReposResponse.json in Resources */,
0322052B2B96708200205DC6 /* GitHubPipelineLocalhost.json in Resources */,
03A3EF932D3459D400407A6F /* GitHubUserResponse.json in Resources */,
03A3EF942D3459D400407A6F /* GitHubUserOrgResponse.json in Resources */,
032205252B913D4000205DC6 /* GitHubReposByUserResponse.json in Resources */,
038FF7162BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json in Resources */,
03825EA5259FFD1500DEB003 /* DefaultPipelines.json in Resources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ class GitHubRepositoryList: ObservableObject {
}

private func fetchRepositories(owner: String, token: String?) async -> [GitHubRepository] {
let ownerRepoRequest = GitHubAPI.requestForRepositories(owner: owner, token: token)
let userRequest = GitHubAPI.requestForUser(user: owner, token: token)
let (user, error) = await fetchUser(request: userRequest)
guard let user else {
return [GitHubRepository(message: error)]
}

let ownerRepoRequest: URLRequest
if user.isOrganization {
ownerRepoRequest = GitHubAPI.requestForAllRepositories(org: owner, token: token)
} else {
ownerRepoRequest = GitHubAPI.requestForAllPublicRepositories(user: owner, token: token)
}
var allRepos = await fetchRepositories(request: ownerRepoRequest)
if allRepos.count > 0 && !allRepos[0].isValid {
return allRepos
}

if let token, !token.isEmpty {
let privateRepoRequest = GitHubAPI.requestForPrivateRepositories(token: token)
if !user.isOrganization, let token, !token.isEmpty {
let privateRepoRequest = GitHubAPI.requestForAllPrivateRepositories(token: token)
let privateRepos = await fetchRepositories(request: privateRepoRequest)
if privateRepos.count > 0 && !privateRepos[0].isValid {
return privateRepos
Expand All @@ -41,6 +52,28 @@ class GitHubRepositoryList: ObservableObject {
return allRepos
}

private func fetchUser(request: URLRequest) async -> (GitHubUser?, String) {
do {
let (data, response) = try await URLSession.feedSession.data(for: request)
guard let response = response as? HTTPURLResponse else { throw URLError(.unsupportedURL) }
// TODO: Somehow refactor this to use the same code as fetchRepositories
if response.statusCode == 403 || response.statusCode == 429 {
if let v = response.value(forHTTPHeaderField: "x-ratelimit-remaining"), Int(v) == 0 {
// HTTPURLResponse doesn't have a specific message for code 429
return (nil, "too many requests")
} else {
return (nil, HTTPURLResponse.localizedString(forStatusCode: response.statusCode))
}
}
if response.statusCode != 200 {
return (nil, HTTPURLResponse.localizedString(forStatusCode: response.statusCode))
}
return (try JSONDecoder().decode(GitHubUser.self, from: data), "OK")
} catch {
return (nil, error.localizedDescription)
}
}

private func fetchRepositories(request: URLRequest) async -> [GitHubRepository] {
do {
let (data, response) = try await URLSession.feedSession.data(for: request)
Expand Down
10 changes: 10 additions & 0 deletions CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubSheetModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

import Foundation

struct GitHubUser: Identifiable, Decodable {
var id: Int
var type: String

var isOrganization: Bool {
return type == "Organization"
}
}


struct GitHubRepository: Identifiable, Hashable, Decodable {

var id: Int
Expand Down
25 changes: 20 additions & 5 deletions CCMenu/Source/Server Monitor/GitHubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,25 @@ class GitHubAPI {
return "4eafcf49451c588fbeac"
}

// MARK: - repositories, workflows, and branches
// MARK: - user, repositories, workflows, and branches

static func requestForRepositories(owner: String, token: String?) -> URLRequest {
let path = String(format: "/users/%@/repos", owner)
static func requestForUser(user: String, token: String?) -> URLRequest {
let path = String(format: "/users/%@", user)
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, token: token)
}

static func requestForAllPublicRepositories(user: String, token: String?) -> URLRequest {
let path = String(format: "/users/%@/repos", user)
let queryParams = [
"type": "all",
"sort": "pushed",
"per_page": "100",
];
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, params: queryParams, token: token)
}

static func requestForAllRepositories(org: String, token: String?) -> URLRequest {
let path = String(format: "/orgs/%@/repos", org)
let queryParams = [
"type": "all",
"sort": "pushed",
Expand All @@ -30,7 +45,7 @@ class GitHubAPI {
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, params: queryParams, token: token)
}

static func requestForPrivateRepositories(token: String) -> URLRequest {
static func requestForAllPrivateRepositories(token: String) -> URLRequest {
let path = String(format: "/user/repos")
let queryParams = [
"type": "private",
Expand Down Expand Up @@ -112,7 +127,7 @@ class GitHubAPI {
return forAPI ? "https://api.github.com" : "https://github.com"
}

private static func makeRequest(method: String = "GET", baseUrl: String, path: String, params: Dictionary<String, String>, token: String? = nil) -> URLRequest {
private static func makeRequest(method: String = "GET", baseUrl: String, path: String, params: Dictionary<String, String> = [:], token: String? = nil) -> URLRequest {
var components = URLComponents(string: baseUrl)!
components.path = path
components.queryItems = params.map({ URLQueryItem(name: $0.key, value: $0.value) })
Expand Down
18 changes: 16 additions & 2 deletions CCMenuUITests/GitHubTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class GitHubTests: XCTestCase {
}

func testAddsGitHubPipeline() throws {
webapp.router.get("/users/erikdoe") { _ in
try TestHelper.contentsOfFile("GitHubUserResponse.json")
}
webapp.router.get("/users/erikdoe/repos") { _ in
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
}
Expand Down Expand Up @@ -130,6 +133,9 @@ class GitHubTests: XCTestCase {
}

func testAddsGitHubPipelineByIdIfNeccessary() throws {
webapp.router.get("/users/erikdoe") { _ in
try TestHelper.contentsOfFile("GitHubUserResponse.json")
}
webapp.router.get("/users/erikdoe/repos") { _ in
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
}
Expand Down Expand Up @@ -177,6 +183,9 @@ class GitHubTests: XCTestCase {

func testAddsGitHubPipelineWithBranch() throws {
var branchParam: String?
webapp.router.get("/users/erikdoe") { _ in
try TestHelper.contentsOfFile("GitHubUserResponse.json")
}
webapp.router.get("/users/erikdoe/repos") { _ in
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
}
Expand Down Expand Up @@ -223,6 +232,9 @@ class GitHubTests: XCTestCase {
}

func testAddGitHubPipelinePrivateRepos() throws {
webapp.router.get("/users/erikdoe") { _ in
try TestHelper.contentsOfFile("GitHubUserResponse.json")
}
webapp.router.get("/users/erikdoe/repos") { _ in
try TestHelper.contentsOfFile("GitHubReposByUserResponse.json")
}
Expand Down Expand Up @@ -263,7 +275,7 @@ class GitHubTests: XCTestCase {
}

func testShowsRateLimitExceededForRepositories() throws {
webapp.router.get("/users/erikdoe/repos", options: .editResponse) { r -> String in
webapp.router.get("/users/erikdoe", options: .editResponse) { r -> String in
r.response.status = .forbidden
r.response.headers.replaceOrAdd(name: "x-ratelimit-remaining", value: "0")
return "{ \"message\": \"API rate limit exceeded for ...\" } "
Expand All @@ -289,6 +301,9 @@ class GitHubTests: XCTestCase {

func testDoesntDoubleFetchRepositories() throws {
var fetchCount = 0
webapp.router.get("/users/erikdoe") { _ in
try TestHelper.contentsOfFile("GitHubUserResponse.json")
}
webapp.router.get("/users/erikdoe/repos") { _ in
fetchCount += 1
return try TestHelper.contentsOfFile("GitHubReposByUserResponse.json")
Expand Down Expand Up @@ -318,7 +333,6 @@ class GitHubTests: XCTestCase {

// Assert that no further fetch occured
XCTAssertEqual(1, fetchCount)

}

}
35 changes: 35 additions & 0 deletions CCMenuUITests/Responses/GitHubUserOrgResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"login": "thoughtworks",
"id": 18878,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE4ODc4",
"avatar_url": "https://avatars.githubusercontent.com/u/18878?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/thoughtworks",
"html_url": "https://github.com/thoughtworks",
"followers_url": "https://api.github.com/users/thoughtworks/followers",
"following_url": "https://api.github.com/users/thoughtworks/following{/other_user}",
"gists_url": "https://api.github.com/users/thoughtworks/gists{/gist_id}",
"starred_url": "https://api.github.com/users/thoughtworks/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/thoughtworks/subscriptions",
"organizations_url": "https://api.github.com/users/thoughtworks/orgs",
"repos_url": "https://api.github.com/users/thoughtworks/repos",
"events_url": "https://api.github.com/users/thoughtworks/events{/privacy}",
"received_events_url": "https://api.github.com/users/thoughtworks/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false,
"name": "Thoughtworks",
"company": null,
"blog": "https://thoughtworks.com",
"location": "Global",
"email": null,
"hireable": null,
"bio": "We're a leading global technology consultancy that integrates strategy, design and software engineering to enable our clients to thrive. ",
"twitter_username": "thoughtworks",
"public_repos": 67,
"public_gists": 0,
"followers": 581,
"following": 0,
"created_at": "2008-07-29T21:54:09Z",
"updated_at": "2024-11-25T18:40:42Z"
}
35 changes: 35 additions & 0 deletions CCMenuUITests/Responses/GitHubUserResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"login": "erikdoe",
"id": 954026,
"node_id": "MDQ6VXNlcjk1NDAyNg==",
"avatar_url": "https://avatars.githubusercontent.com/u/954026?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/erikdoe",
"html_url": "https://github.com/erikdoe",
"followers_url": "https://api.github.com/users/erikdoe/followers",
"following_url": "https://api.github.com/users/erikdoe/following{/other_user}",
"gists_url": "https://api.github.com/users/erikdoe/gists{/gist_id}",
"starred_url": "https://api.github.com/users/erikdoe/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/erikdoe/subscriptions",
"organizations_url": "https://api.github.com/users/erikdoe/orgs",
"repos_url": "https://api.github.com/users/erikdoe/repos",
"events_url": "https://api.github.com/users/erikdoe/events{/privacy}",
"received_events_url": "https://api.github.com/users/erikdoe/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false,
"name": "Erik Doernenburg",
"company": "Thoughtworks",
"blog": "http://erik.doernenburg.com",
"location": "Hamburg, Germany",
"email": null,
"hireable": null,
"bio": null,
"twitter_username": null,
"public_repos": 21,
"public_gists": 6,
"followers": 266,
"following": 0,
"created_at": "2011-08-02T12:34:47Z",
"updated_at": "2025-01-10T21:12:36Z"
}

0 comments on commit 33746ee

Please sign in to comment.