mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
refactor(test): refactor logto client and actions (#1691)
* refactor(test): refactor logto client and actions refactor logto client and related actions * chore(test): rename LogtoClient class rename LogtoClient class * refactor(test): rename client and client methods rename client and client methods * refactor(test): cr fix cr fix * refactor(test): reuse helpers reuser helpers
This commit is contained in:
parent
20c8889be8
commit
939dc0eac8
11 changed files with 251 additions and 203 deletions
|
@ -1,139 +0,0 @@
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import got from 'got';
|
||||
|
||||
import { api } from './api';
|
||||
import { logtoUrl } from './constants';
|
||||
import { extractCookie } from './utils';
|
||||
|
||||
type RegisterResponse = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
type SignInResponse = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
type ConsentResponse = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
export const visitSignInUri = async (signInUri: string) => {
|
||||
const response = await got(signInUri, {
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: After visit the sign in uri successfully, it will redirect the user to the ui sign in page.
|
||||
assert(
|
||||
response.statusCode === 303 && response.headers.location === '/sign-in',
|
||||
new Error('Visit sign in uri failed')
|
||||
);
|
||||
|
||||
const cookie = extractCookie(response);
|
||||
assert(cookie, new Error('Get cookie from authorization endpoint failed'));
|
||||
|
||||
return cookie;
|
||||
};
|
||||
|
||||
export const registerUserWithUsernameAndPassword = async (
|
||||
username: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) => {
|
||||
const { redirectTo } = await api
|
||||
.post('session/register/username-password', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.json<RegisterResponse>();
|
||||
|
||||
// Note: If register successfully, it will redirect the user to the auth endpoint.
|
||||
assert(
|
||||
redirectTo.startsWith(`${logtoUrl}/oidc/auth`),
|
||||
new Error('Register with username and password failed')
|
||||
);
|
||||
};
|
||||
|
||||
export const signInWithUsernameAndPassword = async (
|
||||
username: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) => {
|
||||
const { redirectTo: completeSignInActionUri } = await api
|
||||
.post('session/sign-in/username-password', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<SignInResponse>();
|
||||
|
||||
// Note: If sign in successfully, it will redirect the user to the auth endpoint
|
||||
assert(
|
||||
completeSignInActionUri.startsWith(`${logtoUrl}/oidc/auth`),
|
||||
new Error('Sign in with username and password failed')
|
||||
);
|
||||
|
||||
// Note: visit the completeSignInActionUri to get a new interaction cookie with session.
|
||||
const completeSignInActionResponse = await got.get(completeSignInActionUri, {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: If sign in action completed successfully, it will redirect the user to the consent page.
|
||||
assert(
|
||||
completeSignInActionResponse.statusCode === 303 &&
|
||||
completeSignInActionResponse.headers.location === '/sign-in/consent',
|
||||
new Error('Invoke auth before consent failed')
|
||||
);
|
||||
|
||||
const cookieWithSession = extractCookie(completeSignInActionResponse);
|
||||
|
||||
// Note: If sign in action completed successfully, we will get `_session.sig` in the cookie.
|
||||
assert(
|
||||
Boolean(cookieWithSession) && cookieWithSession.includes('_session.sig'),
|
||||
new Error('Invoke auth before consent failed')
|
||||
);
|
||||
|
||||
return cookieWithSession;
|
||||
};
|
||||
|
||||
export const consentUserAndGetSignInCallbackUri = async (interactionCookie: string) => {
|
||||
const { redirectTo: completeAuthUri } = await api
|
||||
.post('session/consent', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<ConsentResponse>();
|
||||
|
||||
// Note: If consent successfully, it will redirect the user to the auth endpoint.
|
||||
assert(completeAuthUri.startsWith(`${logtoUrl}/oidc/auth`), new Error('Consent failed'));
|
||||
|
||||
// Note: complete the auth process to get the sign in callback uri.
|
||||
const authCodeResponse = await got.get(completeAuthUri, {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: If complete auth successfully, it will redirect the user to the redirect uri.
|
||||
assert(authCodeResponse.statusCode === 303, new Error('Complete auth failed'));
|
||||
|
||||
const signInCallbackUri = authCodeResponse.headers.location;
|
||||
assert(signInCallbackUri, new Error('Get sign in callback uri failed'));
|
||||
|
||||
return signInCallbackUri;
|
||||
};
|
|
@ -3,5 +3,6 @@ export * from './connector';
|
|||
export * from './application';
|
||||
export * from './sign-in-experience';
|
||||
export * from './admin-user';
|
||||
export * from './session';
|
||||
|
||||
export { default as api, authedAdminApi } from './api';
|
||||
|
|
51
packages/integration-tests/src/api/session.ts
Normal file
51
packages/integration-tests/src/api/session.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import api from './api';
|
||||
|
||||
type RedirectResponse = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
export const registerUserWithUsernameAndPassword = async (
|
||||
username: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) =>
|
||||
api
|
||||
.post('session/register/username-password', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<RedirectResponse>();
|
||||
|
||||
export const signInWithUsernameAndPassword = async (
|
||||
username: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) =>
|
||||
api
|
||||
.post('session/sign-in/username-password', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<RedirectResponse>();
|
||||
|
||||
export const consent = async (interactionCookie: string) =>
|
||||
api
|
||||
.post('session/consent', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<RedirectResponse>();
|
114
packages/integration-tests/src/client/index.ts
Normal file
114
packages/integration-tests/src/client/index.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
import LogtoClient, { LogtoConfig } from '@logto/node';
|
||||
import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got from 'got';
|
||||
|
||||
import { consent } from '@/api';
|
||||
import { demoAppRedirectUri, logtoUrl } from '@/constants';
|
||||
import { extractCookie } from '@/utils';
|
||||
|
||||
import { MemoryStorage } from './storage';
|
||||
|
||||
const defaultConfig = {
|
||||
endpoint: logtoUrl,
|
||||
appId: demoAppApplicationId,
|
||||
persistAccessToken: false,
|
||||
};
|
||||
|
||||
export default class MockClient {
|
||||
public interactionCookie?: string;
|
||||
private navigateUrl?: string;
|
||||
|
||||
private readonly storage: MemoryStorage;
|
||||
private readonly logto: LogtoClient;
|
||||
|
||||
constructor(config?: LogtoConfig) {
|
||||
this.storage = new MemoryStorage();
|
||||
|
||||
this.logto = new LogtoClient(
|
||||
{ ...defaultConfig, ...config },
|
||||
{
|
||||
navigate: (url: string) => {
|
||||
this.navigateUrl = url;
|
||||
},
|
||||
storage: this.storage,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async initSession(callbackUri = demoAppRedirectUri) {
|
||||
await this.logto.signIn(callbackUri);
|
||||
|
||||
assert(this.navigateUrl, new Error('Unable to navigate to sign in uri'));
|
||||
assert(
|
||||
this.navigateUrl.startsWith(`${logtoUrl}/oidc/auth`),
|
||||
new Error('Unable to navigate to sign in uri')
|
||||
);
|
||||
|
||||
// Mock SDK sign-in navigation
|
||||
const response = await got(this.navigateUrl, {
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: should redirect to sign-in page
|
||||
assert(
|
||||
response.statusCode === 303 && response.headers.location === '/sign-in',
|
||||
new Error('Visit sign in uri failed')
|
||||
);
|
||||
|
||||
// Get session cookie
|
||||
this.interactionCookie = extractCookie(response);
|
||||
assert(this.interactionCookie, new Error('Get cookie from authorization endpoint failed'));
|
||||
}
|
||||
|
||||
public async processSession(redirectTo: string) {
|
||||
// Note: should redirect to OIDC auth endpoint
|
||||
assert(redirectTo.startsWith(`${logtoUrl}/oidc/auth`), new Error('SignIn or Register failed'));
|
||||
|
||||
const authResponse = await got.get(redirectTo, {
|
||||
headers: {
|
||||
cookie: this.interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: Should redirect to logto consent page
|
||||
assert(
|
||||
authResponse.statusCode === 303 && authResponse.headers.location === '/sign-in/consent',
|
||||
new Error('Invoke auth before consent failed')
|
||||
);
|
||||
|
||||
this.interactionCookie = extractCookie(authResponse);
|
||||
|
||||
await this.consent();
|
||||
}
|
||||
|
||||
public get isAuthenticated() {
|
||||
return this.logto.isAuthenticated;
|
||||
}
|
||||
|
||||
private readonly consent = async () => {
|
||||
// Note: If sign in action completed successfully, we will get `_session.sig` in the cookie.
|
||||
assert(this.interactionCookie, new Error('Session not found'));
|
||||
assert(this.interactionCookie.includes('_session.sig'), new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await consent(this.interactionCookie);
|
||||
|
||||
// Note: should redirect to oidc auth endpoint
|
||||
assert(redirectTo.startsWith(`${logtoUrl}/oidc/auth`), new Error('Consent failed'));
|
||||
|
||||
const authCodeResponse = await got.get(redirectTo, {
|
||||
headers: {
|
||||
cookie: this.interactionCookie,
|
||||
},
|
||||
followRedirect: false,
|
||||
});
|
||||
|
||||
// Note: Should redirect to the signInCallbackUri
|
||||
assert(authCodeResponse.statusCode === 303, new Error('Complete auth failed'));
|
||||
const signInCallbackUri = authCodeResponse.headers.location;
|
||||
assert(signInCallbackUri, new Error('Get sign in callback uri failed'));
|
||||
|
||||
await this.logto.handleSignInCallback(signInCallbackUri);
|
||||
};
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import BaseClient, { LogtoConfig } from '@logto/node';
|
||||
|
||||
import { MemoryStorage } from './storage';
|
||||
|
||||
export default class LogtoClient extends BaseClient {
|
||||
public navigateUrl = '';
|
||||
|
||||
constructor(config: LogtoConfig) {
|
||||
super(
|
||||
// Note: Disable persisting access token in integration tests
|
||||
{ ...config, persistAccessToken: false },
|
||||
{
|
||||
navigate: (url: string) => {
|
||||
this.navigateUrl = url;
|
||||
},
|
||||
storage: new MemoryStorage(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,54 +3,89 @@ import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
|
|||
import { assert } from '@silverhand/essentials';
|
||||
|
||||
import {
|
||||
visitSignInUri,
|
||||
createUser,
|
||||
registerUserWithUsernameAndPassword,
|
||||
consentUserAndGetSignInCallbackUri,
|
||||
signInWithUsernameAndPassword,
|
||||
} from './actions';
|
||||
import { createUser as createUserApi } from './api';
|
||||
import LogtoClient from './client/logto-client';
|
||||
import { demoAppRedirectUri, logtoUrl } from './constants';
|
||||
import { generateUsername, generatePassword } from './utils';
|
||||
} from '@/api';
|
||||
import MockClient from '@/client';
|
||||
import { demoAppRedirectUri, logtoUrl } from '@/constants';
|
||||
import { generateUsername, generatePassword } from '@/utils';
|
||||
|
||||
export const createUser = () => {
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
export const createUserByAdmin = (_username?: string, _password?: string) => {
|
||||
const username = _username ?? generateUsername();
|
||||
const password = _password ?? generatePassword();
|
||||
|
||||
return createUserApi({
|
||||
return createUser({
|
||||
username,
|
||||
password,
|
||||
name: username,
|
||||
}).json<User>();
|
||||
};
|
||||
|
||||
export const registerNewUser = async (username: string, password: string) => {
|
||||
const client = new MockClient();
|
||||
await client.initSession();
|
||||
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
if (!client.interactionCookie) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirectTo } = await registerUserWithUsernameAndPassword(
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
assert(client.isAuthenticated, new Error('Sign in failed'));
|
||||
};
|
||||
|
||||
export const signIn = async (username: string, password: string) => {
|
||||
const client = new MockClient();
|
||||
await client.initSession();
|
||||
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
if (!client.interactionCookie) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
assert(client.isAuthenticated, new Error('Sign in failed'));
|
||||
};
|
||||
|
||||
export const registerUserAndSignIn = async () => {
|
||||
const logtoClient = new LogtoClient({
|
||||
const client = new MockClient({
|
||||
endpoint: logtoUrl,
|
||||
appId: demoAppApplicationId,
|
||||
persistAccessToken: false,
|
||||
});
|
||||
|
||||
await logtoClient.signIn(demoAppRedirectUri);
|
||||
|
||||
assert(logtoClient.navigateUrl, new Error('Unable to navigate to sign in uri'));
|
||||
|
||||
const interactionCookie = await visitSignInUri(logtoClient.navigateUrl);
|
||||
await client.initSession(demoAppRedirectUri);
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
|
||||
await registerUserWithUsernameAndPassword(username, password, interactionCookie);
|
||||
await registerUserWithUsernameAndPassword(username, password, client.interactionCookie);
|
||||
|
||||
const interactionCookieWithSession = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
username,
|
||||
password,
|
||||
interactionCookie
|
||||
client.interactionCookie
|
||||
);
|
||||
|
||||
const signInCallbackUri = await consentUserAndGetSignInCallbackUri(interactionCookieWithSession);
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
await logtoClient.handleSignInCallback(signInCallbackUri);
|
||||
|
||||
assert(logtoClient.isAuthenticated, new Error('Sign in failed'));
|
||||
assert(client.isAuthenticated, new Error('Sign in failed'));
|
||||
};
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { HTTPError } from 'got';
|
||||
|
||||
import { getUser, getUsers, updateUser, deleteUser, updateUserPassword } from '@/api';
|
||||
import { createUser } from '@/helpers';
|
||||
import { createUserByAdmin } from '@/helpers';
|
||||
|
||||
describe('admin console user management', () => {
|
||||
it('should create user successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
const userDetails = await getUser(user.id);
|
||||
expect(userDetails.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it('should get user list successfully', async () => {
|
||||
await createUser();
|
||||
await createUserByAdmin();
|
||||
const users = await getUsers();
|
||||
|
||||
expect(users.length).not.toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should update userinfo successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
const newUserData = {
|
||||
name: 'new name',
|
||||
|
@ -36,7 +36,7 @@ describe('admin console user management', () => {
|
|||
});
|
||||
|
||||
it('should delete user successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
const userEntity = await getUser(user.id);
|
||||
expect(userEntity).toMatchObject(user);
|
||||
|
@ -48,7 +48,7 @@ describe('admin console user management', () => {
|
|||
});
|
||||
|
||||
it('should update user password successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
const userEntity = await updateUserPassword(user.id, 'new_password');
|
||||
expect(userEntity).toMatchObject(user);
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { ArbitraryObject, UserInfo, userInfoSelectFields } from '@logto/schemas';
|
||||
|
||||
import { api } from '@/api';
|
||||
import { createUser } from '@/helpers';
|
||||
import { createUserByAdmin } from '@/helpers';
|
||||
import { generatePassword } from '@/utils';
|
||||
|
||||
describe('api `/me`', () => {
|
||||
it('should get user info successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
const userInfo = await api
|
||||
.get(`me`, { headers: { 'development-user-id': user.id } })
|
||||
.json<UserInfo>();
|
||||
|
@ -19,7 +19,7 @@ describe('api `/me`', () => {
|
|||
});
|
||||
|
||||
it('should get user custom data successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
const customData = await api
|
||||
.get('me/custom-data', {
|
||||
headers: {
|
||||
|
@ -32,7 +32,7 @@ describe('api `/me`', () => {
|
|||
});
|
||||
|
||||
it('should update user custom data successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
|
||||
const headers = Object.freeze({
|
||||
'development-user-id': user.id,
|
||||
|
@ -59,7 +59,7 @@ describe('api `/me`', () => {
|
|||
});
|
||||
|
||||
it('should change user password successfully', async () => {
|
||||
const user = await createUser();
|
||||
const user = await createUserByAdmin();
|
||||
const password = generatePassword();
|
||||
const changePasswordResponse = await api.patch('me/password', {
|
||||
headers: {
|
||||
|
|
15
packages/integration-tests/tests/session.test.ts
Normal file
15
packages/integration-tests/tests/session.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { registerNewUser, signIn } from '@/helpers';
|
||||
import { generateUsername, generatePassword } from '@/utils';
|
||||
|
||||
describe('username and password flow', () => {
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
|
||||
it('register with username & password', async () => {
|
||||
await expect(registerNewUser(username, password)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('sign-in with username & password', async () => {
|
||||
await expect(signIn(username, password)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import { registerUserAndSignIn } from '@/helpers';
|
||||
|
||||
describe('username and password flow', () => {
|
||||
it('should register and sign in with username and password successfully', async () => {
|
||||
expect(async () => {
|
||||
await registerUserAndSignIn();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue