Skip to content

Commit

Permalink
feat: Starting a site updates an outdated sqlite-integration-plugin
Browse files Browse the repository at this point in the history
Ensure existing sites receive the latest fixes and features.
  • Loading branch information
dcalhoun committed May 17, 2024
1 parent 3b20384 commit 847e40a
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 8 deletions.
10 changes: 10 additions & 0 deletions src/__mocks__/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ fs.__setFileContents = ( path: string, fileContents: string | string[] ) => {
}
);

( fs.readFileSync as jest.Mock ).mockImplementation( ( path: string ): string => {
const fileContents = mockFiles[ path ];

if ( typeof fileContents === 'string' ) {
return fileContents;
}

return '';
} );

( fs.promises.readdir as jest.Mock ).mockImplementation(
async ( path: string ): Promise< Array< string > > => {
const dirContents = mockFiles[ path ];
Expand Down
11 changes: 10 additions & 1 deletion src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createPassword } from './lib/passwords';
import { phpGetThemeDetails } from './lib/php-get-theme-details';
import { sanitizeForLogging } from './lib/sanitize-for-logging';
import { sortSites } from './lib/sort-sites';
import { isSqliteInstallationOutdated } from './lib/sqlite-versions';
import { writeLogToFile, type LogLevel } from './logging';
import { SiteServer, createSiteWorkingDirectory } from './site-server';
import { DEFAULT_SITE_PATH, getServerFilesPath, getSiteThumbnailPath } from './storage/paths';
Expand Down Expand Up @@ -105,7 +106,7 @@ async function setupSqliteIntegration( path: string ) {
)
);
const sqlitePluginPath = nodePath.join( wpContentPath, 'mu-plugins', SQLITE_FILENAME );
await copySync( nodePath.join( getServerFilesPath(), SQLITE_FILENAME ), sqlitePluginPath );
copySync( nodePath.join( getServerFilesPath(), SQLITE_FILENAME ), sqlitePluginPath );
}

