diff --git a/src/main/typescript/client/GitHubApiTypes.ts b/src/main/typescript/client/GitHubApiTypes.ts index f58dd0b..6fe670a 100644 --- a/src/main/typescript/client/GitHubApiTypes.ts +++ b/src/main/typescript/client/GitHubApiTypes.ts @@ -11,7 +11,7 @@ export interface Thread { repository: Repository; subject: Subject; unread: boolean; - updated_at: Date; + updated_at: string; url: string; } diff --git a/src/main/typescript/client/GitHubClient.ts b/src/main/typescript/client/GitHubClient.ts index bf8f349..b8224db 100644 --- a/src/main/typescript/client/GitHubClient.ts +++ b/src/main/typescript/client/GitHubClient.ts @@ -96,7 +96,7 @@ export interface GitHubClient { listThreads(showParticipatingOnly?: boolean): Promise; markThreadAsRead(githubNotification: GitHub.Thread): Promise; - markAllThreadsAsRead(): Promise; + markAllThreadsAsRead(updateDate?: Date): Promise; getWebUrlForSubject(subject: GitHub.Subject): Promise; } diff --git a/src/main/typescript/client/GitHubClientImpl.ts b/src/main/typescript/client/GitHubClientImpl.ts index be295b1..d9316e5 100644 --- a/src/main/typescript/client/GitHubClientImpl.ts +++ b/src/main/typescript/client/GitHubClientImpl.ts @@ -58,6 +58,10 @@ export class GitHubClientImpl implements GitHubClient { } } + public get httpEngine(): HttpEngine { + return this.engine; + } + public async listThreads(showParticipatingOnly: boolean = false): Promise { const response = await this.doRequest( HttpMethod.GET, @@ -79,12 +83,12 @@ export class GitHubClientImpl implements GitHubClient { await this.doRequest(HttpMethod.PATCH, notification.url, [HttpStatus.ResetContent, HttpStatus.NotModified]); } - public async markAllThreadsAsRead(): Promise { + public async markAllThreadsAsRead(updateDate: Date = new Date()): Promise { await this.doRequest( HttpMethod.PUT, `https://${this.baseUrl}/notifications`, [HttpStatus.Accepted, HttpStatus.ResetContent, HttpStatus.NotModified], - { last_read_at: new Date().toISOString(), read: true } as GitHub.MarkNotificationsArReadRequest + { last_read_at: updateDate.toISOString(), read: true } as GitHub.MarkNotificationsArReadRequest ); } @@ -131,12 +135,13 @@ export class GitHubClientImpl implements GitHubClient { const enforcedPollInterval = Number(header); if (enforcedPollInterval !== this._pollInterval) { - GitHubClientImpl.LOGGER.info('New polling interval enforced by GitHub {}s', enforcedPollInterval); + GitHubClientImpl.LOGGER.info('New polling interval enforced by GitHub {0}s', enforcedPollInterval); this._pollInterval = enforcedPollInterval; } } private static handleRequestError(error: unknown): never { + GitHubClientImpl.LOGGER.info('{0} - {1} - {2}', error, typeof error, error instanceof Object); if (error instanceof Error) { throw new ApiError(-1, 'Unable to perform API call', error); } else if (typeof error === 'string') { @@ -144,7 +149,7 @@ export class GitHubClientImpl implements GitHubClient { } // Throw a generic error - throw new ApiError(-1, 'Unable to perform API call'); + throw new ApiError(-1, `Unable to perform API call - ${error?.toString() ?? 'error undefined'}`); } private static validateResponseCode(response: HttpResponse, ...validStates: number[]): void { @@ -152,13 +157,14 @@ export class GitHubClientImpl implements GitHubClient { const errorResponse = JSON.parse(response.body) as GitHub.BasicError; const message = errorResponse.message ?? - HttpStatus[response.statusCode] - // insert a space between lower & upper - .replace(/([a-z])([A-Z])/g, '$1 $2') - // space before last upper in a sequence followed by lower - .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') - // uppercase the first character - .replace(/^./, (str: string) => str.toUpperCase()); + 'Invalid status code: ' + + HttpStatus[response.statusCode] + // insert a space between lower & upper + .replace(/([a-z])([A-Z])/g, '$1 $2') + // space before last upper in a sequence followed by lower + .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') + // uppercase the first character + .replace(/^./, (str: string) => str.toUpperCase()); throw new ApiError(response.statusCode, message); } diff --git a/src/main/typescript/client/Http.ts b/src/main/typescript/client/Http.ts index eba6e2a..c7860db 100644 --- a/src/main/typescript/client/Http.ts +++ b/src/main/typescript/client/Http.ts @@ -17,10 +17,11 @@ export class HttpRequest { private _body: RequestBody | undefined; private readonly _headers: Map; - public constructor(method: HttpMethod, url: string) { + public constructor(method: HttpMethod, url: string, body?: RequestBody, headers?: Map) { this._method = method; this._url = url; - this._headers = new Map(); + this._body = body; + this._headers = headers ?? new Map(); } public get method(): string { diff --git a/src/main/typescript/notifications/NotificationAdapter.ts b/src/main/typescript/notifications/NotificationAdapter.ts index ec7baa8..4a8339e 100644 --- a/src/main/typescript/notifications/NotificationAdapter.ts +++ b/src/main/typescript/notifications/NotificationAdapter.ts @@ -116,7 +116,7 @@ export class NotificationAdapter { case NotificationMode.SINGLE: const updatedAtMap = new Map(); - oldData.forEach((notification) => updatedAtMap.set(notification.id, notification.updated_at)); + oldData.forEach((notification) => updatedAtMap.set(notification.id, new Date(notification.updated_at))); return newData .filter((notification) => this.isNotificationNeeded(updatedAtMap, notification)) @@ -133,7 +133,7 @@ export class NotificationAdapter { private isNotificationNeeded(updatedAtMap: Map, notification: GitHub.Thread): boolean { const lastUpdatedAt = updatedAtMap.get(notification.id); - return lastUpdatedAt === undefined || notification.updated_at > lastUpdatedAt; + return lastUpdatedAt === undefined || new Date(notification.updated_at) > lastUpdatedAt; } private buildProjectNotification(data: GitHub.Thread): UINotification { diff --git a/src/test/resources/client/github-client-scenarios.json b/src/test/resources/client/github-client-scenarios.json new file mode 100644 index 0000000..29a3cf8 --- /dev/null +++ b/src/test/resources/client/github-client-scenarios.json @@ -0,0 +1,733 @@ +{ + "fetch-all-threads": { + "request": { + "url": "https://api.github.com/notifications?false", + "method": "GET", + "headers": { + "Authorization": "Bearer fake-token", + "X-GitHub-Api-Version": "2022-11-28" + } + }, + "response": { + "status": 200, + "headers": { + "X-Poll-Interval": "60" + }, + "data": [ + { + "id": "6801720895", + "unread": true, + "reason": "ci_activity", + "updated_at": "2023-06-18T11:24:27Z", + "last_read_at": "2023-06-18T13:30:15Z", + "subject": { + "title": "Sanity checks workflow run failed for unit-testing branch", + "url": null, + "latest_comment_url": null, + "type": "CheckSuite" + }, + "repository": { + "id": 534557096, + "node_id": "R_kgDOH9yxqA", + "name": "gnome-github-manager", + "full_name": "mackdk/gnome-github-manager", + "private": false, + "owner": { + "login": "mackdk", + "id": 2292684, + "node_id": "MDQ6VXNlcjIyOTI2ODQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/2292684?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mackdk", + "html_url": "https://github.com/mackdk", + "followers_url": "https://api.github.com/users/mackdk/followers", + "following_url": "https://api.github.com/users/mackdk/following{/other_user}", + "gists_url": "https://api.github.com/users/mackdk/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mackdk/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mackdk/subscriptions", + "organizations_url": "https://api.github.com/users/mackdk/orgs", + "repos_url": "https://api.github.com/users/mackdk/repos", + "events_url": "https://api.github.com/users/mackdk/events{/privacy}", + "received_events_url": "https://api.github.com/users/mackdk/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/mackdk/gnome-github-manager", + "description": "Integrate GitHub within the GNOME Desktop Environment", + "fork": true, + "url": "https://api.github.com/repos/mackdk/gnome-github-manager", + "forks_url": "https://api.github.com/repos/mackdk/gnome-github-manager/forks", + "keys_url": "https://api.github.com/repos/mackdk/gnome-github-manager/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/mackdk/gnome-github-manager/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/mackdk/gnome-github-manager/teams", + "hooks_url": "https://api.github.com/repos/mackdk/gnome-github-manager/hooks", + "issue_events_url": "https://api.github.com/repos/mackdk/gnome-github-manager/issues/events{/number}", + "events_url": "https://api.github.com/repos/mackdk/gnome-github-manager/events", + "assignees_url": "https://api.github.com/repos/mackdk/gnome-github-manager/assignees{/user}", + "branches_url": "https://api.github.com/repos/mackdk/gnome-github-manager/branches{/branch}", + "tags_url": "https://api.github.com/repos/mackdk/gnome-github-manager/tags", + "blobs_url": "https://api.github.com/repos/mackdk/gnome-github-manager/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/mackdk/gnome-github-manager/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/mackdk/gnome-github-manager/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/mackdk/gnome-github-manager/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/mackdk/gnome-github-manager/statuses/{sha}", + "languages_url": "https://api.github.com/repos/mackdk/gnome-github-manager/languages", + "stargazers_url": "https://api.github.com/repos/mackdk/gnome-github-manager/stargazers", + "contributors_url": "https://api.github.com/repos/mackdk/gnome-github-manager/contributors", + "subscribers_url": "https://api.github.com/repos/mackdk/gnome-github-manager/subscribers", + "subscription_url": "https://api.github.com/repos/mackdk/gnome-github-manager/subscription", + "commits_url": "https://api.github.com/repos/mackdk/gnome-github-manager/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/mackdk/gnome-github-manager/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/mackdk/gnome-github-manager/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/mackdk/gnome-github-manager/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/mackdk/gnome-github-manager/contents/{+path}", + "compare_url": "https://api.github.com/repos/mackdk/gnome-github-manager/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/mackdk/gnome-github-manager/merges", + "archive_url": "https://api.github.com/repos/mackdk/gnome-github-manager/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/mackdk/gnome-github-manager/downloads", + "issues_url": "https://api.github.com/repos/mackdk/gnome-github-manager/issues{/number}", + "pulls_url": "https://api.github.com/repos/mackdk/gnome-github-manager/pulls{/number}", + "milestones_url": "https://api.github.com/repos/mackdk/gnome-github-manager/milestones{/number}", + "notifications_url": "https://api.github.com/repos/mackdk/gnome-github-manager/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/mackdk/gnome-github-manager/labels{/name}", + "releases_url": "https://api.github.com/repos/mackdk/gnome-github-manager/releases{/id}", + "deployments_url": "https://api.github.com/repos/mackdk/gnome-github-manager/deployments" + }, + "url": "https://api.github.com/notifications/threads/6801720895", + "subscription_url": "https://api.github.com/notifications/threads/6801720895/subscription" + }, + { + "id": "6716405570", + "unread": true, + "reason": "author", + "updated_at": "2023-06-12T07:53:11Z", + "last_read_at": "2023-06-18T13:13:40Z", + "subject": { + "title": "Remove wrong dependencies from classpath", + "url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122", + "latest_comment_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/comments/1586777038", + "type": "PullRequest" + }, + "repository": { + "id": 143291989, + "node_id": "MDEwOlJlcG9zaXRvcnkxNDMyOTE5ODk=", + "name": "uyuni", + "full_name": "uyuni-project/uyuni", + "private": false, + "owner": { + "login": "uyuni-project", + "id": 39272261, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM5MjcyMjYx", + "avatar_url": "https://avatars.githubusercontent.com/u/39272261?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/uyuni-project", + "html_url": "https://github.com/uyuni-project", + "followers_url": "https://api.github.com/users/uyuni-project/followers", + "following_url": "https://api.github.com/users/uyuni-project/following{/other_user}", + "gists_url": "https://api.github.com/users/uyuni-project/gists{/gist_id}", + "starred_url": "https://api.github.com/users/uyuni-project/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/uyuni-project/subscriptions", + "organizations_url": "https://api.github.com/users/uyuni-project/orgs", + "repos_url": "https://api.github.com/users/uyuni-project/repos", + "events_url": "https://api.github.com/users/uyuni-project/events{/privacy}", + "received_events_url": "https://api.github.com/users/uyuni-project/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/uyuni-project/uyuni", + "description": "Source code for Uyuni", + "fork": false, + "url": "https://api.github.com/repos/uyuni-project/uyuni", + "forks_url": "https://api.github.com/repos/uyuni-project/uyuni/forks", + "keys_url": "https://api.github.com/repos/uyuni-project/uyuni/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/uyuni-project/uyuni/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/uyuni-project/uyuni/teams", + "hooks_url": "https://api.github.com/repos/uyuni-project/uyuni/hooks", + "issue_events_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/events{/number}", + "events_url": "https://api.github.com/repos/uyuni-project/uyuni/events", + "assignees_url": "https://api.github.com/repos/uyuni-project/uyuni/assignees{/user}", + "branches_url": "https://api.github.com/repos/uyuni-project/uyuni/branches{/branch}", + "tags_url": "https://api.github.com/repos/uyuni-project/uyuni/tags", + "blobs_url": "https://api.github.com/repos/uyuni-project/uyuni/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/uyuni-project/uyuni/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/uyuni-project/uyuni/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/uyuni-project/uyuni/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/uyuni-project/uyuni/statuses/{sha}", + "languages_url": "https://api.github.com/repos/uyuni-project/uyuni/languages", + "stargazers_url": "https://api.github.com/repos/uyuni-project/uyuni/stargazers", + "contributors_url": "https://api.github.com/repos/uyuni-project/uyuni/contributors", + "subscribers_url": "https://api.github.com/repos/uyuni-project/uyuni/subscribers", + "subscription_url": "https://api.github.com/repos/uyuni-project/uyuni/subscription", + "commits_url": "https://api.github.com/repos/uyuni-project/uyuni/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/uyuni-project/uyuni/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/uyuni-project/uyuni/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/uyuni-project/uyuni/contents/{+path}", + "compare_url": "https://api.github.com/repos/uyuni-project/uyuni/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/uyuni-project/uyuni/merges", + "archive_url": "https://api.github.com/repos/uyuni-project/uyuni/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/uyuni-project/uyuni/downloads", + "issues_url": "https://api.github.com/repos/uyuni-project/uyuni/issues{/number}", + "pulls_url": "https://api.github.com/repos/uyuni-project/uyuni/pulls{/number}", + "milestones_url": "https://api.github.com/repos/uyuni-project/uyuni/milestones{/number}", + "notifications_url": "https://api.github.com/repos/uyuni-project/uyuni/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/uyuni-project/uyuni/labels{/name}", + "releases_url": "https://api.github.com/repos/uyuni-project/uyuni/releases{/id}", + "deployments_url": "https://api.github.com/repos/uyuni-project/uyuni/deployments" + }, + "url": "https://api.github.com/notifications/threads/6716405570", + "subscription_url": "https://api.github.com/notifications/threads/6716405570/subscription" + } + ] + } + }, + "fetch-threads-fails": { + "request": { + "url": "https://api.github.com/notifications?false", + "method": "GET", + "headers": { + "Authorization": "Bearer fake-token", + "X-GitHub-Api-Version": "2022-11-28" + } + }, + "response": { + "status": 401, + "data": { + "message": "Custom message from server", + "documentation_url": "https://docs.github.com/rest" + } + } + }, + "get-subject": { + "request": { + "url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122", + "method": "GET", + "headers": { + "Authorization": "Bearer fake-token", + "X-GitHub-Api-Version": "2022-11-28" + } + }, + "response": { + "status": 200, + "headers": { + "X-Poll-Interval": "240" + }, + "data": { + "url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122", + "id": 1386206421, + "node_id": "PR_kwDOCIp2Vc5Sn9TV", + "html_url": "https://github.com/uyuni-project/uyuni/pull/7122", + "diff_url": "https://github.com/uyuni-project/uyuni/pull/7122.diff", + "patch_url": "https://github.com/uyuni-project/uyuni/pull/7122.patch", + "issue_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/7122", + "number": 7122, + "state": "open", + "locked": false, + "title": "Remove wrong dependencies from classpath", + "user": { + "login": "mackdk", + "id": 2292684, + "node_id": "MDQ6VXNlcjIyOTI2ODQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/2292684?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mackdk", + "html_url": "https://github.com/mackdk", + "followers_url": "https://api.github.com/users/mackdk/followers", + "following_url": "https://api.github.com/users/mackdk/following{/other_user}", + "gists_url": "https://api.github.com/users/mackdk/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mackdk/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mackdk/subscriptions", + "organizations_url": "https://api.github.com/users/mackdk/orgs", + "repos_url": "https://api.github.com/users/mackdk/repos", + "events_url": "https://api.github.com/users/mackdk/events{/privacy}", + "received_events_url": "https://api.github.com/users/mackdk/received_events", + "type": "User", + "site_admin": false + }, + "body": "## What does this PR change?\r\n\r\nThis PR removes CheckStyle and JaCoCo from compilation and runtime classpath. This two jars contains bundled all their dependencies which can be used by mistake while coding. For example, Guava was used in unit test even though it should not be a dependency of Uyuni (which is addressed in this PR as well). Moreover, the presence of Saxon in the CheckStyle jar meant that, during the execution of unit tests, it was used in place of Xalan. \r\n\r\n### **NOTE: Currently one test is failing due to this change see the comment below**\r\n\r\nFinally, this PR also contains a small refactoring, moving class `SparkTestUtils` to `com.redhat.rhn.testing` (the package currently used for test utility classes) and moving `ResponseMappersTest` to `test`.\r\n\r\n## GUI diff\r\n\r\nNo difference.\r\n\r\n- [X] **DONE**\r\n\r\n## Documentation\r\n- No documentation needed: only internal and user invisible changes\r\n\r\n- [X] **DONE**\r\n\r\n## Test coverage\r\n- No tests: only build related refactorings\r\n\r\n**WIP: A test is now failing due to the parser change from Saxon to Xerces**\r\n\r\n- [ ] **DONE**\r\n\r\n## Links\r\n\r\nFixes #\r\nTracks # **add downstream PR, if any**\r\n\r\n- [ ] **DONE**\r\n\r\n## Changelogs\r\n\r\nMake sure the changelogs entries you are adding are compliant with https://github.com/uyuni-project/uyuni/wiki/Contributing#changelogs and https://github.com/uyuni-project/uyuni/wiki/Contributing#uyuni-projectuyuni-repository\r\n\r\nIf you don't need a changelog check, please mark this checkbox:\r\n\r\n- [X] No changelog needed\r\n\r\nIf you uncheck the checkbox after the PR is created, you will need to re-run `changelog_test` (see below)\r\n\r\n\r\n## Re-run a test\r\n\r\nIf you need to re-run a test, please mark the related checkbox, it will be unchecked automatically once it has re-run:\r\n\r\n- [ ] Re-run test \"changelog_test\"\r\n- [ ] Re-run test \"backend_unittests_pgsql\"\r\n- [ ] Re-run test \"java_pgsql_tests\"\r\n- [ ] Re-run test \"schema_migration_test_pgsql\"\r\n- [ ] Re-run test \"susemanager_unittests\"\r\n- [ ] Re-run test \"javascript_lint\"\r\n- [ ] Re-run test \"spacecmd_unittests\"\r\n", + "created_at": "2023-06-09T13:30:34Z", + "updated_at": "2023-06-12T07:53:00Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "d2b31bc7fbc68d6c046d9852e45145d75e941115", + "assignee": null, + "assignees": [], + "requested_reviewers": [ + { + "login": "cbosdo", + "id": 397931, + "node_id": "MDQ6VXNlcjM5NzkzMQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/397931?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/cbosdo", + "html_url": "https://github.com/cbosdo", + "followers_url": "https://api.github.com/users/cbosdo/followers", + "following_url": "https://api.github.com/users/cbosdo/following{/other_user}", + "gists_url": "https://api.github.com/users/cbosdo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cbosdo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cbosdo/subscriptions", + "organizations_url": "https://api.github.com/users/cbosdo/orgs", + "repos_url": "https://api.github.com/users/cbosdo/repos", + "events_url": "https://api.github.com/users/cbosdo/events{/privacy}", + "received_events_url": "https://api.github.com/users/cbosdo/received_events", + "type": "User", + "site_admin": false + }, + { + "login": "renner", + "id": 729454, + "node_id": "MDQ6VXNlcjcyOTQ1NA==", + "avatar_url": "https://avatars.githubusercontent.com/u/729454?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/renner", + "html_url": "https://github.com/renner", + "followers_url": "https://api.github.com/users/renner/followers", + "following_url": "https://api.github.com/users/renner/following{/other_user}", + "gists_url": "https://api.github.com/users/renner/gists{/gist_id}", + "starred_url": "https://api.github.com/users/renner/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/renner/subscriptions", + "organizations_url": "https://api.github.com/users/renner/orgs", + "repos_url": "https://api.github.com/users/renner/repos", + "events_url": "https://api.github.com/users/renner/events{/privacy}", + "received_events_url": "https://api.github.com/users/renner/received_events", + "type": "User", + "site_admin": false + }, + { + "login": "admd", + "id": 12951268, + "node_id": "MDQ6VXNlcjEyOTUxMjY4", + "avatar_url": "https://avatars.githubusercontent.com/u/12951268?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/admd", + "html_url": "https://github.com/admd", + "followers_url": "https://api.github.com/users/admd/followers", + "following_url": "https://api.github.com/users/admd/following{/other_user}", + "gists_url": "https://api.github.com/users/admd/gists{/gist_id}", + "starred_url": "https://api.github.com/users/admd/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/admd/subscriptions", + "organizations_url": "https://api.github.com/users/admd/orgs", + "repos_url": "https://api.github.com/users/admd/repos", + "events_url": "https://api.github.com/users/admd/events{/privacy}", + "received_events_url": "https://api.github.com/users/admd/received_events", + "type": "User", + "site_admin": false + } + ], + "requested_teams": [], + "labels": [ + { + "id": 1676996420, + "node_id": "MDU6TGFiZWwxNjc2OTk2NDIw", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/java", + "name": "java", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 1676996422, + "node_id": "MDU6TGFiZWwxNjc2OTk2NDIy", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/java_lint_checkstyle", + "name": "java_lint_checkstyle", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 1676996424, + "node_id": "MDU6TGFiZWwxNjc2OTk2NDI0", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/java_pgsql_tests", + "name": "java_pgsql_tests", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 1727907989, + "node_id": "MDU6TGFiZWwxNzI3OTA3OTg5", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/API", + "name": "API", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 1956574081, + "node_id": "MDU6TGFiZWwxOTU2NTc0MDgx", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/content-management", + "name": "content-management", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 2812694171, + "node_id": "MDU6TGFiZWwyODEyNjk0MTcx", + "url": "https://api.github.com/repos/uyuni-project/uyuni/labels/old-ui", + "name": "old-ui", + "color": "ededed", + "default": false, + "description": null + } + ], + "milestone": null, + "draft": true, + "commits_url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122/commits", + "review_comments_url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122/comments", + "review_comment_url": "https://api.github.com/repos/uyuni-project/uyuni/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/7122/comments", + "statuses_url": "https://api.github.com/repos/uyuni-project/uyuni/statuses/c46a1c800a29b9471b796036d339dbc806de597e", + "head": { + "label": "mackdk:remove-wrong-dependencies-from-classpath", + "ref": "remove-wrong-dependencies-from-classpath", + "sha": "c46a1c800a29b9471b796036d339dbc806de597e", + "user": { + "login": "mackdk", + "id": 2292684, + "node_id": "MDQ6VXNlcjIyOTI2ODQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/2292684?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mackdk", + "html_url": "https://github.com/mackdk", + "followers_url": "https://api.github.com/users/mackdk/followers", + "following_url": "https://api.github.com/users/mackdk/following{/other_user}", + "gists_url": "https://api.github.com/users/mackdk/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mackdk/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mackdk/subscriptions", + "organizations_url": "https://api.github.com/users/mackdk/orgs", + "repos_url": "https://api.github.com/users/mackdk/repos", + "events_url": "https://api.github.com/users/mackdk/events{/privacy}", + "received_events_url": "https://api.github.com/users/mackdk/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 357859750, + "node_id": "MDEwOlJlcG9zaXRvcnkzNTc4NTk3NTA=", + "name": "uyuni", + "full_name": "mackdk/uyuni", + "private": false, + "owner": { + "login": "mackdk", + "id": 2292684, + "node_id": "MDQ6VXNlcjIyOTI2ODQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/2292684?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mackdk", + "html_url": "https://github.com/mackdk", + "followers_url": "https://api.github.com/users/mackdk/followers", + "following_url": "https://api.github.com/users/mackdk/following{/other_user}", + "gists_url": "https://api.github.com/users/mackdk/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mackdk/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mackdk/subscriptions", + "organizations_url": "https://api.github.com/users/mackdk/orgs", + "repos_url": "https://api.github.com/users/mackdk/repos", + "events_url": "https://api.github.com/users/mackdk/events{/privacy}", + "received_events_url": "https://api.github.com/users/mackdk/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/mackdk/uyuni", + "description": "Source code for Uyuni", + "fork": true, + "url": "https://api.github.com/repos/mackdk/uyuni", + "forks_url": "https://api.github.com/repos/mackdk/uyuni/forks", + "keys_url": "https://api.github.com/repos/mackdk/uyuni/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/mackdk/uyuni/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/mackdk/uyuni/teams", + "hooks_url": "https://api.github.com/repos/mackdk/uyuni/hooks", + "issue_events_url": "https://api.github.com/repos/mackdk/uyuni/issues/events{/number}", + "events_url": "https://api.github.com/repos/mackdk/uyuni/events", + "assignees_url": "https://api.github.com/repos/mackdk/uyuni/assignees{/user}", + "branches_url": "https://api.github.com/repos/mackdk/uyuni/branches{/branch}", + "tags_url": "https://api.github.com/repos/mackdk/uyuni/tags", + "blobs_url": "https://api.github.com/repos/mackdk/uyuni/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/mackdk/uyuni/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/mackdk/uyuni/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/mackdk/uyuni/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/mackdk/uyuni/statuses/{sha}", + "languages_url": "https://api.github.com/repos/mackdk/uyuni/languages", + "stargazers_url": "https://api.github.com/repos/mackdk/uyuni/stargazers", + "contributors_url": "https://api.github.com/repos/mackdk/uyuni/contributors", + "subscribers_url": "https://api.github.com/repos/mackdk/uyuni/subscribers", + "subscription_url": "https://api.github.com/repos/mackdk/uyuni/subscription", + "commits_url": "https://api.github.com/repos/mackdk/uyuni/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/mackdk/uyuni/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/mackdk/uyuni/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/mackdk/uyuni/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/mackdk/uyuni/contents/{+path}", + "compare_url": "https://api.github.com/repos/mackdk/uyuni/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/mackdk/uyuni/merges", + "archive_url": "https://api.github.com/repos/mackdk/uyuni/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/mackdk/uyuni/downloads", + "issues_url": "https://api.github.com/repos/mackdk/uyuni/issues{/number}", + "pulls_url": "https://api.github.com/repos/mackdk/uyuni/pulls{/number}", + "milestones_url": "https://api.github.com/repos/mackdk/uyuni/milestones{/number}", + "notifications_url": "https://api.github.com/repos/mackdk/uyuni/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/mackdk/uyuni/labels{/name}", + "releases_url": "https://api.github.com/repos/mackdk/uyuni/releases{/id}", + "deployments_url": "https://api.github.com/repos/mackdk/uyuni/deployments", + "created_at": "2021-04-14T10:09:09Z", + "updated_at": "2022-01-10T10:46:34Z", + "pushed_at": "2023-06-15T07:49:41Z", + "git_url": "git://github.com/mackdk/uyuni.git", + "ssh_url": "git@github.com:mackdk/uyuni.git", + "clone_url": "https://github.com/mackdk/uyuni.git", + "svn_url": "https://github.com/mackdk/uyuni", + "homepage": "https://www.uyuni-project.org/", + "size": 842774, + "stargazers_count": 1, + "watchers_count": 1, + "language": "Java", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0", + "spdx_id": "GPL-2.0", + "url": "https://api.github.com/licenses/gpl-2.0", + "node_id": "MDc6TGljZW5zZTg=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 1, + "default_branch": "master" + } + }, + "base": { + "label": "uyuni-project:master", + "ref": "master", + "sha": "bb2a42d5dc420a72b537d368625e6fbdc6f50b1c", + "user": { + "login": "uyuni-project", + "id": 39272261, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM5MjcyMjYx", + "avatar_url": "https://avatars.githubusercontent.com/u/39272261?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/uyuni-project", + "html_url": "https://github.com/uyuni-project", + "followers_url": "https://api.github.com/users/uyuni-project/followers", + "following_url": "https://api.github.com/users/uyuni-project/following{/other_user}", + "gists_url": "https://api.github.com/users/uyuni-project/gists{/gist_id}", + "starred_url": "https://api.github.com/users/uyuni-project/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/uyuni-project/subscriptions", + "organizations_url": "https://api.github.com/users/uyuni-project/orgs", + "repos_url": "https://api.github.com/users/uyuni-project/repos", + "events_url": "https://api.github.com/users/uyuni-project/events{/privacy}", + "received_events_url": "https://api.github.com/users/uyuni-project/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 143291989, + "node_id": "MDEwOlJlcG9zaXRvcnkxNDMyOTE5ODk=", + "name": "uyuni", + "full_name": "uyuni-project/uyuni", + "private": false, + "owner": { + "login": "uyuni-project", + "id": 39272261, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM5MjcyMjYx", + "avatar_url": "https://avatars.githubusercontent.com/u/39272261?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/uyuni-project", + "html_url": "https://github.com/uyuni-project", + "followers_url": "https://api.github.com/users/uyuni-project/followers", + "following_url": "https://api.github.com/users/uyuni-project/following{/other_user}", + "gists_url": "https://api.github.com/users/uyuni-project/gists{/gist_id}", + "starred_url": "https://api.github.com/users/uyuni-project/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/uyuni-project/subscriptions", + "organizations_url": "https://api.github.com/users/uyuni-project/orgs", + "repos_url": "https://api.github.com/users/uyuni-project/repos", + "events_url": "https://api.github.com/users/uyuni-project/events{/privacy}", + "received_events_url": "https://api.github.com/users/uyuni-project/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/uyuni-project/uyuni", + "description": "Source code for Uyuni", + "fork": false, + "url": "https://api.github.com/repos/uyuni-project/uyuni", + "forks_url": "https://api.github.com/repos/uyuni-project/uyuni/forks", + "keys_url": "https://api.github.com/repos/uyuni-project/uyuni/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/uyuni-project/uyuni/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/uyuni-project/uyuni/teams", + "hooks_url": "https://api.github.com/repos/uyuni-project/uyuni/hooks", + "issue_events_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/events{/number}", + "events_url": "https://api.github.com/repos/uyuni-project/uyuni/events", + "assignees_url": "https://api.github.com/repos/uyuni-project/uyuni/assignees{/user}", + "branches_url": "https://api.github.com/repos/uyuni-project/uyuni/branches{/branch}", + "tags_url": "https://api.github.com/repos/uyuni-project/uyuni/tags", + "blobs_url": "https://api.github.com/repos/uyuni-project/uyuni/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/uyuni-project/uyuni/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/uyuni-project/uyuni/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/uyuni-project/uyuni/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/uyuni-project/uyuni/statuses/{sha}", + "languages_url": "https://api.github.com/repos/uyuni-project/uyuni/languages", + "stargazers_url": "https://api.github.com/repos/uyuni-project/uyuni/stargazers", + "contributors_url": "https://api.github.com/repos/uyuni-project/uyuni/contributors", + "subscribers_url": "https://api.github.com/repos/uyuni-project/uyuni/subscribers", + "subscription_url": "https://api.github.com/repos/uyuni-project/uyuni/subscription", + "commits_url": "https://api.github.com/repos/uyuni-project/uyuni/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/uyuni-project/uyuni/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/uyuni-project/uyuni/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/uyuni-project/uyuni/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/uyuni-project/uyuni/contents/{+path}", + "compare_url": "https://api.github.com/repos/uyuni-project/uyuni/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/uyuni-project/uyuni/merges", + "archive_url": "https://api.github.com/repos/uyuni-project/uyuni/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/uyuni-project/uyuni/downloads", + "issues_url": "https://api.github.com/repos/uyuni-project/uyuni/issues{/number}", + "pulls_url": "https://api.github.com/repos/uyuni-project/uyuni/pulls{/number}", + "milestones_url": "https://api.github.com/repos/uyuni-project/uyuni/milestones{/number}", + "notifications_url": "https://api.github.com/repos/uyuni-project/uyuni/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/uyuni-project/uyuni/labels{/name}", + "releases_url": "https://api.github.com/repos/uyuni-project/uyuni/releases{/id}", + "deployments_url": "https://api.github.com/repos/uyuni-project/uyuni/deployments", + "created_at": "2018-08-02T12:31:20Z", + "updated_at": "2023-06-07T16:19:43Z", + "pushed_at": "2023-06-18T14:10:53Z", + "git_url": "git://github.com/uyuni-project/uyuni.git", + "ssh_url": "git@github.com:uyuni-project/uyuni.git", + "clone_url": "https://github.com/uyuni-project/uyuni.git", + "svn_url": "https://github.com/uyuni-project/uyuni", + "homepage": "https://www.uyuni-project.org/", + "size": 1170180, + "stargazers_count": 317, + "watchers_count": 317, + "language": "Java", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": true, + "forks_count": 144, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 365, + "license": { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0", + "spdx_id": "GPL-2.0", + "url": "https://api.github.com/licenses/gpl-2.0", + "node_id": "MDc6TGljZW5zZTg=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "cucumber", + "hacktoberfest", + "java", + "linux", + "postgresql", + "python", + "reactjs", + "saltstack", + "spacewalk", + "suse-manager", + "system-management", + "uyuni" + ], + "visibility": "public", + "forks": 144, + "open_issues": 365, + "watchers": 317, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122" + }, + "html": { + "href": "https://github.com/uyuni-project/uyuni/pull/7122" + }, + "issue": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/issues/7122" + }, + "comments": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/issues/7122/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/pulls/7122/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/uyuni-project/uyuni/statuses/c46a1c800a29b9471b796036d339dbc806de597e" + } + }, + "author_association": "CONTRIBUTOR", + "auto_merge": null, + "active_lock_reason": null, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 3, + "review_comments": 0, + "maintainer_can_modify": true, + "commits": 4, + "additions": 35, + "deletions": 28, + "changed_files": 15 + } + } + }, + "mark-thread-read": { + "request": { + "url": "https://api.github.com/notifications/threads/6801720895", + "method": "PATCH", + "headers": { + "Authorization": "Bearer fake-token", + "X-GitHub-Api-Version": "2022-11-28" + } + }, + "response": { + "status": 205, + "data": "" + } + }, + "mark-all-threads-read": { + "request": { + "url": "https://api.github.com/notifications", + "method": "PUT", + "headers": { + "Authorization": "Bearer fake-token", + "X-GitHub-Api-Version": "2022-11-28" + }, + "contentType": "application/json", + "data": { + "last_read_at": "2023-06-18T16:30:17.000Z", + "read": true + } + }, + "response": { + "status": 205, + "data": "" + } + } +} diff --git a/src/test/typescript/client/GitHubClentFactory.test.ts b/src/test/typescript/client/GitHubClentFactory.test.ts new file mode 100644 index 0000000..188abc1 --- /dev/null +++ b/src/test/typescript/client/GitHubClentFactory.test.ts @@ -0,0 +1,34 @@ +import { assert } from 'chai'; + +import * as GitHubClientFactory from '@github-manager/client/GitHubClientFactory'; +import { GitHubClientImpl } from '@github-manager/client/GitHubClientImpl'; +import { Soup2HttpEngine } from '@github-manager/client/Soup2HttpEngine'; +import { Soup3HttpEngine } from '@github-manager/client/Soup3HttpEngine'; + +import '@test-suite/globals'; + +describe('GitHubClientFactory', () => { + it('can create client for Soup 2', () => { + imports.gi.versions.Soup = '2.4'; + + const client = GitHubClientFactory.newClient('my-domain', 'my-token'); + + assert.instanceOf(client, GitHubClientImpl); + assert.instanceOf((client as GitHubClientImpl).httpEngine, Soup2HttpEngine); + }); + + it('can create client for Soup 3', () => { + imports.gi.versions.Soup = '3.0'; + + const client = GitHubClientFactory.newClient('my-domain', 'my-token'); + + assert.instanceOf(client, GitHubClientImpl); + assert.instanceOf((client as GitHubClientImpl).httpEngine, Soup3HttpEngine); + }); + + it('throws error if Soup version is not recogniezed', () => { + imports.gi.versions.Soup = '5.3.7'; + + assert.throws(() => GitHubClientFactory.newClient('my-domain', 'my-token'), 'Unsupported Soup version: 5.3.7'); + }); +}); diff --git a/src/test/typescript/client/GitHubClientImpl.test.ts b/src/test/typescript/client/GitHubClientImpl.test.ts new file mode 100644 index 0000000..81dc783 --- /dev/null +++ b/src/test/typescript/client/GitHubClientImpl.test.ts @@ -0,0 +1,239 @@ +import { readFileSync } from 'fs'; + +import { assert } from 'chai'; +import { stub } from 'sinon'; + +import * as GitHub from '@github-manager/client/GitHubApiTypes'; +import { ApiError } from '@github-manager/client/GitHubClient'; +import { GitHubClientImpl } from '@github-manager/client/GitHubClientImpl'; +import { HttpEngine, HttpRequest, HttpResponse } from '@github-manager/client/Http'; + +import '@test-suite/globals'; + +interface ScenarioRequest { + url: string; + method: string; + headers: Record | undefined; + contentType: string | undefined; + data: unknown; +} + +interface ScenarioResponse { + status: number; + headers: Record | undefined; + data: unknown; +} + +interface ClientScenario { + request: ScenarioRequest; + response: ScenarioResponse; +} + +describe('GitHubClientImpl', () => { + let engineStub: HttpEngine; + let client: GitHubClientImpl; + let scenariosMap: Map; + + before(() => { + const scenarioJSON = readFileSync(testResource('github-client-scenarios.json'), { encoding: 'utf-8' }); + scenariosMap = new Map( + Object.entries(JSON.parse(scenarioJSON) as Record) + ); + }); + + beforeEach(() => { + // Default implementation to allow stubbing + engineStub = { send: (_request: HttpRequest) => Promise.resolve(new HttpResponse(200, 0)) }; + }); + + it('initializes the properties correctly', () => { + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + + assert.equal(client.domain, 'github.com'); + assert.equal(client.token, 'fake-token'); + assert.equal(client.pollInterval, 60); + assert.equal(client.baseUrl, 'api.github.com'); + + client = new GitHubClientImpl('mydomain.com', 'fake-token', engineStub); + + assert.equal(client.domain, 'mydomain.com'); + assert.equal(client.token, 'fake-token'); + assert.equal(client.pollInterval, 60); + assert.equal(client.baseUrl, 'mydomain.com/api/v3'); + }); + + it('can change domain and token', () => { + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + + assert.equal(client.domain, 'github.com'); + assert.equal(client.token, 'fake-token'); + assert.equal(client.baseUrl, 'api.github.com'); + + client.domain = 'mydomain.com'; + client.token = 'mydomain-token'; + + assert.equal(client.domain, 'mydomain.com'); + assert.equal(client.token, 'mydomain-token'); + assert.equal(client.baseUrl, 'mydomain.com/api/v3'); + }); + + it('can fetch all notification threads', async () => { + const scenario: ClientScenario = getScenario('fetch-all-threads'); + const sendStub = stub(engineStub, 'send').returns(Promise.resolve(asHttpResponse(scenario.response))); + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + + const threads = await client.listThreads(false); + + assert.isTrue(sendStub.calledOnce); + assertRequest(sendStub.firstCall.firstArg as HttpRequest, scenario.request); + + assert.equal(threads.length, 2); + + assert.equal(threads[0].id, '6801720895'); + assert.equal(threads[0].reason, 'ci_activity'); + assert.equal(threads[0].last_read_at, '2023-06-18T13:30:15Z'); + assert.equal(threads[0].updated_at, '2023-06-18T11:24:27Z'); + assert.equal(threads[0].unread, true); + assert.equal(threads[0].url, 'https://api.github.com/notifications/threads/6801720895'); + + assert.equal(threads[0].repository.id, 534557096); + assert.equal(threads[0].repository.node_id, 'R_kgDOH9yxqA'); + assert.equal(threads[0].repository.name, 'gnome-github-manager'); + assert.equal(threads[0].repository.html_url, 'https://github.com/mackdk/gnome-github-manager'); + + assert.equal(threads[0].repository.owner.id, 2292684); + assert.equal(threads[0].repository.owner.node_id, 'MDQ6VXNlcjIyOTI2ODQ='); + assert.equal(threads[0].repository.owner.login, 'mackdk'); + assert.isUndefined(threads[0].repository.owner.name); + assert.isUndefined(threads[0].repository.owner.email); + assert.equal(threads[0].repository.owner.avatar_url, 'https://avatars.githubusercontent.com/u/2292684?v=4'); + + // Verify the poll interval returned by GitHub + assert.equal(client.pollInterval, 60); + }); + + it('can handle error response while retrieving threads', async () => { + const scenario: ClientScenario = getScenario('fetch-threads-fails'); + const sendStub = stub(engineStub, 'send').returns(Promise.resolve(asHttpResponse(scenario.response))); + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + + try { + await client.listThreads(); + assert.fail('Error should have been thrown'); + } catch (err) { + assert.isTrue(err instanceof ApiError); + + const apiError = err as ApiError; + assert.equal(apiError.statusCode, 401); + assert.equal(apiError.message, 'Custom message from server'); + } + + assert.isTrue(sendStub.calledOnce); + assertRequest(sendStub.firstCall.firstArg as HttpRequest, scenario.request); + }); + + it('can handle error while executing request', async () => { + const error = new Error('Request too fake to be executed'); + const sendStub = stub(engineStub, 'send').throws(error); + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + + try { + await client.listThreads(); + assert.fail('Error should have been thrown'); + } catch (err) { + assert.isTrue(err instanceof ApiError); + + const apiError = err as ApiError; + assert.equal(apiError.statusCode, -1); + assert.equal(apiError.message, 'Unable to perform API call'); + assert.equal(apiError.cause, error); + } + + assert.isTrue(sendStub.calledOnce); + }); + + it('can retrive html url for subject', async () => { + const scenario: ClientScenario = getScenario('get-subject'); + const sendStub = stub(engineStub, 'send').returns(Promise.resolve(asHttpResponse(scenario.response))); + + const subject = { url: 'https://api.github.com/repos/uyuni-project/uyuni/pulls/7122' } as GitHub.Subject; + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + const webUrl = await client.getWebUrlForSubject(subject); + + assert.isTrue(sendStub.calledOnce); + assertRequest(sendStub.firstCall.firstArg as HttpRequest, scenario.request); + + assert.equal(webUrl, 'https://github.com/uyuni-project/uyuni/pull/7122'); + + // Verify the poll interval returned by GitHub + assert.equal(client.pollInterval, 240); + }); + + it('can mark a thread as read', async () => { + const scenario: ClientScenario = getScenario('mark-thread-read'); + const sendStub = stub(engineStub, 'send').returns(Promise.resolve(asHttpResponse(scenario.response))); + + const thread = { url: 'https://api.github.com/notifications/threads/6801720895' } as GitHub.Thread; + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + await client.markThreadAsRead(thread); + + assert.isTrue(sendStub.calledOnce); + assertRequest(sendStub.firstCall.firstArg as HttpRequest, scenario.request); + }); + + it('can mark all threads as read', async () => { + const scenario: ClientScenario = getScenario('mark-all-threads-read'); + const sendStub = stub(engineStub, 'send').returns(Promise.resolve(asHttpResponse(scenario.response))); + + client = new GitHubClientImpl('github.com', 'fake-token', engineStub); + await client.markAllThreadsAsRead(new Date('2023-06-18T16:30:17Z')); + + assert.isTrue(sendStub.calledOnce); + assertRequest(sendStub.firstCall.firstArg as HttpRequest, scenario.request); + }); + + // Retrieves the scenario with the given name + function getScenario(name: string): ClientScenario { + const scenario = scenariosMap.get(name); + if (scenario === undefined) { + assert.fail('Cannot find client scenario ' + name); + } + + return scenario; + } + + // Converts the response in the scenario to an HttpResponse object + function asHttpResponse(response: ScenarioResponse): HttpResponse { + const stringifiedData = response.data !== undefined ? JSON.stringify(response.data) : undefined; + const headersMap = new Map(Object.entries(response.headers ?? {})); + + return new HttpResponse(response.status, stringifiedData?.length ?? 0, stringifiedData, headersMap); + } + + // Checks if the actual HttpRequest as the expected values + function assertRequest(request: HttpRequest, expectedRequest: ScenarioRequest): void { + assert.equal(request.url, expectedRequest.url); + assert.equal(request.method, expectedRequest.method); + if (expectedRequest.data !== undefined) { + if (request.body === undefined) { + assert.fail('Request body is undefined'); + } + + assert.equal(request.body.data, JSON.stringify(expectedRequest.data)); + assert.equal(request.body.contentType, expectedRequest.contentType); + } else { + assert.isUndefined(request.body); + } + + const expectedHeaders = expectedRequest.headers; + if (expectedHeaders !== undefined) { + assert.equal(request.headers.size, Object.keys(expectedHeaders).length); + request.headers.forEach((value, key) => assert.equal(value, expectedHeaders[key])); + } + } +}); diff --git a/src/test/typescript/globals.ts b/src/test/typescript/globals.ts index 2d5015b..a8a040d 100644 --- a/src/test/typescript/globals.ts +++ b/src/test/typescript/globals.ts @@ -1,7 +1,11 @@ +import { dirname } from 'path'; + +import callsites from 'callsites'; + export {}; declare global { - // eslint-disable-next-line no-var + /* eslint-disable no-var -- Var is needed to add stuff to node global */ export var imports: { gi: { versions: { @@ -10,13 +14,27 @@ declare global { }; }; }; + /* eslint-enable no-var */ + + export function testResource(filename: string): string; export function log(message: string): void; - export function logError(e: Error | string | unknown, message: string): void; + export function logError(e: unknown, message: string): void; } +global.imports = { gi: { versions: { Adw: '1', Soup: '3.0' } } }; + +global.testResource = (filename: string) => { + const callerFile = callsites()[1].getFileName(); + if (callerFile === null) { + throw new Error("Unable to retrieve test resources path for file '" + filename + "'"); + } + + return dirname(callerFile).replace('test/typescript', 'test/resources') + '/' + filename; +}; + global.log = (message: string) => console.log(message); -global.logError = (err: Error | string | unknown, message: string) => { +global.logError = (err: unknown, message: string) => { console.error(message); console.error(err); }; diff --git a/src/test/typescript/stubs/gi-types/glib2.ts b/src/test/typescript/stubs/gi-types/glib2.ts index f4c64bd..dc16dda 100644 --- a/src/test/typescript/stubs/gi-types/glib2.ts +++ b/src/test/typescript/stubs/gi-types/glib2.ts @@ -27,3 +27,21 @@ export function source_remove(handle: number): boolean { return false; } + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Uri { + public static parse(_uri_string: string, _flags: number): Uri { + return new Uri(); + } +} + +export class Bytes { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + public constructor(_data?: Uint8Array) { + // Stub, nothing to do + } + + public get_data(): Uint8Array { + return new Uint8Array(); + } +} diff --git a/src/test/typescript/stubs/gi-types/soup2.ts b/src/test/typescript/stubs/gi-types/soup2.ts new file mode 100644 index 0000000..955191c --- /dev/null +++ b/src/test/typescript/stubs/gi-types/soup2.ts @@ -0,0 +1,57 @@ +export enum MemoryUse { + STATIC = 0, + TAKE = 1, + COPY = 2, + TEMPORARY = 3, +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class URI { + public static new: (_url: string) => URI = () => new URI(); +} + +export class MessageHeaders { + public append(_name: string, _value: string): void { + // Stub, nothing to do + } + + public foreach(_func: (name: string, value: string) => void): void { + // Stub, nothing to do + } +} + +export class MessageBody { + public length: number; + public data: string; + + public constructor() { + this.length = 0; + this.data = ''; + } +} + +export class Message { + public requestHeaders: MessageHeaders; + + public statusCode: number; + + public responseBody: MessageBody; + + public constructor(_properties: unknown) { + this.requestHeaders = new MessageHeaders(); + this.statusCode = 0; + this.responseBody = new MessageBody(); + } + + public set_request(_content_type: string | null, _req_use: MemoryUse, _req_body?: Uint8Array | null): void { + // Stub, nothing to do + } +} + +export class Session { + public user_agent: string = 'stub-libsoup2'; + + public queue_message: (_msg: Message, _callback?: unknown) => void = () => { + // Stub, nothing to do + }; +} diff --git a/src/test/typescript/stubs/gi-types/soup3.ts b/src/test/typescript/stubs/gi-types/soup3.ts new file mode 100644 index 0000000..9f7db4d --- /dev/null +++ b/src/test/typescript/stubs/gi-types/soup3.ts @@ -0,0 +1,43 @@ +import { Bytes, Uri } from './glib2'; + +export const HTTP_URI_FLAGS = 0; + +export class MessageHeaders { + public append(_name: string, _value: string): void { + // Stub, nothing to do + } + + public foreach(_func: (name: string, value: string) => void): void { + // Stub, nothing to do + } +} + +export class Message { + public requestHeaders: MessageHeaders; + + public responseHeaders: MessageHeaders; + + public statusCode: number; + + public static new_from_uri(_method: string, _uri: Uri): Message { + return new Message(); + } + + public constructor() { + this.requestHeaders = new MessageHeaders(); + this.responseHeaders = new MessageHeaders(); + this.statusCode = 0; + } + + public set_request_body_from_bytes(_content_type?: string | null, _bytes?: Bytes | null): void { + // Stub, nothing to do + } +} + +export class Session { + public user_agent: string = 'stub-libsoup3'; + + public send_and_read_async(_msg: Message, _io_priority: number, _cancellable?: unknown): Promise { + return Promise.resolve(new Bytes()); + } +}