github-issue-tower-defence-management 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.env.example +6 -0
  2. package/.eslintrc.cjs +55 -0
  3. package/.github/CODEOWNERS +2 -0
  4. package/.github/workflows/assign-all-cards-to-owner.yml +14 -0
  5. package/.github/workflows/commit-lint.yml +54 -0
  6. package/.github/workflows/configs/commitlint.config.js +27 -0
  7. package/.github/workflows/create-pr.yml +64 -0
  8. package/.github/workflows/format.yml +25 -0
  9. package/.github/workflows/publish.yml +47 -0
  10. package/.github/workflows/test.yml +45 -0
  11. package/.github/workflows/umino-project.yml +181 -0
  12. package/.prettierignore +22 -0
  13. package/.prettierrc +5 -0
  14. package/CHANGELOG.md +49 -0
  15. package/CONTRIBUTING.md +107 -0
  16. package/README.md +108 -0
  17. package/bin/adapter/entry-points/cli/index.js +26 -0
  18. package/bin/adapter/entry-points/cli/index.js.map +1 -0
  19. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +142 -0
  20. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -0
  21. package/bin/adapter/repositories/AxiosSlackRepository.js +124 -0
  22. package/bin/adapter/repositories/AxiosSlackRepository.js.map +1 -0
  23. package/bin/adapter/repositories/BaseGitHubRepository.js +136 -0
  24. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -0
  25. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +123 -0
  26. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -0
  27. package/bin/adapter/repositories/GraphqlProjectRepository.js +167 -0
  28. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -0
  29. package/bin/adapter/repositories/LocalStorageCacheRepository.js +46 -0
  30. package/bin/adapter/repositories/LocalStorageCacheRepository.js.map +1 -0
  31. package/bin/adapter/repositories/LocalStorageRepository.js +30 -0
  32. package/bin/adapter/repositories/LocalStorageRepository.js.map +1 -0
  33. package/bin/adapter/repositories/SystemDateRepository.js +23 -0
  34. package/bin/adapter/repositories/SystemDateRepository.js.map +1 -0
  35. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +148 -0
  36. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -0
  37. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js +48 -0
  38. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js.map +1 -0
  39. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +120 -0
  40. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +1 -0
  41. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +485 -0
  42. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -0
  43. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +114 -0
  44. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +1 -0
  45. package/bin/adapter/repositories/issue/RestIssueRepository.js +79 -0
  46. package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -0
  47. package/bin/adapter/repositories/issue/issueTimelineUtils.js +38 -0
  48. package/bin/adapter/repositories/issue/issueTimelineUtils.js.map +1 -0
  49. package/bin/adapter/repositories/utils.js +45 -0
  50. package/bin/adapter/repositories/utils.js.map +1 -0
  51. package/bin/domain/entities/Issue.js +3 -0
  52. package/bin/domain/entities/Issue.js.map +1 -0
  53. package/bin/domain/entities/Member.js +3 -0
  54. package/bin/domain/entities/Member.js.map +1 -0
  55. package/bin/domain/entities/Project.js +3 -0
  56. package/bin/domain/entities/Project.js.map +1 -0
  57. package/bin/domain/entities/ProjectField.js +3 -0
  58. package/bin/domain/entities/ProjectField.js.map +1 -0
  59. package/bin/domain/entities/ProjectFieldSingleSelect.js +3 -0
  60. package/bin/domain/entities/ProjectFieldSingleSelect.js.map +1 -0
  61. package/bin/domain/entities/ProjectFieldSingleSelectOption.js +3 -0
  62. package/bin/domain/entities/ProjectFieldSingleSelectOption.js.map +1 -0
  63. package/bin/domain/entities/WorkingTime.js +3 -0
  64. package/bin/domain/entities/WorkingTime.js.map +1 -0
  65. package/bin/domain/usecases/ActionAnnouncementUseCase.js +46 -0
  66. package/bin/domain/usecases/ActionAnnouncementUseCase.js.map +1 -0
  67. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js +116 -0
  68. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js.map +1 -0
  69. package/bin/domain/usecases/ClearNextActionHourUseCase.js +38 -0
  70. package/bin/domain/usecases/ClearNextActionHourUseCase.js.map +1 -0
  71. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js +180 -0
  72. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js.map +1 -0
  73. package/bin/domain/usecases/HandleScheduledEventUseCase.js +122 -0
  74. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -0
  75. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +35 -0
  76. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -0
  77. package/bin/domain/usecases/adapter-interfaces/DateRepository.js +3 -0
  78. package/bin/domain/usecases/adapter-interfaces/DateRepository.js.map +1 -0
  79. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js +3 -0
  80. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js.map +1 -0
  81. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js +3 -0
  82. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js.map +1 -0
  83. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js +3 -0
  84. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js.map +1 -0
  85. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js +3 -0
  86. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js.map +1 -0
  87. package/bin/index.js +13 -0
  88. package/bin/index.js.map +1 -0
  89. package/commitlint.config.js +6 -0
  90. package/jest.config.js +19 -0
  91. package/package.json +80 -0
  92. package/renovate.json +37 -0
  93. package/src/adapter/entry-points/cli/index.test.ts +20 -0
  94. package/src/adapter/entry-points/cli/index.ts +36 -0
  95. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +95 -0
  96. package/src/adapter/repositories/AxiosSlackRepository.test.ts +119 -0
  97. package/src/adapter/repositories/AxiosSlackRepository.ts +184 -0
  98. package/src/adapter/repositories/BaseGitHubRepository.test.ts +95 -0
  99. package/src/adapter/repositories/BaseGitHubRepository.ts +172 -0
  100. package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +124 -0
  101. package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +151 -0
  102. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +46 -0
  103. package/src/adapter/repositories/GraphqlProjectRepository.ts +236 -0
  104. package/src/adapter/repositories/LocalStorageCacheRepository.test.ts +146 -0
  105. package/src/adapter/repositories/LocalStorageCacheRepository.ts +53 -0
  106. package/src/adapter/repositories/LocalStorageRepository.integration.test.ts +142 -0
  107. package/src/adapter/repositories/LocalStorageRepository.test.ts +161 -0
  108. package/src/adapter/repositories/LocalStorageRepository.ts +21 -0
  109. package/src/adapter/repositories/SystemDateRepository.ts +20 -0
  110. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +158 -0
  111. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -0
  112. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +26 -0
  113. package/src/adapter/repositories/issue/ApiV3IssueRepository.ts +59 -0
  114. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +6610 -0
  115. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +127 -0
  116. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +49 -0
  117. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +745 -0
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +71 -0
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +263 -0
  120. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +29 -0
  121. package/src/adapter/repositories/issue/RestIssueRepository.ts +105 -0
  122. package/src/adapter/repositories/issue/issueTimelineUtils.test.ts +79 -0
  123. package/src/adapter/repositories/issue/issueTimelineUtils.ts +52 -0
  124. package/src/adapter/repositories/utils.test.ts +40 -0
  125. package/src/adapter/repositories/utils.ts +50 -0
  126. package/src/domain/entities/Issue.ts +23 -0
  127. package/src/domain/entities/Member.ts +3 -0
  128. package/src/domain/entities/Project.ts +29 -0
  129. package/src/domain/entities/ProjectField.ts +3 -0
  130. package/src/domain/entities/ProjectFieldSingleSelect.ts +8 -0
  131. package/src/domain/entities/ProjectFieldSingleSelectOption.ts +8 -0
  132. package/src/domain/entities/WorkingTime.ts +8 -0
  133. package/src/domain/usecases/ActionAnnouncementUseCase.ts +76 -0
  134. package/src/domain/usecases/AnalyzeProblemByIssueUseCase.ts +209 -0
  135. package/src/domain/usecases/ClearNextActionHourUseCase.ts +51 -0
  136. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.test.ts +382 -0
  137. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.ts +284 -0
  138. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +58 -0
  139. package/src/domain/usecases/HandleScheduledEventUseCase.ts +209 -0
  140. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +46 -0
  141. package/src/domain/usecases/adapter-interfaces/DateRepository.ts +5 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +44 -0
  143. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  144. package/src/domain/usecases/adapter-interfaces/SlackRepository.ts +20 -0
  145. package/src/domain/usecases/adapter-interfaces/SpreadsheetRepository.ts +18 -0
  146. package/src/index.test.ts +8 -0
  147. package/src/index.ts +7 -0
  148. package/tsconfig.build.json +11 -0
  149. package/tsconfig.json +22 -0
  150. package/types/adapter/entry-points/cli/index.d.ts +3 -0
  151. package/types/adapter/entry-points/cli/index.d.ts.map +1 -0
  152. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts +11 -0
  153. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -0
  154. package/types/adapter/repositories/AxiosSlackRepository.d.ts +13 -0
  155. package/types/adapter/repositories/AxiosSlackRepository.d.ts.map +1 -0
  156. package/types/adapter/repositories/BaseGitHubRepository.d.ts +32 -0
  157. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -0
  158. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +13 -0
  159. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -0
  160. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +13 -0
  161. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -0
  162. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts +12 -0
  163. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts.map +1 -0
  164. package/types/adapter/repositories/LocalStorageRepository.d.ts +7 -0
  165. package/types/adapter/repositories/LocalStorageRepository.d.ts.map +1 -0
  166. package/types/adapter/repositories/SystemDateRepository.d.ts +7 -0
  167. package/types/adapter/repositories/SystemDateRepository.d.ts.map +1 -0
  168. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +38 -0
  169. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -0
  170. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts +17 -0
  171. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts.map +1 -0
  172. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +31 -0
  173. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +1 -0
  174. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +39 -0
  175. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -0
  176. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +83 -0
  177. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +1 -0
  178. package/types/adapter/repositories/issue/RestIssueRepository.d.ts +16 -0
  179. package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -0
  180. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts +12 -0
  181. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts.map +1 -0
  182. package/types/adapter/repositories/utils.d.ts +4 -0
  183. package/types/adapter/repositories/utils.d.ts.map +1 -0
  184. package/types/domain/entities/Issue.d.ts +24 -0
  185. package/types/domain/entities/Issue.d.ts.map +1 -0
  186. package/types/domain/entities/Member.d.ts +4 -0
  187. package/types/domain/entities/Member.d.ts.map +1 -0
  188. package/types/domain/entities/Project.d.ts +29 -0
  189. package/types/domain/entities/Project.d.ts.map +1 -0
  190. package/types/domain/entities/ProjectField.d.ts +3 -0
  191. package/types/domain/entities/ProjectField.d.ts.map +1 -0
  192. package/types/domain/entities/ProjectFieldSingleSelect.d.ts +8 -0
  193. package/types/domain/entities/ProjectFieldSingleSelect.d.ts.map +1 -0
  194. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts +8 -0
  195. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts.map +1 -0
  196. package/types/domain/entities/WorkingTime.d.ts +8 -0
  197. package/types/domain/entities/WorkingTime.d.ts.map +1 -0
  198. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts +17 -0
  199. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts.map +1 -0
  200. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts +26 -0
  201. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts.map +1 -0
  202. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts +14 -0
  203. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts.map +1 -0
  204. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts +50 -0
  205. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts.map +1 -0
  206. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +63 -0
  207. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -0
  208. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +14 -0
  209. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -0
  210. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts +6 -0
  211. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts.map +1 -0
  212. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +23 -0
  213. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -0
  214. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +6 -0
  215. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -0
  216. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts +9 -0
  217. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts.map +1 -0
  218. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts +6 -0
  219. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts.map +1 -0
  220. package/types/index.d.ts +10 -0
  221. package/types/index.d.ts.map +1 -0
