github-issue-tower-defence-management 1.146.3 → 1.146.5

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 (41) hide show
  1. package/.github/workflows/codeql.yml +2 -2
  2. package/CHANGELOG.md +14 -0
  3. package/bin/adapter/entry-points/cli/index.js +13 -20
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +16 -1
  6. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js +22 -1
  8. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/index-CPY6wV8C.js +82 -0
  10. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  11. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +26 -100
  12. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  13. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +49 -576
  14. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  15. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +31 -7
  16. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  17. package/package.json +6 -8
  18. package/src/adapter/entry-points/cli/index.ts +29 -24
  19. package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +68 -20
  20. package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +24 -0
  21. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +2 -2
  22. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +91 -0
  23. package/src/adapter/entry-points/console/consoleProjectResolver.ts +32 -0
  24. package/src/adapter/entry-points/console/ui-dist/assets/index-CPY6wV8C.js +82 -0
  25. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  26. package/src/adapter/entry-points/console/webServer.test.ts +2 -2
  27. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +31 -3
  28. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +68 -3
  29. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +35 -4
  30. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +3 -3
  31. package/tsconfig.json +0 -5
  32. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  33. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +2 -0
  34. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -1
  35. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +5 -0
  36. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  37. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  38. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  39. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  40. package/bin/adapter/entry-points/console/ui-dist/assets/index-Bmc_nF1d.js +0 -82
  41. package/src/adapter/entry-points/console/ui-dist/assets/index-Bmc_nF1d.js +0 -82
@@ -1,9 +1,55 @@
1
1
  import {
2
2
  createConsoleGithubTokenResolver,
3
3
  createConsoleIssueRepositoryResolver,
4
+ createConsoleProjectRepositoryResolver,
5
+ extractProjectOwner,
4
6
  extractRepositoryOwner,
5
7
  } from './consoleGithubTokenResolver';
6
8
 
