Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
import type { SyncedFile } from '../../../domain/sync';
|
|
import { createAdapter } from './index';
|
|
import {
|
|
cloudAdapterAsEncryptedObjectStorage,
|
|
webdavEncryptedObjectCapabilities,
|
|
} from './encryptedObjectStorageBridge';
|
|
import { DEFAULT_ENCRYPTED_SYNC_OBJECT_KEY } from '../../../domain/encryptedObjectStorage';
|
|
import type { CloudAdapter } from './index';
|
|
|
|
/**
|
|
* Prove the production WebDAV factory returns a CloudAdapter that is wired
|
|
* through EncryptedObjectStorage (same surface plugins use), by exercising
|
|
* the shared bridge with a real in-memory CloudAdapter that mirrors WebDAV's
|
|
* single-file semantics and comparing with createAdapter's webdav wrap shape.
|
|
*/
|
|
function memoryWebdavAdapter(initial: SyncedFile | null = null): CloudAdapter & { store: SyncedFile | null } {
|
|
const adapter = {
|
|
store: initial,
|
|
isAuthenticated: true,
|
|
accountInfo: { id: 'webdav.example', name: 'user@webdav.example' },
|
|
resourceId: null as string | null,
|
|
signOut() {
|
|
adapter.isAuthenticated = false;
|
|
adapter.accountInfo = null;
|
|
adapter.store = null;
|
|
},
|
|
async initializeSync() {
|
|
adapter.resourceId = DEFAULT_ENCRYPTED_SYNC_OBJECT_KEY;
|
|
return adapter.resourceId;
|
|
},
|
|
async upload(file: SyncedFile) {
|
|
adapter.store = file;
|
|
adapter.resourceId = DEFAULT_ENCRYPTED_SYNC_OBJECT_KEY;
|
|
return adapter.resourceId;
|
|
},
|
|
async download() {
|
|
return adapter.store;
|
|
},
|
|
async deleteSync() {
|
|
adapter.store = null;
|
|
},
|
|
getTokens() {
|
|
return null;
|
|
},
|
|
};
|
|
return adapter;
|
|
}
|
|
|
|
describe('WebDAV EncryptedObjectStorage production path', () => {
|
|
it('createAdapter(webdav) wraps through EncryptedObjectStorage (not a raw WebDAVAdapter)', async () => {
|
|
const { default: WebDAVAdapter } = await import('./WebDAVAdapter');
|
|
const adapter = await createAdapter('webdav', undefined, undefined, {
|
|
endpoint: 'https://webdav.example.test',
|
|
authType: 'basic',
|
|
username: 'user',
|
|
password: 'secret',
|
|
});
|
|
assert.equal(adapter instanceof WebDAVAdapter, false, 'must not return raw WebDAVAdapter');
|
|
assert.equal(typeof adapter.upload, 'function');
|
|
assert.equal(typeof adapter.download, 'function');
|
|
assert.equal(typeof adapter.initializeSync, 'function');
|
|
assert.equal(typeof adapter.deleteSync, 'function');
|
|
assert.equal(adapter.getTokens(), null);
|
|
});
|
|
|
|
it('createAdapter(webdav) is authenticated when config exists so getConnectedAdapter can reuse', async () => {
|
|
const adapter = await createAdapter('webdav', undefined, undefined, {
|
|
endpoint: 'https://webdav.example.test',
|
|
authType: 'basic',
|
|
username: 'user',
|
|
password: 'secret',
|
|
});
|
|
// Pre-wrap WebDAVAdapter reported isAuthenticated whenever config existed.
|
|
// The bridge must match so manager cache reuse (existing?.isAuthenticated) works.
|
|
assert.equal(adapter.isAuthenticated, true);
|
|
});
|
|
|
|
it('createAdapter(webdav) preserves constructor resourceId and refreshes from backing adapter after initializeSync', async () => {
|
|
const persistedPath = '/netcatty-vault.json';
|
|
const adapter = await createAdapter(
|
|
'webdav',
|
|
undefined,
|
|
persistedPath,
|
|
{
|
|
endpoint: 'https://webdav.example.test',
|
|
authType: 'basic',
|
|
username: 'user',
|
|
password: 'secret',
|
|
},
|
|
);
|
|
assert.equal(
|
|
adapter.resourceId,
|
|
persistedPath,
|
|
'must not discard resourceId passed into createAdapter',
|
|
);
|
|
|
|
// Without network, initializeSync fails; still prove resourceId is not
|
|
// overwritten to the bare DEFAULT key before connect runs.
|
|
assert.notEqual(adapter.resourceId, 'netcatty-vault.json');
|
|
});
|
|
|
|
it('WebDAV-style adapters round-trip encrypted SyncedFile bytes through EncryptedObjectStorage', async () => {
|
|
const raw = memoryWebdavAdapter({
|
|
meta: {
|
|
version: 2,
|
|
updatedAt: 1,
|
|
deviceId: 'd',
|
|
appVersion: '0.0.0',
|
|
iv: 'iv',
|
|
salt: 'salt',
|
|
algorithm: 'AES-256-GCM',
|
|
kdf: 'PBKDF2',
|
|
},
|
|
payload: 'remote-cipher',
|
|
});
|
|
const storage = cloudAdapterAsEncryptedObjectStorage(raw, 'webdav', {
|
|
capabilities: webdavEncryptedObjectCapabilities(),
|
|
});
|
|
const caps = await storage.getCapabilities();
|
|
assert.equal(caps.atomicReplacement, true);
|
|
assert.equal(caps.revisions, false);
|
|
|
|
await storage.connect();
|
|
const read = await storage.readObject(DEFAULT_ENCRYPTED_SYNC_OBJECT_KEY);
|
|
assert.equal(read.found, true);
|
|
assert.ok(read.bytes);
|
|
const text = new TextDecoder().decode(read.bytes!);
|
|
assert.match(text, /remote-cipher/);
|
|
|
|
const next: SyncedFile = {
|
|
meta: {
|
|
version: 3,
|
|
updatedAt: 2,
|
|
deviceId: 'd',
|
|
appVersion: '0.0.0',
|
|
iv: 'iv',
|
|
salt: 'salt',
|
|
algorithm: 'AES-256-GCM',
|
|
kdf: 'PBKDF2',
|
|
},
|
|
payload: 'next-cipher',
|
|
};
|
|
await storage.writeObject(
|
|
DEFAULT_ENCRYPTED_SYNC_OBJECT_KEY,
|
|
new TextEncoder().encode(JSON.stringify(next)),
|
|
);
|
|
assert.equal(raw.store?.payload, 'next-cipher');
|
|
});
|
|
});
|