Skip to content

Commit 836a854

Browse files
authored
Merge pull request #2923 from Brain-up/fix-empty-profile-after-registration
Fix blank profile after registration
2 parents fcec536 + 8a04fa4 commit 836a854

4 files changed

Lines changed: 186 additions & 45 deletions

File tree

frontend/app/components/registration-form/index.gts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ export default class RegistrationFormComponent extends Component {
118118
await this.loginTask.cancelAll();
119119
}
120120

121-
if (this.session.isAuthenticated) {
122-
this.router.transitionTo('index');
123-
}
121+
// Registration deliberately omits the post-login redirect: registrationTask
122+
// still has to PATCH the profile and reload the user, and navigating away
123+
// here would tear down this component and cancel that work, leaving the
124+
// profile blank. registrationTask redirects once it is done.
124125
});
125126

126127
// --- Own getters ---
@@ -232,8 +233,13 @@ export default class RegistrationFormComponent extends Component {
232233
this.network.loadCloudUrl(),
233234
]);
234235
} catch (e) {
236+
// The account exists and the session is authenticated; only the profile
237+
// save failed. Fall through to the redirect rather than trapping the user
238+
// here — a re-submit would re-run registerUser and fail with "user already
239+
// exists". They can finish the profile on the profile page.
235240
const error = e as Error & { errors?: string[] };
236241
const key = error.errors?.pop() ?? error.message;
242+
console.error('Failed to save profile after registration:', error);
237243
if (this.intl.exists(`msg.validation.${key}`)) {
238244
this.errorMessage = this.intl.t(`msg.validation.${key}`);
239245
} else {
@@ -242,7 +248,12 @@ export default class RegistrationFormComponent extends Component {
242248
? this.intl.t(ERRORS_MAP[key as keyof typeof ERRORS_MAP])
243249
: key;
244250
}
245-
await this.registrationTask.cancelAll();
251+
}
252+
253+
// Redirect whether or not the PATCH succeeded; this tears down the component
254+
// and cancels the task, so it must be the last step.
255+
if (this.session.isAuthenticated) {
256+
this.router.transitionTo('index');
246257
}
247258
});
248259

