Merge branch 'main' into feature/following-screen
This commit is contained in:
@ -27,8 +27,8 @@ android {
|
||||
applicationId = "app.omnivore.omnivore"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 194001
|
||||
versionName = "0.195.0"
|
||||
versionCode = 200004
|
||||
versionName = "0.200.4"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -27,7 +27,7 @@ import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||
HighlightChange::class,
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class],
|
||||
version = 24,
|
||||
version = 26,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class OmnivoreDatabase : RoomDatabase() {
|
||||
|
||||
@ -21,5 +21,7 @@ object DatabaseModule {
|
||||
context,
|
||||
OmnivoreDatabase::class.java,
|
||||
"omnivore-database",
|
||||
).build()
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import android.content.Context
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@ -42,6 +43,7 @@ import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
|
||||
|
||||
fun notebookMD(notes: List<Highlight>, highlights: List<Highlight>): String {
|
||||
@ -179,12 +181,15 @@ fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxSize(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
autoCorrect = true,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
value = annotation.value, onValueChange = { annotation.value = it },
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
|
||||
37
packages/api/src/jobs/send_email.ts
Normal file
37
packages/api/src/jobs/send_email.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { env } from '../env'
|
||||
import { sendWithMailJet } from '../services/send_emails'
|
||||
import { Merge } from '../util'
|
||||
import { logger } from '../utils/logger'
|
||||
import { sendEmail } from '../utils/sendEmail'
|
||||
|
||||
export const SEND_EMAIL_JOB = 'send-email'
|
||||
|
||||
type ContentType = { html: string } | { text: string } | { templateId: string }
|
||||
export type SendEmailJobData = Merge<
|
||||
{
|
||||
emailAddress: string
|
||||
subject?: string
|
||||
html?: string
|
||||
text?: string
|
||||
templateId?: string
|
||||
dynamicTemplateData?: Record<string, any>
|
||||
},
|
||||
ContentType
|
||||
>
|
||||
|
||||
export const sendEmailJob = async (data: SendEmailJobData) => {
|
||||
if (process.env.USE_MAILJET && data.dynamicTemplateData) {
|
||||
return sendWithMailJet(data.emailAddress, data.dynamicTemplateData.link)
|
||||
}
|
||||
|
||||
if (!data.html && !data.text && !data.templateId) {
|
||||
logger.error('no email content provided', data)
|
||||
return false
|
||||
}
|
||||
|
||||
return sendEmail({
|
||||
...data,
|
||||
from: env.sender.message,
|
||||
to: data.emailAddress,
|
||||
})
|
||||
}
|
||||
@ -36,6 +36,7 @@ import {
|
||||
import { refreshAllFeeds } from './jobs/rss/refreshAllFeeds'
|
||||
import { refreshFeed } from './jobs/rss/refreshFeed'
|
||||
import { savePageJob } from './jobs/save_page'
|
||||
import { sendEmailJob, SEND_EMAIL_JOB } from './jobs/send_email'
|
||||
import {
|
||||
syncReadPositionsJob,
|
||||
SYNC_READ_POSITIONS_JOB_NAME,
|
||||
@ -157,6 +158,8 @@ export const createWorker = (connection: ConnectionOptions) =>
|
||||
return processYouTubeTranscript(job.data)
|
||||
case EXPORT_ALL_ITEMS_JOB_NAME:
|
||||
return exportAllItems(job.data)
|
||||
case SEND_EMAIL_JOB:
|
||||
return sendEmailJob(job.data)
|
||||
default:
|
||||
logger.warning(`[queue-processor] unhandled job: ${job.name}`)
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ import {
|
||||
} from '../../generated/graphql'
|
||||
import { userRepository } from '../../repository/user'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { sendVerificationEmail } from '../../services/send_emails'
|
||||
import { sendAccountChangeEmail } from '../../services/send_emails'
|
||||
import { softDeleteUser } from '../../services/user'
|
||||
import { userDataToUser } from '../../utils/helpers'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
@ -358,7 +358,7 @@ export const updateEmailResolver = authorized<
|
||||
return { email }
|
||||
}
|
||||
|
||||
const result = await sendVerificationEmail({
|
||||
const result = await sendAccountChangeEmail({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email,
|
||||
|
||||
@ -23,7 +23,7 @@ import { userRepository } from '../../repository/user'
|
||||
import { isErrorWithCode } from '../../resolvers'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import {
|
||||
sendConfirmationEmail,
|
||||
sendNewAccountVerificationEmail,
|
||||
sendPasswordResetEmail,
|
||||
} from '../../services/send_emails'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
@ -440,7 +440,7 @@ export function authRouter() {
|
||||
}
|
||||
|
||||
if (user.status === StatusType.Pending && user.email) {
|
||||
await sendConfirmationEmail({
|
||||
await sendNewAccountVerificationEmail({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { StatusType } from '../../../entity/user'
|
||||
import { userRepository } from '../../../repository/user'
|
||||
import { sendConfirmationEmail } from '../../../services/send_emails'
|
||||
import { sendNewAccountVerificationEmail } from '../../../services/send_emails'
|
||||
import { comparePassword } from '../../../utils/auth'
|
||||
import { logger } from '../../../utils/logger'
|
||||
import { decodeAppleToken } from '../apple_auth'
|
||||
@ -56,7 +56,7 @@ export async function createMobileEmailSignInResponse(
|
||||
}
|
||||
|
||||
if (user.status === StatusType.Pending && user.email) {
|
||||
await sendConfirmationEmail({
|
||||
await sendNewAccountVerificationEmail({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
|
||||
@ -16,7 +16,7 @@ import { IntercomClient } from '../utils/intercom'
|
||||
import { logger } from '../utils/logger'
|
||||
import { validateUsername } from '../utils/usernamePolicy'
|
||||
import { addPopularReadsForNewUser } from './popular_reads'
|
||||
import { sendConfirmationEmail } from './send_emails'
|
||||
import { sendNewAccountVerificationEmail } from './send_emails'
|
||||
|
||||
export const MAX_RECORDS_LIMIT = 1000
|
||||
|
||||
@ -142,7 +142,7 @@ export const createUser = async (input: {
|
||||
})
|
||||
|
||||
if (input.pendingConfirmation) {
|
||||
if (!(await sendConfirmationEmail(user))) {
|
||||
if (!(await sendNewAccountVerificationEmail(user))) {
|
||||
return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import mailjet from 'node-mailjet'
|
||||
import { env } from '../env'
|
||||
import { generateVerificationToken } from '../utils/auth'
|
||||
import { enqueueSendEmail } from '../utils/createTask'
|
||||
import { logger } from '../utils/logger'
|
||||
import { sendEmail } from '../utils/sendEmail'
|
||||
import mailjet from 'node-mailjet'
|
||||
|
||||
export const sendConfirmationEmail = async (user: {
|
||||
export const sendNewAccountVerificationEmail = async (user: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
@ -14,23 +14,19 @@ export const sendConfirmationEmail = async (user: {
|
||||
const link = `${env.client.url}/auth/confirm-email/${token}`
|
||||
// send email
|
||||
const dynamicTemplateData = {
|
||||
name: user.name,
|
||||
link,
|
||||
}
|
||||
|
||||
if (process.env.USE_MAILJET) {
|
||||
return sendWithMailJet(user.email, link)
|
||||
}
|
||||
|
||||
return sendEmail({
|
||||
from: env.sender.message,
|
||||
to: user.email,
|
||||
const result = await enqueueSendEmail({
|
||||
emailAddress: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.confirmationTemplateId,
|
||||
dynamicTemplateData,
|
||||
})
|
||||
|
||||
return !!result
|
||||
}
|
||||
|
||||
const sendWithMailJet = async (
|
||||
export const sendWithMailJet = async (
|
||||
email: string,
|
||||
link: string
|
||||
): Promise<boolean> => {
|
||||
@ -68,7 +64,7 @@ const sendWithMailJet = async (
|
||||
return true
|
||||
}
|
||||
|
||||
export const sendVerificationEmail = async (user: {
|
||||
export const sendAccountChangeEmail = async (user: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
@ -78,20 +74,16 @@ export const sendVerificationEmail = async (user: {
|
||||
const link = `${env.client.url}/auth/reset-password/${token}`
|
||||
// send email
|
||||
const dynamicTemplateData = {
|
||||
name: user.name,
|
||||
link,
|
||||
}
|
||||
|
||||
if (process.env.USE_MAILJET) {
|
||||
return sendWithMailJet(user.email, link)
|
||||
}
|
||||
|
||||
return sendEmail({
|
||||
from: env.sender.message,
|
||||
to: user.email,
|
||||
const result = await enqueueSendEmail({
|
||||
emailAddress: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.verificationTemplateId,
|
||||
dynamicTemplateData,
|
||||
})
|
||||
|
||||
return !!result
|
||||
}
|
||||
|
||||
export const sendPasswordResetEmail = async (user: {
|
||||
@ -104,18 +96,14 @@ export const sendPasswordResetEmail = async (user: {
|
||||
const link = `${env.client.url}/auth/reset-password/${token}`
|
||||
// send email
|
||||
const dynamicTemplateData = {
|
||||
name: user.name,
|
||||
link,
|
||||
}
|
||||
|
||||
if (process.env.USE_MAILJET) {
|
||||
return sendWithMailJet(user.email, link)
|
||||
}
|
||||
|
||||
return sendEmail({
|
||||
from: env.sender.message,
|
||||
to: user.email,
|
||||
const result = await enqueueSendEmail({
|
||||
emailAddress: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.resetPasswordTemplateId,
|
||||
dynamicTemplateData,
|
||||
})
|
||||
|
||||
return !!result
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
REFRESH_ALL_FEEDS_JOB_NAME,
|
||||
REFRESH_FEED_JOB_NAME,
|
||||
} from '../jobs/rss/refreshAllFeeds'
|
||||
import { SendEmailJobData, SEND_EMAIL_JOB } from '../jobs/send_email'
|
||||
import { SYNC_READ_POSITIONS_JOB_NAME } from '../jobs/sync_read_positions'
|
||||
import { TriggerRuleJobData, TRIGGER_RULE_JOB_NAME } from '../jobs/trigger_rule'
|
||||
import {
|
||||
@ -69,6 +70,7 @@ export const getJobPriority = (jobName: string): number => {
|
||||
case UPDATE_LABELS_JOB:
|
||||
case UPDATE_HIGHLIGHT_JOB:
|
||||
case SYNC_READ_POSITIONS_JOB_NAME:
|
||||
case SEND_EMAIL_JOB:
|
||||
return 1
|
||||
case TRIGGER_RULE_JOB_NAME:
|
||||
case CALL_WEBHOOK_JOB_NAME:
|
||||
@ -839,4 +841,16 @@ export const enqueueExportItem = async (jobData: ExportItemJobData) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const enqueueSendEmail = async (jobData: SendEmailJobData) => {
|
||||
const queue = await getBackendQueue()
|
||||
if (!queue) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return queue.add(SEND_EMAIL_JOB, jobData, {
|
||||
attempts: 1, // only try once
|
||||
priority: getJobPriority(SEND_EMAIL_JOB),
|
||||
})
|
||||
}
|
||||
|
||||
export default createHttpTaskWithToken
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { MailDataRequired } from '@sendgrid/helpers/classes/mail'
|
||||
import chai, { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import { expect } from 'chai'
|
||||
import supertest from 'supertest'
|
||||
import { StatusType, User } from '../../src/entity/user'
|
||||
import { getRepository } from '../../src/repository'
|
||||
import { userRepository } from '../../src/repository/user'
|
||||
import { isValidSignupRequest } from '../../src/routers/auth/auth_router'
|
||||
import { AuthProvider } from '../../src/routers/auth/auth_types'
|
||||
import { createPendingUserToken } from '../../src/routers/auth/jwt_helpers'
|
||||
import { searchLibraryItems } from '../../src/services/library_item'
|
||||
@ -15,12 +13,8 @@ import {
|
||||
generateVerificationToken,
|
||||
hashPassword,
|
||||
} from '../../src/utils/auth'
|
||||
import * as util from '../../src/utils/sendEmail'
|
||||
import { createTestUser } from '../db'
|
||||
import { generateFakeUuid, request } from '../util'
|
||||
import { isValidSignupRequest } from '../../src/routers/auth/auth_router'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
describe('auth router', () => {
|
||||
const route = '/api/auth'
|
||||
@ -47,8 +41,6 @@ describe('auth router', () => {
|
||||
let name: string
|
||||
|
||||
context('when inputs are valid and user not exists', () => {
|
||||
let fake: (msg: MailDataRequired) => Promise<boolean>
|
||||
|
||||
before(() => {
|
||||
password = validPassword
|
||||
username = 'Some_username'
|
||||
@ -62,14 +54,6 @@ describe('auth router', () => {
|
||||
})
|
||||
|
||||
context('when confirmation email sent', () => {
|
||||
beforeEach(() => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects to verify email', async () => {
|
||||
const res = await signupRequest(
|
||||
email,
|
||||
@ -90,28 +74,6 @@ describe('auth router', () => {
|
||||
expect(user?.name).to.eql(name)
|
||||
})
|
||||
})
|
||||
|
||||
context('when confirmation email not sent', () => {
|
||||
before(() => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(false))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects to sign up page with error code INVALID_EMAIL', async () => {
|
||||
const res = await signupRequest(
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
username
|
||||
).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/email-signup?errorCodes=INVALID_EMAIL'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when user exists', () => {
|
||||
@ -213,11 +175,8 @@ describe('auth router', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('when user is not confirmed', async () => {
|
||||
let fake: (msg: MailDataRequired) => Promise<boolean>
|
||||
|
||||
context('when user is not confirmed', () => {
|
||||
beforeEach(async () => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
|
||||
await updateUser(user.id, { status: StatusType.Pending })
|
||||
email = user.email
|
||||
password = correctPassword
|
||||
@ -225,7 +184,6 @@ describe('auth router', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await updateUser(user.id, { status: StatusType.Active })
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects with error code PendingVerification', async () => {
|
||||
@ -234,11 +192,6 @@ describe('auth router', () => {
|
||||
'/email-login?errorCodes=PENDING_VERIFICATION'
|
||||
)
|
||||
})
|
||||
|
||||
it('sends a verification email', async () => {
|
||||
await loginRequest(email, password).expect(302)
|
||||
expect(fake).to.have.been.calledOnce
|
||||
})
|
||||
})
|
||||
|
||||
context('when user not exists', () => {
|
||||
@ -254,7 +207,7 @@ describe('auth router', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('when user has no password stored in db', async () => {
|
||||
context('when user has no password stored in db', () => {
|
||||
before(async () => {
|
||||
await updateUser(user.id, { password: '' })
|
||||
email = user.email
|
||||
@ -297,12 +250,10 @@ describe('auth router', () => {
|
||||
let token: string
|
||||
|
||||
before(async () => {
|
||||
sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
|
||||
user = await createTestUser('pendingUser', undefined, 'password', true)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
sinon.restore()
|
||||
await deleteUser(user.id)
|
||||
})
|
||||
|
||||
@ -395,47 +346,16 @@ describe('auth router', () => {
|
||||
})
|
||||
|
||||
context('when email is verified', () => {
|
||||
let fake: (msg: MailDataRequired) => Promise<boolean>
|
||||
|
||||
before(async () => {
|
||||
await updateUser(user.id, { status: StatusType.Active })
|
||||
})
|
||||
|
||||
context('when reset password email sent', () => {
|
||||
before(() => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects to forgot-password page with success message', async () => {
|
||||
const res = await emailResetPasswordReq(email).expect(302)
|
||||
expect(res.header.location).to.endWith('/auth/reset-sent')
|
||||
})
|
||||
})
|
||||
|
||||
context('when reset password email not sent', () => {
|
||||
before(() => {
|
||||
fake = sinon.replace(
|
||||
util,
|
||||
'sendEmail',
|
||||
sinon.fake.resolves(false)
|
||||
)
|
||||
})
|
||||
|
||||
after(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects to sign up page with error code INVALID_EMAIL', async () => {
|
||||
const res = await emailResetPasswordReq(email).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/forgot-password?errorCodes=INVALID_EMAIL'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when email is not verified', () => {
|
||||
@ -499,7 +419,7 @@ describe('auth router', () => {
|
||||
})
|
||||
|
||||
context('when token is valid', () => {
|
||||
before(async () => {
|
||||
before(() => {
|
||||
token = generateVerificationToken({ id: user.id })
|
||||
})
|
||||
|
||||
@ -517,8 +437,8 @@ describe('auth router', () => {
|
||||
const updatedUser = await getRepository(User).findOneBy({
|
||||
id: user?.id,
|
||||
})
|
||||
expect(await comparePassword(password, updatedUser?.password!)).to.be
|
||||
.true
|
||||
const newPassword = updatedUser?.password || ''
|
||||
expect(await comparePassword(password, newPassword)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
@ -580,12 +500,12 @@ describe('auth router', () => {
|
||||
}
|
||||
|
||||
context('when inputs are valid and user not exists', () => {
|
||||
let name = 'test_user'
|
||||
let username = 'test_user'
|
||||
let sourceUserId = 'test_source_user_id'
|
||||
let email = 'test_user@omnivore.app'
|
||||
let bio = 'test_bio'
|
||||
let provider: AuthProvider = 'EMAIL'
|
||||
const name = 'test_user'
|
||||
const username = 'test_user'
|
||||
const sourceUserId = 'test_source_user_id'
|
||||
const email = 'test_user@omnivore.app'
|
||||
const bio = 'test_bio'
|
||||
const provider: AuthProvider = 'EMAIL'
|
||||
|
||||
afterEach(async () => {
|
||||
const user = await userRepository.findOneByOrFail({ name })
|
||||
@ -641,7 +561,7 @@ describe('auth router', () => {
|
||||
})
|
||||
|
||||
describe('isValidSignupRequest', () => {
|
||||
it('returns true for normal looking requests', async () => {
|
||||
it('returns true for normal looking requests', () => {
|
||||
const result = isValidSignupRequest({
|
||||
email: 'email@omnivore.app',
|
||||
password: 'superDuperPassword',
|
||||
@ -650,7 +570,7 @@ describe('isValidSignupRequest', () => {
|
||||
})
|
||||
expect(result).to.be.true
|
||||
})
|
||||
it('returns false for requests w/missing info', async () => {
|
||||
it('returns false for requests w/missing info', () => {
|
||||
let result = isValidSignupRequest({
|
||||
password: 'superDuperPassword',
|
||||
name: "The User's Name",
|
||||
@ -680,8 +600,8 @@ describe('isValidSignupRequest', () => {
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
||||
it('returns false for requests w/malicious info', async () => {
|
||||
let result = isValidSignupRequest({
|
||||
it('returns false for requests w/malicious info', () => {
|
||||
const result = isValidSignupRequest({
|
||||
password: 'superDuperPassword',
|
||||
name: "You've won a cake sign up here: https://foo.bar",
|
||||
username: 'foouser',
|
||||
|
||||
@ -1,22 +1,15 @@
|
||||
import { MailDataRequired } from '@sendgrid/helpers/classes/mail'
|
||||
import chai, { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import { expect } from 'chai'
|
||||
import { Filter } from '../../src/entity/filter'
|
||||
import { StatusType, User } from '../../src/entity/user'
|
||||
import { authTrx, getRepository } from '../../src/repository'
|
||||
import { findProfile } from '../../src/services/profile'
|
||||
import { deleteUser } from '../../src/services/user'
|
||||
import * as util from '../../src/utils/sendEmail'
|
||||
import {
|
||||
createTestUser,
|
||||
createUserWithoutProfile,
|
||||
deleteFiltersFromUser,
|
||||
} from '../db'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
describe('create user', () => {
|
||||
context('creates a user through manual sign up', () => {
|
||||
it('adds the default filters to the user', async () => {
|
||||
@ -95,15 +88,9 @@ describe('create user', () => {
|
||||
|
||||
context('create a user with pending confirmation', () => {
|
||||
const name = 'pendingUser'
|
||||
let fake: (msg: MailDataRequired) => Promise<boolean>
|
||||
|
||||
context('when email sends successfully', () => {
|
||||
beforeEach(() => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
sinon.restore()
|
||||
const user = await getRepository(User).findOneBy({ name })
|
||||
await deleteUser(user!.id)
|
||||
})
|
||||
@ -114,29 +101,6 @@ describe('create user', () => {
|
||||
expect(user.status).to.eql(StatusType.Pending)
|
||||
expect(user.name).to.eql(name)
|
||||
})
|
||||
|
||||
it('sends an email to the user', async () => {
|
||||
await createTestUser(name, undefined, undefined, true)
|
||||
|
||||
expect(fake).to.have.been.calledOnce
|
||||
})
|
||||
})
|
||||
|
||||
context('when failed to send email', () => {
|
||||
before(() => {
|
||||
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(false))
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
sinon.restore()
|
||||
const user = await getRepository(User).findOneBy({ name })
|
||||
await deleteUser(user!.id)
|
||||
})
|
||||
|
||||
it('rejects with error', async () => {
|
||||
return expect(createTestUser(name, undefined, undefined, true)).to.be
|
||||
.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -30,6 +30,7 @@ export type Subscription = {
|
||||
updatedAt: string
|
||||
lastFetchedAt?: string
|
||||
mostRecentItemDate?: string
|
||||
failedAt?: string
|
||||
|
||||
fetchContentType?: FetchContentType
|
||||
}
|
||||
@ -74,6 +75,7 @@ export function useGetSubscriptionsQuery(
|
||||
lastFetchedAt
|
||||
fetchContentType
|
||||
mostRecentItemDate
|
||||
failedAt
|
||||
}
|
||||
}
|
||||
... on SubscriptionsError {
|
||||
|
||||
@ -229,11 +229,20 @@ export default function Rss(): JSX.Element {
|
||||
}}
|
||||
>
|
||||
<SpanBox>{`URL: ${subscription.url}`}</SpanBox>
|
||||
<SpanBox>{`Last refreshed: ${
|
||||
subscription.lastFetchedAt
|
||||
? formattedDateTime(subscription.lastFetchedAt)
|
||||
: 'Never'
|
||||
}`}</SpanBox>
|
||||
{/* show failed timestamp instead of last refreshed timestamp if the feed failed to refresh */}
|
||||
{subscription.failedAt ? (
|
||||
<SpanBox
|
||||
css={{ color: 'red' }}
|
||||
>{`Failed to refresh: ${formattedDateTime(
|
||||
subscription.failedAt
|
||||
)}`}</SpanBox>
|
||||
) : (
|
||||
<SpanBox>{`Last refreshed: ${
|
||||
subscription.lastFetchedAt
|
||||
? formattedDateTime(subscription.lastFetchedAt)
|
||||
: 'Never'
|
||||
}`}</SpanBox>
|
||||
)}
|
||||
<SpanBox>
|
||||
{subscription.mostRecentItemDate &&
|
||||
`Most recent item: ${formattedDateTime(
|
||||
|
||||
Reference in New Issue
Block a user