Skip to content

Commit 93616b4

Browse files
committed
basic tests
1 parent 2084ba9 commit 93616b4

File tree

5 files changed

+137
-6
lines changed

5 files changed

+137
-6
lines changed

integration-tests/testkit/seed.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import {
6060
import { execute } from './graphql';
6161
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
6262
import { collect, CollectedOperation, legacyCollect } from './usage';
63-
import { generateUnique } from './utils';
63+
import { generateUnique, getServiceHost } from './utils';
6464

6565
export function initSeed() {
6666
function createConnectionPool() {
@@ -87,6 +87,17 @@ export function initSeed() {
8787
},
8888
};
8989
},
90+
async purgePersonalAccessTokenById(id: string) {
91+
const registryAddress = await getServiceHost('server', 8082);
92+
(
93+
await fetch(
94+
'http://' + registryAddress + '/cache/personal-access-token-cache/delete/' + id,
95+
{
96+
method: 'POST',
97+
},
98+
)
99+
).json();
100+
},
90101
authenticate,
91102
generateEmail: () => userEmail(generateUnique()),
92103
async createOwner() {
@@ -880,7 +891,10 @@ export function initSeed() {
880891
},
881892
};
882893
},
883-
async inviteAndJoinMember(inviteToken: string = ownerToken) {
894+
async inviteAndJoinMember(
895+
memberRoleId: string | undefined = undefined,
896+
inviteToken: string = ownerToken,
897+
) {
884898
const memberEmail = userEmail(generateUnique());
885899
const memberToken = await authenticate(memberEmail).then(r => r.access_token);
886900

@@ -892,6 +906,7 @@ export function initSeed() {
892906
},
893907
},
894908
email: memberEmail,
909+
memberRoleId,
895910
},
896911
inviteToken,
897912
).then(r => r.expectNoGraphQLErrors());

integration-tests/tests/api/personal-access-tokens.spec.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assertNonNullish } from 'testkit/utils';
2+
import { PersonalAccessTokensCache } from '../../../packages/services/api/src/modules/organization/providers/personal-access-tokens-cache';
23
import { graphql } from '../../testkit/gql';
34
import * as GraphQLSchema from '../../testkit/gql/graphql';
45
import { execute } from '../../testkit/graphql';
@@ -299,7 +300,8 @@ test.concurrent('query GraphQL API on resources with access', async ({ expect })
299300
authToken: ownerToken,
300301
}).then(e => e.expectNoGraphQLErrors());
301302
expect(result.createPersonalAccessToken.error).toEqual(null);
302-
const organizationAccessToken = result.createPersonalAccessToken.ok!.privateAccessKey;
303+
assertNonNullish(result.createPersonalAccessToken.ok);
304+
const personalAccessToken = result.createPersonalAccessToken.ok.privateAccessKey;
303305

