Skip to content

Commit ef5031f

Browse files
Copilotabraham
andcommitted
Update parser to extract app token scopes from API methods for clientCredentials OAuth flow
Co-authored-by: abraham <[email protected]>
1 parent eb9419e commit ef5031f

File tree

5 files changed

+541
-23
lines changed

5 files changed

+541
-23
lines changed

dist/schema.json

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39249,30 +39249,9 @@
3924939249
"clientCredentials": {
3925039250
"tokenUrl": "/oauth/token",
3925139251
"scopes": {
39252-
"read": "Read access",
39253-
"write": "Write access",
39254-
"read:accounts": "Read access to accounts",
39255-
"read:blocks": "Read access to blocks",
39256-
"read:bookmarks": "Read access to bookmarks",
39257-
"read:favourites": "Read access to favourites",
39258-
"read:filters": "Read access to filters",
39259-
"read:follows": "Read access to follows",
39260-
"read:lists": "Read access to lists",
39261-
"read:mutes": "Read access to mutes",
39262-
"read:search": "Read access to search",
39263-
"read:statuses": "Read access to statuses",
3926439252
"write:accounts": "Write access to accounts",
39265-
"write:blocks": "Write access to blocks",
39266-
"write:bookmarks": "Write access to bookmarks",
39267-
"write:conversations": "Write access to conversations",
39268-
"write:favourites": "Write access to favourites",
39269-
"write:filters": "Write access to filters",
39270-
"write:follows": "Write access to follows",
39271-
"write:lists": "Write access to lists",
39272-
"write:media": "Write access to media",
39273-
"write:mutes": "Write access to mutes",
39274-
"write:reports": "Write access to reports",
39275-
"write:statuses": "Write access to statuses"
39253+
"read": "Read access",
39254+
"read:statuses": "Read access to statuses"
3927639255
}
3927739256
}
3927839257
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { MethodConverter } from '../../generators/MethodConverter';
2+
import { TypeParser } from '../../generators/TypeParser';
3+
import { UtilityHelpers } from '../../generators/UtilityHelpers';
4+
import { ErrorExampleRegistry } from '../../generators/ErrorExampleRegistry';
5+
import { ApiMethodsFile } from '../../interfaces/ApiMethodsFile';
6+
7+
describe('MethodConverter collectAppTokenScopes', () => {
8+
let methodConverter: MethodConverter;
9+
10+
beforeEach(() => {
11+
const utilityHelpers = new UtilityHelpers();
12+
const typeParser = new TypeParser(utilityHelpers);
13+
const errorExampleRegistry = new ErrorExampleRegistry();
14+
methodConverter = new MethodConverter(
15+
typeParser,
16+
utilityHelpers,
17+
errorExampleRegistry
18+
);
19+
});
20+
21+
test('should extract scope from "App token + `write:accounts`"', () => {
22+
const methodFiles: ApiMethodsFile[] = [
23+
{
24+
name: 'accounts',
25+
description: 'Accounts API',
26+
methods: [
27+
{
28+
name: 'Register an account',
29+
httpMethod: 'POST',
30+
endpoint: '/api/v1/accounts',
31+
description: 'Creates a user and account records',
32+
oauth: 'App token + `write:accounts`',
33+
},
34+
],
35+
},
36+
];
37+
38+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
39+
40+
expect(scopes.has('write:accounts')).toBe(true);
41+
expect(scopes.size).toBe(1);
42+
});
43+
44+
test('should default to "read" when App token has no explicit scope', () => {
45+
const methodFiles: ApiMethodsFile[] = [
46+
{
47+
name: 'apps',
48+
description: 'Apps API',
49+
methods: [
50+
{
51+
name: 'Verify app credentials',
52+
httpMethod: 'GET',
53+
endpoint: '/api/v1/apps/verify_credentials',
54+
description: 'Verify app credentials',
55+
oauth: 'App token',
56+
},
57+
],
58+
},
59+
];
60+
61+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
62+
63+
expect(scopes.has('read')).toBe(true);
64+
expect(scopes.size).toBe(1);
65+
});
66+
67+
test('should extract scope from "App token + `read:statuses`"', () => {
68+
const methodFiles: ApiMethodsFile[] = [
69+
{
70+
name: 'statuses',
71+
description: 'Statuses API',
72+
methods: [
73+
{
74+
name: 'View status source',
75+
httpMethod: 'GET',
76+
endpoint: '/api/v1/statuses/{id}/source',
77+
description: 'View status source',
78+
oauth: 'App token + `read:statuses`',
79+
},
80+
],
81+
},
82+
];
83+
84+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
85+
86+
expect(scopes.has('read:statuses')).toBe(true);
87+
expect(scopes.size).toBe(1);
88+
});
89+
90+
test('should not include scopes from User token methods', () => {
91+
const methodFiles: ApiMethodsFile[] = [
92+
{
93+
name: 'blocks',
94+
description: 'Blocks API',
95+
methods: [
96+
{
97+
name: 'Block account',
98+
httpMethod: 'POST',
99+
endpoint: '/api/v1/accounts/{id}/block',
100+
description: 'Block an account',
101+
oauth: 'User token + `write:blocks`',
102+
},
103+
],
104+
},
105+
];
106+
107+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
108+
109+
expect(scopes.size).toBe(0);
110+
});
111+
112+
test('should collect scopes from multiple files', () => {
113+
const methodFiles: ApiMethodsFile[] = [
114+
{
115+
name: 'accounts',
116+
description: 'Accounts API',
117+
methods: [
118+
{
119+
name: 'Register an account',
120+
httpMethod: 'POST',
121+
endpoint: '/api/v1/accounts',
122+
description: 'Creates a user and account records',
123+
oauth: 'App token + `write:accounts`',
124+
},
125+
],
126+
},
127+
{
128+
name: 'apps',
129+
description: 'Apps API',
130+
methods: [
131+
{
132+
name: 'Verify app credentials',
133+
httpMethod: 'GET',
134+
endpoint: '/api/v1/apps/verify_credentials',
135+
description: 'Verify app credentials',
136+
oauth: 'App token',
137+
},
138+
],
139+
},
140+
{
141+
name: 'statuses',
142+
description: 'Statuses API',
143+
methods: [
144+
{
145+
name: 'View status source',
146+
httpMethod: 'GET',
147+
endpoint: '/api/v1/statuses/{id}/source',
148+
description: 'View status source',
149+
oauth: 'App token + `read:statuses`',
150+
},
151+
],
152+
},
153+
];
154+
155+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
156+
157+
expect(scopes.has('write:accounts')).toBe(true);
158+
expect(scopes.has('read')).toBe(true);
159+
expect(scopes.has('read:statuses')).toBe(true);
160+
expect(scopes.size).toBe(3);
161+
});
162+
163+
test('should deduplicate scopes across multiple methods', () => {
164+
const methodFiles: ApiMethodsFile[] = [
165+
{
166+
name: 'statuses',
167+
description: 'Statuses API',
168+
methods: [
169+
{
170+
name: 'View status source 1',
171+
httpMethod: 'GET',
172+
endpoint: '/api/v1/statuses/{id}/source',
173+
description: 'View status source',
174+
oauth: 'App token + `read:statuses`',
175+
},
176+
{
177+
name: 'View status source 2',
178+
httpMethod: 'GET',
179+
endpoint: '/api/v1/statuses/{id}/context',
180+
description: 'View status context',
181+
oauth: 'App token + `read:statuses`',
182+
},
183+
],
184+
},
185+
];
186+
187+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
188+
189+
expect(scopes.has('read:statuses')).toBe(true);
190+
expect(scopes.size).toBe(1);
191+
});
192+
193+
test('should handle methods with no oauth field', () => {
194+
const methodFiles: ApiMethodsFile[] = [
195+
{
196+
name: 'test',
197+
description: 'Test API',
198+
methods: [
199+
{
200+
name: 'Test method',
201+
httpMethod: 'GET',
202+
endpoint: '/api/v1/test',
203+
description: 'Test method',
204+
},
205+
],
206+
},
207+
];
208+
209+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
210+
211+
expect(scopes.size).toBe(0);
212+
});
213+
214+
test('should handle mixed User token and App token methods', () => {
215+
const methodFiles: ApiMethodsFile[] = [
216+
{
217+
name: 'mixed',
218+
description: 'Mixed API',
219+
methods: [
220+
{
221+
name: 'User method',
222+
httpMethod: 'POST',
223+
endpoint: '/api/v1/user',
224+
description: 'User method',
225+
oauth: 'User token + `write:accounts`',
226+
},
227+
{
228+
name: 'App method',
229+
httpMethod: 'GET',
230+
endpoint: '/api/v1/app',
231+
description: 'App method',
232+
oauth: 'App token + `read:accounts`',
233+
},
234+
{
235+
name: 'Mixed method',
236+
httpMethod: 'GET',
237+
endpoint: '/api/v1/mixed',
238+
description: 'Mixed method',
239+
oauth: 'User token + `read` or App token + `read`',
240+
},
241+
],
242+
},
243+
];
244+
245+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
246+
247+
expect(scopes.has('read:accounts')).toBe(true);
248+
expect(scopes.has('read')).toBe(true);
249+
// write:accounts should NOT be included as it's User token only
250+
expect(scopes.has('write:accounts')).toBe(false);
251+
expect(scopes.size).toBe(2);
252+
});
253+
254+
test('should handle empty method files array', () => {
255+
const scopes = methodConverter.collectAppTokenScopes([]);
256+
257+
expect(scopes.size).toBe(0);
258+
});
259+
260+
test('should handle Public oauth methods', () => {
261+
const methodFiles: ApiMethodsFile[] = [
262+
{
263+
name: 'public',
264+
description: 'Public API',
265+
methods: [
266+
{
267+
name: 'Public method',
268+
httpMethod: 'GET',
269+
endpoint: '/api/v1/public',
270+
description: 'Public method',
271+
oauth: 'Public',
272+
},
273+
],
274+
},
275+
];
276+
277+
const scopes = methodConverter.collectAppTokenScopes(methodFiles);
278+
279+
expect(scopes.size).toBe(0);
280+
});
281+
});

0 commit comments

Comments
 (0)