export async function createSite(
Expand Down Expand Up @@ -216,6 +217,14 @@ export async function startServer(
return null;
}

if (
await isSqliteInstallationOutdated(
`${ server.details.path }/wp-content/mu-plugins/${ SQLITE_FILENAME }`
)
) {
await setupSqliteIntegration( server.details.path );
}

const parentWindow = BrowserWindow.fromWebContents( event.sender );
await server.start();
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
Expand Down
15 changes: 13 additions & 2 deletions src/lib/sqlite-versions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'path';
import * as Sentry from '@sentry/electron/main';
import fs from 'fs-extra';
import semver from 'semver';
import { downloadSqliteIntegrationPlugin } from '../../vendor/wp-now/src/download';
Expand All @@ -12,13 +13,23 @@ export async function updateLatestSqliteVersion() {
: [];
const latestVersion = await getLatestSqliteVersion();
if ( installedFiles.length !== 0 ) {
const installedVersion = getSqliteVersionFromInstallation( installedPath );
shouldOverwrite = !! installedVersion && !! latestVersion && installedVersion !== latestVersion;
shouldOverwrite = await isSqliteInstallationOutdated( installedPath );
}

await downloadSqliteIntegrationPlugin( latestVersion, { overwrite: shouldOverwrite } );
}

export async function isSqliteInstallationOutdated( installationPath: string ): Promise< boolean > {
try {
const installedVersion = getSqliteVersionFromInstallation( installationPath );
const latestVersion = await getLatestSqliteVersion();
return semver.lt( installedVersion, latestVersion );
} catch ( error ) {
Sentry.captureException( error );
return true;
}
}

function getSqliteVersionFromInstallation( installationPath: string ): string {
let versionFileContent = '';
try {
Expand Down
62 changes: 57 additions & 5 deletions src/tests/ipc-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
*/
import { shell, IpcMainInvokeEvent } from 'electron';
import fs from 'fs';
import { createSite } from '../ipc-handlers';
import { copySync } from 'fs-extra';
import { SQLITE_FILENAME } from '../../vendor/wp-now/src/constants';
import { downloadSqliteIntegrationPlugin } from '../../vendor/wp-now/src/download';
import { createSite, startServer } from '../ipc-handlers';
import { isEmptyDir, pathExists } from '../lib/fs-utils';
import { isSqliteInstallationOutdated } from '../lib/sqlite-versions';
import { SiteServer, createSiteWorkingDirectory } from '../site-server';

jest.mock( 'fs' );
jest.mock( 'fs-extra' );
jest.mock( '../lib/fs-utils' );
jest.mock( '../site-server' );
jest.mock( '../lib/sqlite-versions' );
jest.mock( '../../vendor/wp-now/src/download' );

( SiteServer.create as jest.Mock ).mockImplementation( ( details ) => ( {
start: jest.fn(),
Expand All @@ -36,10 +43,15 @@ const mockIpcMainInvokeEvent = {
// Double assert the type with `unknown` to simplify mocking this value
} as unknown as IpcMainInvokeEvent;

afterEach( () => {
jest.clearAllMocks();
} );

describe( 'createSite', () => {
it( 'should create a site', async () => {
( isEmptyDir as jest.Mock ).mockResolvedValue( true );
( pathExists as jest.Mock ).mockResolvedValue( true );
( isEmptyDir as jest.Mock ).mockResolvedValueOnce( true );
( pathExists as jest.Mock ).mockResolvedValueOnce( true );

const [ site ] = await createSite( mockIpcMainInvokeEvent, '/test', 'Test' );

expect( site ).toEqual( {
Expand All @@ -53,8 +65,8 @@ describe( 'createSite', () => {

describe( 'when the site path started as an empty directory', () => {
it( 'should reset the directory when site creation fails', () => {
( isEmptyDir as jest.Mock ).mockResolvedValue( true );
( pathExists as jest.Mock ).mockResolvedValue( true );
( isEmptyDir as jest.Mock ).mockResolvedValueOnce( true );
( pathExists as jest.Mock ).mockResolvedValueOnce( true );
( createSiteWorkingDirectory as jest.Mock ).mockImplementation( () => {
throw new Error( 'Intentional test error' );
} );
Expand All @@ -66,3 +78,43 @@ describe( 'createSite', () => {
} );
} );
} );

describe( 'startServer', () => {
describe( 'when sqlite-database-integration plugin is outdated', () => {
it( 'should update sqlite-database-integration plugin', async () => {
const mockSitePath = 'mock-site-path';
( isSqliteInstallationOutdated as jest.Mock ).mockResolvedValue( true );
( SiteServer.get as jest.Mock ).mockReturnValue( {
details: { path: mockSitePath },
start: jest.fn(),
updateSiteDetails: jest.fn(),
updateCachedThumbnail: jest.fn( () => Promise.resolve() ),
} );

await startServer( mockIpcMainInvokeEvent, 'mock-site-id' );

expect( downloadSqliteIntegrationPlugin ).toHaveBeenCalledTimes( 1 );
expect( copySync ).toHaveBeenCalledWith(
`/path/to/app/appData/App Name/server-files/sqlite-database-integration-main`,
`${ mockSitePath }/wp-content/mu-plugins/${ SQLITE_FILENAME }`
);
} );
} );

describe( 'when sqlite-database-integration plugin is up-to-date', () => {
it( 'should not update sqlite-database-integration plugin', async () => {
( isSqliteInstallationOutdated as jest.Mock ).mockResolvedValue( false );
( SiteServer.get as jest.Mock ).mockReturnValue( {
details: { path: 'mock-site-path' },
start: jest.fn(),
updateSiteDetails: jest.fn(),
updateCachedThumbnail: jest.fn( () => Promise.resolve() ),
} );

await startServer( mockIpcMainInvokeEvent, 'mock-site-id' );

expect( downloadSqliteIntegrationPlugin ).not.toHaveBeenCalled();
expect( copySync ).not.toHaveBeenCalled();
} );
} );
} );

0 comments on commit 847e40a

Please sign in to comment.