mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
test: add me api tests
This commit is contained in:
parent
643d418bb1
commit
bf8e4c0f6e
9 changed files with 198 additions and 29 deletions
|
@ -1,11 +1,21 @@
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
|
||||||
import { logtoUrl } from '#src/constants.js';
|
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
||||||
|
|
||||||
export default got.extend({ prefixUrl: new URL('/api', logtoUrl) });
|
const api = got.extend({ prefixUrl: new URL('/api', logtoUrl) });
|
||||||
|
|
||||||
export const authedAdminApi = got.extend({
|
export default api;
|
||||||
prefixUrl: new URL('/api', logtoUrl),
|
|
||||||
|
// TODO: @gao rename
|
||||||
|
export const authedAdminApi = api.extend({
|
||||||
|
headers: {
|
||||||
|
'development-user-id': 'integration-test-admin-user',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminTenantApi = got.extend({ prefixUrl: new URL('/api', logtoConsoleUrl) });
|
||||||
|
|
||||||
|
export const authedAdminTenantApi = adminTenantApi.extend({
|
||||||
headers: {
|
headers: {
|
||||||
'development-user-id': 'integration-test-admin-user',
|
'development-user-id': 'integration-test-admin-user',
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
Profile,
|
Profile,
|
||||||
RequestVerificationCodePayload,
|
RequestVerificationCodePayload,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
import type { Got } from 'got';
|
||||||
|
|
||||||
import api from './api.js';
|
import api from './api.js';
|
||||||
|
|
||||||
|
@ -11,13 +12,13 @@ export type RedirectResponse = {
|
||||||
redirectTo: string;
|
redirectTo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type interactionPayload = {
|
export type InteractionPayload = {
|
||||||
event: InteractionEvent;
|
event: InteractionEvent;
|
||||||
identifier?: IdentifierPayload;
|
identifier?: IdentifierPayload;
|
||||||
profile?: Profile;
|
profile?: Profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const putInteraction = async (cookie: string, payload: interactionPayload) =>
|
export const putInteraction = async (cookie: string, payload: InteractionPayload) =>
|
||||||
api
|
api
|
||||||
.put('interaction', {
|
.put('interaction', {
|
||||||
headers: { cookie },
|
headers: { cookie },
|
||||||
|
@ -66,7 +67,7 @@ export const deleteInteractionProfile = async (cookie: string) =>
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
export const submitInteraction = async (cookie: string) =>
|
export const submitInteraction = async (api: Got, cookie: string) =>
|
||||||
api
|
api
|
||||||
.post('interaction/submit', { headers: { cookie }, followRedirect: false })
|
.post('interaction/submit', { headers: { cookie }, followRedirect: false })
|
||||||
.json<RedirectResponse>();
|
.json<RedirectResponse>();
|
||||||
|
@ -97,7 +98,7 @@ export const createSocialAuthorizationUri = async (
|
||||||
followRedirect: false,
|
followRedirect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const consent = async (cookie: string) =>
|
export const consent = async (api: Got, cookie: string) =>
|
||||||
api
|
api
|
||||||
.post('interaction/consent', {
|
.post('interaction/consent', {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import LogtoClient from '@logto/node';
|
||||||
import { demoAppApplicationId } from '@logto/schemas';
|
import { demoAppApplicationId } from '@logto/schemas';
|
||||||
import type { Nullable, Optional } from '@silverhand/essentials';
|
import type { Nullable, Optional } from '@silverhand/essentials';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
|
import type { Got } from 'got';
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
|
||||||
import { consent, submitInteraction } from '#src/api/index.js';
|
import { consent, submitInteraction } from '#src/api/index.js';
|
||||||
|
@ -18,22 +19,24 @@ export const defaultConfig = {
|
||||||
export default class MockClient {
|
export default class MockClient {
|
||||||
public rawCookies: string[] = [];
|
public rawCookies: string[] = [];
|
||||||
|
|
||||||
|
protected readonly config: LogtoConfig;
|
||||||
|
|
||||||
private navigateUrl?: string;
|
private navigateUrl?: string;
|
||||||
private readonly storage: MemoryStorage;
|
private readonly storage: MemoryStorage;
|
||||||
private readonly logto: LogtoClient;
|
private readonly logto: LogtoClient;
|
||||||
|
private readonly api: Got;
|
||||||
|
|
||||||
constructor(config?: Partial<LogtoConfig>) {
|
constructor(config?: Partial<LogtoConfig>) {
|
||||||
this.storage = new MemoryStorage();
|
this.storage = new MemoryStorage();
|
||||||
|
this.config = { ...defaultConfig, ...config };
|
||||||
|
this.api = got.extend({ prefixUrl: this.config.endpoint + '/api' });
|
||||||
|
|
||||||
this.logto = new LogtoClient(
|
this.logto = new LogtoClient(this.config, {
|
||||||
{ ...defaultConfig, ...config },
|
navigate: (url: string) => {
|
||||||
{
|
this.navigateUrl = url;
|
||||||
navigate: (url: string) => {
|
},
|
||||||
this.navigateUrl = url;
|
storage: this.storage,
|
||||||
},
|
});
|
||||||
storage: this.storage,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename to sessionCookies or something accurate
|
// TODO: Rename to sessionCookies or something accurate
|
||||||
|
@ -62,7 +65,7 @@ export default class MockClient {
|
||||||
|
|
||||||
assert(this.navigateUrl, new Error('Unable to navigate to sign in uri'));
|
assert(this.navigateUrl, new Error('Unable to navigate to sign in uri'));
|
||||||
assert(
|
assert(
|
||||||
this.navigateUrl.startsWith(`${logtoUrl}/oidc/auth`),
|
this.navigateUrl.startsWith(`${this.config.endpoint}/oidc/auth`),
|
||||||
new Error('Unable to navigate to sign in uri')
|
new Error('Unable to navigate to sign in uri')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -84,7 +87,10 @@ export default class MockClient {
|
||||||
|
|
||||||
public async processSession(redirectTo: string) {
|
public async processSession(redirectTo: string) {
|
||||||
// Note: should redirect to OIDC auth endpoint
|
// Note: should redirect to OIDC auth endpoint
|
||||||
assert(redirectTo.startsWith(`${logtoUrl}/oidc/auth`), new Error('SignIn or Register failed'));
|
assert(
|
||||||
|
redirectTo.startsWith(`${this.config.endpoint}/oidc/auth`),
|
||||||
|
new Error('SignIn or Register failed')
|
||||||
|
);
|
||||||
|
|
||||||
const authResponse = await got.get(redirectTo, {
|
const authResponse = await got.get(redirectTo, {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -149,7 +155,7 @@ export default class MockClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async submitInteraction() {
|
public async submitInteraction() {
|
||||||
return submitInteraction(this.interactionCookie);
|
return submitInteraction(this.api, this.interactionCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly consent = async () => {
|
private readonly consent = async () => {
|
||||||
|
@ -157,10 +163,10 @@ export default class MockClient {
|
||||||
assert(this.interactionCookie, new Error('Session not found'));
|
assert(this.interactionCookie, new Error('Session not found'));
|
||||||
assert(this.interactionCookie.includes('_session.sig'), new Error('Session not found'));
|
assert(this.interactionCookie.includes('_session.sig'), new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await consent(this.interactionCookie);
|
const { redirectTo } = await consent(this.api, this.interactionCookie);
|
||||||
|
|
||||||
// Note: should redirect to oidc auth endpoint
|
// Note: should redirect to oidc auth endpoint
|
||||||
assert(redirectTo.startsWith(`${logtoUrl}/oidc/auth`), new Error('Consent failed'));
|
assert(redirectTo.startsWith(`${this.config.endpoint}/oidc/auth`), new Error('Consent failed'));
|
||||||
|
|
||||||
const authCodeResponse = await got.get(redirectTo, {
|
const authCodeResponse = await got.get(redirectTo, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
97
packages/integration-tests/src/helpers/admin-tenant.ts
Normal file
97
packages/integration-tests/src/helpers/admin-tenant.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// To refactor: should combine into other similar utils
|
||||||
|
// Since they are just different in URLs
|
||||||
|
|
||||||
|
import type { LogtoConfig } from '@logto/node';
|
||||||
|
import type { Role, User } from '@logto/schemas';
|
||||||
|
import {
|
||||||
|
PredefinedScope,
|
||||||
|
adminTenantId,
|
||||||
|
defaultTenantId,
|
||||||
|
getManagementApiResourceIndicator,
|
||||||
|
adminConsoleApplicationId,
|
||||||
|
InteractionEvent,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
|
||||||
|
import { authedAdminTenantApi as api, adminTenantApi } from '#src/api/api.js';
|
||||||
|
import type { InteractionPayload } from '#src/api/interaction.js';
|
||||||
|
import { adminConsoleRedirectUri, logtoConsoleUrl } from '#src/constants.js';
|
||||||
|
import { initClient } from '#src/helpers/client.js';
|
||||||
|
import { generatePassword, generateUsername } from '#src/utils.js';
|
||||||
|
|
||||||
|
export const resourceDefault = getManagementApiResourceIndicator(defaultTenantId);
|
||||||
|
export const resourceMe = getManagementApiResourceIndicator(adminTenantId, 'me');
|
||||||
|
|
||||||
|
export const createResponseWithCode = (statusCode: number) => ({
|
||||||
|
response: { statusCode },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createUserWithAllRoles = async () => {
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const user = await api
|
||||||
|
.post('users', {
|
||||||
|
json: { username, password },
|
||||||
|
})
|
||||||
|
.json<User>();
|
||||||
|
|
||||||
|
// Should have roles for default tenant Management API and admin tenant Me API
|
||||||
|
const roles = await api.get('roles').json<Role[]>();
|
||||||
|
await Promise.all(
|
||||||
|
roles.map(async ({ id }) =>
|
||||||
|
api.post(`roles/${id}/users`, {
|
||||||
|
json: { userIds: [user.id] },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [user, { username, password }] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUser = async (id: string) => {
|
||||||
|
await api.delete(`users/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putInteraction = async (cookie: string, payload: InteractionPayload) =>
|
||||||
|
adminTenantApi
|
||||||
|
.put('interaction', {
|
||||||
|
headers: { cookie },
|
||||||
|
json: payload,
|
||||||
|
followRedirect: false,
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
export const initClientAndSignIn = async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
config?: Partial<LogtoConfig>
|
||||||
|
) => {
|
||||||
|
const client = await initClient(
|
||||||
|
{
|
||||||
|
endpoint: logtoConsoleUrl,
|
||||||
|
appId: adminConsoleApplicationId,
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
adminConsoleRedirectUri
|
||||||
|
);
|
||||||
|
await client.successSend(putInteraction, {
|
||||||
|
event: InteractionEvent.SignIn,
|
||||||
|
identifier: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { redirectTo } = await client.submitInteraction();
|
||||||
|
await client.processSession(redirectTo);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUserAndSignInWithClient = async () => {
|
||||||
|
const [{ id }, { username, password }] = await createUserWithAllRoles();
|
||||||
|
const client = await initClientAndSignIn(username, password, {
|
||||||
|
resources: [resourceDefault, resourceMe],
|
||||||
|
scopes: [PredefinedScope.All],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id, client };
|
||||||
|
};
|
|
@ -1,10 +1,11 @@
|
||||||
|
import type { LogtoConfig } from '@logto/node';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
|
|
||||||
import MockClient from '#src/client/index.js';
|
import MockClient from '#src/client/index.js';
|
||||||
|
|
||||||
export const initClient = async () => {
|
export const initClient = async (config?: Partial<LogtoConfig>, redirectUri?: string) => {
|
||||||
const client = new MockClient();
|
const client = new MockClient(config);
|
||||||
await client.initSession();
|
await client.initSession(redirectUri);
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { api } from '#src/api/index.js';
|
import { api } from '#src/api/index.js';
|
||||||
|
|
||||||
describe('Health check', () => {
|
describe('health check', () => {
|
||||||
it('should have a health state', async () => {
|
it('should have a health state', async () => {
|
||||||
expect(await api.get('status')).toHaveProperty('statusCode', 204);
|
expect(await api.get('status')).toHaveProperty('statusCode', 204);
|
||||||
});
|
});
|
||||||
|
|
57
packages/integration-tests/src/tests/api/me.test.ts
Normal file
57
packages/integration-tests/src/tests/api/me.test.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { got } from 'got';
|
||||||
|
|
||||||
|
import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
||||||
|
import {
|
||||||
|
createResponseWithCode,
|
||||||
|
createUserAndSignInWithClient,
|
||||||
|
deleteUser,
|
||||||
|
resourceDefault,
|
||||||
|
resourceMe,
|
||||||
|
} from '#src/helpers/admin-tenant.js';
|
||||||
|
|
||||||
|
describe('me', () => {
|
||||||
|
it('should only be available in admin tenant', async () => {
|
||||||
|
await expect(got.get(new URL('/me/custom-data', logtoConsoleUrl))).rejects.toMatchObject(
|
||||||
|
createResponseWithCode(401)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Redirect to UI
|
||||||
|
const response = await got.get(new URL('/me/custom-data', logtoUrl));
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.headers['content-type']?.startsWith('text/html;')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only recognize the access token with correct resource and scope', async () => {
|
||||||
|
const { id, client } = await createUserAndSignInWithClient();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
got.get(logtoConsoleUrl + '/me/custom-data', {
|
||||||
|
headers: { authorization: `Bearer ${await client.getAccessToken(resourceDefault)}` },
|
||||||
|
})
|
||||||
|
).rejects.toMatchObject(createResponseWithCode(401));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
got.get(logtoConsoleUrl + '/me/custom-data', {
|
||||||
|
headers: { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` },
|
||||||
|
})
|
||||||
|
).resolves.toHaveProperty('statusCode', 200);
|
||||||
|
|
||||||
|
await deleteUser(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to update custom data', async () => {
|
||||||
|
const { id, client } = await createUserAndSignInWithClient();
|
||||||
|
const headers = { authorization: `Bearer ${await client.getAccessToken(resourceMe)}` };
|
||||||
|
|
||||||
|
const data = await got
|
||||||
|
.get(logtoConsoleUrl + '/me/custom-data', { headers })
|
||||||
|
.json<Record<string, unknown>>();
|
||||||
|
const newData = await got
|
||||||
|
.patch(logtoConsoleUrl + '/me/custom-data', { headers, json: { foo: 'bar' } })
|
||||||
|
.json();
|
||||||
|
|
||||||
|
expect({ ...data, foo: 'bar' }).toStrictEqual(newData);
|
||||||
|
|
||||||
|
await deleteUser(id);
|
||||||
|
});
|
||||||
|
});
|
|
@ -58,7 +58,6 @@
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
"eslint": "^8.21.0",
|
"eslint": "^8.21.0",
|
||||||
"jest": "^29.0.3",
|
"jest": "^29.0.3",
|
||||||
"jest-matcher-specific-error": "^1.0.0",
|
|
||||||
"lint-staged": "^13.0.0",
|
"lint-staged": "^13.0.0",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^2.8.1",
|
||||||
|
|
|
@ -709,7 +709,6 @@ importers:
|
||||||
color: ^4.2.3
|
color: ^4.2.3
|
||||||
eslint: ^8.21.0
|
eslint: ^8.21.0
|
||||||
jest: ^29.0.3
|
jest: ^29.0.3
|
||||||
jest-matcher-specific-error: ^1.0.0
|
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
nanoid: ^4.0.0
|
nanoid: ^4.0.0
|
||||||
postcss: ^8.4.6
|
postcss: ^8.4.6
|
||||||
|
@ -736,7 +735,6 @@ importers:
|
||||||
'@types/react': 18.0.26
|
'@types/react': 18.0.26
|
||||||
eslint: 8.21.0
|
eslint: 8.21.0
|
||||||
jest: 29.3.1_@types+node@18.11.18
|
jest: 29.3.1_@types+node@18.11.18
|
||||||
jest-matcher-specific-error: 1.0.0
|
|
||||||
lint-staged: 13.0.0
|
lint-staged: 13.0.0
|
||||||
postcss: 8.4.18
|
postcss: 8.4.18
|
||||||
prettier: 2.8.1
|
prettier: 2.8.1
|
||||||
|
|
Loading…
Reference in a new issue