9
+ describe('extractProjectOwner', () => {
10
+ it('should return the organization login of an organization project url', () => {
11
+ expect(
12
+ extractProjectOwner('https://github.com/orgs/acme/projects/18'),
13
+ ).toBe('acme');
14
+ });
15
+
16
+ it('should return the user login of a user project url', () => {
17
+ expect(
18
+ extractProjectOwner('https://github.com/users/acme-owner/projects/48'),
19
+ ).toBe('acme-owner');
20
+ });
21
+
22
+ it('should return null for a url that is not a project url', () => {
23
+ expect(
24
+ extractProjectOwner('https://github.com/acme/acme-repository/issues/178'),
25
+ ).toBeNull();
26
+ });
27
+ });
28
+
29
+ describe('createConsoleProjectRepositoryResolver', () => {
30
+ it('builds the project repository from the token of the owner in the project url', () => {
31
+ const resolve = createConsoleProjectRepositoryResolver<string>(
32
+ (repositoryOwner) => `token-of-${repositoryOwner}`,
33
+ (githubToken) => `repository-with-${githubToken}`,
34
+ );
35
+
36
+ expect(resolve('https://github.com/orgs/acme/projects/18')).toBe(
37
+ 'repository-with-token-of-acme',
38
+ );
39
+ });
40
+
41
+ it('throws when the project owner cannot be read from the project url', () => {
42
+ const resolve = createConsoleProjectRepositoryResolver<string>(
43
+ (repositoryOwner) => `token-of-${repositoryOwner}`,
44
+ (githubToken) => `repository-with-${githubToken}`,
45
+ );
46
+
47
+ expect(() => resolve('https://github.com/orgs/acme')).toThrow(
48
+ 'The project owner cannot be read from the project url: https://github.com/orgs/acme',
49
+ );
50
+ });
51
+ });
52
+
7
53
  describe('createConsoleIssueRepositoryResolver', () => {
8
54
  it('builds the issue repository from the token of the owner in the url', () => {
9
55
  const resolve = createConsoleIssueRepositoryResolver<string>(
@@ -12,8 +58,8 @@ describe('createConsoleIssueRepositoryResolver', () => {
12
58
  );
13
59
 
14
60
  expect(
15
- resolve('https://github.com/meta-site/hr-audit-mock/issues/178'),
16
- ).toBe('repository-with-token-of-meta-site');
61
+ resolve('https://github.com/acme-labs/acme-portal-mock/issues/178'),
62
+ ).toBe('repository-with-token-of-acme-labs');
17
63
  });
18
64
 
19
65
  it('throws when the repository owner cannot be read from the url', () => {
@@ -22,8 +68,10 @@ describe('createConsoleIssueRepositoryResolver', () => {
22
68
  (githubToken) => `repository-with-${githubToken}`,
23
69
  );
24
70
 
25
- expect(() => resolve('https://github.com/meta-site/hr-audit-mock')).toThrow(
26
- 'The repository owner cannot be read from the operated url: https://github.com/meta-site/hr-audit-mock',
71
+ expect(() =>
72
+ resolve('https://github.com/acme-labs/acme-portal-mock'),
73
+ ).toThrow(
74
+ 'The repository owner cannot be read from the operated url: https://github.com/acme-labs/acme-portal-mock',
27
75
  );
28
76
  });
29
77
  });
@@ -32,9 +80,9 @@ describe('extractRepositoryOwner', () => {
32
80
  it('should return the owner of an issue url', () => {
33
81
  expect(
34
82
  extractRepositoryOwner(
35
- 'https://github.com/meta-site/hr-audit-mock/issues/178',
83
+ 'https://github.com/acme-labs/acme-portal-mock/issues/178',
36
84
  ),
37
- ).toBe('meta-site');
85
+ ).toBe('acme-labs');
38
86
  });
39
87
 
40
88
  it('should return the owner of a pull request url', () => {
@@ -47,7 +95,7 @@ describe('extractRepositoryOwner', () => {
47
95
 
48
96
  it('should return null for a url that is not an issue or a pull request', () => {
49
97
  expect(
50
- extractRepositoryOwner('https://github.com/meta-site/hr-audit-mock'),
98
+ extractRepositoryOwner('https://github.com/acme-labs/acme-portal-mock'),
51
99
  ).toBeNull();
52
100
  });
53
101
  });
@@ -68,41 +116,41 @@ describe('createConsoleGithubTokenResolver', () => {
68
116
  it('should return the token read from the file configured for that owner', () => {
69
117
  const resolve = createConsoleGithubTokenResolver(
70
118
  'default-token',
71
- { 'meta-site': '/creds/meta-site-token.txt' },
119
+ { 'acme-labs': '/creds/acme-labs-token.txt' },
72
120
  (filePath) =>
73
- filePath === '/creds/meta-site-token.txt'
121
+ filePath === '/creds/acme-labs-token.txt'
74
122
  ? 'fine-grained-token\n'
75
123
  : (() => {
76
124
  throw new Error(`unexpected file path: ${filePath}`);
77
125
  })(),
78
126
  );
79
127
 
80
- expect(resolve('meta-site')).toBe('fine-grained-token');
128
+ expect(resolve('acme-labs')).toBe('fine-grained-token');
81
129
  });
82
130
 
83
131
  it('should keep using the default token for the other owners when one owner has a token file', () => {
84
132
  const resolve = createConsoleGithubTokenResolver(
85
133
  'default-token',
86
- { 'meta-site': '/creds/meta-site-token.txt' },
134
+ { 'acme-labs': '/creds/acme-labs-token.txt' },
87
135
  () => 'fine-grained-token',
88
136
  );
89
137
 
90
- expect(resolve('X-Mile')).toBe('default-token');
138
+ expect(resolve('globex-inc')).toBe('default-token');
91
139
  });
92
140
 
93
141
  it('should read the token file only once per owner', () => {
94
142
  let readCount = 0;
95
143
  const resolve = createConsoleGithubTokenResolver(
96
144
  'default-token',
97
- { 'meta-site': '/creds/meta-site-token.txt' },
145
+ { 'acme-labs': '/creds/acme-labs-token.txt' },
98
146
  () => {
99
147
  readCount += 1;
100
148
  return 'fine-grained-token';
101
149
  },
102
150
  );
103
151
 
104
- resolve('meta-site');
105
- resolve('meta-site');
152
+ resolve('acme-labs');
153
+ resolve('acme-labs');
106
154
 
107
155
  expect(readCount).toBe(1);
108
156
  });
@@ -110,25 +158,25 @@ describe('createConsoleGithubTokenResolver', () => {
110
158
  it('should throw when the configured token file contains no token', () => {
111
159
  const resolve = createConsoleGithubTokenResolver(
112
160
  'default-token',
113
- { 'meta-site': '/creds/meta-site-token.txt' },
161
+ { 'acme-labs': '/creds/acme-labs-token.txt' },
114
162
  () => ' \n',
115
163
  );
116
164
 
117
- expect(() => resolve('meta-site')).toThrow(
118
- 'The GitHub token file configured for repository owner "meta-site" contains no token: /creds/meta-site-token.txt',
165
+ expect(() => resolve('acme-labs')).toThrow(
166
+ 'The GitHub token file configured for repository owner "acme-labs" contains no token: /creds/acme-labs-token.txt',
119
167
  );
120
168
  });
121
169
 
122
170
  it('should surface the read failure when the configured token file cannot be read', () => {
123
171
  const resolve = createConsoleGithubTokenResolver(
124
172
  'default-token',
125
- { 'meta-site': '/creds/meta-site-token.txt' },
173
+ { 'acme-labs': '/creds/acme-labs-token.txt' },
126
174
  () => {
127
175
  throw new Error('ENOENT: no such file or directory');
128
176
  },
129
177
  );
130
178
 
131
- expect(() => resolve('meta-site')).toThrow(
179
+ expect(() => resolve('acme-labs')).toThrow(
132
180
  'ENOENT: no such file or directory',
133
181
  );
134
182
  });
@@ -26,6 +26,30 @@ export const createConsoleIssueRepositoryResolver = <IssueRepositoryType>(
26
26
  };
27
27
  };
28
28
 
29
+ export const extractProjectOwner = (projectUrl: string): string | null => {
30
+ const match = projectUrl.match(
31
+ /^https:\/\/github\.com\/(?:orgs|users)\/([A-Za-z0-9._-]+)\/projects\/\d+/,
32
+ );
33
+ return match ? match[1] : null;
34
+ };
35
+
36
+ export const createConsoleProjectRepositoryResolver = <ProjectRepositoryType>(
37
+ resolveGithubToken: ConsoleGithubTokenResolver,
38
+ buildProjectRepositoryForToken: (
39
+ githubToken: string,
40
+ ) => ProjectRepositoryType,
41
+ ): ((projectUrl: string) => ProjectRepositoryType) => {
42
+ return (projectUrl: string): ProjectRepositoryType => {
43
+ const projectOwner = extractProjectOwner(projectUrl);
44
+ if (projectOwner === null) {
45
+ throw new Error(
46
+ `The project owner cannot be read from the project url: ${projectUrl}`,
47
+ );
48
+ }
49
+ return buildProjectRepositoryForToken(resolveGithubToken(projectOwner));
50
+ };
51
+ };
52
+
29
53
  export const createConsoleGithubTokenResolver = (
30
54
  defaultToken: string,
31
55
  githubTokenFilePathByRepositoryOwner: Record<string, string> | null,
@@ -141,12 +141,12 @@ describe('consoleOperationApi', () => {
141
141
  await handleReview(contextRecordingResolvedUrls, {
142
142
  pjcode: 'acme',
143
143
  action: 'approve',
144
- prUrl: 'https://github.com/meta-site/hr-audit-mock/pull/178',
144
+ prUrl: 'https://github.com/acme-labs/acme-portal-mock/pull/178',
145
145
  projectItemId: 'PVTI_resolver',
146
146
  });
147
147
 
148
148
  expect(resolvedUrls).toContain(
149
- 'https://github.com/meta-site/hr-audit-mock/pull/178',
149
+ 'https://github.com/acme-labs/acme-portal-mock/pull/178',
150
150
  );
151
151
  });
152
152
 
@@ -2,6 +2,7 @@ import { mock } from 'jest-mock-extended';
2
2
  import { Project } from '../../../domain/entities/Project';
3
3
  import {
4
4
  buildPjcodeToProjectUrl,
5
+ createConsoleProjectLoader,
5
6
  createConsoleProjectResolver,
6
7
  createPjcodeConfigChecker,
7
8
  } from './consoleProjectResolver';
@@ -107,3 +108,93 @@ describe('createConsoleProjectResolver', () => {
107
108
  expect(loadProject).toHaveBeenCalledTimes(1);
108
109
  });
109
110
  });
111
+
112
+ describe('createConsoleProjectLoader', () => {
113
+ const globexProjectOfLoader: Project = {
114
+ ...mock<Project>(),
115
+ id: 'PVT_globex',
116
+ };
117
+
118
+ const buildProjectRepositoryResolver = (
119
+ findProjectIdByUrl: (projectUrl: string) => Promise<string | null>,
120
+ getProject: (projectId: string) => Promise<Project | null>,
121
+ ) =>
122
+ jest.fn((projectUrl: string) => {
123
+ if (projectUrl !== 'https://github.com/orgs/globex/projects/18') {
124
+ throw new Error(`unexpected project url: ${projectUrl}`);
125
+ }
126
+ return { findProjectIdByUrl, getProject };
127
+ });
128
+
129
+ it('resolves the project id through the repository chosen for the project url', async () => {
130
+ const resolveProjectRepository = buildProjectRepositoryResolver(
131
+ async () => 'PVT_globex',
132
+ async () => globexProjectOfLoader,
133
+ );
134
+ const loadProject = createConsoleProjectLoader(
135
+ resolveProjectRepository,
136
+ async () => null,
137
+ () => undefined,
138
+ );
139
+
140
+ await expect(
141
+ loadProject('https://github.com/orgs/globex/projects/18'),
142
+ ).resolves.toBe(globexProjectOfLoader);
143
+ expect(resolveProjectRepository).toHaveBeenCalledWith(
144
+ 'https://github.com/orgs/globex/projects/18',
145
+ );
146
+ });
147
+
148
+ it('prefers the locally cached project over a GraphQL project load', async () => {
149
+ const getProject = jest.fn(async () => globexProjectOfLoader);
150
+ const loadProject = createConsoleProjectLoader(
151
+ buildProjectRepositoryResolver(async () => 'PVT_globex', getProject),
152
+ async (projectId: string) =>
153
+ projectId === 'PVT_globex' ? globexProjectOfLoader : null,
154
+ () => undefined,
155
+ );
156
+
157
+ await expect(
158
+ loadProject('https://github.com/orgs/globex/projects/18'),
159
+ ).resolves.toBe(globexProjectOfLoader);
160
+ expect(getProject).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it('reports and returns null when the project id cannot be resolved', async () => {
164
+ const reportedMessages: string[] = [];
165
+ const loadProject = createConsoleProjectLoader(
166
+ buildProjectRepositoryResolver(
167
+ async () => null,
168
+ async () => globexProjectOfLoader,
169
+ ),
170
+ async () => null,
171
+ (message: string) => reportedMessages.push(message),
172
+ );
173
+
174
+ await expect(
175
+ loadProject('https://github.com/orgs/globex/projects/18'),
176
+ ).resolves.toBeNull();
177
+ expect(reportedMessages).toEqual([
178
+ 'No project found for projectUrl https://github.com/orgs/globex/projects/18',
179
+ ]);
180
+ });
181
+
182
+ it('reports and returns null when the project cannot be loaded', async () => {
183
+ const reportedMessages: string[] = [];
184
+ const loadProject = createConsoleProjectLoader(
185
+ buildProjectRepositoryResolver(
186
+ async () => 'PVT_globex',
187
+ async () => null,
188
+ ),
189
+ async () => null,
190
+ (message: string) => reportedMessages.push(message),
191
+ );
192
+
193
+ await expect(
194
+ loadProject('https://github.com/orgs/globex/projects/18'),
195
+ ).resolves.toBeNull();
196
+ expect(reportedMessages).toEqual([
197
+ 'Failed to load project for projectUrl https://github.com/orgs/globex/projects/18',
198
+ ]);
199
+ });
200
+ });
@@ -9,6 +9,38 @@ export type ConsoleProjectLoader = (
9
9
  projectUrl: string,
10
10
  ) => Promise<Project | null>;
11
11
 
12
+ export type ConsoleProjectIdAndProjectReader = {
13
+ findProjectIdByUrl: (projectUrl: string) => Promise<string | null>;
14
+ getProject: (projectId: string) => Promise<Project | null>;
15
+ };
16
+
17
+ export const createConsoleProjectLoader = (
18
+ resolveProjectRepository: (
19
+ projectUrl: string,
20
+ ) => ConsoleProjectIdAndProjectReader,
21
+ getCachedProject: (projectId: string) => Promise<Project | null>,
22
+ reportLoadFailure: (message: string) => void,
23
+ ): ConsoleProjectLoader => {
24
+ return async (projectUrl: string): Promise<Project | null> => {
25
+ const projectRepository = resolveProjectRepository(projectUrl);
26
+ const projectId = await projectRepository.findProjectIdByUrl(projectUrl);
27
+ if (!projectId) {
28
+ reportLoadFailure(`No project found for projectUrl ${projectUrl}`);
29
+ return null;
30
+ }
31
+ const cachedProject = await getCachedProject(projectId);
32
+ if (cachedProject) {
33
+ return cachedProject;
34
+ }
35
+ const loadedProject = await projectRepository.getProject(projectId);
36
+ if (!loadedProject) {
37
+ reportLoadFailure(`Failed to load project for projectUrl ${projectUrl}`);
38
+ return null;
39
+ }
40
+ return loadedProject;
41
+ };
42
+ };
43
+
12
44
  export const buildPjcodeToProjectUrl = (
13
45
  defaultPjcode: string,
14
46
  defaultProjectUrl: string,