Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: importer service #2674

Merged
merged 16 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
import data from './testData'
import { GenerateUuid } from '@standardnotes/services'

describe('AegisConverter', () => {
const crypto = {
generateUUID: () => String(Math.random()),
} as unknown as PureCryptoInterface

const generateUuid = new GenerateUuid(crypto)

it('should parse entries', () => {
const converter = new AegisToAuthenticatorConverter(generateUuid)
const converter = new AegisToAuthenticatorConverter()

const result = converter.parseEntries(data)

Expand All @@ -31,58 +22,4 @@ describe('AegisConverter', () => {
notes: 'Some other service',
})
})

it('should create note from entries with editor info', () => {
const converter = new AegisToAuthenticatorConverter(generateUuid)

const parsedEntries = converter.parseEntries(data)

const result = converter.createNoteFromEntries(
parsedEntries!,
{
lastModified: 123456789,
name: 'test.json',
},
true,
)

expect(result).not.toBeNull()
expect(result.content_type).toBe('Note')
expect(result.created_at).toBeInstanceOf(Date)
expect(result.updated_at).toBeInstanceOf(Date)
expect(result.uuid).not.toBeNull()
expect(result.content.title).toBe('test')
expect(result.content.text).toBe(
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
)
expect(result.content.noteType).toBe(NoteType.Authentication)
expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor)
})

it('should create note from entries without editor info', () => {
const converter = new AegisToAuthenticatorConverter(generateUuid)

const parsedEntries = converter.parseEntries(data)

const result = converter.createNoteFromEntries(
parsedEntries!,
{
lastModified: 123456789,
name: 'test.json',
},
false,
)

expect(result).not.toBeNull()
expect(result.content_type).toBe('Note')
expect(result.created_at).toBeInstanceOf(Date)
expect(result.updated_at).toBeInstanceOf(Date)
expect(result.uuid).not.toBeNull()
expect(result.content.title).toBe('test')
expect(result.content.text).toBe(
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
)
expect(result.content.noteType).toBeFalsy()
expect(result.content.editorIdentifier).toBeFalsy()
})
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils'
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { ContentType } from '@standardnotes/domain-core'
import { GenerateUuid } from '@standardnotes/services'
import { Converter } from '../Converter'

type AegisData = {
db: {
Expand All @@ -26,19 +23,29 @@ type AuthenticatorEntry = {
notes: string
}

export class AegisToAuthenticatorConverter {
constructor(private _generateUuid: GenerateUuid) {}
export class AegisToAuthenticatorConverter implements Converter {
constructor() {}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isValidAegisJson(json: any): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
getImportType(): string {
return 'aegis'
}

async convertAegisBackupFileToNote(
file: File,
addEditorInfo: boolean,
): Promise<DecryptedTransferPayload<NoteContent>> {
getSupportedFileTypes(): string[] {
return ['application/json']
}

isContentValid(content: string): boolean {
try {
const json = JSON.parse(content)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
} catch (error) {
console.error(error)
}
return false
}

convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => {
const content = await readFileAsText(file)

const entries = this.parseEntries(content)
Expand All @@ -47,34 +54,21 @@ export class AegisToAuthenticatorConverter {
throw new Error('Could not parse entries')
}

return this.createNoteFromEntries(entries, file, addEditorInfo)
}
const createdAt = file.lastModified ? new Date(file.lastModified) : new Date()
const updatedAt = file.lastModified ? new Date(file.lastModified) : new Date()
const title = file.name.split('.')[0]
const text = JSON.stringify(entries)

createNoteFromEntries(
entries: AuthenticatorEntry[],
file: {
lastModified: number
name: string
},
addEditorInfo: boolean,
): DecryptedTransferPayload<NoteContent> {
return {
created_at: new Date(file.lastModified),
created_at_timestamp: file.lastModified,
updated_at: new Date(file.lastModified),
updated_at_timestamp: file.lastModified,
uuid: this._generateUuid.execute().getValue(),
content_type: ContentType.TYPES.Note,
content: {
title: file.name.split('.')[0],
text: JSON.stringify(entries),
references: [],
...(addEditorInfo && {
noteType: NoteType.Authentication,
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
}),
},
}
return [
createNote({
createdAt,
updatedAt,
title,
text,
noteType: NoteType.Authentication,
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
}),
]
}

parseEntries(data: string): AuthenticatorEntry[] | null {
Expand Down
41 changes: 41 additions & 0 deletions packages/ui-services/src/Import/Converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NoteType } from '@standardnotes/features'
import { DecryptedTransferPayload, ItemContent, NoteContent, TagContent } from '@standardnotes/models'

export interface Converter {
getImportType(): string

getSupportedFileTypes?: () => string[]
getFileExtension?: () => string

isContentValid: (content: string) => boolean

convert(
file: File,
dependencies: {
createNote: CreateNoteFn
createTag: CreateTagFn
canUseSuper: boolean
convertHTMLToSuper: (html: string) => string
convertMarkdownToSuper: (markdown: string) => string
readFileAsText: (file: File) => Promise<string>
},
): Promise<DecryptedTransferPayload<ItemContent>[]>
}

export type CreateNoteFn = (options: {
createdAt: Date
updatedAt: Date
title: string
text: string
noteType?: NoteType
archived?: boolean
pinned?: boolean
trashed?: boolean
editorIdentifier?: NoteContent['editorIdentifier']
}) => DecryptedTransferPayload<NoteContent>

export type CreateTagFn = (options: {
createdAt: Date
updatedAt: Date
title: string
}) => DecryptedTransferPayload<TagContent>
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
*/

import { ContentType } from '@standardnotes/domain-core'
import { DecryptedTransferPayload, FileItem, NoteContent, TagContent } from '@standardnotes/models'
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
import { EvernoteConverter, EvernoteResource } from './EvernoteConverter'
import { createTestResourceElement, enex, enexWithNoNoteOrTag } from './testData'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { GenerateUuid } from '@standardnotes/services'
import { SuperConverterServiceInterface } from '@standardnotes/files'
import { Converter } from '../Converter'

// Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts
jest.mock('dayjs', () => {
Expand All @@ -28,43 +28,43 @@ describe('EvernoteConverter', () => {
generateUUID: () => String(Math.random()),
} as unknown as PureCryptoInterface

const superConverterService: SuperConverterServiceInterface = {
isValidSuperString: () => true,
convertOtherFormatToSuperString: (data: string) => data,
convertSuperStringToOtherFormat: async (data: string) => data,
getEmbeddedFileIDsFromSuperString: () => [],
uploadAndReplaceInlineFilesInSuperString: async (
superString: string,
_uploadFile: (file: File) => Promise<FileItem | undefined>,
_linkFile: (file: FileItem) => Promise<void>,
_generateUuid: GenerateUuid,
) => superString,
}

const generateUuid = new GenerateUuid(crypto)

it('should throw error if DOMParser is not available', () => {
const converter = new EvernoteConverter(superConverterService, generateUuid)

const originalDOMParser = window.DOMParser
// @ts-ignore
window.DOMParser = undefined

expect(() => converter.parseENEXData(enex)).toThrowError()

window.DOMParser = originalDOMParser
})
const readFileAsText = async (file: File) => file as unknown as string

const dependencies: Parameters<Converter['convert']>[1] = {
createNote: ({ text }) =>
({
content_type: ContentType.TYPES.Note,
content: {
text,
references: [],
},
}) as unknown as DecryptedTransferPayload<NoteContent>,
createTag: ({ title }) =>
({
content_type: ContentType.TYPES.Tag,
content: {
title,
references: [],
},
}) as unknown as DecryptedTransferPayload<TagContent>,
convertHTMLToSuper: (data) => data,
convertMarkdownToSuper: jest.fn(),
readFileAsText,
canUseSuper: false,
}

it('should throw error if no note or tag in enex', () => {
const converter = new EvernoteConverter(superConverterService, generateUuid)
const converter = new EvernoteConverter(generateUuid)

expect(() => converter.parseENEXData(enexWithNoNoteOrTag)).toThrowError()
expect(converter.convert(enexWithNoNoteOrTag as unknown as File, dependencies)).rejects.toThrowError()
})

it('should parse and strip html', () => {
const converter = new EvernoteConverter(superConverterService, generateUuid)
it('should parse and strip html', async () => {
const converter = new EvernoteConverter(generateUuid)

const result = converter.parseENEXData(enex, false)
const result = await converter.convert(enex as unknown as File, dependencies)

expect(result).not.toBeNull()
expect(result?.length).toBe(3)
Expand All @@ -81,10 +81,13 @@ describe('EvernoteConverter', () => {
expect((result?.[2] as DecryptedTransferPayload<TagContent>).content.references[1].uuid).toBe(result?.[1].uuid)
})

it('should parse and not strip html', () => {
const converter = new EvernoteConverter(superConverterService, generateUuid)
it('should parse and not strip html', async () => {
const converter = new EvernoteConverter(generateUuid)

const result = converter.parseENEXData(enex, true)
const result = await converter.convert(enex as unknown as File, {
...dependencies,
canUseSuper: true,
})

expect(result).not.toBeNull()
expect(result?.length).toBe(3)
Expand Down Expand Up @@ -117,7 +120,7 @@ describe('EvernoteConverter', () => {

const array = [unorderedList1, unorderedList2]

const converter = new EvernoteConverter(superConverterService, generateUuid)
const converter = new EvernoteConverter(generateUuid)
converter.convertListsToSuperFormatIfApplicable(array)

expect(unorderedList1.getAttribute('__lexicallisttype')).toBe('check')
Expand Down Expand Up @@ -148,14 +151,14 @@ describe('EvernoteConverter', () => {

const array = [mediaElement1, mediaElement2, mediaElement3]

const converter = new EvernoteConverter(superConverterService, generateUuid)
const converter = new EvernoteConverter(generateUuid)
const replacedCount = converter.replaceMediaElementsWithResources(array, resources)

expect(replacedCount).toBe(1)
})

describe('getResourceFromElement', () => {
const converter = new EvernoteConverter(superConverterService, generateUuid)
const converter = new EvernoteConverter(generateUuid)

it('should return undefined if no mime type is present', () => {
const resourceElementWithoutMimeType = createTestResourceElement(false)
Expand Down