mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(core,console): filter out webhook logs from audit logs list (#4243)
* refactor(core,console): filter out webhook logs from audit logs list * refactor(core): separate the method of finding audit logs and webhook logs * refactor(test): update integration tests * chore: adopt code review suggestions * refactor(core): refactor build log condition method and update its use cases
This commit is contained in:
parent
74e9734ef8
commit
028ffae068
12 changed files with 222 additions and 113 deletions
|
@ -6,7 +6,7 @@ import useSWR from 'swr';
|
|||
|
||||
import ApplicationName from '@/components/ApplicationName';
|
||||
import UserName from '@/components/UserName';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import { auditLogEventTitle, defaultPageSize } from '@/consts';
|
||||
import Table from '@/ds-components/Table';
|
||||
import type { Column } from '@/ds-components/Table/types';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
|
@ -21,6 +21,11 @@ import EventName from './components/EventName';
|
|||
import EventSelector from './components/EventSelector';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const auditLogEventOptions = Object.entries(auditLogEventTitle).map(([value, title]) => ({
|
||||
value,
|
||||
title: title ?? value,
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
userId?: string;
|
||||
className?: string;
|
||||
|
@ -103,6 +108,7 @@ function AuditLogTable({ userId, className }: Props) {
|
|||
<div className={styles.eventSelector}>
|
||||
<EventSelector
|
||||
value={event}
|
||||
options={auditLogEventOptions}
|
||||
onChange={(event) => {
|
||||
updateSearchParameters({ event, page: undefined });
|
||||
}}
|
||||
|
|
|
@ -1,54 +1,64 @@
|
|||
import type { LogKey } from '@logto/schemas';
|
||||
import type { AuditLogKey, LogKey, WebhookLogKey } from '@logto/schemas';
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
|
||||
export const logEventTitle: Record<string, Optional<string>> & Record<LogKey, Optional<string>> =
|
||||
Object.freeze({
|
||||
'ExchangeTokenBy.AuthorizationCode': 'Exchange token by Code',
|
||||
'ExchangeTokenBy.ClientCredentials': 'Exchange token by Client Credentials',
|
||||
'ExchangeTokenBy.RefreshToken': 'Exchange token by Refresh Token',
|
||||
'ExchangeTokenBy.Unknown': undefined,
|
||||
'Interaction.Create': 'Interaction started',
|
||||
'Interaction.End': 'Interaction ended',
|
||||
'Interaction.ForgotPassword.Identifier.Password.Submit':
|
||||
'Submit forgot-password identifier with password',
|
||||
'Interaction.ForgotPassword.Identifier.Social.Create': undefined,
|
||||
'Interaction.ForgotPassword.Identifier.Social.Submit': undefined,
|
||||
'Interaction.ForgotPassword.Identifier.VerificationCode.Create':
|
||||
'Create and send forgot-password verification code',
|
||||
'Interaction.ForgotPassword.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify forgot-password verification code',
|
||||
'Interaction.ForgotPassword.Profile.Create': 'Put new forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Profile.Delete': 'Delete forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Profile.Update': 'Patch update forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Submit': 'Submit forgot-password interaction',
|
||||
'Interaction.ForgotPassword.Update': 'Update forgot-password interaction',
|
||||
'Interaction.Register.Identifier.Password.Submit': undefined,
|
||||
'Interaction.Register.Identifier.Social.Create': undefined,
|
||||
'Interaction.Register.Identifier.Social.Submit': undefined,
|
||||
'Interaction.Register.Identifier.VerificationCode.Create':
|
||||
'Create and send register identifier with verification code',
|
||||
'Interaction.Register.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify register verification code',
|
||||
'Interaction.Register.Profile.Create': 'Put new register interaction profile',
|
||||
'Interaction.Register.Profile.Delete': 'Delete register interaction profile',
|
||||
'Interaction.Register.Profile.Update': 'Patch update register interaction profile',
|
||||
'Interaction.Register.Submit': 'Submit register interaction',
|
||||
'Interaction.Register.Update': 'Update register interaction',
|
||||
'Interaction.SignIn.Identifier.Password.Submit': 'Submit sign-in identifier with password',
|
||||
'Interaction.SignIn.Identifier.Social.Create': 'Create social sign-in authorization-url',
|
||||
'Interaction.SignIn.Identifier.Social.Submit': 'Authenticate and submit social identifier',
|
||||
'Interaction.SignIn.Identifier.VerificationCode.Create':
|
||||
'Create and send sign-in verification code',
|
||||
'Interaction.SignIn.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify sign-in identifier with verification code',
|
||||
'Interaction.SignIn.Profile.Create': 'Put new sign-in interaction profile',
|
||||
'Interaction.SignIn.Profile.Delete': 'Delete sign-in interaction profile',
|
||||
'Interaction.SignIn.Profile.Update': 'Patch Update sign-in interaction profile',
|
||||
'Interaction.SignIn.Submit': 'Submit sign-in interaction',
|
||||
'Interaction.SignIn.Update': 'Update sign-in interaction',
|
||||
'TriggerHook.PostRegister': undefined,
|
||||
'TriggerHook.PostResetPassword': undefined,
|
||||
'TriggerHook.PostSignIn': undefined,
|
||||
RevokeToken: undefined,
|
||||
Unknown: undefined,
|
||||
});
|
||||
export const auditLogEventTitle: Record<string, Optional<string>> &
|
||||
Record<AuditLogKey, Optional<string>> = Object.freeze({
|
||||
'ExchangeTokenBy.AuthorizationCode': 'Exchange token by Code',
|
||||
'ExchangeTokenBy.ClientCredentials': 'Exchange token by Client Credentials',
|
||||
'ExchangeTokenBy.RefreshToken': 'Exchange token by Refresh Token',
|
||||
'ExchangeTokenBy.Unknown': undefined,
|
||||
'Interaction.Create': 'Interaction started',
|
||||
'Interaction.End': 'Interaction ended',
|
||||
'Interaction.ForgotPassword.Identifier.Password.Submit':
|
||||
'Submit forgot-password identifier with password',
|
||||
'Interaction.ForgotPassword.Identifier.Social.Create': undefined,
|
||||
'Interaction.ForgotPassword.Identifier.Social.Submit': undefined,
|
||||
'Interaction.ForgotPassword.Identifier.VerificationCode.Create':
|
||||
'Create and send forgot-password verification code',
|
||||
'Interaction.ForgotPassword.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify forgot-password verification code',
|
||||
'Interaction.ForgotPassword.Profile.Create': 'Put new forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Profile.Delete': 'Delete forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Profile.Update': 'Patch update forgot-password interaction profile',
|
||||
'Interaction.ForgotPassword.Submit': 'Submit forgot-password interaction',
|
||||
'Interaction.ForgotPassword.Update': 'Update forgot-password interaction',
|
||||
'Interaction.Register.Identifier.Password.Submit': undefined,
|
||||
'Interaction.Register.Identifier.Social.Create': undefined,
|
||||
'Interaction.Register.Identifier.Social.Submit': undefined,
|
||||
'Interaction.Register.Identifier.VerificationCode.Create':
|
||||
'Create and send register identifier with verification code',
|
||||
'Interaction.Register.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify register verification code',
|
||||
'Interaction.Register.Profile.Create': 'Put new register interaction profile',
|
||||
'Interaction.Register.Profile.Delete': 'Delete register interaction profile',
|
||||
'Interaction.Register.Profile.Update': 'Patch update register interaction profile',
|
||||
'Interaction.Register.Submit': 'Submit register interaction',
|
||||
'Interaction.Register.Update': 'Update register interaction',
|
||||
'Interaction.SignIn.Identifier.Password.Submit': 'Submit sign-in identifier with password',
|
||||
'Interaction.SignIn.Identifier.Social.Create': 'Create social sign-in authorization-url',
|
||||
'Interaction.SignIn.Identifier.Social.Submit': 'Authenticate and submit social identifier',
|
||||
'Interaction.SignIn.Identifier.VerificationCode.Create':
|
||||
'Create and send sign-in verification code',
|
||||
'Interaction.SignIn.Identifier.VerificationCode.Submit':
|
||||
'Submit and verify sign-in identifier with verification code',
|
||||
'Interaction.SignIn.Profile.Create': 'Put new sign-in interaction profile',
|
||||
'Interaction.SignIn.Profile.Delete': 'Delete sign-in interaction profile',
|
||||
'Interaction.SignIn.Profile.Update': 'Patch Update sign-in interaction profile',
|
||||
'Interaction.SignIn.Submit': 'Submit sign-in interaction',
|
||||
'Interaction.SignIn.Update': 'Update sign-in interaction',
|
||||
RevokeToken: undefined,
|
||||
Unknown: undefined,
|
||||
});
|
||||
|
||||
// `webhookLogEventTitle` and `logEventTitle` are not used yet, keep them just in case.
|
||||
const webhookLogEventTitle: Record<string, Optional<string>> &
|
||||
Record<WebhookLogKey, Optional<string>> = Object.freeze({
|
||||
'TriggerHook.PostRegister': undefined,
|
||||
'TriggerHook.PostResetPassword': undefined,
|
||||
'TriggerHook.PostSignIn': undefined,
|
||||
});
|
||||
|
||||
export const logEventTitle: Record<string, Optional<string>> & Record<LogKey, Optional<string>> = {
|
||||
...auditLogEventTitle,
|
||||
...webhookLogEventTitle,
|
||||
};
|
||||
|
|
|
@ -1,33 +1,50 @@
|
|||
import type { HookExecutionStats, Log } from '@logto/schemas';
|
||||
import { token, Logs } from '@logto/schemas';
|
||||
import {
|
||||
token,
|
||||
type hook,
|
||||
Logs,
|
||||
type HookExecutionStats,
|
||||
type Log,
|
||||
type interaction,
|
||||
type LogKeyUnknown,
|
||||
} from '@logto/schemas';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { conditional, conditionalArray } from '@silverhand/essentials';
|
||||
import { subDays } from 'date-fns';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js';
|
||||
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Logs);
|
||||
|
||||
export type AllowedKeyPrefix = hook.Type | token.Type | interaction.Prefix | typeof LogKeyUnknown;
|
||||
|
||||
type LogCondition = {
|
||||
logKey?: string;
|
||||
applicationId?: string;
|
||||
userId?: string;
|
||||
hookId?: string;
|
||||
payload?: { applicationId?: string; userId?: string; hookId?: string };
|
||||
startTimeExclusive?: number;
|
||||
includeKeyPrefix?: AllowedKeyPrefix[];
|
||||
};
|
||||
|
||||
const buildLogConditionSql = (logCondition: LogCondition) =>
|
||||
conditionalSql(logCondition, ({ logKey, applicationId, userId, hookId, startTimeExclusive }) => {
|
||||
conditionalSql(logCondition, ({ logKey, payload, startTimeExclusive, includeKeyPrefix = [] }) => {
|
||||
const keyPrefixFilter = conditional(
|
||||
includeKeyPrefix.length > 0 &&
|
||||
includeKeyPrefix.map((prefix) => sql`${fields.key} like ${`${prefix}%`}`)
|
||||
);
|
||||
const subConditions = [
|
||||
conditionalSql(logKey, (logKey) => sql`${fields.key}=${logKey}`),
|
||||
conditionalSql(userId, (userId) => sql`${fields.payload}->>'userId'=${userId}`),
|
||||
conditionalSql(
|
||||
applicationId,
|
||||
(applicationId) => sql`${fields.payload}->>'applicationId'=${applicationId}`
|
||||
keyPrefixFilter,
|
||||
(keyPrefixFilter) => sql`(${sql.join(keyPrefixFilter, sql` or `)})`
|
||||
),
|
||||
conditionalSql(hookId, (hookId) => sql`${fields.payload}->>'hookId'=${hookId}`),
|
||||
...conditionalArray(
|
||||
payload &&
|
||||
Object.entries(payload).map(([key, value]) =>
|
||||
value ? sql`${fields.payload}->>${key}=${value}` : sql``
|
||||
)
|
||||
),
|
||||
conditionalSql(logKey, (logKey) => sql`${fields.key}=${logKey}`),
|
||||
conditionalSql(
|
||||
startTimeExclusive,
|
||||
(startTimeExclusive) =>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
type CreateHook,
|
||||
LogResult,
|
||||
type Log,
|
||||
hook,
|
||||
} from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
import { subDays } from 'date-fns';
|
||||
|
@ -155,8 +156,18 @@ describe('hook routes', () => {
|
|||
await hookRequest.get(
|
||||
`/hooks/${hookId}/recent-logs?logKey=${logKey}&page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
expect(countLogs).toHaveBeenCalledWith({ hookId, logKey, startTimeExclusive });
|
||||
expect(findLogs).toHaveBeenCalledWith(5, 0, { hookId, logKey, startTimeExclusive });
|
||||
expect(countLogs).toHaveBeenCalledWith({
|
||||
payload: { hookId },
|
||||
logKey,
|
||||
startTimeExclusive,
|
||||
includeKeyPrefix: [hook.Type.TriggerHook],
|
||||
});
|
||||
expect(findLogs).toHaveBeenCalledWith(5, 0, {
|
||||
payload: { hookId },
|
||||
logKey,
|
||||
startTimeExclusive,
|
||||
includeKeyPrefix: [hook.Type.TriggerHook],
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { Hooks, Logs, hookConfigGuard, hookEventsGuard, hookResponseGuard } from '@logto/schemas';
|
||||
import {
|
||||
Hooks,
|
||||
Logs,
|
||||
hookConfigGuard,
|
||||
hookEventsGuard,
|
||||
hookResponseGuard,
|
||||
hook,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { conditional, deduplicate, yes } from '@silverhand/essentials';
|
||||
import { subDays } from 'date-fns';
|
||||
|
@ -8,6 +15,7 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
||||
import { type AllowedKeyPrefix } from '#src/queries/log.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
@ -114,11 +122,22 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
query: { logKey },
|
||||
} = ctx.guard;
|
||||
|
||||
const includeKeyPrefix: AllowedKeyPrefix[] = [hook.Type.TriggerHook];
|
||||
const startTimeExclusive = subDays(new Date(), 1).getTime();
|
||||
|
||||
const [{ count }, logs] = await Promise.all([
|
||||
countLogs({ logKey, hookId: id, startTimeExclusive }),
|
||||
findLogs(limit, offset, { logKey, hookId: id, startTimeExclusive }),
|
||||
countLogs({
|
||||
logKey,
|
||||
payload: { hookId: id },
|
||||
startTimeExclusive,
|
||||
includeKeyPrefix,
|
||||
}),
|
||||
findLogs(limit, offset, {
|
||||
logKey,
|
||||
payload: { hookId: id },
|
||||
startTimeExclusive,
|
||||
includeKeyPrefix,
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LogResult } from '@logto/schemas';
|
||||
import { LogResult, token, interaction, LogKeyUnknown } from '@logto/schemas';
|
||||
import type { Log } from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
|
@ -42,8 +42,26 @@ describe('logRoutes', () => {
|
|||
await logRequest.get(
|
||||
`/logs?userId=${userId}&applicationId=${applicationId}&logKey=${logKey}&page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
expect(countLogs).toHaveBeenCalledWith({ userId, applicationId, logKey });
|
||||
expect(findLogs).toHaveBeenCalledWith(5, 0, { userId, applicationId, logKey });
|
||||
expect(countLogs).toHaveBeenCalledWith({
|
||||
payload: { userId, applicationId },
|
||||
logKey,
|
||||
includeKeyPrefix: [
|
||||
token.Type.ExchangeTokenBy,
|
||||
token.Type.RevokeToken,
|
||||
interaction.prefix,
|
||||
LogKeyUnknown,
|
||||
],
|
||||
});
|
||||
expect(findLogs).toHaveBeenCalledWith(5, 0, {
|
||||
payload: { userId, applicationId },
|
||||
logKey,
|
||||
includeKeyPrefix: [
|
||||
token.Type.ExchangeTokenBy,
|
||||
token.Type.RevokeToken,
|
||||
interaction.prefix,
|
||||
LogKeyUnknown,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct response', async () => {
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { Logs } from '@logto/schemas';
|
||||
import { Logs, interaction, token, LogKeyUnknown } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import { type AllowedKeyPrefix } from '#src/queries/log.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function logRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { countLogs, findLogById, findLogs } = queries.logs;
|
||||
const { findLogById, countLogs, findLogs } = queries.logs;
|
||||
|
||||
router.get(
|
||||
'/logs',
|
||||
|
@ -29,10 +30,25 @@ export default function logRoutes<T extends AuthedRouter>(
|
|||
query: { userId, applicationId, logKey },
|
||||
} = ctx.guard;
|
||||
|
||||
const includeKeyPrefix: AllowedKeyPrefix[] = [
|
||||
token.Type.ExchangeTokenBy,
|
||||
token.Type.RevokeToken,
|
||||
interaction.prefix,
|
||||
LogKeyUnknown,
|
||||
];
|
||||
|
||||
// TODO: @Gao refactor like user search
|
||||
const [{ count }, logs] = await Promise.all([
|
||||
countLogs({ logKey, applicationId, userId }),
|
||||
findLogs(limit, offset, { logKey, userId, applicationId }),
|
||||
countLogs({
|
||||
logKey,
|
||||
payload: { applicationId, userId },
|
||||
includeKeyPrefix,
|
||||
}),
|
||||
findLogs(limit, offset, {
|
||||
logKey,
|
||||
payload: { userId, applicationId },
|
||||
includeKeyPrefix,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Return totalCount to pagination middleware
|
||||
|
|
|
@ -3,7 +3,12 @@ import { conditionalString } from '@silverhand/essentials';
|
|||
|
||||
import { authedAdminApi } from './api.js';
|
||||
|
||||
export const getLogs = async (params?: URLSearchParams) =>
|
||||
export const getAuditLogs = async (params?: URLSearchParams) =>
|
||||
authedAdminApi.get('logs?' + conditionalString(params?.toString())).json<Log[]>();
|
||||
|
||||
export const getWebhookRecentLogs = async (hookId: string, params?: URLSearchParams) =>
|
||||
authedAdminApi
|
||||
.get(`hooks/${hookId}/recent-logs?` + conditionalString(params?.toString()))
|
||||
.json<Log[]>();
|
||||
|
||||
export const getLog = async (logId: string) => authedAdminApi.get(`logs/${logId}`).json<Log>();
|
||||
|
|
|
@ -3,7 +3,7 @@ import { assert } from '@silverhand/essentials';
|
|||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { putInteraction } from '#src/api/interaction.js';
|
||||
import { getLogs } from '#src/api/logs.js';
|
||||
import { getAuditLogs } from '#src/api/logs.js';
|
||||
import MockClient from '#src/client/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile } from '#src/helpers/user.js';
|
||||
|
@ -26,7 +26,7 @@ describe('audit logs for interaction', () => {
|
|||
console.debug('Testing interaction', interactionId);
|
||||
|
||||
// Expect interaction create log
|
||||
const createLogs = await getLogs(
|
||||
const createLogs = await getAuditLogs(
|
||||
new URLSearchParams({ logKey: `${interaction.prefix}.${interaction.Action.Create}` })
|
||||
);
|
||||
expect(createLogs.some((value) => value.payload.interactionId === interactionId)).toBeTruthy();
|
||||
|
@ -43,7 +43,7 @@ describe('audit logs for interaction', () => {
|
|||
await client.processSession(response.redirectTo);
|
||||
|
||||
// Expect interaction end log
|
||||
const endLogs = await getLogs(
|
||||
const endLogs = await getAuditLogs(
|
||||
new URLSearchParams({ logKey: `${interaction.prefix}.${interaction.Action.End}` })
|
||||
);
|
||||
expect(endLogs.some((value) => value.payload.interactionId === interactionId)).toBeTruthy();
|
||||
|
|
|
@ -10,10 +10,11 @@ import {
|
|||
type Log,
|
||||
ConnectorType,
|
||||
} from '@logto/schemas';
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { authedAdminApi } from '#src/api/api.js';
|
||||
import { getLogs } from '#src/api/logs.js';
|
||||
import { getWebhookRecentLogs } from '#src/api/logs.js';
|
||||
import {
|
||||
clearConnectorsByTypes,
|
||||
setEmailConnector,
|
||||
|
@ -87,13 +88,14 @@ describe('trigger hooks', () => {
|
|||
await signInWithPassword({ username, password });
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
const logs = await getWebhookRecentLogs(
|
||||
createdHook.id,
|
||||
new URLSearchParams({ logKey, page_size: '100' })
|
||||
);
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === createdHook.id &&
|
||||
result === LogResult.Error &&
|
||||
error === 'RequestError: Invalid URL'
|
||||
({ payload: { result, error } }) =>
|
||||
result === LogResult.Error && error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
|
@ -128,23 +130,23 @@ describe('trigger hooks', () => {
|
|||
const userId = await registerNewUser(username, password);
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === hook1.id && result === LogResult.Error && error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) => hookId === hook2.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) => hookId === hook3.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
for (const [hook, expectedResult, expectedError] of [
|
||||
[hook1, LogResult.Error, 'RequestError: Invalid URL'],
|
||||
[hook2, LogResult.Success, undefined],
|
||||
[hook3, LogResult.Success, undefined],
|
||||
] satisfies Array<[Hook, LogResult, Optional<string>]>) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const logs = await getWebhookRecentLogs(
|
||||
hook.id,
|
||||
new URLSearchParams({ logKey, page_size: '100' })
|
||||
);
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { result, error } }) =>
|
||||
result === expectedResult && (!expectedError || error === expectedError)
|
||||
)
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
// Clean up
|
||||
await Promise.all([
|
||||
|
@ -222,13 +224,15 @@ describe('trigger hooks', () => {
|
|||
// Wait for the hook to be trigged
|
||||
await waitFor(1000);
|
||||
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
const relatedLogs = logs.filter(
|
||||
({ payload: { hookId, result } }) =>
|
||||
hookId === resetPasswordHook.id && result === LogResult.Success
|
||||
const relatedLogs = await getWebhookRecentLogs(
|
||||
resetPasswordHook.id,
|
||||
new URLSearchParams({ logKey, page_size: '100' })
|
||||
);
|
||||
const succeedLogs = relatedLogs.filter(
|
||||
({ payload: { result } }) => result === LogResult.Success
|
||||
);
|
||||
|
||||
expect(relatedLogs).toHaveLength(2);
|
||||
expect(succeedLogs).toHaveLength(2);
|
||||
|
||||
await authedAdminApi.delete(`hooks/${resetPasswordHook.id}`);
|
||||
await deleteUser(user.id);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { getLog, getLogs } from '#src/api/index.js';
|
||||
import { getLog, getAuditLogs } from '#src/api/index.js';
|
||||
import { createResponseWithCode } from '#src/helpers/admin-tenant.js';
|
||||
|
||||
describe('logs', () => {
|
||||
it('should get logs successfully', async () => {
|
||||
const logs = await getLogs();
|
||||
const logs = await getAuditLogs();
|
||||
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should get log detail successfully', async () => {
|
||||
const logs = await getLogs();
|
||||
const logs = await getAuditLogs();
|
||||
const logId = logs[0]?.id;
|
||||
|
||||
expect(logId).not.toBeUndefined();
|
||||
|
|
|
@ -9,6 +9,9 @@ export * as hook from './hook.js';
|
|||
/** Fallback for empty or unrecognized log keys. */
|
||||
export const LogKeyUnknown = 'Unknown';
|
||||
|
||||
export type AuditLogKey = typeof LogKeyUnknown | interaction.LogKey | token.LogKey;
|
||||
export type WebhookLogKey = hook.LogKey;
|
||||
|
||||
/**
|
||||
* The union type of all available log keys.
|
||||
* Note duplicate keys are allowed but should be avoided.
|
||||
|
@ -16,4 +19,4 @@ export const LogKeyUnknown = 'Unknown';
|
|||
* @see {@link interaction.LogKey} for interaction log keys.
|
||||
* @see {@link token.LogKey} for token log keys.
|
||||
**/
|
||||
export type LogKey = typeof LogKeyUnknown | interaction.LogKey | token.LogKey | hook.LogKey;
|
||||
export type LogKey = AuditLogKey | WebhookLogKey;
|
||||
|
|
Loading…
Reference in a new issue