0
Fork 0
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:
simeng-li 2022-07-28 10:13:21 +08:00 committed by GitHub
parent 20c8889be8
commit 939dc0eac8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 251 additions and 203 deletions

View file

@ -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;
};

View file

@ -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';

View 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>();

View 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);
};
}

View file

@ -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(),
}
);
}
}

View file

@ -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'));
};

View file

@ -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);
});

View file

@ -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: {

View 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();
});
});

View file

@ -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();
});
});