@@ -34,11 +34,11 @@
I've checked the link. Skip ahead
-

+
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 3defe585a..f652a2727 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -5,6 +5,7 @@ export const BULK_UPLOAD_HEADER = 'Original links to be shortened'
export const TAG_SEPARATOR = ';'
export const MAX_NUM_TAGS_PER_LINK = 3
export const MIN_TAG_SEARCH_LENGTH = 3
+export const DEFAULT_URL_SCAN_RESULT_EXPIRY_SECONDS = 60 * 60 * 24 // 24 hours
export enum BULK_QR_DOWNLOAD_FORMATS {
CSV = 'CSV',
PNG = 'PNG',
diff --git a/test/end-to-end/LinkAudit.test.ts b/test/end-to-end/LinkAudit.test.ts
index b345902e6..6cfd69002 100644
--- a/test/end-to-end/LinkAudit.test.ts
+++ b/test/end-to-end/LinkAudit.test.ts
@@ -93,8 +93,6 @@ test('Changing the link owner should update the link history with Link Owner upd
.pressKey('ctrl+a delete')
.typeText(linkTransferField, `${transferEmail}`)
.click(transferButton)
- // Close drawer
- await t.click(closeButtonSnackBar)
// Sign out
await t.click(signOutButton)
// Login using the new link owner
diff --git a/test/end-to-end/UrlCreation.test.ts b/test/end-to-end/UrlCreation.test.ts
index 8d7622060..be5aa87f2 100644
--- a/test/end-to-end/UrlCreation.test.ts
+++ b/test/end-to-end/UrlCreation.test.ts
@@ -325,7 +325,7 @@ test('The update file test', async (t) => {
const generatedfileUrl = await shortUrlTextField.value
const fileRow = Selector(`h6[title="${generatedfileUrl}"]`)
- const directoryPath = `${process.env.HOME}/Downloads/${generatedfileUrl}.pdf`
+ const directoryPath = `${process.env.HOME}/Downloads/${generatedfileUrl}.csv`
// Generate 1mb file
await createEmptyFileOfSize(dummyFilePath, smallFileSize)
diff --git a/test/end-to-end/util/config.ts b/test/end-to-end/util/config.ts
index 9fa3fbb7e..69b8c9634 100644
--- a/test/end-to-end/util/config.ts
+++ b/test/end-to-end/util/config.ts
@@ -16,8 +16,8 @@ export const dummyMaliciousFilePath = './test/end-to-end/eicar.com.txt'
export const dummyMaliciousRelativePath = './eicar.com.txt'
export const dummyFilePath = './test/end-to-end/anotherDummy.txt'
export const dummyRelativePath = './anotherDummy.txt'
-export const dummyChangedFilePath = './test/end-to-end/changedDummy.pdf'
-export const dummyRelativeChangedFilePath = './changedDummy.pdf'
+export const dummyChangedFilePath = './test/end-to-end/changedDummy.csv'
+export const dummyRelativeChangedFilePath = './changedDummy.csv'
export const dummyBulkCsv = './test/end-to-end/bulkCsv.csv'
export const dummyBulkCsvRelativePath = './bulkCsv.csv'
export const smallFileSize = 1024 * 1024 * 1
diff --git a/test/integration/api/user/Urls.test.ts b/test/integration/api/user/Urls.test.ts
index d7d1401b6..1b9d99871 100644
--- a/test/integration/api/user/Urls.test.ts
+++ b/test/integration/api/user/Urls.test.ts
@@ -86,6 +86,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
})
})
@@ -108,6 +109,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
})
})
@@ -151,6 +153,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
})
// Should be able to get updated link URL
@@ -172,6 +175,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
},
],
count: 1,
@@ -203,6 +207,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
})
// Should be able to get updated file URL
@@ -224,6 +229,7 @@ describe('Url integration tests', () => {
tagStrings: '',
createdAt: expect.stringMatching(DATETIME_REGEX),
updatedAt: expect.stringMatching(DATETIME_REGEX),
+ safeBrowsingExpiry: expect.stringMatching(DATETIME_REGEX),
},
],
count: 1,
diff --git a/test/integration/util/db.ts b/test/integration/util/db.ts
index a7b94da4e..822a2e2a9 100644
--- a/test/integration/util/db.ts
+++ b/test/integration/util/db.ts
@@ -21,7 +21,9 @@ export const createDbUser = async (
)
} catch (e) {
throw new Error(
- `Failed to create user with email ${email} for integration tests: ${e.message}`,
+ `Failed to create user with email ${email} for integration tests: ${
+ (e as Error).message
+ }`,
)
}
}
@@ -49,7 +51,9 @@ export const deleteDbUser = async (email: string): Promise
=> {
)
} catch (e) {
throw new Error(
- `Failed to delete user with email ${email} for integration tests: ${e.message}`,
+ `Failed to delete user with email ${email} for integration tests: ${
+ (e as Error).message
+ }`,
)
}
}
diff --git a/test/server/api/LoginRoute.test.ts b/test/server/api/LoginRoute.test.ts
index 337e11738..4efe97c9b 100644
--- a/test/server/api/LoginRoute.test.ts
+++ b/test/server/api/LoginRoute.test.ts
@@ -56,7 +56,7 @@ describe('POST /api/login/otp', () => {
describe('POST /api/login/verify', () => {
test('verify the OTP', async (done) => {
// Prime cache
- getOtpCache().setOtpForEmail('otpgo.gov@open.test.sg', {
+ getOtpCache().setOtpForEmail('otpgo.gov@open.test.sg', '127.0.0.1', {
hashedOtp: '1',
retries: 100,
})
diff --git a/test/server/api/StatisticsRoute.test.ts b/test/server/api/StatisticsRoute.test.ts
deleted file mode 100644
index 704db2f68..000000000
--- a/test/server/api/StatisticsRoute.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import request from 'supertest'
-import { container } from '../../../src/server/util/inversify'
-import { DependencyIds } from '../../../src/server/constants'
-import { StatisticsRepository } from '../../../src/server/modules/statistics/interfaces'
-
-const getGlobalStatistics = jest.fn()
-getGlobalStatistics.mockResolvedValue({
- linkCount: 1,
- userCount: 2,
- clickCount: 3,
-})
-// Binds mockups before binding default
-container
- .bind(DependencyIds.statisticsRepository)
- .toConstantValue({ getGlobalStatistics })
-
-// Importing setup app
-// eslint-disable-next-line import/first
-import app from './setup'
-
-describe('GET /api/stats', () => {
- test('get statistics', async (done) => {
- const res = await request(app).get('/api/stats')
- expect(res.status).toBe(200)
- expect(res.ok).toBe(true)
- expect(Object.keys(res.body).sort()).toEqual(
- ['linkCount', 'clickCount', 'userCount'].sort(),
- )
- done()
- })
-})
diff --git a/test/server/api/util.ts b/test/server/api/util.ts
index 6e7f1b44c..26fe96e92 100644
--- a/test/server/api/util.ts
+++ b/test/server/api/util.ts
@@ -87,13 +87,18 @@ export function createRequestWithEmail(email: any): Request {
* @param {any} user
* @returns A mock Request with the input email and otp.
*/
-export function createRequestWithEmailAndOtp(email: any, otp: any): Request {
+export function createRequestWithEmailAndIpAndOtp(
+ email: any,
+ ip: string,
+ otp: any,
+): Request {
return httpMocks.createRequest({
session: {},
body: {
email,
otp,
},
+ ip,
})
}
@@ -117,6 +122,8 @@ export const urlModelMock = sequelizeMock.define(
{
shortUrl: 'a',
longUrl: 'aa',
+ isFile: false,
+ safeBrowsingExpiry: null,
state: ACTIVE,
UrlClicks: {
clicks: 3,
diff --git a/test/server/config.ts b/test/server/config.ts
index 9f7d69c9d..c53beeec2 100644
--- a/test/server/config.ts
+++ b/test/server/config.ts
@@ -54,4 +54,7 @@ jest.mock('../../src/server/config', () => ({
gaTrackingId: 'UA-000000-2',
otpRateLimit: 5,
ogHostname: 'go.gov.sg',
+ userCount: 1,
+ clickCount: 2,
+ linkCount: 3,
}))
diff --git a/test/server/mappers/UrlV1Mapper.test.ts b/test/server/mappers/UrlV1Mapper.test.ts
index 4fc88cc0b..14a7c9af5 100644
--- a/test/server/mappers/UrlV1Mapper.test.ts
+++ b/test/server/mappers/UrlV1Mapper.test.ts
@@ -21,6 +21,7 @@ describe('url v1 mapper', () => {
tagStrings: '1;abc;foo-bar',
contactEmail: 'bar@open.gov.sg',
description: 'this-is-a-description',
+ safeBrowsingExpiry: null,
}
const urlV1DTO = urlV1Mapper.persistenceToDto(storableUrl)
expect(urlV1DTO).toEqual({
diff --git a/test/server/mocks/repositories/OtpRepository.ts b/test/server/mocks/repositories/OtpRepository.ts
index 8a137ac7b..f6ebaee4e 100644
--- a/test/server/mocks/repositories/OtpRepository.ts
+++ b/test/server/mocks/repositories/OtpRepository.ts
@@ -7,41 +7,56 @@ import { OtpRepository } from '../../../../src/server/modules/auth/interfaces/Ot
export class OtpRepositoryMock implements OtpRepository {
cache = new Map()
- deleteOtpByEmail = (email: string) => {
- this.cache.delete(email)
+ private getCacheKey(email: string, ip: string): string {
+ return `${email}:${ip}`
+ }
+
+ getRedisKey(email: string, ip: string): string {
+ return this.getCacheKey(email, ip)
+ }
+
+ deleteOtpByEmail = (email: string, ip: string) => {
+ const key = this.getCacheKey(email, ip)
+ this.cache.delete(key)
return Promise.resolve()
}
- setOtpForEmail = (email: string, otp: StorableOtp) => {
- this.cache.set(email, otp)
+ setOtpForEmail = (email: string, ip: string, otp: StorableOtp) => {
+ const key = this.getCacheKey(email, ip)
+ this.cache.set(key, otp)
return Promise.resolve()
}
- getOtpForEmail = (email: string) => {
- if (!this.cache.has(email)) {
+ getOtpForEmail = (email: string, ip: string) => {
+ const key = this.getCacheKey(email, ip)
+ if (!this.cache.has(key)) {
return Promise.resolve(null)
}
- return Promise.resolve(this.cache.get(email)!)
+ return Promise.resolve(this.cache.get(key)!)
}
}
@injectable()
export class OtpRepositoryMockDown implements OtpRepository {
- deleteOtpByEmail(_: string): Promise {
+ getRedisKey(email: string, ip: string): string {
+ return `${email}:${ip}`
+ }
+
+ deleteOtpByEmail(_: string, __: string): Promise {
return Promise.reject(Error())
}
- setOtpForEmail(_: string, __: StorableOtp): Promise {
+ setOtpForEmail(_: string, __: string, ___: StorableOtp): Promise {
return Promise.reject(Error())
}
- getOtpForEmail(_: string): Promise {
+ getOtpForEmail(_: string, __: string): Promise {
return Promise.reject(Error())
}
}
export class OtpRepositoryMockNoWrite extends OtpRepositoryMock {
- deleteOtpByEmail = (_: string) => Promise.reject()
+ deleteOtpByEmail = (_: string, __: string) => Promise.reject()
- setOtpForEmail = (__: string, _: StorableOtp) => Promise.reject()
+ setOtpForEmail = (_: string, __: string, ___: StorableOtp) => Promise.reject()
}
diff --git a/test/server/mocks/repositories/UrlRepository.ts b/test/server/mocks/repositories/UrlRepository.ts
index f9b46f255..32ee1ddca 100644
--- a/test/server/mocks/repositories/UrlRepository.ts
+++ b/test/server/mocks/repositories/UrlRepository.ts
@@ -4,6 +4,7 @@ import { injectable } from 'inversify'
import { UrlRepositoryInterface } from '../../../../src/server/repositories/interfaces/UrlRepositoryInterface'
import {
BulkUrlMapping,
+ RedirectDestination,
StorableFile,
StorableUrl,
UrlDirectoryPaginated,
@@ -39,7 +40,11 @@ export class UrlRepositoryMock implements UrlRepositoryInterface {
throw new Error('Not implemented')
}
- getLongUrl: (shortUrl: string) => Promise = () => {
+ isShortUrlAvailable: (shortUrl: string) => Promise = () => {
+ throw new Error('Not implemented')
+ }
+
+ getLongUrl: (shortUrl: string) => Promise = () => {
throw new Error('Not implemented')
}
@@ -80,6 +85,7 @@ export class UrlRepositoryMock implements UrlRepositoryInterface {
clicks: 0,
source: StorableUrlSource.Console,
tagStrings: '',
+ safeBrowsingExpiry: null,
},
],
count: 0,
@@ -92,6 +98,16 @@ export class UrlRepositoryMock implements UrlRepositoryInterface {
}) => Promise = () => {
return Promise.resolve()
}
+
+ updateSafeBrowsingExpiry(_: string, __: Date): Promise {
+ // Mock implementation for updating safe browsing expiry
+ return Promise.resolve()
+ }
+
+ deactivateShortUrl(_: string): Promise {
+ // Mock implementation for deactivating a short URL
+ return Promise.resolve()
+ }
}
export default UrlRepositoryMock
diff --git a/test/server/mocks/services/email.ts b/test/server/mocks/services/email.ts
index 3b26ed97e..e8c84b5b3 100644
--- a/test/server/mocks/services/email.ts
+++ b/test/server/mocks/services/email.ts
@@ -23,6 +23,14 @@ export class MailerMock implements Mailer {
this.mailsSent.push({ email })
return Promise.resolve()
}
+
+ mailDeactivatedMaliciousShortUrl(
+ email: string,
+ shortUrl: string,
+ ): Promise {
+ this.mailsSent.push({ email, shortUrl })
+ return Promise.resolve()
+ }
}
@injectable()
@@ -40,4 +48,8 @@ export class MailerMockDown implements Mailer {
mailJobFailure(_: string): Promise {
return Promise.reject()
}
+
+ mailDeactivatedMaliciousShortUrl(_: string, __: string): Promise {
+ return Promise.reject()
+ }
}
diff --git a/test/server/repositories/UrlRepository.test.ts b/test/server/repositories/UrlRepository.test.ts
index b38688f4f..c72d96230 100644
--- a/test/server/repositories/UrlRepository.test.ts
+++ b/test/server/repositories/UrlRepository.test.ts
@@ -24,6 +24,7 @@ import {
import { DirectoryQueryConditions } from '../../../src/server/modules/directory'
import TagRepositoryMock from '../mocks/repositories/TagRepository'
import { TAG_SEPARATOR } from '../../../src/shared/constants'
+import { StorableUrl } from '../../../src/server/repositories/types'
jest.mock('../../../src/server/models/url', () => ({
Url: urlModelMock,
@@ -75,17 +76,18 @@ describe('UrlRepository', () => {
const baseUrlClicks = {
clicks: 2,
}
- const baseTemplate = {
+ const baseTemplate: Omit = {
shortUrl: baseShortUrl,
longUrl: baseLongUrl,
- state: 'ACTIVE',
+ state: StorableUrlState.Active,
isFile: false,
- createdAt: new Date(),
- updatedAt: new Date(),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
description: 'An agency of the Singapore Government',
contactEmail: 'contact-us@agency.gov.sg',
source: StorableUrlSource.Console,
tags: [],
+ safeBrowsingExpiry: null,
}
const baseUrl = {
...baseTemplate,
@@ -595,12 +597,22 @@ describe('UrlRepository', () => {
describe('getLongUrl', () => {
it('should return from db when cache is empty', async () => {
- await expect(repository.getLongUrl('a')).resolves.toBe('aa')
+ await expect(repository.getLongUrl('a')).resolves.toEqual({
+ longUrl: 'aa',
+ isFile: false,
+ safeBrowsingExpiry: null,
+ })
})
it('should return from cache when cache is filled', async () => {
- redisMockClient.set('a', 'aaa')
- await expect(repository.getLongUrl('a')).resolves.toBe('aaa')
+ // Arrange
+ const cacheUrl = {
+ longUrl: 'aaa',
+ isFile: false,
+ safeBrowsingExpiry: null,
+ }
+ redisMockClient.set('a', JSON.stringify(cacheUrl))
+ await expect(repository.getLongUrl('a')).resolves.toEqual(cacheUrl)
})
it('should return from db when cache is down', async () => {
@@ -611,7 +623,11 @@ describe('UrlRepository', () => {
callback(new Error('Cache down'), 'Error')
return false
})
- await expect(repository.getLongUrl('a')).resolves.toBe('aa')
+ await expect(repository.getLongUrl('a')).resolves.toEqual({
+ longUrl: 'aa',
+ isFile: false,
+ safeBrowsingExpiry: null,
+ })
})
})
@@ -678,4 +694,34 @@ describe('UrlRepository', () => {
)
})
})
+
+ describe('updateSafeBrowsingExpiry', () => {
+ it('should throw NotFoundError if the shortUrl does not exist', async () => {
+ // Arrange
+ const shortUrl = 'nonexistent'
+ const expiry = new Date(Date.now() + 1000)
+ jest.spyOn(urlModelMock, 'findOne').mockResolvedValue(null)
+
+ // Act & Assert
+ await expect(
+ repository.updateSafeBrowsingExpiry(shortUrl, expiry),
+ ).rejects.toThrow(NotFoundError)
+ })
+
+ it.skip('should update the safe browsing expiry for an existing shortUrl', () => {})
+ })
+
+ describe('deactivateShortUrl', () => {
+ it('should throw NotFoundError if the shortUrl does not exist', async () => {
+ // Arrange
+ const shortUrl = 'nonexistent'
+
+ // Act & Assert
+ await expect(repository.deactivateShortUrl(shortUrl)).rejects.toThrow(
+ NotFoundError,
+ )
+ })
+
+ it.skip('should deactivate an existing shortUrl', () => {})
+ })
})
diff --git a/test/server/repositories/UserRepository.test.ts b/test/server/repositories/UserRepository.test.ts
index 2af0e9876..146e3cae7 100644
--- a/test/server/repositories/UserRepository.test.ts
+++ b/test/server/repositories/UserRepository.test.ts
@@ -3,6 +3,11 @@ import { UserRepository } from '../../../src/server/repositories/UserRepository'
import { UrlMapper } from '../../../src/server/mappers/UrlMapper'
import { UserMapper } from '../../../src/server/mappers/UserMapper'
import { NotFoundError } from '../../../src/server/util/error'
+import { StorableUrl } from '../../../src/server/repositories/types'
+import {
+ StorableUrlSource,
+ StorableUrlState,
+} from '../../../src/server/repositories/enums'
jest.mock('../../../src/server/models/user', () => ({
User: userModelMock,
@@ -17,16 +22,17 @@ const userRepo = new UserRepository(
new UrlMapper(),
)
-const baseUrlTemplate = {
+const baseUrlTemplate: Omit = {
shortUrl: 'short-link',
longUrl: 'https://www.agency.gov.sg',
- state: 'ACTIVE',
+ state: StorableUrlState.Active,
isFile: false,
- createdAt: Date.now(),
- updatedAt: Date.now(),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
description: 'An agency of the Singapore Government',
contactEmail: 'contact-us@agency.gov.sg',
- source: 'CONSOLE',
+ source: StorableUrlSource.Console,
+ safeBrowsingExpiry: null,
}
const urlClicks = {
diff --git a/webpack.config.ts b/webpack.config.ts
index 51fe5a336..932eec3f7 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -110,7 +110,7 @@ module.exports = () => {
'!/(assets/**|bundle.js|favicon*)': 'http://localhost:8080',
},
historyApiFallback: true,
- disableHostCheck: true,
+ allowedHosts: 'all',
},
devtool: 'source-map',
plugins: [