Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cypress Test Case: Login Via SSO #1354

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
env:
CYPRESS_TEST_USERNAME: ${{ secrets.CYPRESS_TEST_USERNAME}}
CYPRESS_TEST_PASSWORD: ${{ secrets.CYPRESS_TEST_PASSWORD }}
CYPRESS_TEST_GOOGLE_PASSWORD: ${{ secrets.CYPRESS_TEST_GOOGLE_PASSWORD }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_TEST_RECORD_ID}}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_TEST_PROJECT_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion app/components/login-component/sso/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from='irene/components/login-component/index.scss'
}}'
>
<AkIcon @iconName='person' class="input-icon" />
<AkIcon @iconName='person' class='input-icon' />
</label>
<Input
data-test-login-sso-forced-username-input
Expand Down
103 changes: 84 additions & 19 deletions cypress/support/Actions/auth/LoginActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,51 @@ export default class LoginActions {
cy.findByPlaceholderText('Username / Email').type(username); // Username/Email field
cy.findByLabelText('login-user-check-icon').click();

cy.wait('@checkUserRoute', { timeout: 30000 });
cy.wait('@checkUserRoute');

cy.findByPlaceholderText('Password').type(password); // Password field
cy.findByLabelText('login-submit-button').click();

cy.wait('@loginAPIReq', { timeout: 30000 });
cy.wait('@loginAPIReq');
}

/**
* Logs user in via SSO using the UI
*/
doLoginViaSSO({ username }: Omit<UserLoginCredentialProps, 'password'>) {
// Intercepts user check request
cy.intercept(API_ROUTES.check.route).as('checkUserRoute');

// Intercepts frontend config request
cy.intercept(API_ROUTES.frontendConfig.route).as('frontendConfig');

cy.intercept(API_ROUTES.saml2Login.route).as('saml2LoginApiReq');

cy.visit(APPLICATION_ROUTES.login);

// Wait for frontend config request to resolve
cy.wait('@frontendConfig');

cy.findByPlaceholderText('Username / Email').type(username); // Username/Email field

cy.findByLabelText('login-user-check-icon').click();

cy.wait('@checkUserRoute');

cy.contains(APP_TRANSLATIONS.ssoLogin).should('exist').click();

cy.origin(
'https://accounts.google.com',
{ args: { username, password: Cypress.env('TEST_GOOGLE_PASSWORD') } },
({ username, password }) => {
cy.get('input[type="email"]').type(`${username}{enter}`);

// Enter password
cy.get('input[type="password"]').type(`${password}{enter}`);
}
);

cy.wait('@saml2LoginApiReq');
}

/**
Expand Down Expand Up @@ -73,6 +112,18 @@ export default class LoginActions {
cy.findAllByLabelText('login-user-check-icon').should('exist'); // User check button;
}

validateCurrentUserSession() {
// If these elements are displayed, token is expired
cy.findByPlaceholderText('Username / Email').should('not.exist'); // Username/Email field
cy.findByLabelText('login-user-check-icon').should('not.exist'); // User check button;s

// Validate presence of access token in localStorage.
cy.window()
.its('localStorage')
.invoke('getItem', 'ember_simple_auth-session')
.should('exist');
}

/**
* Logs in with user credentials and caches session across specs
*/
Expand All @@ -85,24 +136,38 @@ export default class LoginActions {
() => this.doLoginWithUI(userCreds),
{
cacheAcrossSpecs,
validate: () => {
// If these elements are displayed, token is expired
cy.findByPlaceholderText('Username / Email').should('not.exist'); // Username/Email field
cy.findByLabelText('login-user-check-icon').should('not.exist'); // User check button;s

// Validate presence of access token in localStorage.
cy.window()
.its('localStorage')
.invoke('getItem', 'ember_simple_auth-session')
.then((authInfo) => {
const authDetails = authInfo ? JSON.parse(authInfo) : {};

expect(authDetails)
.and.to.have.property('authenticated')
.to.have.any.keys('authenticator', 'b64token', 'token');
});
},
validate: this.validateCurrentUserSession.bind(this),
}
);
}

/**
* Logs in with SSO and caches session across specs
*/
loginWithSSOAndSaveSession(
userCreds: Omit<UserLoginCredentialProps, 'password'>,
cacheAcrossSpecs = true
) {
cy.session(
session.createSessionIdWithSSO(userCreds),
() => this.doLoginViaSSO(userCreds),
{
cacheAcrossSpecs,
validate: this.validateCurrentUserSession.bind(this),
}
);
}

