diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cc3e2e14..fac302698b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -#### [v6.123.0](https://github.com/opengovsg/FormSG/compare/v6.123.0...v6.123.0) +#### [v6.124.0](https://github.com/opengovsg/FormSG/compare/v6.123.0...v6.124.0) -- revert: "feat: allow wildcard domains for domain-restricted email fields" [`#7363`](https://github.com/opengovsg/FormSG/pull/7363) +- fix(submission): capture split error [`#7360`](https://github.com/opengovsg/FormSG/pull/7360) +- chore: remove unused deps [`#7336`](https://github.com/opengovsg/FormSG/pull/7336) +- fix(deps): bump libphonenumber-js from 1.11.2 to 1.11.3 in /shared [`#7367`](https://github.com/opengovsg/FormSG/pull/7367) +- chore(deps-dev): bump prettier from 3.3.0 to 3.3.1 in /shared [`#7366`](https://github.com/opengovsg/FormSG/pull/7366) +- feat(i18n): replace hardcoded text in login features [`#7344`](https://github.com/opengovsg/FormSG/pull/7344) +- feat(mockpass): enable admin login using mockpass locally [`#7359`](https://github.com/opengovsg/FormSG/pull/7359) +- build: merge release back to develop [`#7364`](https://github.com/opengovsg/FormSG/pull/7364) +- build: release v6.123.0 [`#7362`](https://github.com/opengovsg/FormSG/pull/7362) #### [v6.123.0](https://github.com/opengovsg/FormSG/compare/v6.122.0...v6.123.0) > 5 June 2024 +- revert: "feat: allow wildcard domains for domain-restricted email fields" [`#7363`](https://github.com/opengovsg/FormSG/pull/7363) - fix(deps): bump type-fest from 4.18.3 to 4.19.0 in /shared [`#7361`](https://github.com/opengovsg/FormSG/pull/7361) - chore(payments): remove payment flags checks from payment controller [`#7354`](https://github.com/opengovsg/FormSG/pull/7354) - chore(deps-dev): bump prettier from 3.2.5 to 3.3.0 in /shared [`#7357`](https://github.com/opengovsg/FormSG/pull/7357) @@ -20,7 +28,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: replace secrets-check with git-secrets [`#7353`](https://github.com/opengovsg/FormSG/pull/7353) - build: merge release v6.122.0 into develop [`#7352`](https://github.com/opengovsg/FormSG/pull/7352) - build: release v6.122.0 [`#7351`](https://github.com/opengovsg/FormSG/pull/7351) -- chore: bump version to v6.123.0 [`314a4af`](https://github.com/opengovsg/FormSG/commit/314a4af772730e4d0196c1a53422c44f3d1421af) +- chore: bump version to v6.123.0 [`d436640`](https://github.com/opengovsg/FormSG/commit/d436640bee5d64fe7ac6c5174dce8d34a611b5fd) #### [v6.122.0](https://github.com/opengovsg/FormSG/compare/v6.121.0...v6.122.0) diff --git a/README.md b/README.md index 1071120c53..b3b9d879d6 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [Running Locally](#running-locally) - [Adding dependencies](#adding-dependencies) - [Accessing email locally](#accessing-email-locally) + - [Login using mockpass locally](#login-using-mockpass-locally) - [Environment variables](#environment-variables) - [Trouble-shooting](#trouble-shooting) - [Testing](#testing) @@ -130,6 +131,15 @@ As frontend project is currently not using Docker, no other steps are required. We use [MailDev](https://github.com/maildev/maildev) to access emails in the development environment. The MailDev UI can be accessed at [localhost:1080](localhost:1080) when the Docker container runs. +### Login using mockpass locally + +1. Click on the `Login with Singpass` button on the login page +2. In the dropdown menu, select `S9812379B [MyInfo]` +3. Choose the profile with the email `lim_yong_xiang@was.gov.sg` +4. You should now be successfully logged in + +**Note**: Remember to renew your formsg_mongodb_data volume + ### Environment variables Docker-compose looks at various places for environment variables to inject into the containers. diff --git a/docker-compose.yml b/docker-compose.yml index 8221eca158..36805e02b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,7 +89,7 @@ services: - SGID_CLIENT_ID=sgidclientid - SGID_CLIENT_SECRET=sgidclientsecret - SGID_JWT_SECRET=sgidjwtsecret - - SGID_ADMIN_LOGIN_REDIRECT_URI=http://localhost:5001/api/v3/auth/sgid/login + - SGID_ADMIN_LOGIN_REDIRECT_URI=http://localhost:5001/api/v3/auth/sgid/login/callback - SGID_FORM_LOGIN_REDIRECT_URI=http://localhost:5001/sgid/login - SGID_PRIVATE_KEY=./node_modules/@opengovsg/mockpass/static/certs/key.pem - SGID_PUBLIC_KEY=./node_modules/@opengovsg/mockpass/static/certs/server.crt @@ -136,7 +136,7 @@ services: mockpass: - build: https://github.com/opengovsg/mockpass.git#v4.0.4 + build: https://github.com/opengovsg/mockpass.git#v4.3.1 depends_on: - backend environment: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b17ade409a..dc16f24441 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.123.0", + "version": "6.124.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.123.0", + "version": "6.124.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", @@ -42,7 +42,6 @@ "i18next": "^21.6.16", "i18next-browser-languagedetector": "^6.1.4", "i18next-icu": "^2.0.3", - "immer": "^9.0.6", "inter-ui": "^3.19.3", "intl-messageformat": "^9.13.0", "jszip": "^3.10.0", diff --git a/frontend/package.json b/frontend/package.json index a3abe6f8be..adfe49b689 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.123.0", + "version": "6.124.0", "homepage": ".", "private": true, "dependencies": { @@ -37,7 +37,6 @@ "i18next": "^21.6.16", "i18next-browser-languagedetector": "^6.1.4", "i18next-icu": "^2.0.3", - "immer": "^9.0.6", "inter-ui": "^3.19.3", "intl-messageformat": "^9.13.0", "jszip": "^3.10.0", diff --git a/frontend/src/features/login/LoginPage.tsx b/frontend/src/features/login/LoginPage.tsx index a28b77f111..461fa4302d 100644 --- a/frontend/src/features/login/LoginPage.tsx +++ b/frontend/src/features/login/LoginPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useSearchParams } from 'react-router-dom' import { Stack } from '@chakra-ui/react' import { StatusCodes } from 'http-status-codes' @@ -25,6 +26,7 @@ export type LoginOtpData = { } export const LoginPage = (): JSX.Element => { + const { t } = useTranslation() const { data: isIntranetIp } = useIsIntranetCheck() const [, setIsAuthenticated] = useLocalStorage(LOGGED_IN_KEY) @@ -41,9 +43,9 @@ export const LoginPage = (): JSX.Element => { case StatusCodes.OK.toString(): return case StatusCodes.UNAUTHORIZED.toString(): - return 'Your sgID login session has expired. Please login again.' + return t('features.login.LoginPage.expiredSgIdSession') default: - return 'Something went wrong. Please try again later.' + return t('features.common.errors.generic') } }, [statusCode]) diff --git a/frontend/src/features/login/LoginPageTemplate.tsx b/frontend/src/features/login/LoginPageTemplate.tsx index 641e6a4eaa..eef546982d 100644 --- a/frontend/src/features/login/LoginPageTemplate.tsx +++ b/frontend/src/features/login/LoginPageTemplate.tsx @@ -118,7 +118,7 @@ export const LoginPageTemplate: FC = ({ children }) => { ) : null} diff --git a/frontend/src/features/login/SelectProfilePage.tsx b/frontend/src/features/login/SelectProfilePage.tsx index 17aaa25c75..7a222ccfa3 100644 --- a/frontend/src/features/login/SelectProfilePage.tsx +++ b/frontend/src/features/login/SelectProfilePage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { BiChevronRight } from 'react-icons/bi' import { Link as ReactLink } from 'react-router-dom' import { @@ -46,32 +47,6 @@ type ModalErrorMessages = { onCtaClick: (disclosureProps: ErrorDisclosureProps) => void } -const MODAL_ERRORS: Record = { - NO_WORKEMAIL: { - hideCloseButton: true, - preventBackdropDismissal: true, - header: "Singpass login isn't available to you yet", - body: 'It is progressively being made available to agencies. In the meantime, please log in using your email address.', - cta: 'Back to login', - onCtaClick: () => window.location.assign(LOGIN_ROUTE), - }, - INVALID_WORKEMAIL: { - header: "You don't have access to this service", - body: () => ( - - It may be available only to select agencies or authorised individuals. - If you believe you should have access to this service, please{' '} - - contact us - - . - - ), - cta: 'Choose another account', - onCtaClick: (disclosureProps) => disclosureProps.onClose(), - }, -} - const ErrorDisclosure = ( props: { errorMessages: ModalErrorMessages | undefined @@ -118,6 +93,7 @@ const ErrorDisclosure = ( ) } export const SelectProfilePage = (): JSX.Element => { + const { t } = useTranslation() const profilesResponse = useSgidProfiles() const [, setIsAuthenticated] = useLocalStorage(LOGGED_IN_KEY) const { user } = useUser() @@ -128,6 +104,31 @@ export const SelectProfilePage = (): JSX.Element => { const errorDisclosure = useDisclosure() const toast = useToast({ isClosable: true, status: 'danger' }) + const MODAL_ERRORS: Record = { + NO_WORKEMAIL: { + hideCloseButton: true, + preventBackdropDismissal: true, + header: t('features.login.SelectProfilePage.noWorkEmailHeader'), + body: t('features.login.SelectProfilePage.noWorkEmailBody'), + cta: t('features.login.SelectProfilePage.noWorkEmailCta'), + onCtaClick: () => window.location.assign(LOGIN_ROUTE), + }, + INVALID_WORKEMAIL: { + header: t('features.login.SelectProfilePage.invalidWorkEmailHeader'), + body: () => ( + + {`${t('features.login.SelectProfilePage.invalidWorkEmailBodyRestriction')} `} + + {t('features.login.SelectProfilePage.invalidWorkEmailBodyContact')} + + . + + ), + cta: t('features.login.SelectProfilePage.invalidWorkEmailCta'), + onCtaClick: (disclosureProps) => disclosureProps.onClose(), + }, + } + // If redirected back here but already authed, redirect to dashboard. if (user) window.location.replace(DASHBOARD_ROUTE) // User doesn't have any profiles, should reattempt to login @@ -156,7 +157,7 @@ export const SelectProfilePage = (): JSX.Element => { setErrorContext(MODAL_ERRORS.INVALID_WORKEMAIL) return } - toast({ description: 'Something went wrong. Please try again later.' }) + toast({ description: t('features.common.errors.generic') }) }) } @@ -173,7 +174,7 @@ export const SelectProfilePage = (): JSX.Element => { divider={} > - Choose an account to continue to FormSG + {t('features.login.SelectProfilePage.accountSelection')} {!profilesResponse.data ? ( @@ -194,7 +195,7 @@ export const SelectProfilePage = (): JSX.Element => { as={ReactLink} to={LOGIN_ROUTE} > - Or, login manually using email and OTP + {t('features.login.SelectProfilePage.manualLogin')} diff --git a/frontend/src/features/login/components/OtpForm.tsx b/frontend/src/features/login/components/OtpForm.tsx index c5292d92eb..3cf69e7b67 100644 --- a/frontend/src/features/login/components/OtpForm.tsx +++ b/frontend/src/features/login/components/OtpForm.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react' import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { FormControl, Stack, useBreakpointValue } from '@chakra-ui/react' import Button from '~components/Button' @@ -25,13 +26,17 @@ export const OtpForm = ({ onSubmit, onResendOtp, }: OtpFormProps): JSX.Element => { + const { t } = useTranslation() + const { handleSubmit, register, formState, setError } = useForm() const isMobile = useBreakpointValue({ base: true, xs: true, lg: false }) const validateOtp = useCallback( - (value: string) => value.length === 6 || 'Please enter a 6 digit OTP.', + (value: string) => + value.length === 6 || + t('features.login.components.OTPForm.otpLengthCheck'), [], ) @@ -45,7 +50,9 @@ export const OtpForm = ({
- {`Enter OTP sent to ${email.toLowerCase()}`} + {t('features.login.components.OTPForm.otpFromEmail', { + email: email.toLowerCase(), + })} - Sign in + {t('features.login.components.OTPForm.signin')} diff --git a/frontend/src/features/login/components/SgidLoginButton.tsx b/frontend/src/features/login/components/SgidLoginButton.tsx index f19ebe0641..7b87707bf0 100644 --- a/frontend/src/features/login/components/SgidLoginButton.tsx +++ b/frontend/src/features/login/components/SgidLoginButton.tsx @@ -1,4 +1,5 @@ import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { useMutation } from 'react-query' import { Flex, Link, Text, VStack } from '@chakra-ui/react' @@ -10,6 +11,7 @@ import Button from '~components/Button' export const SgidLoginButton = (): JSX.Element => { const { formState } = useForm() + const { t } = useTranslation() const handleLoginMutation = useMutation(getSgidAuthUrl, { onSuccess: (data) => { @@ -27,15 +29,19 @@ export const SgidLoginButton = (): JSX.Element => { variant="outline" > - Log in with + + {`${t('features.login.components.SgidLoginButton.loginText')} `} + - app + + {` ${t('features.login.components.SgidLoginButton.appText')}`} + - For{' '} + {`${t('features.login.components.SgidLoginButton.forText')} `} - select agencies + {t('features.login.components.SgidLoginButton.selectAgenciesText')} diff --git a/frontend/src/i18n/locales/features/common/en-sg.ts b/frontend/src/i18n/locales/features/common/en-sg.ts index 3128e9d3c3..3278b0bd5d 100644 --- a/frontend/src/i18n/locales/features/common/en-sg.ts +++ b/frontend/src/i18n/locales/features/common/en-sg.ts @@ -49,6 +49,7 @@ export const enSG: Common = { homeNo: 'Please enter a valid landline number', }, pageNotFound: 'This page could not be found.', + generic: 'Something went wrong. Please try again later.', }, tooltip: { deleteField: 'Delete field', diff --git a/frontend/src/i18n/locales/features/common/index.ts b/frontend/src/i18n/locales/features/common/index.ts index cde026595b..a25a5d678a 100644 --- a/frontend/src/i18n/locales/features/common/index.ts +++ b/frontend/src/i18n/locales/features/common/index.ts @@ -49,6 +49,7 @@ export interface Common { homeNo: string } pageNotFound: string + generic: string } tooltip: { deleteField: string diff --git a/frontend/src/i18n/locales/features/login/en-sg.ts b/frontend/src/i18n/locales/features/login/en-sg.ts index e596523b6c..9051b5669e 100644 --- a/frontend/src/i18n/locales/features/login/en-sg.ts +++ b/frontend/src/i18n/locales/features/login/en-sg.ts @@ -3,6 +3,22 @@ import { Login } from '.' export const enSG: Login = { LoginPage: { slogan: 'Build secure government forms in minutes', + banner: 'You can now collect payments directly on your form!', + expiredSgIdSession: + 'Your sgID login session has expired. Please login again.', + }, + SelectProfilePage: { + accountSelection: 'Choose an account to continue to FormSG', + manualLogin: 'Or, login manually using email and OTP', + noWorkEmailHeader: "Singpass login isn't available to you yet", + noWorkEmailBody: + 'It is progressively being made available to agencies. In the meantime, please log in using your email address.', + noWorkEmailCta: 'Back to login', + invalidWorkEmailHeader: "You don't have access to this service", + invalidWorkEmailBodyRestriction: + 'It may be available only to select agencies or authorised individuals. If you believe you should have access to this service, please', + invalidWorkEmailBodyContact: 'contact us', + invalidWorkEmailCta: 'Choose another account', }, components: { LoginForm: { @@ -12,5 +28,18 @@ export const enSG: Login = { login: 'Log in', haveAQuestion: 'Have a question?', }, + OTPForm: { + signin: 'Sign in', + otpRequired: 'OTP is required.', + otpLengthCheck: 'Please enter a 6 digit OTP.', + otpTypeCheck: 'Only numbers are allowed.', + otpFromEmail: 'Enter OTP sent to {email}', + }, + SgidLoginButton: { + forText: 'For', + selectAgenciesText: 'select agencies', + loginText: 'Log in with', + appText: 'app', + }, }, } diff --git a/frontend/src/i18n/locales/features/login/index.ts b/frontend/src/i18n/locales/features/login/index.ts index 83c08cda3c..4f23d2571e 100644 --- a/frontend/src/i18n/locales/features/login/index.ts +++ b/frontend/src/i18n/locales/features/login/index.ts @@ -9,8 +9,34 @@ export interface Login { login: string haveAQuestion: string } + OTPForm: { + signin: string + otpRequired: string + otpLengthCheck: string + otpTypeCheck: string + otpFromEmail: string + } + SgidLoginButton: { + forText: string + selectAgenciesText: string + loginText: string + appText: string + } } LoginPage: { slogan: string + banner: string + expiredSgIdSession: string + } + SelectProfilePage: { + accountSelection: string + manualLogin: string + noWorkEmailHeader: string + noWorkEmailBody: string + noWorkEmailCta: string + invalidWorkEmailHeader: string + invalidWorkEmailBodyRestriction: string + invalidWorkEmailBodyContact: string + invalidWorkEmailCta: string } } diff --git a/init-mongo.js b/init-mongo.js index 6c73ab55c8..7bbc9851bb 100644 --- a/init-mongo.js +++ b/init-mongo.js @@ -17,3 +17,16 @@ db.agencies.update( }, { upsert: true }, ) + +db.agencies.update( + { shortName: 'was' }, + { + $setOnInsert: { + shortName: 'was', + fullName: 'Work Allocation Singapore', + logo: 'https://logos.demos.sg/was.svg', + emailDomain: ['was.gov.sg'], + }, + }, + { upsert: true }, +) diff --git a/package-lock.json b/package-lock.json index 2a26f680a9..e10fe3d1d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.123.0", + "version": "6.124.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.123.0", + "version": "6.124.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", diff --git a/package.json b/package.json index 22755985d3..e25cf611dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.123.0", + "version": "6.124.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " diff --git a/shared/package-lock.json b/shared/package-lock.json index 7f91789474..281693f9e7 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -590,9 +590,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.2.tgz", - "integrity": "sha512-V9mGLlaXN1WETzqQvSu6qf6XVAr3nFuJvWsHcuzCCCo6xUKawwSxOPTpan5CGOSKTn5w/bQuCZcLPJkyysgC3w==" + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.3.tgz", + "integrity": "sha512-RU0CTsLCu2v6VEzdP+W6UU2n5+jEpMDRkGxUeBgsAJgre3vKgm17eApISH9OQY4G0jZYJVIc8qXmz6CJFueAFg==" }, "node_modules/lie": { "version": "3.3.0", @@ -683,9 +683,9 @@ } }, "node_modules/prettier": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz", - "integrity": "sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", + "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/src/app/loaders/mongoose.ts b/src/app/loaders/mongoose.ts index 99b14aa467..7f78086b20 100644 --- a/src/app/loaders/mongoose.ts +++ b/src/app/loaders/mongoose.ts @@ -93,13 +93,21 @@ export default async (): Promise => { // Seed the db with govtech agency if using the mocked db if (usingMockedDb) { const Agency = mongoose.model('Agency') - const agency = new Agency({ - shortName: 'govtech', - fullName: 'Government Technology Agency', - emailDomain: 'data.gov.sg', - logo: '/public/modules/core/img/govtech.jpg', - }) - await agency.save() + const agencyList = [ + { + shortName: 'govtech', + fullName: 'Government Technology Agency', + emailDomain: 'data.gov.sg', + logo: '/public/modules/core/img/govtech.jpg', + }, + { + shortName: 'was', + fullName: 'Work Allocation Singapore', + emailDomain: 'was.gov.sg', + logo: '/public/modules/core/img/was.jpg', + }, + ] + await Agency.create(agencyList) } return mongoose.connection diff --git a/src/app/modules/submission/__tests__/submission.service.spec.ts b/src/app/modules/submission/__tests__/submission.service.spec.ts index d97de25f05..6436e1a4dc 100644 --- a/src/app/modules/submission/__tests__/submission.service.spec.ts +++ b/src/app/modules/submission/__tests__/submission.service.spec.ts @@ -1914,6 +1914,30 @@ describe('submission.service', () => { expect(result._unsafeUnwrapErr()).toEqual(new InvalidFileExtensionError()) }) + it('should reject submissions when attachment responses are invalid', async () => { + // Special case where we found instances where the filename was not a string + // See https://www.notion.so/opengov/TypeError-Cannot-read-properties-of-undefined-reading-split-in-file-validation-js-6f4dcc17e6fc48319d8f7f0f997685c2?pvs=4 + // We can remove this test case when the issue is found and fixed + const processedResponse1 = generateNewAttachmentResponse({ + content: readFileSync('./__tests__/unit/backend/resources/invalid.py'), + filename: 'mock.jpg', + }) + + processedResponse1.filename = null as unknown as string + + // Omit attributes only present in processed fields + const response1 = omit(processedResponse1, [ + 'isVisible', + 'isUserVerified', + ]) + + const result = await SubmissionService.validateAttachments( + [response1], + FormResponseMode.Email, + ) + expect(result._unsafeUnwrapErr()).toEqual(new InvalidFileExtensionError()) + }) + it('should reject submissions when there are invalid file types in zip', async () => { const processedResponse1 = generateNewAttachmentResponse({ content: readFileSync( diff --git a/src/app/modules/submission/__tests__/submission.utils.spec.ts b/src/app/modules/submission/__tests__/submission.utils.spec.ts index 87f3876943..a594bfa240 100644 --- a/src/app/modules/submission/__tests__/submission.utils.spec.ts +++ b/src/app/modules/submission/__tests__/submission.utils.spec.ts @@ -111,6 +111,16 @@ describe('submission.utils', () => { }) describe('getInvalidFileExtensions', () => { + it('should throw error when filename is not a string', async () => { + // Special case where we found instances where the filename was not a string + // See https://www.notion.so/opengov/TypeError-Cannot-read-properties-of-undefined-reading-split-in-file-validation-js-6f4dcc17e6fc48319d8f7f0f997685c2?pvs=4 + // We can remove this test case when the issue is found and fixed + const promiseOutcome = getInvalidFileExtensions([ + { ...validSingleFile, filename: null as unknown as string }, + ]) + await expect(promiseOutcome).toReject() + }) + it('should return empty array when given a single valid file', async () => { const invalid = await getInvalidFileExtensions([validSingleFile]) expect(invalid).toEqual([]) diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index dcf7f104f8..1cb5fc5698 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -576,7 +576,21 @@ export const getInvalidFileExtensions = ( // Turn it into an array of promises that each resolve // to an array of file extensions that are invalid (if any) const promises = attachments.map((attachment) => { - const extension = FileValidation.getFileExtension(attachment.filename) + const { filename } = attachment + // Special case where we found instances where the filename was not a string + // See https://www.notion.so/opengov/TypeError-Cannot-read-properties-of-undefined-reading-split-in-file-validation-js-6f4dcc17e6fc48319d8f7f0f997685c2?pvs=4 + // We can remove this handling when the issue is found and fixed + if (filename == null) { + logger.error({ + message: 'A string is expected, but received null or undefined', + meta: { + action: 'getInvalidFileExtensions', + filename, + }, + }) + return Promise.reject(new Error('filename is required')) + } + const extension = FileValidation.getFileExtension(filename) if (FileValidation.isInvalidFileExtension(extension)) { return Promise.resolve([extension]) }