2019-07-30 09:35:42 +02:00
/* eslint-disable require-atomic-updates */
2021-05-21 15:17:21 +02:00
import BaseApplication from '../BaseApplication' ;
import BaseModel from '../BaseModel' ;
2023-07-27 17:05:56 +02:00
import Logger , { TargetType , LoggerWrapper , LogLevel } from '@joplin/utils/Logger' ;
2024-06-25 15:01:39 +02:00
import Setting , { AppType , Env } from '../models/Setting' ;
2021-05-21 15:17:21 +02:00
import BaseService from '../services/BaseService' ;
import FsDriverNode from '../fs-driver-node' ;
import time from '../time' ;
import shim from '../shim' ;
import uuid from '../uuid' ;
import ResourceService from '../services/ResourceService' ;
import KeymapService from '../services/KeymapService' ;
import KvStore from '../services/KvStore' ;
2024-08-08 20:53:43 +02:00
import KeychainServiceDriverNode from '../services/keychain/KeychainServiceDriver.node' ;
2021-05-21 15:17:21 +02:00
import KeychainServiceDriverDummy from '../services/keychain/KeychainServiceDriver.dummy' ;
import FileApiDriverJoplinServer from '../file-api-driver-joplinServer' ;
import OneDriveApi from '../onedrive-api' ;
import SyncTargetOneDrive from '../SyncTargetOneDrive' ;
import JoplinDatabase from '../JoplinDatabase' ;
2021-06-20 12:19:59 +02:00
import * as fs from 'fs-extra' ;
2021-05-21 15:17:21 +02:00
const { DatabaseDriverNode } = require ( '../database-driver-node.js' ) ;
import Folder from '../models/Folder' ;
import Note from '../models/Note' ;
import ItemChange from '../models/ItemChange' ;
import Resource from '../models/Resource' ;
import Tag from '../models/Tag' ;
import NoteTag from '../models/NoteTag' ;
import Revision from '../models/Revision' ;
import MasterKey from '../models/MasterKey' ;
import BaseItem from '../models/BaseItem' ;
2021-08-12 17:54:10 +02:00
import { FileApi } from '../file-api' ;
2021-06-18 18:17:25 +02:00
const FileApiDriverMemory = require ( '../file-api-driver-memory' ) . default ;
2024-04-25 14:31:48 +02:00
import FileApiDriverLocal from '../file-api-driver-local' ;
2021-05-21 15:17:21 +02:00
const { FileApiDriverWebDav } = require ( '../file-api-driver-webdav.js' ) ;
const { FileApiDriverDropbox } = require ( '../file-api-driver-dropbox.js' ) ;
const { FileApiDriverOneDrive } = require ( '../file-api-driver-onedrive.js' ) ;
2021-08-16 16:20:14 +02:00
import SyncTargetRegistry from '../SyncTargetRegistry' ;
2021-05-21 15:17:21 +02:00
const SyncTargetMemory = require ( '../SyncTargetMemory.js' ) ;
2024-04-25 14:31:48 +02:00
import SyncTargetFilesystem from '../SyncTargetFilesystem' ;
2021-05-21 15:17:21 +02:00
const SyncTargetNextcloud = require ( '../SyncTargetNextcloud.js' ) ;
const SyncTargetDropbox = require ( '../SyncTargetDropbox.js' ) ;
const SyncTargetAmazonS3 = require ( '../SyncTargetAmazonS3.js' ) ;
2023-08-14 19:12:49 +02:00
const SyncTargetWebDAV = require ( '../SyncTargetWebDAV.js' ) ;
2021-05-21 15:17:21 +02:00
import SyncTargetJoplinServer from '../SyncTargetJoplinServer' ;
2021-08-23 19:47:07 +02:00
import EncryptionService from '../services/e2ee/EncryptionService' ;
2021-05-21 15:17:21 +02:00
import DecryptionWorker from '../services/DecryptionWorker' ;
import RevisionService from '../services/RevisionService' ;
import ResourceFetcher from '../services/ResourceFetcher' ;
const WebDavApi = require ( '../WebDavApi' ) ;
const DropboxApi = require ( '../DropboxApi' ) ;
import JoplinServerApi from '../JoplinServerApi' ;
2023-12-13 21:24:58 +02:00
import { FolderEntity , ResourceEntity } from '../services/database/types' ;
2021-06-07 15:46:35 +02:00
import { credentialFile , readCredentialFile } from '../utils/credentialFiles' ;
2021-06-03 17:12:07 +02:00
import SyncTargetJoplinCloud from '../SyncTargetJoplinCloud' ;
2021-08-06 19:37:33 +02:00
import KeychainService from '../services/keychain/KeychainService' ;
import { loadKeychainServiceAndSettings } from '../services/SettingUtils' ;
2021-08-12 17:54:10 +02:00
import { setActiveMasterKeyId , setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils' ;
import Synchronizer from '../Synchronizer' ;
2021-08-16 19:05:22 +02:00
import SyncTargetNone from '../SyncTargetNone' ;
2021-10-03 17:00:49 +02:00
import { setRSA } from '../services/e2ee/ppk' ;
2020-05-31 22:07:24 +02:00
const md5 = require ( 'md5' ) ;
2021-05-21 15:17:21 +02:00
const { Dirnames } = require ( '../services/synchronizer/utils/types' ) ;
2021-10-03 17:00:49 +02:00
import RSA from '../services/e2ee/RSA.node' ;
2023-07-16 18:42:42 +02:00
import { State as ShareState } from '../services/share/reducer' ;
2023-08-04 11:57:54 +02:00
import initLib from '../initLib' ;
2024-01-05 16:43:24 +02:00
import OcrDriverTesseract from '../services/ocr/drivers/OcrDriverTesseract' ;
import OcrService from '../services/ocr/OcrService' ;
import { createWorker } from 'tesseract.js' ;
2024-04-15 19:17:34 +02:00
import { reg } from '../registry' ;
2024-07-26 13:35:50 +02:00
import { Store } from 'redux' ;
2017-06-14 21:59:02 +02:00
2020-11-29 13:29:43 +02:00
// Each suite has its own separate data and temp directory so that multiple
// suites can be run at the same time. suiteName is what is used to
// differentiate between suite and it is currently set to a random string
// (Ideally it would be something like the filename currently being executed by
// Jest, to make debugging easier, but it's not clear how to get this info).
const suiteName_ = uuid . createNano ( ) ;
2021-08-12 17:54:10 +02:00
const databases_ : JoplinDatabase [ ] = [ ] ;
let synchronizers_ : Synchronizer [ ] = [ ] ;
const fileApis_ : Record < number , FileApi > = { } ;
const encryptionServices_ : EncryptionService [ ] = [ ] ;
const revisionServices_ : RevisionService [ ] = [ ] ;
const decryptionWorkers_ : DecryptionWorker [ ] = [ ] ;
const resourceServices_ : ResourceService [ ] = [ ] ;
const resourceFetchers_ : ResourceFetcher [ ] = [ ] ;
2020-12-08 22:01:33 +02:00
const kvStores_ : KvStore [ ] = [ ] ;
2017-06-18 22:19:13 +02:00
let currentClient_ = 1 ;
2017-06-14 21:59:02 +02:00
2019-11-02 12:19:24 +02:00
// The line `process.on('unhandledRejection'...` in all the test files is going to
// make it throw this error. It's not too big a problem so disable it for now.
// https://stackoverflow.com/questions/9768444/possible-eventemitter-memory-leak-detected
process . setMaxListeners ( 0 ) ;
shim . setIsTestingEnv ( true ) ;
2017-07-06 00:29:03 +02:00
const fsDriver = new FsDriverNode ( ) ;
Logger . fsDriver_ = fsDriver ;
Resource . fsDriver_ = fsDriver ;
2017-12-18 21:54:03 +02:00
EncryptionService . fsDriver_ = fsDriver ;
2018-01-17 20:51:15 +02:00
FileApiDriverLocal . fsDriver_ = fsDriver ;
2017-07-06 00:29:03 +02:00
2021-05-21 15:17:21 +02:00
// Most test units were historically under /app-cli so most test-related
// directories are there but that should be moved eventually under the right
// packages, or even out of the monorepo for temp files, logs, etc.
const oldTestDir = ` ${ __dirname } /../../app-cli/tests ` ;
const logDir = ` ${ oldTestDir } /logs ` ;
const baseTempDir = ` ${ oldTestDir } /tmp/ ${ suiteName_ } ` ;
const supportDir = ` ${ oldTestDir } /support ` ;
2023-12-13 21:24:58 +02:00
export const ocrSampleDir = ` ${ oldTestDir } /ocr_samples ` ;
2020-12-09 14:50:51 +02:00
// We add a space in the data directory path as that will help uncover
// various space-in-path issues.
2021-05-21 15:17:21 +02:00
const dataDir = ` ${ oldTestDir } /test data/ ${ suiteName_ } ` ;
2021-02-09 19:54:29 +02:00
const profileDir = ` ${ dataDir } /profile ` ;
2022-04-11 17:49:32 +02:00
const rootProfileDir = profileDir ;
2020-12-09 14:50:51 +02:00
2021-06-20 12:19:59 +02:00
fs . mkdirpSync ( logDir ) ;
fs . mkdirpSync ( baseTempDir ) ;
2020-11-29 13:29:43 +02:00
fs . mkdirpSync ( dataDir ) ;
2021-02-09 19:54:29 +02:00
fs . mkdirpSync ( profileDir ) ;
2017-07-06 21:48:17 +02:00
2021-08-16 19:05:22 +02:00
SyncTargetRegistry . addClass ( SyncTargetNone ) ;
2017-11-24 21:21:30 +02:00
SyncTargetRegistry . addClass ( SyncTargetMemory ) ;
SyncTargetRegistry . addClass ( SyncTargetFilesystem ) ;
2017-11-28 00:50:46 +02:00
SyncTargetRegistry . addClass ( SyncTargetOneDrive ) ;
2018-01-25 23:15:58 +02:00
SyncTargetRegistry . addClass ( SyncTargetNextcloud ) ;
2018-03-24 21:35:10 +02:00
SyncTargetRegistry . addClass ( SyncTargetDropbox ) ;
2020-07-15 11:22:55 +02:00
SyncTargetRegistry . addClass ( SyncTargetAmazonS3 ) ;
2023-08-14 19:12:49 +02:00
SyncTargetRegistry . addClass ( SyncTargetWebDAV ) ;
2020-12-28 13:48:47 +02:00
SyncTargetRegistry . addClass ( SyncTargetJoplinServer ) ;
2021-06-03 17:12:07 +02:00
SyncTargetRegistry . addClass ( SyncTargetJoplinCloud ) ;
2017-11-24 21:21:30 +02:00
2020-08-02 13:28:50 +02:00
let syncTargetName_ = '' ;
2020-12-08 22:01:33 +02:00
let syncTargetId_ : number = null ;
2020-08-02 13:28:50 +02:00
let sleepTime = 0 ;
let isNetworkSyncTarget_ = false ;
function syncTargetName() {
return syncTargetName_ ;
}
2017-07-23 16:11:44 +02:00
2020-12-08 22:01:33 +02:00
function setSyncTargetName ( name : string ) {
2020-08-02 13:28:50 +02:00
if ( name === syncTargetName_ ) return syncTargetName_ ;
const previousName = syncTargetName_ ;
syncTargetName_ = name ;
syncTargetId_ = SyncTargetRegistry . nameToId ( syncTargetName_ ) ;
2022-07-23 09:31:32 +02:00
sleepTime = syncTargetId_ === SyncTargetRegistry . nameToId ( 'filesystem' ) ? 1001 : 100 ; // 400;
2023-07-16 18:42:42 +02:00
isNetworkSyncTarget_ = [ 'nextcloud' , 'dropbox' , 'onedrive' , 'amazon_s3' , 'joplinServer' , 'joplinCloud' ] . includes ( syncTargetName_ ) ;
2020-08-02 13:28:50 +02:00
synchronizers_ = [ ] ;
return previousName ;
}
setSyncTargetName ( 'memory' ) ;
2021-08-12 17:54:10 +02:00
// setSyncTargetName('filesystem');
2020-08-02 13:28:50 +02:00
// setSyncTargetName('nextcloud');
// setSyncTargetName('dropbox');
// setSyncTargetName('onedrive');
// setSyncTargetName('amazon_s3');
2020-12-28 13:48:47 +02:00
// setSyncTargetName('joplinServer');
2023-07-16 18:42:42 +02:00
// setSyncTargetName('joplinCloud');
2017-07-24 20:58:11 +02:00
2020-11-10 17:59:30 +02:00
// console.info(`Testing with sync target: ${syncTargetName_}`);
2020-08-02 13:28:50 +02:00
2021-05-21 15:17:21 +02:00
const syncDir = ` ${ oldTestDir } /sync/ ${ suiteName_ } ` ;
2020-08-02 13:28:50 +02:00
2020-12-08 21:45:08 +02:00
// 90 seconds now that the tests are running in parallel and have been
// split into smaller suites might not be necessary but for now leave it
// anyway.
let defaultJestTimeout = 90 * 1000 ;
if ( isNetworkSyncTarget_ ) defaultJestTimeout = 60 * 1000 * 10 ;
2021-08-12 17:54:10 +02:00
if ( typeof jest !== 'undefined' ) jest . setTimeout ( defaultJestTimeout ) ;
2018-01-25 23:15:58 +02:00
2019-05-14 23:23:34 +02:00
const dbLogger = new Logger ( ) ;
2020-12-08 22:01:33 +02:00
dbLogger . addTarget ( TargetType . Console ) ;
2019-05-14 23:23:34 +02:00
dbLogger . setLevel ( Logger . LEVEL_WARN ) ;
2017-06-25 17:17:40 +02:00
const logger = new Logger ( ) ;
2020-12-08 22:01:33 +02:00
logger . addTarget ( TargetType . Console ) ;
2021-05-03 12:55:38 +02:00
logger . setLevel ( LogLevel . Warn ) ; // Set to DEBUG to display sync process in console
2017-06-25 17:17:40 +02:00
2020-11-19 17:25:02 +02:00
Logger . initializeGlobalLogger ( logger ) ;
2023-08-04 11:57:54 +02:00
initLib ( logger ) ;
2020-11-19 17:25:02 +02:00
2018-03-09 22:59:12 +02:00
BaseItem . loadClass ( 'Note' , Note ) ;
BaseItem . loadClass ( 'Folder' , Folder ) ;
BaseItem . loadClass ( 'Resource' , Resource ) ;
BaseItem . loadClass ( 'Tag' , Tag ) ;
BaseItem . loadClass ( 'NoteTag' , NoteTag ) ;
BaseItem . loadClass ( 'MasterKey' , MasterKey ) ;
2019-05-06 22:35:29 +02:00
BaseItem . loadClass ( 'Revision' , Revision ) ;
2017-07-06 21:48:17 +02:00
2020-06-03 18:07:50 +02:00
Setting . setConstant ( 'appId' , 'net.cozic.joplintest-cli' ) ;
2024-06-25 15:01:39 +02:00
Setting . setConstant ( 'appType' , AppType . Cli ) ;
2020-10-22 16:55:29 +02:00
Setting . setConstant ( 'tempDir' , baseTempDir ) ;
2021-01-20 00:58:09 +02:00
Setting . setConstant ( 'cacheDir' , baseTempDir ) ;
2024-03-11 11:39:57 +02:00
Setting . setConstant ( 'resourceDir' , baseTempDir ) ;
2021-02-09 19:54:29 +02:00
Setting . setConstant ( 'pluginDataDir' , ` ${ profileDir } /profile/plugin-data ` ) ;
Setting . setConstant ( 'profileDir' , profileDir ) ;
2022-04-11 17:49:32 +02:00
Setting . setConstant ( 'rootProfileDir' , rootProfileDir ) ;
2024-06-25 15:01:39 +02:00
Setting . setConstant ( 'env' , Env . Dev ) ;
2017-07-07 00:15:31 +02:00
2018-03-15 19:46:54 +02:00
BaseService . logger_ = logger ;
2018-02-15 19:12:09 +02:00
Setting . autoSaveEnabled = false ;
2017-07-23 16:11:44 +02:00
function syncTargetId() {
2017-07-24 20:58:11 +02:00
return syncTargetId_ ;
2017-07-23 16:11:44 +02:00
}
2020-08-02 13:28:50 +02:00
function isNetworkSyncTarget() {
return isNetworkSyncTarget_ ;
}
2020-12-08 22:01:33 +02:00
function sleep ( n : number ) {
return new Promise ( ( resolve ) = > {
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) = > {
2020-12-16 03:00:53 +02:00
resolve ( null ) ;
2017-06-19 21:18:22 +02:00
} , Math . round ( n * 1000 ) ) ;
2017-06-18 22:19:13 +02:00
} ) ;
}
2023-07-22 12:19:18 +02:00
function msleep ( ms : number ) : Promise < void > {
2021-01-03 18:27:51 +02:00
// It seems setTimeout can sometimes last less time than the provided
// interval:
//
// https://stackoverflow.com/a/50912029/561309
//
// This can cause issues in tests where we expect the actual duration to be
// the same as the provided interval or more, but not less. So the code
// below check that the elapsed time is no less than the provided interval,
// and if it is, it waits a bit longer.
const startTime = Date . now ( ) ;
2020-12-08 22:01:33 +02:00
return new Promise ( ( resolve ) = > {
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) = > {
2021-01-03 18:27:51 +02:00
if ( Date . now ( ) - startTime < ms ) {
const iid = setInterval ( ( ) = > {
if ( Date . now ( ) - startTime >= ms ) {
clearInterval ( iid ) ;
resolve ( null ) ;
}
} , 2 ) ;
} else {
resolve ( null ) ;
}
2020-08-02 13:28:50 +02:00
} , ms ) ;
} ) ;
}
2020-03-16 04:30:54 +02:00
function currentClientId() {
return currentClient_ ;
}
2020-11-10 17:59:30 +02:00
async function afterEachCleanUp() {
await ItemChange . waitForAllSaved ( ) ;
2020-12-08 21:45:08 +02:00
KeymapService . destroyInstance ( ) ;
2020-11-10 17:59:30 +02:00
}
2020-12-28 13:48:47 +02:00
async function afterAllCleanUp() {
if ( fileApi ( ) ) {
try {
await fileApi ( ) . clearRoot ( ) ;
} catch ( error ) {
console . warn ( 'Could not clear sync target root:' , error ) ;
}
}
}
2022-05-05 19:15:10 +02:00
const settingFilename = ( id : number ) : string = > {
return ` settings- ${ id } .json ` ;
} ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function switchClient ( id : number , options : any = null ) {
2023-06-01 13:02:36 +02:00
options = { keychainEnabled : false , . . . options } ;
2020-06-03 18:07:50 +02:00
2019-09-19 23:51:18 +02:00
if ( ! databases_ [ id ] ) throw new Error ( ` Call setupDatabaseAndSynchronizer( ${ id } ) first!! ` ) ;
2019-04-21 14:49:40 +02:00
2017-07-24 20:58:11 +02:00
await time . msleep ( sleepTime ) ; // Always leave a little time so that updated_time properties don't overlap
2017-07-03 20:29:19 +02:00
await Setting . saveAll ( ) ;
2017-06-20 21:18:19 +02:00
2017-06-18 22:19:13 +02:00
currentClient_ = id ;
2020-03-16 04:30:54 +02:00
BaseModel . setDb ( databases_ [ id ] ) ;
2024-08-08 20:53:43 +02:00
KvStore . instance ( ) . setDb ( databases_ [ id ] ) ;
2017-06-20 21:18:19 +02:00
2017-12-13 20:57:40 +02:00
BaseItem . encryptionService_ = encryptionServices_ [ id ] ;
2017-12-18 21:54:03 +02:00
Resource . encryptionService_ = encryptionServices_ [ id ] ;
2019-05-06 22:35:29 +02:00
BaseItem . revisionService_ = revisionServices_ [ id ] ;
2024-10-11 23:14:18 +02:00
ResourceFetcher . instance_ = resourceFetchers_ [ id ] ;
2017-12-18 21:54:03 +02:00
2020-10-09 19:35:46 +02:00
await Setting . reset ( ) ;
2022-05-05 19:15:10 +02:00
Setting . settingFilename = settingFilename ( id ) ;
2021-08-12 17:54:10 +02:00
2022-04-11 17:49:32 +02:00
Setting . setConstant ( 'profileDir' , rootProfileDir ) ;
Setting . setConstant ( 'rootProfileDir' , rootProfileDir ) ;
2020-04-14 00:55:24 +02:00
Setting . setConstant ( 'resourceDirName' , resourceDirName ( id ) ) ;
2018-03-09 22:59:12 +02:00
Setting . setConstant ( 'resourceDir' , resourceDir ( id ) ) ;
2020-11-20 01:46:04 +02:00
Setting . setConstant ( 'pluginDir' , pluginDir ( id ) ) ;
2022-05-05 19:15:10 +02:00
Setting . setConstant ( 'isSubProfile' , false ) ;
2017-12-13 20:57:40 +02:00
2024-08-08 20:53:43 +02:00
await loadKeychainServiceAndSettings ( [ options . keychainEnabled ? KeychainServiceDriverNode : KeychainServiceDriverDummy ] ) ;
2019-09-25 20:40:04 +02:00
2021-08-16 19:05:22 +02:00
Setting . setValue ( 'sync.target' , syncTargetId ( ) ) ;
2022-05-05 19:15:10 +02:00
Setting . setValue ( 'sync.wipeOutFailSafe' , false ) ; // To keep things simple, always disable fail-safe unless explicitly set in the test itself
2021-08-12 17:54:10 +02:00
// More generally, this function should clear all data, and so that should
// include settings.json
await clearSettingFile ( id ) ;
2017-06-18 22:19:13 +02:00
}
2020-12-08 22:01:33 +02:00
async function clearDatabase ( id : number = null ) {
2017-06-18 22:19:13 +02:00
if ( id === null ) id = currentClient_ ;
2020-02-27 20:25:42 +02:00
if ( ! databases_ [ id ] ) return ;
2017-06-18 22:19:13 +02:00
2018-12-10 20:54:46 +02:00
await ItemChange . waitForAllSaved ( ) ;
2019-05-06 22:35:29 +02:00
const tableNames = [
2024-01-05 20:16:09 +02:00
'deleted_items' ,
2019-05-06 22:35:29 +02:00
'folders' ,
'item_changes' ,
2024-01-05 20:16:09 +02:00
'items_normalized' ,
'key_values' ,
'master_keys' ,
2019-05-06 22:35:29 +02:00
'note_resources' ,
2024-01-05 20:16:09 +02:00
'note_tags' ,
2019-05-06 22:35:29 +02:00
'notes_normalized' ,
2024-01-05 20:16:09 +02:00
'notes' ,
'resources' ,
2019-05-06 22:35:29 +02:00
'revisions' ,
2024-01-05 20:16:09 +02:00
'settings' ,
'sync_items' ,
'tags' ,
2017-06-18 22:19:13 +02:00
] ;
2019-05-06 22:35:29 +02:00
const queries = [ ] ;
for ( const n of tableNames ) {
2019-09-19 23:51:18 +02:00
queries . push ( ` DELETE FROM ${ n } ` ) ;
queries . push ( ` DELETE FROM sqlite_sequence WHERE name=" ${ n } " ` ) ; // Reset autoincremented IDs
2019-05-06 22:35:29 +02:00
}
2017-12-20 21:45:25 +02:00
await databases_ [ id ] . transactionExecBatch ( queries ) ;
2017-06-18 22:19:13 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function setupDatabase ( id : number = null , options : any = null ) {
2023-06-01 13:02:36 +02:00
options = { keychainEnabled : false , . . . options } ;
2020-06-03 18:07:50 +02:00
2017-06-18 22:19:13 +02:00
if ( id === null ) id = currentClient_ ;
2017-12-20 21:45:25 +02:00
Setting . cancelScheduleSave ( ) ;
2020-12-08 22:01:33 +02:00
// Note that this was changed from `Setting.cache_ = []` to `await
// Setting.reset()` during the TypeScript conversion. Normally this is
// more correct but something to keep in mind anyway in case there are
// some strange async issue related to settings when the tests are
// running.
await Setting . reset ( ) ;
2017-12-20 21:45:25 +02:00
2022-04-11 17:49:32 +02:00
Setting . setConstant ( 'profileDir' , rootProfileDir ) ;
Setting . setConstant ( 'rootProfileDir' , rootProfileDir ) ;
2022-05-05 19:15:10 +02:00
Setting . setConstant ( 'isSubProfile' , false ) ;
2022-04-11 17:49:32 +02:00
2017-06-18 22:19:13 +02:00
if ( databases_ [ id ] ) {
2020-03-16 04:30:54 +02:00
BaseModel . setDb ( databases_ [ id ] ) ;
2017-12-20 21:45:25 +02:00
await clearDatabase ( id ) ;
2024-08-08 20:53:43 +02:00
await loadKeychainServiceAndSettings ( [ options . keychainEnabled ? KeychainServiceDriverNode : KeychainServiceDriverDummy ] ) ;
2021-08-16 19:05:22 +02:00
Setting . setValue ( 'sync.target' , syncTargetId ( ) ) ;
2017-12-20 21:45:25 +02:00
return ;
2017-06-14 21:59:02 +02:00
}
2020-11-29 13:29:43 +02:00
const filePath = ` ${ dataDir } /test- ${ id } .sqlite ` ;
2017-12-20 21:45:25 +02:00
try {
await fs . unlink ( filePath ) ;
} catch ( error ) {
2017-06-14 21:59:02 +02:00
// Don't care if the file doesn't exist
2019-07-30 09:35:42 +02:00
}
2017-12-20 21:45:25 +02:00
databases_ [ id ] = new JoplinDatabase ( new DatabaseDriverNode ( ) ) ;
2019-05-14 23:23:34 +02:00
databases_ [ id ] . setLogger ( dbLogger ) ;
2017-12-20 21:45:25 +02:00
await databases_ [ id ] . open ( { name : filePath } ) ;
2020-03-16 04:30:54 +02:00
BaseModel . setDb ( databases_ [ id ] ) ;
2021-08-12 17:54:10 +02:00
await clearSettingFile ( id ) ;
2024-08-08 20:53:43 +02:00
await loadKeychainServiceAndSettings ( [ options . keychainEnabled ? KeychainServiceDriverNode : KeychainServiceDriverDummy ] ) ;
2021-08-16 19:05:22 +02:00
2024-04-15 19:17:34 +02:00
reg . setDb ( databases_ [ id ] ) ;
2021-08-16 19:05:22 +02:00
Setting . setValue ( 'sync.target' , syncTargetId ( ) ) ;
2017-06-14 21:59:02 +02:00
}
2021-08-12 17:54:10 +02:00
async function clearSettingFile ( id : number ) {
Setting . settingFilename = ` settings- ${ id } .json ` ;
await fs . remove ( Setting . settingFilePath ) ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-06-30 10:11:26 +02:00
export async function createFolderTree ( parentId : string , tree : any [ ] , num = 0 ) : Promise < FolderEntity > {
2021-05-13 18:57:37 +02:00
let rootFolder : FolderEntity = null ;
for ( const item of tree ) {
const isFolder = ! ! item . children ;
num ++ ;
const data = { . . . item } ;
delete data . children ;
if ( isFolder ) {
const folder = await Folder . save ( { title : ` Folder ${ num } ` , parent_id : parentId , . . . data } ) ;
if ( ! rootFolder ) rootFolder = folder ;
if ( item . children . length ) await createFolderTree ( folder . id , item . children , num ) ;
} else {
await Note . save ( { title : ` Note ${ num } ` , parent_id : parentId , . . . data } ) ;
}
}
return rootFolder ;
}
2020-12-09 14:50:51 +02:00
function exportDir ( id : number = null ) {
if ( id === null ) id = currentClient_ ;
return ` ${ dataDir } /export ` ;
}
2020-12-08 22:01:33 +02:00
function resourceDirName ( id : number = null ) {
2020-04-14 00:55:24 +02:00
if ( id === null ) id = currentClient_ ;
return ` resources- ${ id } ` ;
}
2020-12-08 22:01:33 +02:00
function resourceDir ( id : number = null ) {
2017-12-18 21:54:03 +02:00
if ( id === null ) id = currentClient_ ;
2020-11-29 13:29:43 +02:00
return ` ${ dataDir } / ${ resourceDirName ( id ) } ` ;
2017-12-18 21:54:03 +02:00
}
2020-12-08 22:01:33 +02:00
function pluginDir ( id : number = null ) {
2020-11-20 01:46:04 +02:00
if ( id === null ) id = currentClient_ ;
2020-11-29 13:29:43 +02:00
return ` ${ dataDir } /plugins- ${ id } ` ;
2020-11-20 01:46:04 +02:00
}
2023-12-13 21:24:58 +02:00
export interface CreateNoteAndResourceOptions {
path? : string ;
}
const createNoteAndResource = async ( options : CreateNoteAndResourceOptions = null ) = > {
options = {
path : ` ${ supportDir } /photo.jpg ` ,
. . . options ,
} ;
let note = await Note . save ( { } ) ;
note = await shim . attachFileToNote ( note , options . path ) ;
const resourceIds = await Note . linkedItemIds ( note . body ) ;
const resource : ResourceEntity = await Resource . load ( resourceIds [ 0 ] ) ;
return { note , resource } ;
} ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function setupDatabaseAndSynchronizer ( id : number , options : any = null ) {
2017-06-18 22:19:13 +02:00
if ( id === null ) id = currentClient_ ;
2017-06-18 01:49:52 +02:00
2020-03-16 04:30:54 +02:00
BaseService . logger_ = logger ;
2020-06-03 18:07:50 +02:00
await setupDatabase ( id , options ) ;
2017-06-18 22:19:13 +02:00
2017-12-20 21:45:25 +02:00
EncryptionService . instance_ = null ;
DecryptionWorker . instance_ = null ;
2017-12-18 21:54:03 +02:00
await fs . remove ( resourceDir ( id ) ) ;
2021-06-20 12:19:59 +02:00
await fs . mkdirp ( resourceDir ( id ) ) ;
2017-12-18 21:54:03 +02:00
2020-11-20 01:46:04 +02:00
await fs . remove ( pluginDir ( id ) ) ;
2021-06-20 12:19:59 +02:00
await fs . mkdirp ( pluginDir ( id ) ) ;
2020-11-20 01:46:04 +02:00
2017-06-18 22:19:13 +02:00
if ( ! synchronizers_ [ id ] ) {
2017-11-24 20:37:40 +02:00
const SyncTargetClass = SyncTargetRegistry . classById ( syncTargetId_ ) ;
const syncTarget = new SyncTargetClass ( db ( id ) ) ;
2021-05-13 18:57:37 +02:00
await initFileApi ( ) ;
2017-11-24 20:37:40 +02:00
syncTarget . setFileApi ( fileApi ( ) ) ;
syncTarget . setLogger ( logger ) ;
synchronizers_ [ id ] = await syncTarget . synchronizer ( ) ;
2021-12-20 18:24:45 +02:00
// For now unset the share service as it's not properly initialised.
// Share service tests are in ShareService.test.ts normally, and if it
// becomes necessary to test integration with the synchroniser we can
// initialize it here.
synchronizers_ [ id ] . setShareService ( null ) ;
2017-06-18 01:49:52 +02:00
}
2017-06-18 22:19:13 +02:00
2017-12-19 21:01:29 +02:00
encryptionServices_ [ id ] = new EncryptionService ( ) ;
2019-05-06 22:35:29 +02:00
revisionServices_ [ id ] = new RevisionService ( ) ;
2017-12-19 21:01:29 +02:00
decryptionWorkers_ [ id ] = new DecryptionWorker ( ) ;
decryptionWorkers_ [ id ] . setEncryptionService ( encryptionServices_ [ id ] ) ;
2019-04-21 14:49:40 +02:00
resourceServices_ [ id ] = new ResourceService ( ) ;
2020-05-31 01:31:29 +02:00
resourceFetchers_ [ id ] = new ResourceFetcher ( ( ) = > { return synchronizers_ [ id ] . api ( ) ; } ) ;
2019-06-08 00:11:08 +02:00
kvStores_ [ id ] = new KvStore ( ) ;
2017-12-13 20:57:40 +02:00
2021-10-03 17:00:49 +02:00
setRSA ( RSA ) ;
2020-12-28 13:48:47 +02:00
await fileApi ( ) . initialize ( ) ;
2018-01-26 00:44:09 +02:00
await fileApi ( ) . clearRoot ( ) ;
2017-06-14 21:59:02 +02:00
}
2021-01-29 20:45:11 +02:00
function db ( id : number = null ) : JoplinDatabase {
2017-06-18 22:19:13 +02:00
if ( id === null ) id = currentClient_ ;
return databases_ [ id ] ;
2017-06-14 21:59:02 +02:00
}
2020-12-08 22:01:33 +02:00
function synchronizer ( id : number = null ) {
2017-06-18 22:19:13 +02:00
if ( id === null ) id = currentClient_ ;
return synchronizers_ [ id ] ;
2017-06-14 21:59:02 +02:00
}
2020-08-02 13:28:50 +02:00
// This is like calling synchronizer.start() but it handles the
// complexity of passing around the sync context depending on
// the client.
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function synchronizerStart ( id : number = null , extraOptions : any = null ) {
2020-08-02 13:28:50 +02:00
if ( id === null ) id = currentClient_ ;
2021-05-03 12:55:38 +02:00
const contextKey = ` sync. ${ syncTargetId ( ) } .context ` ;
2021-06-07 15:46:35 +02:00
const contextString = Setting . value ( contextKey ) ;
const context = contextString ? JSON . parse ( contextString ) : { } ;
2021-05-03 12:55:38 +02:00
2023-06-01 13:02:36 +02:00
const options = { . . . extraOptions } ;
2020-08-02 13:28:50 +02:00
if ( context ) options . context = context ;
const newContext = await synchronizer ( id ) . start ( options ) ;
2021-05-03 12:55:38 +02:00
Setting . setValue ( contextKey , JSON . stringify ( newContext ) ) ;
2020-08-02 13:28:50 +02:00
return newContext ;
}
2020-12-08 22:01:33 +02:00
function encryptionService ( id : number = null ) {
2017-12-13 20:57:40 +02:00
if ( id === null ) id = currentClient_ ;
return encryptionServices_ [ id ] ;
}
2020-12-08 22:01:33 +02:00
function kvStore ( id : number = null ) {
2019-06-08 00:11:08 +02:00
if ( id === null ) id = currentClient_ ;
const o = kvStores_ [ id ] ;
o . setDb ( db ( id ) ) ;
return o ;
}
2020-12-08 22:01:33 +02:00
function revisionService ( id : number = null ) {
2019-05-06 22:35:29 +02:00
if ( id === null ) id = currentClient_ ;
return revisionServices_ [ id ] ;
}
2020-12-08 22:01:33 +02:00
function decryptionWorker ( id : number = null ) {
2017-12-19 21:01:29 +02:00
if ( id === null ) id = currentClient_ ;
2019-06-08 00:11:08 +02:00
const o = decryptionWorkers_ [ id ] ;
o . setKvStore ( kvStore ( id ) ) ;
return o ;
2017-12-19 21:01:29 +02:00
}
2020-12-08 22:01:33 +02:00
function resourceService ( id : number = null ) {
2019-04-21 14:49:40 +02:00
if ( id === null ) id = currentClient_ ;
return resourceServices_ [ id ] ;
}
2020-12-08 22:01:33 +02:00
function resourceFetcher ( id : number = null ) {
2020-05-31 01:31:29 +02:00
if ( id === null ) id = currentClient_ ;
return resourceFetchers_ [ id ] ;
}
2020-12-08 22:01:33 +02:00
async function loadEncryptionMasterKey ( id : number = null , useExisting = false ) {
2017-12-13 20:57:40 +02:00
const service = encryptionService ( id ) ;
2021-10-03 17:00:49 +02:00
const password = '123456' ;
2017-12-13 20:57:40 +02:00
2017-12-17 21:51:45 +02:00
let masterKey = null ;
2018-03-09 22:59:12 +02:00
if ( ! useExisting ) { // Create it
2021-10-03 17:00:49 +02:00
masterKey = await service . generateMasterKey ( password ) ;
2017-12-17 21:51:45 +02:00
masterKey = await MasterKey . save ( masterKey ) ;
2018-03-09 22:59:12 +02:00
} else { // Use the one already available
2019-07-30 09:35:42 +02:00
const masterKeys = await MasterKey . all ( ) ;
2019-10-29 11:02:42 +02:00
if ( ! masterKeys . length ) throw new Error ( 'No master key available' ) ;
2019-07-30 09:35:42 +02:00
masterKey = masterKeys [ 0 ] ;
2017-12-17 21:51:45 +02:00
}
2017-12-13 20:57:40 +02:00
2021-10-03 17:00:49 +02:00
const passwordCache = Setting . value ( 'encryption.passwordCache' ) ;
passwordCache [ masterKey . id ] = password ;
Setting . setValue ( 'encryption.passwordCache' , passwordCache ) ;
await Setting . saveAll ( ) ;
await service . loadMasterKey ( masterKey , password , true ) ;
2021-08-12 17:54:10 +02:00
setActiveMasterKeyId ( masterKey . id ) ;
2017-12-13 20:57:40 +02:00
return masterKey ;
}
2021-05-13 18:57:37 +02:00
function mustRunInBand() {
if ( ! process . argv . includes ( '--runInBand' ) ) {
2021-12-21 18:55:30 +02:00
throw new Error ( 'Tests must be run sequentially for this sync target, with the --runInBand arg. eg `yarn test --runInBand`' ) ;
2021-05-13 18:57:37 +02:00
}
}
async function initFileApi() {
2020-08-02 13:28:50 +02:00
if ( fileApis_ [ syncTargetId_ ] ) return ;
2017-06-18 22:19:13 +02:00
2020-08-02 13:28:50 +02:00
let fileApi = null ;
2022-07-23 09:31:32 +02:00
if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'filesystem' ) ) {
2019-07-30 09:35:42 +02:00
fs . removeSync ( syncDir ) ;
2021-06-20 12:19:59 +02:00
fs . mkdirpSync ( syncDir ) ;
2020-08-02 13:28:50 +02:00
fileApi = new FileApi ( syncDir , new FileApiDriverLocal ( ) ) ;
2022-07-23 09:31:32 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'memory' ) ) {
2020-08-02 13:28:50 +02:00
fileApi = new FileApi ( '/root' , new FileApiDriverMemory ( ) ) ;
2022-07-23 09:31:32 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'nextcloud' ) ) {
2021-05-21 15:17:21 +02:00
const options = require ( ` ${ oldTestDir } /support/nextcloud-auth.json ` ) ;
2020-08-02 13:28:50 +02:00
const api = new WebDavApi ( {
baseUrl : ( ) = > options . baseUrl ,
username : ( ) = > options . username ,
password : ( ) = > options . password ,
} ) ;
fileApi = new FileApi ( '' , new FileApiDriverWebDav ( api ) ) ;
2022-07-23 09:31:32 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'dropbox' ) ) {
2020-08-02 13:28:50 +02:00
// To get a token, go to the App Console:
// https://www.dropbox.com/developers/apps/
// Then select "JoplinTest" and click "Generated access token"
2018-03-24 21:35:10 +02:00
const api = new DropboxApi ( ) ;
2021-05-21 15:17:21 +02:00
const authTokenPath = ` ${ oldTestDir } /support/dropbox-auth.txt ` ;
2018-03-24 21:35:10 +02:00
const authToken = fs . readFileSync ( authTokenPath , 'utf8' ) ;
2019-09-19 23:51:18 +02:00
if ( ! authToken ) throw new Error ( ` Dropbox auth token missing in ${ authTokenPath } ` ) ;
2018-03-24 21:35:10 +02:00
api . setAuthToken ( authToken ) ;
2020-08-02 13:28:50 +02:00
fileApi = new FileApi ( '' , new FileApiDriverDropbox ( api ) ) ;
2022-07-23 09:31:32 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'onedrive' ) ) {
2021-08-08 19:38:04 +02:00
// To get a token, open the URL below corresponding to your account type,
// then copy the *complete* redirection URL in onedrive-auth.txt. Keep in mind that auth
2020-12-16 13:19:35 +02:00
// data only lasts 1h for OneDrive.
//
2021-08-08 19:38:04 +02:00
// Personal OneDrive Account:
2020-08-02 13:28:50 +02:00
// https://login.live.com/oauth20_authorize.srf?client_id=f1e68e1e-a729-4514-b041-4fdd5c7ac03a&scope=files.readwrite,offline_access&response_type=token&redirect_uri=https://joplinapp.org
2020-12-16 13:19:35 +02:00
//
2021-08-08 19:38:04 +02:00
// Business OneDrive Account:
// https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=f1e68e1e-a729-4514-b041-4fdd5c7ac03a&scope=files.readwrite offline_access&response_type=token&redirect_uri=https://joplinapp.org
//
2020-12-16 13:19:35 +02:00
// Also for now OneDrive tests cannot be run in parallel because
// for that each suite would need its own sub-directory within the
// OneDrive app directory, and it's not clear how to get that
// working.
2021-05-13 18:57:37 +02:00
mustRunInBand ( ) ;
2020-12-16 13:19:35 +02:00
2021-05-21 15:17:21 +02:00
const { parameters , setEnvOverride } = require ( '../parameters.js' ) ;
2024-06-25 15:01:39 +02:00
Setting . setConstant ( 'env' , Env . Dev ) ;
2020-08-02 13:28:50 +02:00
setEnvOverride ( 'test' ) ;
const config = parameters ( ) . oneDriveTest ;
const api = new OneDriveApi ( config . id , config . secret , false ) ;
2020-12-16 13:19:35 +02:00
const authData = fs . readFileSync ( await credentialFile ( 'onedrive-auth.txt' ) , 'utf8' ) ;
2020-08-02 13:28:50 +02:00
const urlInfo = require ( 'url-parse' ) ( authData , true ) ;
const auth = require ( 'querystring' ) . parse ( urlInfo . hash . substr ( 1 ) ) ;
api . setAuth ( auth ) ;
2020-12-16 03:00:53 +02:00
const accountProperties = await api . execAccountPropertiesRequest ( ) ;
api . setAccountProperties ( accountProperties ) ;
2020-08-02 13:28:50 +02:00
const appDir = await api . appDirectory ( ) ;
fileApi = new FileApi ( appDir , new FileApiDriverOneDrive ( api ) ) ;
2022-07-23 09:31:32 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'amazon_s3' ) ) {
2023-08-18 10:31:45 +02:00
// (Most of?) the @aws-sdk libraries depend on an old version of uuid
// that doesn't work with jest (without converting ES6 exports to CommonJS).
//
// Require it dynamically so that this doesn't break test environments that
// aren't configured to do this conversion.
const { FileApiDriverAmazonS3 } = require ( '../file-api-driver-amazon-s3.js' ) ;
const { S3Client } = require ( '@aws-sdk/client-s3' ) ;
2021-11-25 01:03:03 +02:00
// We make sure for S3 tests run in band because tests
// share the same directory which will cause locking errors.
mustRunInBand ( ) ;
2021-05-21 15:17:21 +02:00
const amazonS3CredsPath = ` ${ oldTestDir } /support/amazon-s3-auth.json ` ;
2020-07-15 11:22:55 +02:00
const amazonS3Creds = require ( amazonS3CredsPath ) ;
2021-11-25 01:03:03 +02:00
if ( ! amazonS3Creds || ! amazonS3Creds . credentials ) throw new Error ( ` AWS auth JSON missing in ${ amazonS3CredsPath } format should be: { "credentials": { "accessKeyId": "", "secretAccessKey": "", } "bucket": "mybucket", region: "", forcePathStyle: ""} ` ) ;
const api = new S3Client ( { region : amazonS3Creds.region , credentials : amazonS3Creds.credentials , s3UseArnRegion : true , forcePathStyle : amazonS3Creds.forcePathStyle , endpoint : amazonS3Creds.endpoint } ) ;
2020-08-02 13:28:50 +02:00
fileApi = new FileApi ( '' , new FileApiDriverAmazonS3 ( api , amazonS3Creds . bucket ) ) ;
2023-07-16 18:42:42 +02:00
} else if ( syncTargetId_ === SyncTargetRegistry . nameToId ( 'joplinServer' ) || syncTargetId_ === SyncTargetRegistry . nameToId ( 'joplinCloud' ) ) {
2021-05-13 18:57:37 +02:00
mustRunInBand ( ) ;
2021-06-07 15:46:35 +02:00
const joplinServerAuth = JSON . parse ( await readCredentialFile ( 'joplin-server-test-units-2.json' ) ) ;
// const joplinServerAuth = {
// "email": "admin@localhost",
// "password": "admin",
2021-06-10 19:33:04 +02:00
// "baseUrl": "http://api.joplincloud.local:22300",
2021-06-07 15:46:35 +02:00
// "userContentBaseUrl": ""
// }
2020-12-28 13:48:47 +02:00
// Note that to test the API in parallel mode, you need to use Postgres
// as database, as the SQLite database is not reliable when being
// read/write from multiple processes at the same time.
const api = new JoplinServerApi ( {
2021-06-07 15:46:35 +02:00
baseUrl : ( ) = > joplinServerAuth . baseUrl ,
userContentBaseUrl : ( ) = > joplinServerAuth . userContentBaseUrl ,
username : ( ) = > joplinServerAuth . email ,
password : ( ) = > joplinServerAuth . password ,
2020-12-28 13:48:47 +02:00
} ) ;
2021-05-13 18:57:37 +02:00
fileApi = new FileApi ( '' , new FileApiDriverJoplinServer ( api ) ) ;
2017-07-24 20:58:11 +02:00
}
2018-01-25 23:15:58 +02:00
2020-08-02 13:28:50 +02:00
fileApi . setLogger ( logger ) ;
fileApi . setSyncTargetId ( syncTargetId_ ) ;
2020-09-12 00:33:34 +02:00
fileApi . setTempDirName ( Dirnames . Temp ) ;
2020-08-02 13:28:50 +02:00
fileApi . requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0 ;
2020-07-15 11:22:55 +02:00
2020-08-02 13:28:50 +02:00
fileApis_ [ syncTargetId_ ] = fileApi ;
}
function fileApi() {
return fileApis_ [ syncTargetId_ ] ;
2017-06-14 21:59:02 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function objectsEqual ( o1 : any , o2 : any ) {
2017-12-13 20:57:40 +02:00
if ( Object . getOwnPropertyNames ( o1 ) . length !== Object . getOwnPropertyNames ( o2 ) . length ) return false ;
2020-03-14 01:46:14 +02:00
for ( const n in o1 ) {
2017-12-13 20:57:40 +02:00
if ( ! o1 . hasOwnProperty ( n ) ) continue ;
if ( o1 [ n ] !== o2 [ n ] ) return false ;
}
return true ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function checkThrowAsync ( asyncFn : Function ) {
2017-12-13 20:57:40 +02:00
let hasThrown = false ;
try {
await asyncFn ( ) ;
} catch ( error ) {
hasThrown = true ;
}
return hasThrown ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
2024-01-26 12:32:35 +02:00
async function expectThrow ( asyncFn : Function , errorCode : any = undefined , errorMessage : string = undefined ) {
2020-08-02 13:28:50 +02:00
let hasThrown = false ;
let thrownError = null ;
try {
await asyncFn ( ) ;
} catch ( error ) {
hasThrown = true ;
thrownError = error ;
}
if ( ! hasThrown ) {
2020-12-08 22:01:33 +02:00
expect ( 'not throw' ) . toBe ( 'throw' ) ;
2024-01-26 12:32:35 +02:00
} else if ( errorMessage !== undefined ) {
if ( thrownError . message !== errorMessage ) {
expect ( ` error message: ${ thrownError . message } ` ) . toBe ( ` error message: ${ errorMessage } ` ) ;
} else {
expect ( true ) . toBe ( true ) ;
}
2020-08-02 13:28:50 +02:00
} else if ( thrownError . code !== errorCode ) {
console . error ( thrownError ) ;
expect ( ` error code: ${ thrownError . code } ` ) . toBe ( ` error code: ${ errorCode } ` ) ;
} else {
expect ( true ) . toBe ( true ) ;
}
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
async function expectNotThrow ( asyncFn : Function ) {
2020-08-02 13:28:50 +02:00
let thrownError = null ;
try {
await asyncFn ( ) ;
} catch ( error ) {
thrownError = error ;
}
if ( thrownError ) {
2020-11-09 11:21:05 +02:00
console . error ( thrownError ) ;
2020-12-08 22:01:33 +02:00
expect ( thrownError . message ) . toBe ( '' ) ;
2020-08-02 13:28:50 +02:00
} else {
expect ( true ) . toBe ( true ) ;
}
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function checkThrow ( fn : Function ) {
2020-06-13 17:20:18 +02:00
let hasThrown = false ;
try {
fn ( ) ;
} catch ( error ) {
hasThrown = true ;
}
return hasThrown ;
}
2020-12-08 22:01:33 +02:00
function fileContentEqual ( path1 : string , path2 : string ) {
2018-03-09 22:59:12 +02:00
const fs = require ( 'fs-extra' ) ;
const content1 = fs . readFileSync ( path1 , 'base64' ) ;
const content2 = fs . readFileSync ( path2 , 'base64' ) ;
2017-12-18 21:54:03 +02:00
return content1 === content2 ;
}
2019-04-21 14:49:40 +02:00
async function allSyncTargetItemsEncrypted() {
2020-08-02 13:28:50 +02:00
const list = await fileApi ( ) . list ( '' , { includeDirs : false } ) ;
2019-04-21 14:49:40 +02:00
const files = list . items ;
let totalCount = 0 ;
let encryptedCount = 0 ;
for ( let i = 0 ; i < files . length ; i ++ ) {
const file = files [ i ] ;
2020-08-02 13:28:50 +02:00
if ( ! BaseItem . isSystemPath ( file . path ) ) continue ;
2019-04-21 14:49:40 +02:00
const remoteContentString = await fileApi ( ) . get ( file . path ) ;
const remoteContent = await BaseItem . unserialize ( remoteContentString ) ;
const ItemClass = BaseItem . itemClass ( remoteContent ) ;
if ( ! ItemClass . encryptionSupported ( ) ) continue ;
totalCount ++ ;
if ( remoteContent . type_ === BaseModel . TYPE_RESOURCE ) {
2019-09-19 23:51:18 +02:00
const content = await fileApi ( ) . get ( ` .resource/ ${ remoteContent . id } ` ) ;
2019-04-21 14:49:40 +02:00
totalCount ++ ;
2019-07-30 09:35:42 +02:00
if ( content . substr ( 0 , 5 ) === 'JED01' ) encryptedCount ++ ;
2019-04-21 14:49:40 +02:00
}
2019-07-30 09:35:42 +02:00
if ( remoteContent . encryption_applied ) encryptedCount ++ ;
2019-04-21 14:49:40 +02:00
}
if ( ! totalCount ) throw new Error ( 'No encryptable item on sync target' ) ;
return totalCount === encryptedCount ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function id ( a : any ) {
2020-02-27 20:25:42 +02:00
return a . id ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function ids ( a : any [ ] ) {
2020-05-21 10:14:33 +02:00
return a . map ( n = > n . id ) ;
2020-02-27 20:25:42 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function sortedIds ( a : any [ ] ) {
2020-02-27 20:25:42 +02:00
return ids ( a ) . sort ( ) ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
function at ( a : any [ ] , indexes : any [ ] ) {
2020-03-14 01:46:14 +02:00
const out = [ ] ;
2020-02-27 20:25:42 +02:00
for ( let i = 0 ; i < indexes . length ; i ++ ) {
out . push ( a [ indexes [ i ] ] ) ;
}
return out ;
}
2020-12-08 22:01:33 +02:00
async function createNTestFolders ( n : number ) {
2020-03-14 01:46:14 +02:00
const folders = [ ] ;
2020-02-27 20:25:42 +02:00
for ( let i = 0 ; i < n ; i ++ ) {
2020-03-14 01:46:14 +02:00
const folder = await Folder . save ( { title : 'folder' } ) ;
2020-02-27 20:25:42 +02:00
folders . push ( folder ) ;
2020-03-16 04:30:54 +02:00
await time . msleep ( 10 ) ;
2020-02-27 20:25:42 +02:00
}
return folders ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-06-30 10:11:26 +02:00
async function createNTestNotes ( n : number , folder : any , tagIds : string [ ] = null , title = 'note' ) {
2020-03-14 01:46:14 +02:00
const notes = [ ] ;
2020-02-27 20:25:42 +02:00
for ( let i = 0 ; i < n ; i ++ ) {
2020-03-14 01:46:14 +02:00
const title_ = n > 1 ? ` ${ title } ${ i } ` : title ;
2024-03-02 16:25:27 +02:00
const note = await Note . save ( { title : title_ , parent_id : folder.id , is_conflict : 0 , deleted_time : 0 } ) ;
2020-02-27 20:25:42 +02:00
notes . push ( note ) ;
2020-03-16 04:30:54 +02:00
await time . msleep ( 10 ) ;
2020-02-27 20:25:42 +02:00
}
if ( tagIds ) {
for ( let i = 0 ; i < notes . length ; i ++ ) {
await Tag . setNoteTagsByIds ( notes [ i ] . id , tagIds ) ;
2020-03-16 04:30:54 +02:00
await time . msleep ( 10 ) ;
2020-02-27 20:25:42 +02:00
}
}
return notes ;
}
2020-12-08 22:01:33 +02:00
async function createNTestTags ( n : number ) {
2020-03-14 01:46:14 +02:00
const tags = [ ] ;
2020-02-27 20:25:42 +02:00
for ( let i = 0 ; i < n ; i ++ ) {
2020-03-14 01:46:14 +02:00
const tag = await Tag . save ( { title : 'tag' } ) ;
2020-02-27 20:25:42 +02:00
tags . push ( tag ) ;
2020-03-16 04:30:54 +02:00
await time . msleep ( 10 ) ;
2020-02-27 20:25:42 +02:00
}
return tags ;
}
2020-12-08 22:01:33 +02:00
function tempFilePath ( ext : string ) {
2020-05-31 22:07:24 +02:00
return ` ${ Setting . value ( 'tempDir' ) } / ${ md5 ( Date . now ( ) + Math . random ( ) ) } . ${ ext } ` ;
}
2023-07-16 18:42:42 +02:00
const createTempFile = async ( content = '' ) = > {
const path = tempFilePath ( 'txt' ) ;
await fs . writeFile ( path , content , 'utf8' ) ;
return path ;
} ;
2020-10-22 16:55:29 +02:00
async function createTempDir() {
const tempDirPath = ` ${ baseTempDir } / ${ uuid . createNano ( ) } ` ;
await fs . mkdirp ( tempDirPath ) ;
return tempDirPath ;
}
2021-01-09 00:20:59 +02:00
async function waitForFolderCount ( count : number ) {
const timeout = 2000 ;
const startTime = Date . now ( ) ;
while ( true ) {
const folders = await Folder . all ( ) ;
if ( folders . length >= count ) return ;
if ( Date . now ( ) - startTime > timeout ) throw new Error ( 'Timeout waiting for folders to be created' ) ;
await msleep ( 10 ) ;
}
}
2021-06-20 12:19:59 +02:00
let naughtyStrings_ : string [ ] = null ;
export async function naughtyStrings() {
if ( naughtyStrings_ ) return naughtyStrings_ ;
const t = await fs . readFile ( ` ${ supportDir } /big-list-of-naughty-strings.txt ` , 'utf8' ) ;
const lines = t . split ( '\n' ) ;
naughtyStrings_ = [ ] ;
for ( const line of lines ) {
const trimmed = line . trim ( ) ;
if ( ! trimmed ) continue ;
if ( trimmed . indexOf ( '#' ) === 0 ) continue ;
naughtyStrings_ . push ( line ) ;
}
return naughtyStrings_ ;
}
2020-11-19 14:44:20 +02:00
// TODO: Update for Jest
2020-10-09 22:51:11 +02:00
2020-11-19 14:44:20 +02:00
// function mockDate(year, month, day, tick) {
// const fixedDate = new Date(2020, 0, 1);
// jasmine.clock().install();
// jasmine.clock().mockDate(fixedDate);
// }
// function restoreDate() {
// jasmine.clock().uninstall();
// }
2020-10-09 22:51:11 +02:00
2020-03-16 04:30:54 +02:00
// Application for feature integration testing
2020-02-22 13:25:16 +02:00
class TestApp extends BaseApplication {
2020-12-08 22:01:33 +02:00
private hasGui_ : boolean ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-08 22:01:33 +02:00
private middlewareCalls_ : any [ ] ;
private logger_ : LoggerWrapper ;
2020-12-28 13:48:47 +02:00
public constructor ( hasGui = true ) {
2021-08-06 19:37:33 +02:00
KeychainService . instance ( ) . enabled = false ;
2020-02-22 13:25:16 +02:00
super ( ) ;
2020-03-16 04:30:54 +02:00
this . hasGui_ = hasGui ;
2020-02-22 13:25:16 +02:00
this . middlewareCalls_ = [ ] ;
2020-03-16 04:30:54 +02:00
this . logger_ = super . logger ( ) ;
}
2020-12-28 13:48:47 +02:00
public hasGui() {
2020-03-16 04:30:54 +02:00
return this . hasGui_ ;
2020-02-22 13:25:16 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-28 13:48:47 +02:00
public async start ( argv : any [ ] ) {
2020-03-16 04:30:54 +02:00
this . logger_ . info ( 'Test app starting...' ) ;
2020-02-27 20:25:42 +02:00
2020-03-16 04:30:54 +02:00
if ( ! argv . includes ( '--profile' ) ) {
argv = argv . concat ( [ '--profile' , ` tests-build/profile/ ${ uuid . create ( ) } ` ] ) ;
}
2023-08-04 11:57:54 +02:00
argv = await super . start ( [ '' , '' ] . concat ( argv ) , { setupGlobalLogger : false } ) ;
2020-03-16 04:30:54 +02:00
// For now, disable sync and encryption to avoid spurious intermittent failures
// caused by them interupting processing and causing delays.
Setting . setValue ( 'sync.interval' , 0 ) ;
2021-08-12 17:54:10 +02:00
setEncryptionEnabled ( true ) ;
2020-03-16 04:30:54 +02:00
2020-02-22 13:25:16 +02:00
this . initRedux ( ) ;
Setting . dispatchUpdateAll ( ) ;
2020-03-16 04:30:54 +02:00
await ItemChange . waitForAllSaved ( ) ;
await this . wait ( ) ;
this . logger_ . info ( 'Test app started...' ) ;
2020-02-22 13:25:16 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-28 13:48:47 +02:00
public async generalMiddleware ( store : any , next : any , action : any ) {
2020-02-22 13:25:16 +02:00
this . middlewareCalls_ . push ( true ) ;
try {
await super . generalMiddleware ( store , next , action ) ;
} finally {
this . middlewareCalls_ . pop ( ) ;
}
}
2020-12-28 13:48:47 +02:00
public async wait() {
2020-02-22 13:25:16 +02:00
return new Promise ( ( resolve ) = > {
2020-10-09 19:35:46 +02:00
const iid = shim . setInterval ( ( ) = > {
2020-02-22 13:25:16 +02:00
if ( ! this . middlewareCalls_ . length ) {
clearInterval ( iid ) ;
2020-12-16 03:00:53 +02:00
resolve ( null ) ;
2020-02-22 13:25:16 +02:00
}
} , 100 ) ;
} ) ;
}
2020-12-28 13:48:47 +02:00
public async profileDir() {
2020-11-20 01:46:04 +02:00
return Setting . value ( 'profileDir' ) ;
2020-03-16 04:30:54 +02:00
}
2020-12-28 13:48:47 +02:00
public async destroy() {
2020-03-16 04:30:54 +02:00
this . logger_ . info ( 'Test app stopping...' ) ;
await this . wait ( ) ;
await ItemChange . waitForAllSaved ( ) ;
2020-02-27 20:25:42 +02:00
this . deinitRedux ( ) ;
2020-02-22 13:25:16 +02:00
await super . destroy ( ) ;
2020-03-16 04:30:54 +02:00
await time . msleep ( 100 ) ;
2020-02-22 13:25:16 +02:00
}
}
2023-07-16 18:42:42 +02:00
const createTestShareData = ( shareId : string ) : ShareState = > {
return {
processingShareInvitationResponse : false ,
shares : [ ] ,
shareInvitations : [
{
id : '' ,
master_key : { } ,
status : 0 ,
share : {
id : shareId ,
folder_id : '' ,
master_key_id : '' ,
note_id : '' ,
type : 1 ,
} ,
can_read : 1 ,
can_write : 0 ,
} ,
] ,
shareUsers : { } ,
} ;
} ;
2024-07-26 13:35:50 +02:00
const simulateReadOnlyShareEnv = ( shareId : string , store? : Store ) = > {
2023-07-16 18:42:42 +02:00
Setting . setValue ( 'sync.target' , 10 ) ;
Setting . setValue ( 'sync.userId' , 'abcd' ) ;
2024-07-26 13:35:50 +02:00
const shareData = createTestShareData ( shareId ) ;
BaseItem . syncShareCache = shareData ;
if ( store ) {
store . dispatch ( {
type : 'SHARE_SET' ,
shares : shareData.shares ,
} ) ;
store . dispatch ( {
type : 'SHARE_INVITATION_SET' ,
shareInvitations : shareData.shareInvitations ,
} ) ;
store . dispatch ( {
type : 'SHARE_USER_SET' ,
shareUsers : shareData.shareUsers ,
} ) ;
}
2023-07-16 18:42:42 +02:00
return ( ) = > {
BaseItem . syncShareCache = null ;
Setting . setValue ( 'sync.userId' , '' ) ;
} ;
} ;
2024-01-05 16:43:24 +02:00
export const newOcrService = ( ) = > {
2024-08-09 12:29:39 +02:00
const driver = new OcrDriverTesseract ( { createWorker } , { workerPath : null , corePath : null , languageDataPath : null } ) ;
2024-01-05 16:43:24 +02:00
return new OcrService ( driver ) ;
} ;
2024-03-29 14:40:54 +02:00
export const mockMobilePlatform = ( platform : string ) = > {
2024-04-03 19:51:09 +02:00
const originalMobilePlatform = shim . mobilePlatform ;
const originalIsNode = shim . isNode ;
2024-03-29 14:40:54 +02:00
shim . mobilePlatform = ( ) = > platform ;
shim . isNode = ( ) = > false ;
2024-04-03 19:51:09 +02:00
return {
reset : ( ) = > {
shim . mobilePlatform = originalMobilePlatform ;
shim . isNode = originalIsNode ;
} ,
} ;
2024-03-29 14:40:54 +02:00
} ;
2024-09-27 16:23:02 +02:00
// Waits for callback to not throw. Similar to react-native-testing-library's waitFor, but works better
// with Joplin's mix of real and fake Jest timers.
const realSetTimeout = setTimeout ;
export const waitFor = async ( callback : ( ) = > Promise < void > ) = > {
const timeout = 10 _000 ;
const startTime = performance . now ( ) ;
let passed = false ;
let lastError : Error | null = null ;
while ( ! passed && performance . now ( ) - startTime < timeout ) {
try {
await callback ( ) ;
passed = true ;
lastError = null ;
} catch ( error ) {
lastError = error ;
await new Promise < void > ( resolve = > {
realSetTimeout ( ( ) = > resolve ( ) , 10 ) ;
} ) ;
}
}
if ( lastError ) {
throw lastError ;
}
} ;
2024-09-24 16:12:02 +02:00
export const runWithFakeTimers = async ( callback : ( ) = > Promise < void > ) = > {
2024-07-26 13:35:50 +02:00
if ( typeof jest === 'undefined' ) {
throw new Error ( 'Fake timers are only supported in jest.' ) ;
}
2024-09-28 17:20:46 +02:00
// advanceTimers: Needed by Joplin's database driver
jest . useFakeTimers ( { advanceTimers : true } ) ;
2024-09-24 16:12:02 +02:00
// The shim.setTimeout and similar functions need to be changed to
// use fake timers.
const originalSetTimeout = shim . setTimeout ;
const originalSetInterval = shim . setInterval ;
const originalClearTimeout = shim . clearTimeout ;
const originalClearInterval = shim . clearInterval ;
shim . setTimeout = setTimeout ;
shim . setInterval = setInterval ;
shim . clearInterval = clearInterval ;
shim . clearTimeout = clearTimeout ;
2024-07-26 13:35:50 +02:00
try {
2024-09-24 16:12:02 +02:00
return await callback ( ) ;
2024-07-26 13:35:50 +02:00
} finally {
jest . runOnlyPendingTimers ( ) ;
2024-09-24 16:12:02 +02:00
shim . setTimeout = originalSetTimeout ;
shim . setInterval = originalSetInterval ;
shim . clearTimeout = originalClearTimeout ;
shim . clearInterval = originalClearInterval ;
2024-07-26 13:35:50 +02:00
jest . useRealTimers ( ) ;
}
} ;
2023-12-13 21:24:58 +02:00
export { supportDir , createNoteAndResource , createTempFile , createTestShareData , simulateReadOnlyShareEnv , waitForFolderCount , afterAllCleanUp , exportDir , synchronizerStart , afterEachCleanUp , syncTargetName , setSyncTargetName , syncDir , createTempDir , isNetworkSyncTarget , kvStore , expectThrow , logger , expectNotThrow , resourceService , resourceFetcher , tempFilePath , allSyncTargetItemsEncrypted , msleep , setupDatabase , revisionService , setupDatabaseAndSynchronizer , db , synchronizer , fileApi , sleep , clearDatabase , switchClient , syncTargetId , objectsEqual , checkThrowAsync , checkThrow , encryptionService , loadEncryptionMasterKey , fileContentEqual , decryptionWorker , currentClientId , id , ids , sortedIds , at , createNTestNotes , createNTestFolders , createNTestTags , TestApp } ;