diff --git a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx
index 3dd906d5ed8..12e4638be9e 100644
--- a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx
+++ b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx
@@ -90,9 +90,7 @@ function LinkMsUser({
-
- {t('learn.ms.link-signin')}
-
+ {t('learn.ms.link-signin')}
>
) : (
@@ -114,11 +112,9 @@ function LinkMsUser({
-
- {t('learn.ms.unlinked')}
-
+ {t('learn.ms.unlinked')}
- -
+
-
- -
- {t('learn.ms.link-li-2')}
-
- -
- {t('learn.ms.link-li-3')}
-
- -
- {t('learn.ms.link-li-4')}
-
- -
+
- {t('learn.ms.link-li-2')}
+ - {t('learn.ms.link-li-3')}
+ - {t('learn.ms.link-li-4')}
+ -
placeholder
- -
- {t('learn.ms.link-li-6')}
-
+ - {t('learn.ms.link-li-6')}
diff --git a/e2e/link-ms-user.spec.ts b/e2e/link-ms-user.spec.ts
index fe3aa3451b6..022982715fa 100644
--- a/e2e/link-ms-user.spec.ts
+++ b/e2e/link-ms-user.spec.ts
@@ -7,63 +7,115 @@ test.beforeEach(async ({ page }) => {
);
});
-test.describe('Link MS user component (unlinked signedOut user)', () => {
- test('Component has proper main heading and relevant sections', async ({
+test.describe('Link MS user component (signed-out user)', () => {
+ test('should display the page content with a signin CTA', async ({
page
}) => {
- const mainHeading = page.getByRole('heading', {
- name: translations.learn.ms['link-header']
- });
- await expect(mainHeading).toBeVisible();
+ await expect(
+ page.getByRole('heading', {
+ name: 'Trophy - Write Your First Code Using C#',
+ level: 1
+ })
+ ).toBeVisible();
- const linkSignInText = page.getByTestId('link-signin-text');
- await expect(linkSignInText).toBeVisible();
+ await expect(
+ page.getByRole('heading', {
+ name: translations.learn.ms['link-header'],
+ level: 2
+ })
+ ).toBeVisible();
+
+ await expect(
+ page.getByText(translations.learn.ms['link-signin'])
+ ).toBeVisible();
+
+ // There are 2 sign in button on the page: one in the navbar, and one in the page content
+ const signInButtons = await page
+ .getByRole('link', { name: translations.buttons['sign-in'] })
+ .all();
+ expect(signInButtons).toHaveLength(2);
});
});
-test.describe('Link MS user component (unlinked signedIn user)', () => {
+test.describe('Link MS user component (signed-in user)', () => {
test.use({ storageState: 'playwright/.auth/certified-user.json' });
- test('Component has proper main heading and relevant sections', async ({
- page
- }) => {
- const mainHeading = page.getByRole('heading', {
- name: translations.learn.ms['link-header']
- });
- await expect(mainHeading).toBeVisible();
+ test("should recognize the user's MS account", async ({ page }) => {
+ await expect(
+ page.getByRole('heading', {
+ name: 'Trophy - Write Your First Code Using C#',
+ level: 1
+ })
+ ).toBeVisible();
- const linkSignInText = page.getByTestId('unlinked-text');
- await expect(linkSignInText).toBeVisible();
+ await expect(
+ page.getByText(
+ 'The Microsoft account with username "certifieduser" is currently linked to your freeCodeCamp account. If this is not your Microsoft username, remove the link.'
+ )
+ ).toBeVisible();
});
- test('Component has proper list of actions', async ({ page }) => {
- const linkText1 = page.getByTestId('link-li-1-text');
- await expect(linkText1).toBeVisible();
- await expect(linkText1).toContainText(
- 'Using a browser where you are logged into your Microsoft account, go to https://learn.microsoft.com/users/me/transcript'
- );
+ test('should allow the user to unlink their MS account and display a form for re-link', async ({
+ page
+ }) => {
+ // Intercept the endpoint to prevent `msUsername` from being deleted
+ // as the deletion will cause subsequent tests to fail
+ await page.route('*/**/user/ms-username', async route => {
+ const json = { msUsername: null };
+ await route.fulfill({ json });
+ });
- const linkText2 = page.getByTestId('link-li-2-text');
- await expect(linkText2).toBeVisible();
- await expect(linkText2).toHaveText(translations.learn.ms['link-li-2']);
+ const unlinkButton = page.getByRole('button', {
+ name: translations.buttons['unlink-account']
+ });
+ await expect(unlinkButton).toBeVisible();
+ await unlinkButton.click();
- const linkText3 = page.getByTestId('link-li-3-text');
- await expect(linkText3).toBeVisible();
- await expect(linkText3).toHaveText(translations.learn.ms['link-li-3']);
+ await expect(
+ page
+ .getByRole('alert')
+ .filter({ hasText: translations.flash.ms.transcript.unlinked })
+ ).toBeVisible();
- const linkText4 = page.getByTestId('link-li-4-text');
- await expect(linkText4).toBeVisible();
- await expect(linkText4).toHaveText(translations.learn.ms['link-li-4']);
-
- const linkText5 = page.getByTestId('link-li-5-text');
- await expect(linkText5).toBeVisible();
- await expect(linkText5).toHaveText(
- 'Paste the URL into the input below, it should look similar to this: https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
- );
-
- const linkText6 = page.getByTestId('link-li-6-text');
- await expect(linkText6).toBeVisible();
- await expect(linkText6).toHaveText(translations.learn.ms['link-li-6']);
+ await expect(
+ page.getByRole('heading', {
+ name: translations.learn.ms['link-header'],
+ level: 2
+ })
+ ).toBeVisible();
+ await expect(page.getByText(translations.learn.ms.unlinked)).toBeVisible();
+ await expect(
+ page.getByRole('listitem').filter({
+ hasText:
+ 'Using a browser where you are logged into your Microsoft account, go to https://learn.microsoft.com/users/me/transcript'
+ })
+ ).toBeVisible();
+ await expect(
+ page
+ .getByRole('listitem')
+ .filter({ hasText: translations.learn.ms['link-li-2'] })
+ ).toBeVisible();
+ await expect(
+ page
+ .getByRole('listitem')
+ .filter({ hasText: translations.learn.ms['link-li-3'] })
+ ).toBeVisible();
+ await expect(
+ page
+ .getByRole('listitem')
+ .filter({ hasText: translations.learn.ms['link-li-4'] })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('listitem').filter({
+ hasText:
+ 'Paste the URL into the input below, it should look similar to this: https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
+ })
+ ).toBeVisible();
+ await expect(
+ page
+ .getByRole('listitem')
+ .filter({ hasText: translations.learn.ms['link-li-6'] })
+ ).toBeVisible();
const transcriptLinkInput = page.getByLabel(
translations.learn.ms['transcript-label']
diff --git a/package.json b/package.json
index 7e62413ff31..f483c67a0bf 100644
--- a/package.json
+++ b/package.json
@@ -72,9 +72,10 @@
"playwright:install-build-tools-linux": "sh ./playwright-install.sh",
"rename-challenges": "ts-node tools/challenge-helper-scripts/rename-challenge-files.ts",
"seed": "pnpm seed:surveys && pnpm seed:exams && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user",
- "seed:certified-user": "pnpm seed:surveys && pnpm seed:exams && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user",
+ "seed:certified-user": "pnpm seed:surveys && pnpm seed:exams && pnpm seed:ms-username && cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user",
"seed:exams": "cross-env DEBUG=fcc:* node tools/scripts/seed-exams/create-exams",
"seed:surveys": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-surveys",
+ "seed:ms-username": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-ms-username",
"serve:client": "cd ./client && pnpm run serve",
"serve:client-ci": "cd ./client && pnpm run serve-ci",
"start": "npm-run-all create:shared -p develop:server serve:client",
diff --git a/tools/scripts/seed/seed-ms-username.js b/tools/scripts/seed/seed-ms-username.js
new file mode 100644
index 00000000000..8b989234d30
--- /dev/null
+++ b/tools/scripts/seed/seed-ms-username.js
@@ -0,0 +1,74 @@
+const path = require('path');
+const debug = require('debug');
+const { MongoClient, ObjectId } = require('mongodb');
+require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
+
+const log = debug('fcc:tools:seedMsUsername');
+const { MONGOHQ_URL } = process.env;
+
+const args = process.argv.slice(2);
+
+const allowedArgs = ['--delete-only'];
+
+// Check for invalid arguments
+args.forEach(arg => {
+ if (!allowedArgs.includes(arg))
+ throw new Error(
+ `Invalid argument ${arg}. Allowed arguments are ${allowedArgs.join(', ')}`
+ );
+});
+
+function handleError(err, client) {
+ if (err) {
+ console.error('Oh noes!! Error seeding MS username.');
+ console.error(err);
+ try {
+ client.close();
+ } catch (e) {
+ // no-op
+ } finally {
+ /* eslint-disable-next-line no-process-exit */
+ process.exit(1);
+ }
+ }
+}
+
+const msAccountId = new ObjectId('65785b25d4c5bd0565c0184d');
+
+const certifiedUserAccount = {
+ _id: msAccountId,
+ userId: new ObjectId('5fa2db00a25c1c1fa49ce067'),
+ ttl: 77760000000,
+ msUsername: 'certifieduser'
+};
+
+const client = new MongoClient(MONGOHQ_URL, { useNewUrlParser: true });
+
+log('Connected successfully to mongo');
+
+const db = client.db('freecodecamp');
+const msUsername = db.collection('MsUsername');
+
+const run = async () => {
+ if (args.includes('--delete-only')) {
+ await msUsername.deleteOne({
+ _id: { $eq: msAccountId }
+ });
+
+ log('MS username deleted');
+ return;
+ }
+
+ // Rewrite if the object exists, create new if it doesn't
+ await msUsername.updateOne(
+ { _id: msAccountId },
+ { $set: certifiedUserAccount },
+ { upsert: true }
+ );
+
+ log('MS username seeded');
+};
+
+run()
+ .then(() => client.close())
+ .catch(err => handleError(err, client));