@@ -0,0 +1,95 @@
1
+ import YAML from 'yaml';
2
+ import TYPIA from 'typia';
3
+ import fs from 'fs';
4
+ import { SystemDateRepository } from '../../repositories/SystemDateRepository';
5
+ import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
6
+ import { GoogleSpreadsheetRepository } from '../../repositories/GoogleSpreadsheetRepository';
7
+ import { GraphqlProjectRepository } from '../../repositories/GraphqlProjectRepository';
8
+ import { ApiV3IssueRepository } from '../../repositories/issue/ApiV3IssueRepository';
9
+ import { CheerioIssueRepository } from '../../repositories/issue/CheerioIssueRepository';
10
+ import { RestIssueRepository } from '../../repositories/issue/RestIssueRepository';
11
+ import { GraphqlProjectItemRepository } from '../../repositories/issue/GraphqlProjectItemRepository';
12
+ import { ApiV3CheerioRestIssueRepository } from '../../repositories/issue/ApiV3CheerioRestIssueRepository';
13
+ import { GenerateWorkingTimeReportUseCase } from '../../../domain/usecases/GenerateWorkingTimeReportUseCase';
14
+ import { HandleScheduledEventUseCase } from '../../../domain/usecases/HandleScheduledEventUseCase';
15
+ import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
16
+ import { ActionAnnouncementUseCase } from '../../../domain/usecases/ActionAnnouncementUseCase';
17
+ import { SetWorkflowManagementIssueToStoryUseCase } from '../../../domain/usecases/SetWorkflowManagementIssueToStoryUseCase';
18
+ import { InternalGraphqlIssueRepository } from '../../repositories/issue/InternalGraphqlIssueRepository';
19
+ import { ClearNextActionHourUseCase } from '../../../domain/usecases/ClearNextActionHourUseCase';
20
+ import { AnalyzeProblemByIssueUseCase } from '../../../domain/usecases/AnalyzeProblemByIssueUseCase';
21
+ import { Issue } from '../../../domain/entities/Issue';
22
+ import { Project } from '../../../domain/entities/Project';
23
+
24
+ export class HandleScheduledEventUseCaseHandler {
25
+ handle = async (
26
+ configFilePath: string,
27
+ ): Promise<{
28
+ project: Project;
29
+ issues: Issue[];
30
+ cacheUsed: boolean;
31
+ targetDateTimes: Date[];
32
+ }> => {
33
+ const configFileContent = fs.readFileSync(configFilePath, 'utf8');
34
+ const input: unknown = YAML.parse(configFileContent);
35
+ type inputType = Parameters<HandleScheduledEventUseCase['run']>[0];
36
+ if (!TYPIA.is<inputType>(input)) {
37
+ throw new Error(
38
+ `Invalid input: ${JSON.stringify(input)}\n\n${JSON.stringify(TYPIA.validate<inputType>(input))}`,
39
+ );
40
+ }
41
+
42
+ const systemDateRepository = new SystemDateRepository();
43
+ const localStorageRepository = new LocalStorageRepository();
44
+ const googleSpreadsheetRepository = new GoogleSpreadsheetRepository(
45
+ localStorageRepository,
46
+ );
47
+ const localStorageCacheRepository = new LocalStorageCacheRepository(
48
+ localStorageRepository,
49
+ );
50
+ const projectRepository = new GraphqlProjectRepository();
51
+ const apiV3IssueRepository = new ApiV3IssueRepository();
52
+ const internalGraphqlIssueRepository = new InternalGraphqlIssueRepository();
53
+ const cheerioIssueRepository = new CheerioIssueRepository(
54
+ internalGraphqlIssueRepository,
55
+ );
56
+ const restIssueRepository = new RestIssueRepository();
57
+ const graphqlProjectItemRepository = new GraphqlProjectItemRepository();
58
+ const issueRepository = new ApiV3CheerioRestIssueRepository(
59
+ apiV3IssueRepository,
60
+ cheerioIssueRepository,
61
+ restIssueRepository,
62
+ graphqlProjectItemRepository,
63
+ localStorageCacheRepository,
64
+ );
65
+ const generateWorkingTimeReportUseCase =
66
+ new GenerateWorkingTimeReportUseCase(
67
+ issueRepository,
68
+ googleSpreadsheetRepository,
69
+ systemDateRepository,
70
+ );
71
+ const actionAnnouncement = new ActionAnnouncementUseCase(issueRepository);
72
+ const setWorkflowManagementIssueToStoryUseCase =
73
+ new SetWorkflowManagementIssueToStoryUseCase(issueRepository);
74
+ const clearNextActionHourUseCase = new ClearNextActionHourUseCase(
75
+ issueRepository,
76
+ );
77
+ const analyzeProblemByIssueUseCase = new AnalyzeProblemByIssueUseCase(
78
+ issueRepository,
79
+ systemDateRepository,
80
+ );
81
+ const handleScheduledEventUseCase = new HandleScheduledEventUseCase(
82
+ generateWorkingTimeReportUseCase,
83
+ actionAnnouncement,
84
+ setWorkflowManagementIssueToStoryUseCase,
85
+ clearNextActionHourUseCase,
86
+ analyzeProblemByIssueUseCase,
87
+ systemDateRepository,
88
+ googleSpreadsheetRepository,
89
+ projectRepository,
90
+ issueRepository,
91
+ );
92
+
93
+ return await handleScheduledEventUseCase.run(input);
94
+ };
95
+ }
@@ -0,0 +1,119 @@
1
+ import dotenv from 'dotenv';
2
+ import { AxiosSlackRepository } from './AxiosSlackRepository';
3
+ import fs from 'fs';
4
+ import https from 'https';
5
+ import path from 'path';
6
+
7
+ dotenv.config();
8
+
9
+ const SLACK_USER_TOKEN = process.env.SLACK_USER_TOKEN;
10
+ const TEST_CHANNEL_NAME = 'test-integration';
11
+ const TEST_USER_NAME = 'shikata.hiromi_test2';
12
+ const TEST_IMAGE_URL = 'https://i.imgur.com/Zi3qToQ.jpeg';
13
+ const TEST_IMAGE_PATH = './tmp/test/fixtures/test-image.png';
14
+
15
+ if (!SLACK_USER_TOKEN) {
16
+ throw new Error('SLACK_USER_TOKEN is required');
17
+ }
18
+
19
+ describe('AxiosSlackRepository Integration Tests', () => {
20
+ jest.setTimeout(30 * 1000);
21
+ let slackRepository: AxiosSlackRepository;
22
+
23
+ beforeAll(() => {
24
+ slackRepository = new AxiosSlackRepository(SLACK_USER_TOKEN);
25
+ });
26
+
27
+ describe('postMessageToChannel', () => {
28
+ it('should post a message to a channel', async () => {
29
+ const message = `Test message ${new Date().toISOString()}`;
30
+
31
+ await expect(
32
+ slackRepository.postMessageToChannel(message, TEST_CHANNEL_NAME),
33
+ ).resolves.not.toThrow();
34
+ });
35
+
36
+ it('should throw error for non-existent channel', async () => {
37
+ const message = 'Test message';
38
+
39
+ await expect(
40
+ slackRepository.postMessageToChannel(message, 'non-existent-channel'),
41
+ ).rejects.toThrow('Channel non-existent-channel not found');
42
+ });
43
+ });
44
+
45
+ describe('postMessageToChannelThread', () => {
46
+ it('should post a message to a thread', async () => {
47
+ const message = `Test thread message ${new Date().toISOString()}`;
48
+ const { threadTs } = await slackRepository.postMessageToChannel(
49
+ `message for thread`,
50
+ TEST_CHANNEL_NAME,
51
+ );
52
+
53
+ await expect(
54
+ slackRepository.postMessageToChannelThread(
55
+ message,
56
+ TEST_CHANNEL_NAME,
57
+ threadTs,
58
+ ),
59
+ ).resolves.not.toThrow();
60
+ });
61
+ });
62
+
63
+ describe('postMessageToChannelWithImage', () => {
64
+ it.skip('should post a message with image', async () => {
65
+ const message = `Test image message ${new Date().toISOString()}`;
66
+ if (!fs.existsSync(path.dirname(TEST_IMAGE_PATH))) {
67
+ fs.mkdirSync(path.dirname(TEST_IMAGE_PATH), { recursive: true });
68
+ const res = https.get(TEST_IMAGE_URL, (res) =>
69
+ res.pipe(fs.createWriteStream(TEST_IMAGE_PATH)),
70
+ );
71
+ await new Promise((resolve, reject) => {
72
+ res.on('end', resolve);
73
+ res.on('error', reject);
74
+ });
75
+ }
76
+
77
+ await expect(
78
+ slackRepository.postMessageToChannelWithImage(
79
+ message,
80
+ TEST_CHANNEL_NAME,
81
+ TEST_IMAGE_PATH,
82
+ ),
83
+ ).resolves.not.toThrow();
84
+ });
85
+
86
+ it('should throw error for non-existent image', async () => {
87
+ const message = 'Test message';
88
+
89
+ await expect(
90
+ slackRepository.postMessageToChannelWithImage(
91
+ message,
92
+ TEST_CHANNEL_NAME,
93
+ 'non-existent-image.png',
94
+ ),
95
+ ).rejects.toThrow();
96
+ });
97
+ });
98
+
99
+ describe('postMessageToDirectMessage', () => {
100
+ it('should post a direct message', async () => {
101
+ const message = `Test DM ${new Date().toISOString()}`;
102
+
103
+ await expect(
104
+ slackRepository.postMessageToDirectMessage(message, TEST_USER_NAME),
105
+ ).resolves.not.toThrow();
106
+ });
107
+
108
+ it('should throw error for non-existent user', async () => {
109
+ const message = 'Test message';
110
+
111
+ await expect(
112
+ slackRepository.postMessageToDirectMessage(
113
+ message,
114
+ 'non-existent-user',
115
+ ),
116
+ ).rejects.toThrow('User non-existent-user not found');
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,184 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import fs from 'fs';
3
+ import { SlackRepository } from '../../domain/usecases/adapter-interfaces/SlackRepository';
4
+
5
+ export class AxiosSlackRepository implements SlackRepository {
6
+ private readonly client: AxiosInstance;
7
+ private readonly baseUrl = 'https://slack.com/api';
8
+
9
+ constructor(userToken: string) {
10
+ if (!userToken.startsWith('xoxp-')) {
11
+ throw new Error('Invalid user token. It should start with xoxp-');
12
+ }
13
+ this.client = axios.create({
14
+ baseURL: this.baseUrl,
15
+ headers: {
16
+ Authorization: `Bearer ${userToken}`,
17
+ },
18
+ });
19
+ }
20
+
21
+ async postMessageToChannel(
22
+ message: string,
23
+ channelName: string,
24
+ ): Promise<{
25
+ threadTs: string;
26
+ }> {
27
+ const { data } = await this.client.get<{
28
+ ok: boolean;
29
+ channels: { id: string; name: string }[];
30
+ }>('/conversations.list');
31
+
32
+ const channel = data.channels.find((c) => c.name === channelName);
33
+ if (!channel) throw new Error(`Channel ${channelName} not found`);
34
+
35
+ const res = await this.client.post<{ ok: boolean; ts: string }>(
36
+ '/chat.postMessage',
37
+ {
38
+ channel: channel.id,
39
+ text: message,
40
+ },
41
+ );
42
+ if (!res.data.ok) {
43
+ throw new Error(`Failed to post message: ${JSON.stringify(res.data)}`);
44
+ }
45
+ return {
46
+ threadTs: res.data.ts,
47
+ };
48
+ }
49
+
50
+ async postMessageToChannelThread(
51
+ message: string,
52
+ channelName: string,
53
+ threadTs: string,
54
+ ): Promise<void> {
55
+ const { data } = await this.client.get<{
56
+ ok: boolean;
57
+ channels: { id: string; name: string }[];
58
+ }>('/conversations.list');
59
+
60
+ const channel = data.channels.find((c) => c.name === channelName);
61
+ if (!channel) throw new Error(`Channel ${channelName} not found`);
62
+
63
+ const res = await this.client.post<{ ok: boolean }>('/chat.postMessage', {
64
+ channel: channel.id,
65
+ text: message,
66
+ thread_ts: threadTs,
67
+ });
68
+ if (!res.data.ok) {
69
+ throw new Error(`Failed to post message: ${JSON.stringify(res.data)}`);
70
+ }
71
+ }
72
+
73
+ async postMessageToDirectMessage(
74
+ message: string,
75
+ userName: string,
76
+ ): Promise<void> {
77
+ const { data } = await this.client.get<{
78
+ ok: boolean;
79
+ members: { id: string; name: string }[];
80
+ }>('/users.list');
81
+
82
+ const user = data.members.find((u) => u.name === userName);
83
+ if (!user) throw new Error(`User ${userName} not found`);
84
+
85
+ const res = await this.client.post<{ ok: boolean }>('/chat.postMessage', {
86
+ channel: user.id,
87
+ text: message,
88
+ });
89
+ if (!res.data.ok) {
90
+ throw new Error(`Failed to post message: ${JSON.stringify(res.data)}`);
91
+ }
92
+ }
93
+ async postMessageToChannelWithImage(
94
+ message: string,
95
+ channelName: string,
96
+ imageFilePath: string,
97
+ ): Promise<void> {
98
+ const { data } = await this.client.get<{
99
+ ok: boolean;
100
+ channels: { id: string; name: string }[];
101
+ }>('/conversations.list');
102
+
103
+ const channel = data.channels.find((c) => c.name === channelName);
104
+ if (!channel) throw new Error(`Channel ${channelName} not found`);
105
+
106
+ const fileStats = fs.statSync(imageFilePath);
107
+ const fileName = imageFilePath.split('/').pop();
108
+
109
+ const uploadUrlRes = await this.client.post<{
110
+ ok: boolean;
111
+ upload_url: string;
112
+ file_id: string;
113
+ }>(
114
+ '/files.getUploadURLExternal',
115
+ {
116
+ filename: fileName,
117
+ length: fileStats.size,
118
+ },
119
+ {
120
+ headers: {
121
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
122
+ },
123
+ },
124
+ );
125
+ if (!uploadUrlRes.data.ok) {
126
+ throw new Error(
127
+ `Failed to get upload URL: ${JSON.stringify(uploadUrlRes.data)}`,
128
+ );
129
+ }
130
+ await new Promise((resolve) => setTimeout(resolve, 1000));
131
+
132
+ const fileContent = fs.readFileSync(imageFilePath);
133
+ await axios.post(uploadUrlRes.data.upload_url, fileContent, {
134
+ headers: {
135
+ 'Content-Type': 'application/octet-stream',
136
+ },
137
+ });
138
+ await new Promise((resolve) => setTimeout(resolve, 1000));
139
+
140
+ const completeRes = await this.client.post<{ ok: boolean }>(
141
+ '/files.completeUploadExternal',
142
+ {
143
+ files: [
144
+ {
145
+ id: uploadUrlRes.data.file_id,
146
+ },
147
+ ],
148
+ },
149
+ );
150
+ if (!completeRes.data.ok) {
151
+ throw new Error(
152
+ `Failed to complete upload: ${JSON.stringify(completeRes.data)}`,
153
+ );
154
+ }
155
+
156
+ const fileInfo = await this.client.get<{
157
+ ok: boolean;
158
+ file: { url_private: string };
159
+ }>(`/files.info?file=${uploadUrlRes.data.file_id}`);
160
+
161
+ if (!fileInfo.data.ok) {
162
+ throw new Error(
163
+ `Failed to get file info: ${JSON.stringify(fileInfo.data)}`,
164
+ );
165
+ }
166
+
167
+ const imageUrl = fileInfo.data.file.url_private;
168
+ await new Promise((resolve) => setTimeout(resolve, 2000));
169
+ const res = await this.client.post<{ ok: boolean }>('/chat.postMessage', {
170
+ channel: channel.id,
171
+ text: message,
172
+ blocks: [
173
+ {
174
+ type: 'image',
175
+ image_url: imageUrl,
176
+ alt_text: fileName,
177
+ },
178
+ ],
179
+ });
180
+ if (!res.data.ok) {
181
+ throw new Error(`Failed to post message: ${JSON.stringify(res.data)}`);
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,95 @@
1
+ import fs from 'fs';
2
+ import { BaseGitHubRepository } from './BaseGitHubRepository';
3
+ import resetAllMocks = jest.resetAllMocks;
4
+ describe('BaseGitHubRepository', () => {
5
+ const jsonFilePath = './tmp/github.com.cookies.json';
6
+ class TestGitHubRepository extends BaseGitHubRepository {
7
+ constructor() {
8
+ super(jsonFilePath, process.env.GH_TOKEN);
9
+ }
10
+ extractIssueFromUrlPublic = this.extractIssueFromUrl;
11
+ createHeaderPublic = this.createHeader;
12
+ createCookieStringFromFilePublic = this.createCookieStringFromFile;
13
+ isCookiePublic = this.isCookie;
14
+ }
15
+ const baseGitHubRepository: TestGitHubRepository = new TestGitHubRepository();
16
+ beforeAll(() => {
17
+ resetAllMocks();
18
+ const cookies = [
19
+ {
20
+ name: 'name',
21
+ value: 'value',
22
+ domain: 'domain',
23
+ path: 'path',
24
+ expires: 1,
25
+ httpOnly: true,
26
+ secure: true,
27
+ sameSite: 'Lax',
28
+ },
29
+ ];
30
+ fs.writeFileSync(jsonFilePath, JSON.stringify(cookies));
31
+ });
32
+ afterAll(() => {
33
+ fs.rmSync(jsonFilePath);
34
+ });
35
+
36
+ describe('extractIssueFromUrl', () => {
37
+ it('should return issue number', () => {
38
+ const extracted = baseGitHubRepository.extractIssueFromUrlPublic(
39
+ 'https://github.com/HiromiShikata/test-repository/issues/38',
40
+ );
41
+ expect(extracted).toEqual({
42
+ owner: 'HiromiShikata',
43
+ repo: 'test-repository',
44
+ issueNumber: 38,
45
+ isIssue: true,
46
+ });
47
+ });
48
+ });
49
+
50
+ describe('createHeader', () => {
51
+ it('should return headers with cookie', async () => {
52
+ const headers = await baseGitHubRepository.createHeaderPublic();
53
+ expect(headers).toHaveProperty('cookie');
54
+ });
55
+ });
56
+
57
+ describe('createCookieStringFromFile', () => {
58
+ it('should return cookie string', async () => {
59
+ const cookie =
60
+ await baseGitHubRepository.createCookieStringFromFilePublic();
61
+ expect(cookie).toEqual(
62
+ 'name=value; Domain=domain; Path=path; Expires=Thu, 01 Jan 1970 00:00:01 GMT; HttpOnly; Secure; SameSite=Lax',
63
+ );
64
+ });
65
+ });
66
+
67
+ describe('isCookie', () => {
68
+ it('should return true if cookie is valid', () => {
69
+ const cookie = {
70
+ name: 'name',
71
+ value: 'value',
72
+ domain: 'domain',
73
+ path: 'path',
74
+ expires: 1,
75
+ httpOnly: true,
76
+ secure: true,
77
+ sameSite: 'lax',
78
+ };
79
+ expect(baseGitHubRepository.isCookiePublic(cookie)).toBe(true);
80
+ });
81
+
82
+ it('should return false if cookie is invalid', () => {
83
+ const cookie = {
84
+ name: 'name',
85
+ value: 'value',
86
+ domain: 'domain',
87
+ path: 'path',
88
+ expires: 1,
89
+ httpOnly: true,
90
+ secure: true,
91
+ };
92
+ expect(baseGitHubRepository.isCookiePublic(cookie)).toBe(false);
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,172 @@
1
+ import { promises as fsPromises } from 'fs';
2
+ import { serialize } from 'cookie';
3
+ import axios, { AxiosError } from 'axios';
4
+ import { getCookieContent } from 'gh-cookie';
5
+ import fs from 'fs';
6
+
7
+ axios.interceptors.response.use(
8
+ (response) => response,
9
+ (error: AxiosError) => {
10
+ if (error.response) {
11
+ throw new Error(`API Error: ${error.response.status}`);
12
+ }
13
+ throw new Error('Network Error');
14
+ },
15
+ );
16
+
17
+ interface Cookie {
18
+ name: string;
19
+ value: string;
20
+ domain?: string;
21
+ path?: string;
22
+ expires?: number;
23
+ httpOnly?: boolean;
24
+ secure?: boolean;
25
+ sameSite?: 'lax' | 'strict' | 'none';
26
+ }
27
+
28
+ export class BaseGitHubRepository {
29
+ cookie: string | null;
30
+ constructor(
31
+ readonly jsonFilePath: string = './tmp/github.com.cookies.json',
32
+ readonly ghToken: string = process.env.GH_TOKEN || 'dummy',
33
+ readonly ghUserName: string | undefined = process.env.GH_USER_NAME,
34
+ readonly ghUserPassword: string | undefined = process.env.GH_USER_PASSWORD,
35
+ readonly ghAuthenticatorKey: string | undefined = process.env
36
+ .GH_AUTHENTICATOR_KEY,
37
+ ) {
38
+ this.cookie = null;
39
+ }
40
+ protected extractIssueFromUrl = (
41
+ issueUrl: string,
42
+ ): { owner: string; repo: string; issueNumber: number; isIssue: boolean } => {
43
+ const match = issueUrl.match(
44
+ /https:\/\/github.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/,
45
+ );
46
+ if (!match) {
47
+ throw new Error(`Invalid issue URL: ${issueUrl}`);
48
+ }
49
+ const [, owner, repo, pullOrIssue, issueNumberStr] = match;
50
+ const issueNumber = parseInt(issueNumberStr, 10);
51
+ if (isNaN(issueNumber)) {
52
+ throw new Error(
53
+ `Invalid issue number: ${issueNumberStr}. URL: ${issueUrl}`,
54
+ );
55
+ }
56
+ return { owner, repo, issueNumber, isIssue: pullOrIssue === 'issues' };
57
+ };
58
+
59
+ getCookie = async (): Promise<string> => {
60
+ if (!this.cookie) {
61
+ this.cookie = await this.createCookieStringFromFile();
62
+ }
63
+ return this.cookie;
64
+ };
65
+ createHeader = async (): Promise<object> => {
66
+ const cookie = await this.getCookie();
67
+ const headers = {
68
+ accept:
69
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
70
+ 'accept-language':
71
+ 'en-US,en;q=0.9,es-MX;q=0.8,es;q=0.7,ja-JP;q=0.6,ja;q=0.5',
72
+ 'cache-control': 'max-age=0',
73
+ 'sec-ch-ua':
74
+ '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
75
+ 'sec-ch-ua-mobile': '?0',
76
+ 'sec-ch-ua-platform': '"Linux"',
77
+ 'sec-fetch-dest': 'document',
78
+ 'sec-fetch-mode': 'navigate',
79
+ 'sec-fetch-site': 'same-origin',
80
+ 'sec-fetch-user': '?1',
81
+ 'upgrade-insecure-requests': '1',
82
+ Referer: 'https://github.com/orgs/community/discussions/30979',
83
+ 'Referrer-Policy': 'no-referrer-when-downgrade',
84
+ };
85
+ return {
86
+ ...headers,
87
+ cookie: cookie,
88
+ };
89
+ };
90
+ protected createCookieStringFromFile = async (): Promise<string> => {
91
+ if (!fs.existsSync(this.jsonFilePath)) {
92
+ if (
93
+ !this.ghUserName ||
94
+ !this.ghUserPassword ||
95
+ !this.ghAuthenticatorKey
96
+ ) {
97
+ throw new Error('No cookie file and no credentials provided');
98
+ }
99
+ const cookie = await getCookieContent(
100
+ this.ghUserName,
101
+ this.ghUserPassword,
102
+ this.ghAuthenticatorKey,
103
+ );
104
+ await fsPromises.writeFile(this.jsonFilePath, cookie);
105
+ }
106
+ const data = await fsPromises.readFile(this.jsonFilePath, {
107
+ encoding: 'utf-8',
108
+ });
109
+ const cookiesData: unknown = JSON.parse(data);
110
+ return this.generateCookieHeaderFromJson(cookiesData);
111
+ };
112
+ protected isCookie = (cookie: object): cookie is Cookie => {
113
+ return (
114
+ 'name' in cookie &&
115
+ typeof cookie.name === 'string' &&
116
+ 'value' in cookie &&
117
+ typeof cookie.value === 'string' &&
118
+ 'domain' in cookie &&
119
+ typeof cookie.domain === 'string' &&
120
+ 'path' in cookie &&
121
+ typeof cookie.path === 'string' &&
122
+ 'expires' in cookie &&
123
+ typeof cookie.expires === 'number' &&
124
+ 'httpOnly' in cookie &&
125
+ typeof cookie.httpOnly === 'boolean' &&
126
+ 'secure' in cookie &&
127
+ typeof cookie.secure === 'boolean' &&
128
+ 'sameSite' in cookie &&
129
+ typeof cookie.sameSite === 'string' &&
130
+ ['lax', 'strict', 'none'].indexOf(cookie.sameSite) !== -1
131
+ );
132
+ };
133
+
134
+ protected generateCookieHeaderFromJson = async (
135
+ cookieData: unknown,
136
+ ): Promise<string> => {
137
+ if (!Array.isArray(cookieData)) {
138
+ throw new Error('Invalid cookie array');
139
+ }
140
+
141
+ const cookies: Cookie[] = cookieData.map((cookieOrig: object) => {
142
+ const sameSite =
143
+ typeof cookieOrig !== 'object' ||
144
+ !('sameSite' in cookieOrig) ||
145
+ typeof cookieOrig.sameSite !== 'string'
146
+ ? 'none'
147
+ : cookieOrig.sameSite.toLowerCase();
148
+ const cookie = {
149
+ ...cookieOrig,
150
+ sameSite,
151
+ };
152
+
153
+ if (!this.isCookie(cookie)) {
154
+ throw new Error(`Invalid cookie properties: ${JSON.stringify(cookie)}`);
155
+ }
156
+ return cookie;
157
+ });
158
+ const cookieHeader = cookies
159
+ .map((cookie) =>
160
+ serialize(cookie.name, cookie.value, {
161
+ domain: cookie.domain,
162
+ path: cookie.path,
163
+ expires: cookie.expires ? new Date(cookie.expires * 1000) : undefined,
164
+ httpOnly: cookie.httpOnly,
165
+ secure: cookie.secure,
166
+ sameSite: cookie.sameSite,
167
+ }),
168
+ )
169
+ .join('; ');
170
+ return cookieHeader;
171
+ };
172
+ }