frontend/app/services/network.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,19 @@ export interface LatestUserDTO {
2929

3030
function fromLatestUserDto(user: LatestUserDTO): UserDTO {
3131
const [firstName = '', lastName = ''] = (user.name || '').split(' ');
32-
const bDate = new Date();
33-
34-
bDate.setFullYear(user.bornYear);
32+
// `birthday` is just the four-digit year; guard a missing/invalid bornYear so
33+
// the field renders empty instead of "NaN".
34+
const bornYear = Number(user.bornYear);
35+
const birthday =
36+
Number.isInteger(bornYear) && bornYear > 0 ? String(bornYear) : '';
3537

3638
return {
37-
firstName: firstName || '',
38-
lastName: lastName || '',
39+
firstName,
40+
lastName,
3941
avatar: user.avatar,
4042
email: user.email,
4143
gender: user.gender,
42-
birthday: bDate.getFullYear().toString(),
44+
birthday,
4345
id: user.id as string,
4446
};
4547
}

frontend/tests/integration/components/registration-form/component-test.gjs

Lines changed: 133 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ function getDate(num) {
1111
return date.getFullYear() + num;
1212
}
1313

14+
async function fillAndSubmit() {
15+
await fillIn('[name="firstName"]', 'b');
16+
await fillIn('[name="email"]', 'c@name.com');
17+
await fillIn('[name="password"]', 'Test1234');
18+
await fillIn('[name="repeatPassword"]', 'Test1234');
19+
await fillIn('[name="birthday"]', '1991');
20+
await click('[name="agreement"]');
21+
await click('[id="male"]');
22+
await click('[data-test-submit-form]');
23+
}
24+
1425
module('Integration | Component | registration-form', function (hooks) {
1526
setupRenderingTest(hooks);
1627
setupIntl(hooks, 'en-us');
@@ -25,42 +36,32 @@ module('Integration | Component | registration-form', function (hooks) {
2536
});
2637

2738
test('it send register request if all fields filled', async function (assert) {
28-
assert.expect(6);
29-
3039
// eslint-disable-next-line ember/no-classic-classes
3140
const MockFirebaseAuthenticator = EmberObject.extend({
3241
registerUser() {
3342
return Promise.resolve();
3443
},
3544
});
3645

37-
let loadCurrentUserCallCount = 0;
38-
let patchUserInfoCalled = false;
39-
4046
class MockNetwork extends Service {
4147
loadCurrentUser() {
42-
loadCurrentUserCallCount++;
43-
if (patchUserInfoCalled) {
44-
assert.ok(true, 'loadCurrentUser called after patchUserInfo to refresh profile data');
45-
}
48+
assert.step('loadCurrentUser');
4649
return Promise.resolve();
4750
}
4851
loadCloudUrl() {
4952
return Promise.resolve();
5053
}
5154
patchUserInfo(fields) {
52-
patchUserInfoCalled = true;
55+
assert.step('patchUserInfo');
5356
assert.ok(fields, 'patchUserInfo called with user fields');
5457
return Promise.resolve(fields);
5558
}
5659
}
5760

5861
class MockSession extends Service {
5962
isAuthenticated = false;
60-
authenticate(type, login, password) {
61-
assert.ok(type, 'authenticate called with type');
62-
assert.ok(login, 'authenticate called with login');
63-
assert.ok(password, 'authenticate called with password');
63+
authenticate() {
64+
assert.step('authenticate');
6465
return Promise.resolve();
6566
}
6667
}
@@ -70,25 +71,126 @@ module('Integration | Component | registration-form', function (hooks) {
7071
this.owner.register('service:network', MockNetwork);
7172

7273
await render(<template><RegistrationForm /></template>);
73-
await fillIn('[name="firstName"]', 'b');
74-
await fillIn('[name="email"]', 'c@name.com');
75-
await fillIn('[name="password"]', 'Test1234');
76-
await fillIn('[name="repeatPassword"]', 'Test1234');
77-
await fillIn('[name="birthday"]', '1991');
78-
await click('[name="agreement"]');
79-
await click('[id="male"]');
80-
await click('[data-test-submit-form]');
81-
82-
assert.strictEqual(loadCurrentUserCallCount, 2, 'loadCurrentUser called twice: once during login, once after patchUserInfo');
74+
await fillAndSubmit();
75+
76+
// Login loads the user, then the profile is patched and the user reloaded.
77+
assert.verifySteps([
78+
'authenticate',
79+
'loadCurrentUser',
80+
'patchUserInfo',
81+
'loadCurrentUser',
82+
]);
8383
});
8484

85-
test('it able to handle registration error', async function (assert) {
86-
assert.expect(2);
85+
test('redirects to index only after the profile is patched and reloaded', async function (assert) {
86+
// eslint-disable-next-line ember/no-classic-classes
87+
const MockFirebaseAuthenticator = EmberObject.extend({
88+
registerUser() {
89+
return Promise.resolve();
90+
},
91+
});
92+
93+
class MockNetwork extends Service {
94+
loadCurrentUser() {
95+
assert.step('loadCurrentUser');
96+
return Promise.resolve();
97+
}
98+
loadCloudUrl() {
99+
return Promise.resolve();
100+
}
101+
patchUserInfo(fields) {
102+
assert.step('patchUserInfo');
103+
return Promise.resolve(fields);
104+
}
105+
}
87106

107+
class MockSession extends Service {
108+
isAuthenticated = false;
109+
authenticate() {
110+
// Mirror production: the session becomes authenticated after login.
111+
this.isAuthenticated = true;
112+
return Promise.resolve();
113+
}
114+
}
115+
116+
this.owner.register('authenticator:firebase', MockFirebaseAuthenticator);
117+
this.owner.register('service:session', MockSession);
118+
this.owner.register('service:network', MockNetwork);
119+
120+
// Spy on the real router's transitionTo so the template's <LinkTo>s keep
121+
// rendering while we record when the redirect happens.
122+
this.owner.lookup('service:router').transitionTo = (route) => {
123+
assert.step(`transitionTo:${route}`);
124+
};
125+
126+
await render(<template><RegistrationForm /></template>);
127+
await fillAndSubmit();
128+
129+
// The redirect must come last — after the profile is patched and reloaded.
130+
// Redirecting earlier cancels the in-flight task and leaves the profile
131+
// blank (the bug this fixes).
132+
assert.verifySteps([
133+
'loadCurrentUser',
134+
'patchUserInfo',
135+
'loadCurrentUser',
136+
'transitionTo:index',
137+
]);
138+
});
139+
140+
test('still redirects into the app when the profile patch fails after auth', async function (assert) {
88141
// eslint-disable-next-line ember/no-classic-classes
89142
const MockFirebaseAuthenticator = EmberObject.extend({
90143
registerUser() {
91-
assert.ok(true, 'registerUser was called');
144+
return Promise.resolve();
145+
},
146+
});
147+
148+
class MockNetwork extends Service {
149+
loadCurrentUser() {
150+
return Promise.resolve();
151+
}
152+
loadCloudUrl() {
153+
return Promise.resolve();
154+
}
155+
patchUserInfo() {
156+
assert.step('patchUserInfo');
157+
// The account is already created/authenticated; only the profile save
158+
// fails (e.g. a transient backend error).
159+
return Promise.reject(
160+
Object.assign(new Error('save failed'), { errors: ['save failed'] }),
161+
);
162+
}
163+
}
164+
165+
class MockSession extends Service {
166+
isAuthenticated = false;
167+
authenticate() {
168+
this.isAuthenticated = true;
169+
return Promise.resolve();
170+
}
171+
}
172+
173+
this.owner.register('authenticator:firebase', MockFirebaseAuthenticator);
174+
this.owner.register('service:session', MockSession);
175+
this.owner.register('service:network', MockNetwork);
176+
177+
this.owner.lookup('service:router').transitionTo = (route) => {
178+
assert.step(`transitionTo:${route}`);
179+
};
180+
181+
await render(<template><RegistrationForm /></template>);
182+
await fillAndSubmit();
183+
184+
// The registered+authenticated user is sent into the app instead of being
185+
// trapped on the form, even though the profile save failed.
186+
assert.verifySteps(['patchUserInfo', 'transitionTo:index']);
187+
});
188+
189+
test('it able to handle registration error', async function (assert) {
190+
// eslint-disable-next-line ember/no-classic-classes
191+
const MockFirebaseAuthenticator = EmberObject.extend({
192+
registerUser() {
193+
assert.step('registerUser');
92194
return Promise.reject(new Error('foo'));
93195
},
94196
});
@@ -117,14 +219,10 @@ module('Integration | Component | registration-form', function (hooks) {
117219
this.owner.register('service:network', MockNetwork);
118220

119221
await render(<template><RegistrationForm /></template>);
120-
await fillIn('[name="firstName"]', 'b');
121-
await fillIn('[name="email"]', 'c@name.com');
122-
await fillIn('[name="password"]', 'Test1234');
123-
await fillIn('[name="repeatPassword"]', 'Test1234');
124-
await fillIn('[name="birthday"]', '1991');
125-
await click('[name="agreement"]');
126-
await click('[id="male"]');
127-
await click('[data-test-submit-form]');
222+
await fillAndSubmit();
223+
224+
// registerUser fails, so login/patch never run.
225+
assert.verifySteps(['registerUser']);
128226
assert.dom('[data-test-form-error]').hasText('foo');
129227
});
130228

frontend/tests/unit/services/network-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,37 @@ module('Unit | Service | network', function (hooks) {
116116
assert.strictEqual(userData.userModel.email, 'test@example.com', 'email is set');
117117
assert.strictEqual(userData.userModel.avatar, '3', 'avatar is set');
118118
assert.strictEqual(userData.userModel.gender, 'MALE', 'gender is set');
119+
assert.strictEqual(userData.userModel.birthday, '1990', 'birthday parsed from bornYear');
119120
assert.strictEqual(userData.userModel.id, '42', 'id is set');
120121
assert.strictEqual(userData.userModel.initials, 'TU', 'initials computed correctly');
121122
});
123+
124+
test('loadCurrentUser leaves birthday empty when bornYear is missing', async function (assert) {
125+
window.server.get('users/current', () => ({
126+
data: [
127+
{
128+
id: '43',
129+
name: 'No Year',
130+
email: 'noyear@example.com',
131+
gender: 'FEMALE',
132+
active: true,
133+
avatar: '1',
134+
roles: ['ROLE_USER'],
135+
},
136+
],
137+
errors: [],
138+
meta: [],
139+
}));
140+
141+
const network = this.owner.lookup('service:network');
142+
const userData = this.owner.lookup('service:user-data');
143+
144+
await network.loadCurrentUser();
145+
146+
assert.strictEqual(
147+
userData.userModel.birthday,
148+
'',
149+
'birthday is empty (not "NaN") when bornYear is absent',
150+
);
151+
});
122152
});

0 commit comments

Comments
 (0)