From b7206457b4378ad18aacf2080345635bbb6bae52 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 27 May 2022 18:07:54 +0800 Subject: [PATCH] Add test for revoke api key --- packages/api/src/generated/graphql.ts | 104 ++++++++--------- packages/api/src/generated/schema.graphql | 37 +++--- packages/api/src/resolvers/api_key/index.ts | 43 ++++--- .../api/src/resolvers/function_resolvers.ts | 6 +- packages/api/src/schema.ts | 15 +-- packages/api/test/resolvers/api_key.test.ts | 106 ++++++++++++++---- 6 files changed, 193 insertions(+), 118 deletions(-) diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index e8a6ffc45..0a4c456de 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -39,6 +39,7 @@ export type ApiKey = { createdAt: Scalars['Date']; expiresAt: Scalars['Date']; id: Scalars['ID']; + key?: Maybe; name: Scalars['String']; scopes?: Maybe>; usedAt?: Maybe; @@ -421,24 +422,6 @@ export type CreateReminderSuccess = { reminder: Reminder; }; -export type DeleteApiKeyError = { - __typename?: 'DeleteApiKeyError'; - errorCodes: Array; -}; - -export enum DeleteApiKeyErrorCode { - BadRequest = 'BAD_REQUEST', - NotFound = 'NOT_FOUND', - Unauthorized = 'UNAUTHORIZED' -} - -export type DeleteApiKeyResult = DeleteApiKeyError | DeleteApiKeySuccess; - -export type DeleteApiKeySuccess = { - __typename?: 'DeleteApiKeySuccess'; - apiKey: ApiKey; -}; - export type DeleteHighlightError = { __typename?: 'DeleteHighlightError'; errorCodes: Array; @@ -630,7 +613,7 @@ export type GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess; export type GenerateApiKeySuccess = { __typename?: 'GenerateApiKeySuccess'; - apiKey: Scalars['String']; + apiKey: ApiKey; }; export type GetFollowersError = { @@ -877,7 +860,6 @@ export type Mutation = { createNewsletterEmail: CreateNewsletterEmailResult; createReaction: CreateReactionResult; createReminder: CreateReminderResult; - deleteApiKey: DeleteApiKeyResult; deleteHighlight: DeleteHighlightResult; deleteHighlightReply: DeleteHighlightReplyResult; deleteLabel: DeleteLabelResult; @@ -892,6 +874,7 @@ export type Mutation = { logOut: LogOutResult; mergeHighlight: MergeHighlightResult; reportItem: ReportItemResult; + revokeApiKey: RevokeApiKeyResult; saveArticleReadingProgress: SaveArticleReadingProgressResult; saveFile: SaveResult; savePage: SaveResult; @@ -961,11 +944,6 @@ export type MutationCreateReminderArgs = { }; -export type MutationDeleteApiKeyArgs = { - id: Scalars['ID']; -}; - - export type MutationDeleteHighlightArgs = { highlightId: Scalars['ID']; }; @@ -1031,6 +1009,11 @@ export type MutationReportItemArgs = { }; +export type MutationRevokeApiKeyArgs = { + id: Scalars['ID']; +}; + + export type MutationSaveArticleReadingProgressArgs = { input: SaveArticleReadingProgressInput; }; @@ -1420,6 +1403,24 @@ export enum ReportType { Spam = 'SPAM' } +export type RevokeApiKeyError = { + __typename?: 'RevokeApiKeyError'; + errorCodes: Array; +}; + +export enum RevokeApiKeyErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type RevokeApiKeyResult = RevokeApiKeyError | RevokeApiKeySuccess; + +export type RevokeApiKeySuccess = { + __typename?: 'RevokeApiKeySuccess'; + apiKey: ApiKey; +}; + export type SaveArticleReadingProgressError = { __typename?: 'SaveArticleReadingProgressError'; errorCodes: Array; @@ -2394,10 +2395,6 @@ export type ResolversTypes = { CreateReminderResult: ResolversTypes['CreateReminderError'] | ResolversTypes['CreateReminderSuccess']; CreateReminderSuccess: ResolverTypeWrapper; Date: ResolverTypeWrapper; - DeleteApiKeyError: ResolverTypeWrapper; - DeleteApiKeyErrorCode: DeleteApiKeyErrorCode; - DeleteApiKeyResult: ResolversTypes['DeleteApiKeyError'] | ResolversTypes['DeleteApiKeySuccess']; - DeleteApiKeySuccess: ResolverTypeWrapper; DeleteHighlightError: ResolverTypeWrapper; DeleteHighlightErrorCode: DeleteHighlightErrorCode; DeleteHighlightReplyError: ResolverTypeWrapper; @@ -2506,6 +2503,10 @@ export type ResolversTypes = { ReportItemInput: ReportItemInput; ReportItemResult: ResolverTypeWrapper; ReportType: ReportType; + RevokeApiKeyError: ResolverTypeWrapper; + RevokeApiKeyErrorCode: RevokeApiKeyErrorCode; + RevokeApiKeyResult: ResolversTypes['RevokeApiKeyError'] | ResolversTypes['RevokeApiKeySuccess']; + RevokeApiKeySuccess: ResolverTypeWrapper; SaveArticleReadingProgressError: ResolverTypeWrapper; SaveArticleReadingProgressErrorCode: SaveArticleReadingProgressErrorCode; SaveArticleReadingProgressInput: SaveArticleReadingProgressInput; @@ -2724,9 +2725,6 @@ export type ResolversParentTypes = { CreateReminderResult: ResolversParentTypes['CreateReminderError'] | ResolversParentTypes['CreateReminderSuccess']; CreateReminderSuccess: CreateReminderSuccess; Date: Scalars['Date']; - DeleteApiKeyError: DeleteApiKeyError; - DeleteApiKeyResult: ResolversParentTypes['DeleteApiKeyError'] | ResolversParentTypes['DeleteApiKeySuccess']; - DeleteApiKeySuccess: DeleteApiKeySuccess; DeleteHighlightError: DeleteHighlightError; DeleteHighlightReplyError: DeleteHighlightReplyError; DeleteHighlightReplyResult: ResolversParentTypes['DeleteHighlightReplyError'] | ResolversParentTypes['DeleteHighlightReplySuccess']; @@ -2814,6 +2812,9 @@ export type ResolversParentTypes = { ReminderSuccess: ReminderSuccess; ReportItemInput: ReportItemInput; ReportItemResult: ReportItemResult; + RevokeApiKeyError: RevokeApiKeyError; + RevokeApiKeyResult: ResolversParentTypes['RevokeApiKeyError'] | ResolversParentTypes['RevokeApiKeySuccess']; + RevokeApiKeySuccess: RevokeApiKeySuccess; SaveArticleReadingProgressError: SaveArticleReadingProgressError; SaveArticleReadingProgressInput: SaveArticleReadingProgressInput; SaveArticleReadingProgressResult: ResolversParentTypes['SaveArticleReadingProgressError'] | ResolversParentTypes['SaveArticleReadingProgressSuccess']; @@ -2964,6 +2965,7 @@ export type ApiKeyResolvers; expiresAt?: Resolver; id?: Resolver; + key?: Resolver, ParentType, ContextType>; name?: Resolver; scopes?: Resolver>, ParentType, ContextType>; usedAt?: Resolver, ParentType, ContextType>; @@ -3219,20 +3221,6 @@ export interface DateScalarConfig extends GraphQLScalarTypeConfig = { - errorCodes?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - -export type DeleteApiKeyResultResolvers = { - __resolveType: TypeResolveFn<'DeleteApiKeyError' | 'DeleteApiKeySuccess', ParentType, ContextType>; -}; - -export type DeleteApiKeySuccessResolvers = { - apiKey?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type DeleteHighlightErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -3383,7 +3371,7 @@ export type GenerateApiKeyResultResolvers = { - apiKey?: Resolver; + apiKey?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -3575,7 +3563,6 @@ export type MutationResolvers; createReaction?: Resolver>; createReminder?: Resolver>; - deleteApiKey?: Resolver>; deleteHighlight?: Resolver>; deleteHighlightReply?: Resolver>; deleteLabel?: Resolver>; @@ -3590,6 +3577,7 @@ export type MutationResolvers; mergeHighlight?: Resolver>; reportItem?: Resolver>; + revokeApiKey?: Resolver>; saveArticleReadingProgress?: Resolver>; saveFile?: Resolver>; savePage?: Resolver>; @@ -3742,6 +3730,20 @@ export type ReportItemResultResolvers; }; +export type RevokeApiKeyErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type RevokeApiKeyResultResolvers = { + __resolveType: TypeResolveFn<'RevokeApiKeyError' | 'RevokeApiKeySuccess', ParentType, ContextType>; +}; + +export type RevokeApiKeySuccessResolvers = { + apiKey?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type SaveArticleReadingProgressErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4316,9 +4318,6 @@ export type Resolvers = { CreateReminderResult?: CreateReminderResultResolvers; CreateReminderSuccess?: CreateReminderSuccessResolvers; Date?: GraphQLScalarType; - DeleteApiKeyError?: DeleteApiKeyErrorResolvers; - DeleteApiKeyResult?: DeleteApiKeyResultResolvers; - DeleteApiKeySuccess?: DeleteApiKeySuccessResolvers; DeleteHighlightError?: DeleteHighlightErrorResolvers; DeleteHighlightReplyError?: DeleteHighlightReplyErrorResolvers; DeleteHighlightReplyResult?: DeleteHighlightReplyResultResolvers; @@ -4395,6 +4394,9 @@ export type Resolvers = { ReminderResult?: ReminderResultResolvers; ReminderSuccess?: ReminderSuccessResolvers; ReportItemResult?: ReportItemResultResolvers; + RevokeApiKeyError?: RevokeApiKeyErrorResolvers; + RevokeApiKeyResult?: RevokeApiKeyResultResolvers; + RevokeApiKeySuccess?: RevokeApiKeySuccessResolvers; SaveArticleReadingProgressError?: SaveArticleReadingProgressErrorResolvers; SaveArticleReadingProgressResult?: SaveArticleReadingProgressResultResolvers; SaveArticleReadingProgressSuccess?: SaveArticleReadingProgressSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index a8650a612..4d37de9b8 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -20,6 +20,7 @@ type ApiKey { createdAt: Date! expiresAt: Date! id: ID! + key: String name: String! scopes: [String!] usedAt: Date @@ -368,22 +369,6 @@ type CreateReminderSuccess { scalar Date -type DeleteApiKeyError { - errorCodes: [DeleteApiKeyErrorCode!]! -} - -enum DeleteApiKeyErrorCode { - BAD_REQUEST - NOT_FOUND - UNAUTHORIZED -} - -union DeleteApiKeyResult = DeleteApiKeyError | DeleteApiKeySuccess - -type DeleteApiKeySuccess { - apiKey: ApiKey! -} - type DeleteHighlightError { errorCodes: [DeleteHighlightErrorCode!]! } @@ -554,7 +539,7 @@ input GenerateApiKeyInput { union GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess type GenerateApiKeySuccess { - apiKey: String! + apiKey: ApiKey! } type GetFollowersError { @@ -778,7 +763,6 @@ type Mutation { createNewsletterEmail: CreateNewsletterEmailResult! createReaction(input: CreateReactionInput!): CreateReactionResult! createReminder(input: CreateReminderInput!): CreateReminderResult! - deleteApiKey(id: ID!): DeleteApiKeyResult! deleteHighlight(highlightId: ID!): DeleteHighlightResult! deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult! deleteLabel(id: ID!): DeleteLabelResult! @@ -793,6 +777,7 @@ type Mutation { logOut: LogOutResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! reportItem(input: ReportItemInput!): ReportItemResult! + revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! saveFile(input: SaveFileInput!): SaveResult! savePage(input: SavePageInput!): SaveResult! @@ -989,6 +974,22 @@ enum ReportType { SPAM } +type RevokeApiKeyError { + errorCodes: [RevokeApiKeyErrorCode!]! +} + +enum RevokeApiKeyErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union RevokeApiKeyResult = RevokeApiKeyError | RevokeApiKeySuccess + +type RevokeApiKeySuccess { + apiKey: ApiKey! +} + type SaveArticleReadingProgressError { errorCodes: [SaveArticleReadingProgressErrorCode!]! } diff --git a/packages/api/src/resolvers/api_key/index.ts b/packages/api/src/resolvers/api_key/index.ts index 8cd6f601f..f6c067e75 100644 --- a/packages/api/src/resolvers/api_key/index.ts +++ b/packages/api/src/resolvers/api_key/index.ts @@ -2,14 +2,14 @@ import { ApiKeysError, ApiKeysErrorCode, ApiKeysSuccess, - DeleteApiKeyError, - DeleteApiKeyErrorCode, - DeleteApiKeySuccess, GenerateApiKeyError, GenerateApiKeyErrorCode, GenerateApiKeySuccess, - MutationDeleteApiKeyArgs, MutationGenerateApiKeyArgs, + MutationRevokeApiKeyArgs, + RevokeApiKeyError, + RevokeApiKeyErrorCode, + RevokeApiKeySuccess, } from '../../generated/graphql' import { analytics } from '../../utils/analytics' import { env } from '../../env' @@ -32,6 +32,7 @@ export const apiKeysResolver = authorized( } const apiKeys = await getRepository(ApiKey).find({ + select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'], where: { user: { id: uid } }, order: { usedAt: 'DESC', createdAt: 'DESC' }, }) @@ -75,7 +76,7 @@ export const generateApiKeyResolver = authorized< const exp = new Date(expiresAt) const apiKey = generateApiKey() - await getRepository(ApiKey).save({ + const apiKeyData = await getRepository(ApiKey).save({ user: { id: uid }, name, key: hashApiKey(apiKey), @@ -92,7 +93,12 @@ export const generateApiKeyResolver = authorized< }, }) - return { apiKey } + return { + apiKey: { + ...apiKeyData, + key: apiKey, + }, + } } catch (error) { console.error(error) @@ -100,18 +106,18 @@ export const generateApiKeyResolver = authorized< } }) -export const deleteApiKeyResolver = authorized< - DeleteApiKeySuccess, - DeleteApiKeyError, - MutationDeleteApiKeyArgs +export const revokeApiKeyResolver = authorized< + RevokeApiKeySuccess, + RevokeApiKeyError, + MutationRevokeApiKeyArgs >(async (_, { id }, { claims: { uid }, log }) => { - log.info('deleteApiKeyResolver') + log.info('RevokeApiKeyResolver') try { const user = await getRepository(User).findOneBy({ id: uid }) if (!user) { return { - errorCodes: [DeleteApiKeyErrorCode.Unauthorized], + errorCodes: [RevokeApiKeyErrorCode.Unauthorized], } } @@ -121,27 +127,30 @@ export const deleteApiKeyResolver = authorized< }) if (!apiKey) { return { - errorCodes: [DeleteApiKeyErrorCode.NotFound], + errorCodes: [RevokeApiKeyErrorCode.NotFound], } } if (apiKey.user.id !== uid) { return { - errorCodes: [DeleteApiKeyErrorCode.Unauthorized], + errorCodes: [RevokeApiKeyErrorCode.Unauthorized], } } const deletedApiKey = await getRepository(ApiKey).remove(apiKey) - deletedApiKey.id = id return { - apiKey: deletedApiKey, + apiKey: { + ...deletedApiKey, + id, + key: null, + }, } } catch (e) { log.error(e) return { - errorCodes: [DeleteApiKeyErrorCode.BadRequest], + errorCodes: [RevokeApiKeyErrorCode.BadRequest], } } }) diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 4a3c828a8..6fba0ae11 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -30,7 +30,6 @@ import { createLabelResolver, createNewsletterEmailResolver, createReminderResolver, - deleteApiKeyResolver, deleteHighlightResolver, deleteLabelResolver, deleteNewsletterEmailResolver, @@ -56,6 +55,7 @@ import { newsletterEmailsResolver, reminderResolver, reportItemResolver, + revokeApiKeyResolver, saveArticleReadingProgressResolver, saveFileResolver, savePageResolver, @@ -160,7 +160,7 @@ export const functionResolvers = { addPopularRead: addPopularReadResolver, setWebhook: setWebhookResolver, deleteWebhook: deleteWebhookResolver, - deleteApiKey: deleteApiKeyResolver, + revokeApiKey: revokeApiKeyResolver, }, Query: { me: getMeUserResolver, @@ -581,5 +581,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('DeleteWebhook'), ...resultResolveTypeResolver('Webhook'), ...resultResolveTypeResolver('ApiKeys'), - ...resultResolveTypeResolver('DeleteApiKey'), + ...resultResolveTypeResolver('RevokeApiKey'), } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 7fddad848..d868c230a 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1420,7 +1420,7 @@ const schema = gql` union GenerateApiKeyResult = GenerateApiKeySuccess | GenerateApiKeyError type GenerateApiKeySuccess { - apiKey: String! + apiKey: ApiKey! } type GenerateApiKeyError { @@ -1679,6 +1679,7 @@ const schema = gql` type ApiKey { id: ID! name: String! + key: String scopes: [String!] createdAt: Date! expiresAt: Date! @@ -1694,17 +1695,17 @@ const schema = gql` BAD_REQUEST } - union DeleteApiKeyResult = DeleteApiKeySuccess | DeleteApiKeyError + union RevokeApiKeyResult = RevokeApiKeySuccess | RevokeApiKeyError - type DeleteApiKeySuccess { + type RevokeApiKeySuccess { apiKey: ApiKey! } - type DeleteApiKeyError { - errorCodes: [DeleteApiKeyErrorCode!]! + type RevokeApiKeyError { + errorCodes: [RevokeApiKeyErrorCode!]! } - enum DeleteApiKeyErrorCode { + enum RevokeApiKeyErrorCode { UNAUTHORIZED BAD_REQUEST NOT_FOUND @@ -1777,7 +1778,7 @@ const schema = gql` addPopularRead(name: String!): AddPopularReadResult! setWebhook(input: SetWebhookInput!): SetWebhookResult! deleteWebhook(id: ID!): DeleteWebhookResult! - deleteApiKey(id: ID!): DeleteApiKeyResult! + revokeApiKey(id: ID!): RevokeApiKeyResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/test/resolvers/api_key.test.ts b/packages/api/test/resolvers/api_key.test.ts index a9a51d109..40503d434 100644 --- a/packages/api/test/resolvers/api_key.test.ts +++ b/packages/api/test/resolvers/api_key.test.ts @@ -22,7 +22,7 @@ const testAPIKey = (apiKey: string): supertest.Test => { return graphqlRequest(query, apiKey) } -describe('generate api key', () => { +describe('Api Key resolver', () => { const username = 'fake_user' let authToken: string @@ -46,15 +46,18 @@ describe('generate api key', () => { await deleteTestUser(username) }) - beforeEach(() => { - query = ` + describe('generate api key', () => { + beforeEach(() => { + query = ` mutation { generateApiKey(input: { name: "${name}" expiresAt: "${expiresAt}" }) { ... on GenerateApiKeySuccess { - apiKey + apiKey { + key + } } ... on GenerateApiKeyError { errorCodes @@ -62,33 +65,92 @@ describe('generate api key', () => { } } ` - }) - - context('when api key is not expired', () => { - before(() => { - name = 'test' - expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString() }) - it('should generate an api key', async () => { - const response = await graphqlRequest(query, authToken) - expect(response.body.data.generateApiKey.apiKey).to.be.a('string') + context('when api key is not expired', () => { + before(() => { + name = 'test' + expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString() + }) - return testAPIKey(response.body.data.generateApiKey.apiKey).expect(200) + it('should generate an api key', async () => { + const response = await graphqlRequest(query, authToken).expect(200) + expect(response.body.data.generateApiKey.apiKey.key).to.be.a('string') + + return testAPIKey(response.body.data.generateApiKey.apiKey.key).expect( + 200 + ) + }) + }) + + context('when api key is expired', () => { + before(() => { + name = 'test-expired' + expiresAt = new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString() + }) + + it('should generate an expired api key', async () => { + const response = await graphqlRequest(query, authToken).expect(200) + expect(response.body.data.generateApiKey.apiKey.key).to.be.a('string') + + return testAPIKey(response.body.data.generateApiKey.apiKey.key).expect( + 500 + ) + }) }) }) - context('when api key is expired', () => { - before(() => { - name = 'test-expired' - expiresAt = new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString() + describe('revoke api key', () => { + let apiKey: string + let apiKeyId: string + + before(async () => { + query = ` + mutation { + generateApiKey(input: { + name: "test-revoke" + expiresAt: "${new Date( + Date.now() + 1000 * 60 * 60 * 24 + ).toISOString()}" + }) { + ... on GenerateApiKeySuccess { + apiKey { + id + key + } + } + ... on GenerateApiKeyError { + errorCodes + } + } + } + ` + + const response = await graphqlRequest(query, authToken) + apiKey = response.body.data.generateApiKey.apiKey.key + apiKeyId = response.body.data.generateApiKey.apiKey.id }) - it('should generate an expired api key', async () => { - const response = await graphqlRequest(query, authToken) - expect(response.body.data.generateApiKey.apiKey).to.be.a('string') + it('should revoke an api key', async () => { + query = ` + mutation { + revokeApiKey(id: "${apiKeyId}") { + ... on RevokeApiKeySuccess { + apiKey { + id + } + } + ... on RevokeApiKeyError { + errorCodes + } + } + } + ` - return testAPIKey(response.body.data.generateApiKey.apiKey).expect(500) + const response = await graphqlRequest(query, authToken).expect(200) + expect(response.body.data.revokeApiKey.apiKey.id).to.be.a('string') + + return testAPIKey(apiKey).expect(500) }) }) })