Skip to content
Merged
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
9 changes: 7 additions & 2 deletions app/components/identity/reload.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
<div class='identity-box-desc' data-test-reload-desc>Reload to complete and
verify the link between Profile Service and RealDevSquad Service.</div>
<button
class='identity-box-button' type="button" {{on 'click' this.handleReload}}
>Reload</button>
class='identity-box-button'
data-test-blocked-button
type="button"
{{on "click" (fn this.setState "step1")}}
>
Retry
</button>
1 change: 1 addition & 0 deletions app/components/spinner.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<i class="fa fa-spinner fa-spin spinner" ...attributes></i>
1 change: 1 addition & 0 deletions app/constants/error-messages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const ERROR_MESSAGES = {
somethingWentWrong: 'Something went wrong!',
usernameGeneration: 'Username cannot be generated',
notLoggedIn: 'You are not logged in. Please login to continue.',
};
9 changes: 6 additions & 3 deletions app/routes/identity.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { APPS } from '../constants/urls';
import redirectAuth from '../utils/redirect-auth';
import { TOAST_OPTIONS } from '../constants/toast-options';
import { ERROR_MESSAGES } from '../constants/error-messages';
export default class IdentityRoute extends Route {
@service router;
@service login;
@service fastboot;

beforeModel(transition) {
if (transition?.to?.queryParams?.dev !== 'true') {
this.router.transitionTo('page-not-found');
return;
this.router.transitionTo('/page-not-found');
}
}

Expand All @@ -29,7 +31,8 @@ export default class IdentityRoute extends Route {

if (!response.ok) {
if (response.status === 401) {
this.router.transitionTo('index');
this.toast.error(ERROR_MESSAGES.notLoggedIn, '', TOAST_OPTIONS);
setTimeout(redirectAuth, 2000);
return null;
}
throw new Error(`HTTP error! status: ${response.status}`);
Expand Down
30 changes: 30 additions & 0 deletions app/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,33 @@ button {
#toast-container > div {
opacity: 1;
}

.loading {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 90vh;
width: 100%;
text-align: center;
}

.spinner {
display: inline-block;
width: 1.875 rem;
height: 1.875 rem;
border: 4px solid rgb(0 0 0 / 30%);
border-radius: 50%;
border-top: 4px solid var(--color-black);
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}
27 changes: 26 additions & 1 deletion app/styles/identity.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
}

.identity-box-input {
border: 1px solid var(--color-black-50);
border: 1px solid var(--color-black-25);
background: var(--color-white);
width: 50%;
margin-top: 16px;
Expand Down Expand Up @@ -261,6 +261,31 @@
width: 100%;
}

.identity-chaincode-copy-box {
display: flex;
width: 60%;
}

.identity-chaincode-box {
display: flex;
position: relative;
justify-content: space-between;
align-items: center;
padding-left: 0.5rem;
padding-right: 0.5rem;
margin-top: 16px;
border: 1px solid var(--color-black-25);
background: var(--color-white);
width: 90%;
height: 2rem;
color: var(--color-black);
font-family: Inter, sans-serif;
font-size: 1rem;
font-style: normal;
font-weight: 500;
line-height: normal;
}

@media (width <= 460px) {
.identity-box-input {
width: 90%;
Expand Down
2 changes: 1 addition & 1 deletion app/templates/identity.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
{{else if (eq this.initialState 'verified')}}
<Identity::Verified />
{{else if (eq this.initialState 'blocked')}}
<Identity::Blocked />
<Identity::Blocked @setState={{this.setState}} />
{{/if}}
</div>

Expand Down
3 changes: 3 additions & 0 deletions app/templates/loading.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="loading">
<Spinner />
</div>
7 changes: 7 additions & 0 deletions app/utils/redirect-auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { AUTH } from '../constants/urls';

/**
* Redirects to the GitHub authorization URL with the current window's location
* as the redirect URL.
* @function redirectAuth
* @memberof utils
*/

export default function redirectAuth() {
let authUrl = AUTH.GITHUB_SIGN_IN;
if (typeof window !== 'undefined' && window.location) {
Expand Down
179 changes: 179 additions & 0 deletions tests/unit/routes/identity-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { module, test } from 'qunit';
import { setupTest } from 'website-www/tests/helpers';
import { TOAST_OPTIONS } from 'website-www/constants/toast-options';
import { APPS } from 'website-www/constants/urls';
import sinon from 'sinon';

module('Unit | Route | identity', function (hooks) {
setupTest(hooks);

hooks.beforeEach(function () {
this.route = this.owner.lookup('route:identity');

this.route.router = {
transitionTo: sinon.stub(),
};
this.route.toast = {
error: sinon.stub(),
};
this.route.fastboot = {
isFastBoot: false,
};
});

test('it exists', function (assert) {
assert.ok(this.route);
});

test('beforeModel redirects to page-not-found when dev param is not true', function (assert) {
const transition = {
to: {
queryParams: {
dev: 'false',
},
},
};

this.route.beforeModel(transition);
assert.ok(
this.route.router.transitionTo.calledWith('/page-not-found'),
'should redirect to page-not-found',
);
});

test('beforeModel allows navigation when dev param is true', function (assert) {
const transition = {
to: {
queryParams: {
dev: 'true',
},
},
};

this.route.beforeModel(transition);
assert.notOk(this.route.router.transitionTo.called, 'should not redirect');
});

test('model returns null in FastBoot', async function (assert) {
this.route.fastboot.isFastBoot = true;
const result = await this.route.model();
assert.strictEqual(result, null, 'should return null in FastBoot');
});

test('model handles 401 unauthorized response', async function (assert) {
const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: false,
status: 401,
});

const result = await this.route.model();

assert.strictEqual(result, null, 'should return null');
assert.ok(
this.route.toast.error.calledWith(
'You are not logged in. Please login to continue.',
'',
TOAST_OPTIONS,
),
'should show error toast',
);

fetchStub.restore();
});

test('model handles successful response with invalid discord role', async function (assert) {
const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: true,
json: () =>
Promise.resolve({
roles: {
in_discord: false,
},
}),
});

const result = await this.route.model();

assert.strictEqual(result, null, 'should return null');
assert.ok(
this.route.router.transitionTo.calledWith('index'),
'should redirect to index',
);

fetchStub.restore();
});

test('model handles network error', async function (assert) {
const fetchStub = sinon
.stub(window, 'fetch')
.rejects(new Error('Network error'));

const result = await this.route.model();

assert.deepEqual(result, null, 'should return null');
assert.ok(
this.route.router.transitionTo.calledWith('index'),
'should redirect to index',
);

fetchStub.restore();
});

test('model handles non-401 error response', async function (assert) {
const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: false,
status: 500,
});

const result = await this.route.model();

assert.strictEqual(result, null, 'should return null');
assert.ok(
this.route.router.transitionTo.calledWith('index'),
'should redirect to index',
);

fetchStub.restore();
});

test('model handles successful response with valid discord role', async function (assert) {
const mockData = {
roles: {
in_discord: true,
},
someOtherData: 'test',
};

const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: true,
json: () => Promise.resolve(mockData),
});

const result = await this.route.model();

assert.deepEqual(result, mockData, 'should return the API response data');
assert.ok(fetchStub.called, 'fetch should be called');

const [actualUrl, actualOptions] = fetchStub.firstCall.args;

assert.strictEqual(
actualUrl,
`${APPS.API_BACKEND}/users?profile=true`,
'should call correct URL',
);

assert.deepEqual(
actualOptions,
{
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
'should pass correct options',
);

fetchStub.restore();
});
});