304306
const projectQuery = await execute({
305307
document: OrganizationProjectTargetQuery1,
@@ -308,7 +310,7 @@ test.concurrent('query GraphQL API on resources with access', async ({ expect })
308310
projectSlug: project.project.slug,
309311
targetSlug: project.target.slug,
310312
},
311-
authToken: organizationAccessToken,
313+
authToken: personalAccessToken,
312314
}).then(e => e.expectNoGraphQLErrors());
313315
expect(projectQuery).toEqual({
314316
organization: {
@@ -376,3 +378,107 @@ test.concurrent('query GraphQL API on resources without access', async ({ expect
376378
},
377379
});
378380
});
381+
382+
test.concurrent(
383+
'query GraphQL API after membership resources have been downgraded',
384+
async ({ expect }) => {
385+
const seed = await initSeed();
386+
const { createOrg } = await seed.createOwner();
387+
const org = await createOrg();
388+
const project = await org.createProject(GraphQLSchema.ProjectType.Federation);
389+
390+
const { member, memberToken, assignMemberRole, createMemberRole } =
391+
await org.inviteAndJoinMember();
392+
393+
const newRole = await createMemberRole([
394+
'organization:describe',
395+
'project:describe',
396+
'personalAccessToken:modify',
397+
]);
398+
399+
// make user also an admin
400+
await assignMemberRole({
401+
userId: member.id,
402+
roleId: newRole.id,
403+
});
404+
405+
const result = await execute({
406+
document: CreatePersonalAccessTokenMutation,
407+
variables: {
408+
input: {
409+
organization: {
410+
byId: org.organization.id,
411+
},
412+
title: 'a access token',
413+
description: 'a description',
414+
resources: { mode: GraphQLSchema.ResourceAssignmentModeType.All },
415+
permissions: ['organization:describe', 'project:describe'],
416+
},
417+
},
418+
authToken: memberToken,
419+
}).then(e => e.expectNoGraphQLErrors());
420+
421+
expect(result.createPersonalAccessToken.error).toEqual(null);
422+
assertNonNullish(result.createPersonalAccessToken.ok);
423+
424+
const personalAccessToken = result.createPersonalAccessToken.ok.privateAccessKey;
425+
426+
let projectQuery = await execute({
427+
document: OrganizationProjectTargetQuery1,
428+
variables: {
429+
organizationSlug: org.organization.slug,
430+
projectSlug: project.project.slug,
431+
targetSlug: project.target.slug,
432+
},
433+
authToken: personalAccessToken,
434+
}).then(e => e.expectNoGraphQLErrors());
435+
436+
expect(projectQuery).toEqual({
437+
organization: {
438+
id: org.organization.id,
439+
project: {
440+
id: project.project.id,
441+
slug: project.project.slug,
442+
targetBySlug: {
443+
id: project.target.id,
444+
slug: project.target.slug,
445+
},
446+
},
447+
slug: org.organization.slug,
448+
},
449+
});
450+
451+
// Update member role assignment so it looses access to describe project/target on the resources
452+
await assignMemberRole({
453+
userId: member.id,
454+
roleId: newRole.id,
455+
resources: {
456+
mode: GraphQLSchema.ResourceAssignmentModeType.Granular,
457+
projects: [],
458+
},
459+
});
460+
461+
// simulate 5 minutes passing by...
462+
await seed.purgePersonalAccessTokenById(
463+
result.createPersonalAccessToken.ok.createdPersonalAccessToken.id,
464+
);
465+
466+
projectQuery = await execute({
467+
document: OrganizationProjectTargetQuery1,
468+
variables: {
469+
organizationSlug: org.organization.slug,
470+
projectSlug: project.project.slug,
471+
targetSlug: project.target.slug,
472+
},
473+
authToken: personalAccessToken,
474+
}).then(e => e.expectNoGraphQLErrors());
475+
476+
expect(projectQuery).toEqual({
477+
organization: {
478+
id: org.organization.id,
479+
project: null,
480+
slug: org.organization.slug,
481+
},
482+
});
483+
},
484+
);

packages/services/api/src/modules/organization/providers/personal-access-tokens-cache.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export type CachedPersonalAccessToken = Omit<
2121
>;
2222
};
2323

24+
type PersonalAccessTokeDeleteInput = Pick<PersonalAccessToken, 'id'>;
25+
2426
/**
2527
* Cache for performant PersonalAccessToken lookups.
2628
*/
@@ -124,7 +126,8 @@ export class PersonalAccessTokensCache {
124126
});
125127
}
126128

127-
purge(token: PersonalAccessToken) {
129+
/** Delete a personal access token from the cache e.g. upon deletion or update of permissions */
130+
delete(token: PersonalAccessTokeDeleteInput) {
128131
return this.cache.delete({
129132
key: token.id,
130133
});

packages/services/api/src/modules/organization/providers/personal-access-tokens.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class PersonalAccessTokens {
190190
"id" = ${args.personalAccessTokenId}
191191
`);
192192

193-
await this.cache.purge(record);
193+
await this.cache.delete(record);
194194

195195
await this.auditLogs.record({
196196
organizationId: record.organizationId,

packages/services/server/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,13 @@ export async function main() {
605605
return;
606606
});
607607

608+
// only for integration testing :)
609+
server.post('/cache/personal-access-token-cache/delete/:id', async (req, res) => {
610+
await registry.injector.get(PersonalAccessTokensCache).delete({ id: (req.params as any).id });
611+
void res.status(200).send({});
612+
return;
613+
});
614+
608615
createOtelAuthEndpoint({
609616
server,
610617
authN,

0 commit comments

Comments
 (0)