/**
* Assertions after Login
*/
verifyDashboardElements() {
// Assertion for different dashboard elements
cy.findByText(APP_TRANSLATIONS.startNewScan).should('exist');
cy.findByText(APP_TRANSLATIONS.uploadApp).should('exist');
cy.findByText(APP_TRANSLATIONS.allProjects).should('exist');
cy.findByText(APP_TRANSLATIONS.allProjectsDescription).should('exist');
cy.findByText(APP_TRANSLATIONS.support).should('exist');
cy.findByText(APP_TRANSLATIONS.knowledgeBase).should('exist');
}
}
6 changes: 6 additions & 0 deletions cypress/support/Actions/common/SessionActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export default class SessionActions {
return `${username}-${password}`;
}

createSessionIdWithSSO({
username,
}: Omit<UserLoginCredentialProps, 'password'>) {
return `SSO-${username}`;
}

resetCurrentSession() {
return Cypress.session.clearCurrentSessionData();
}
Expand Down
4 changes: 4 additions & 0 deletions cypress/support/api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const API_ROUTES = {

// Auth
login: { route: '/api/login' },
saml2Login: {
route: '/api/sso/saml2/login',
alias: 'saml2LoginReq',
},

// Listing Routes
sbomProjectList: {
Expand Down
90 changes: 83 additions & 7 deletions cypress/tests/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,59 @@ const password = Cypress.env('TEST_PASSWORD');
describe('User Login', () => {
beforeEach(() => {
networkActions.hideNetworkLogsFor({ ...API_ROUTES.websockets });

cy.intercept(API_ROUTES.check.route).as('checkUserRoute');
cy.intercept(API_ROUTES.userInfo.route).as('userInfoRoute');

// Intercept vulnerabilities route
networkActions.mockNetworkReq({
...API_ROUTES.vulnerabilityList,
dataOverride: {
data: mirageServer.createRecordList('vulnerability', 1).map((_) => ({
id: _.id,
type: 'vulnerabilities',
attributes: _,
relationships: {},
})),
},
});

// Intercept unknown analysis request
const unknownAnalysisStatus = mirageServer.createRecord(
'unknown-analysis-status'
);

networkActions.mockNetworkReq({
...API_ROUTES.unknownAnalysisStatus,
dataOverride: unknownAnalysisStatus,
});

// Intercept file request
const file = mirageServer.createRecord('file');

networkActions.mockNetworkReq({
...API_ROUTES.file,
dataOverride: file,
});

// Intercept projects route
networkActions.mockPaginatedNetworkReq({
...API_ROUTES.projectList,
resDataOverride: {
results: mirageServer
.createRecordList('project', 1)
.map((_) => ({ ..._, file: file.id })),
},
});

// Return empty submissions data
networkActions.mockPaginatedNetworkReq({
route: API_ROUTES.submissionList.route,
alias: 'submissionList',
resDataOverride: {
results: [],
},
});
});

it('should redirect unauthenticated user to login page', function () {
Expand Down Expand Up @@ -158,12 +211,35 @@ describe('User Login', () => {
}
});

// Assertion for different dashboard elements
cy.findByText(APP_TRANSLATIONS.startNewScan).should('exist');
cy.findByText(APP_TRANSLATIONS.uploadApp).should('exist');
cy.findByText(APP_TRANSLATIONS.allProjects).should('exist');
cy.findByText(APP_TRANSLATIONS.allProjectsDescription).should('exist');
cy.findByText(APP_TRANSLATIONS.support).should('exist');
cy.findByText(APP_TRANSLATIONS.knowledgeBase).should('exist');
loginActions.verifyDashboardElements();
});

it('should redirect authenticated user to dashboard after logging in via SSO', () => {
// Logs user to dashboard via API
loginActions.loginWithSSOAndSaveSession({ username });

cy.visit('/');

// Navigates to project listing route
cy.url({ timeout: 15000 }).should('include', APPLICATION_ROUTES.projects);

// Necessary API call before showing dashboard elements
cy.wait('@submissionList', { timeout: 15000 });

// Programmatically check for page elements based on user information.
cy.wait('@userInfoRoute').then(({ response }) => {
const orgUser = response?.body?.data?.attributes as Partial<
MirageFactoryDefProps['user']
>;

const username = orgUser?.username;

// Programmatically check for user name in navbar
if (username) {
cy.findByText(username).should('exist');
}
});

loginActions.verifyDashboardElements();
});
});
Loading