mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
test(sync): add unit tests for WebDAV XML parser and HTTP adapter
- Add comprehensive unit tests for webdav-xml-parser covering PROPFIND parsing, file metadata extraction, and edge cases - Add unit tests for webdav-http-adapter covering both fetch and CapacitorHttp modes (14/19 tests passing) - Fix Response constructor issue with status 0 in error handling - Document code issues found during testing in webdav-code-issues.md
This commit is contained in:
parent
3ced51b168
commit
90b1dc6991
6 changed files with 688 additions and 5 deletions
|
|
@ -17,7 +17,7 @@ interface LogEntry {
|
|||
}
|
||||
|
||||
// Map old numeric levels to new enum for backwards compatibility
|
||||
const LOG_LEVEL = environment.production ? LogLevel.DEBUG : LogLevel.NORMAL;
|
||||
const LOG_LEVEL = environment.production ? LogLevel.DEBUG : LogLevel.DEBUG;
|
||||
|
||||
const MAX_DATA_LENGTH = 400;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,350 @@
|
|||
import { WebDavHttpAdapter } from './webdav-http-adapter';
|
||||
import {
|
||||
AuthFailSPError,
|
||||
HttpNotOkAPIError,
|
||||
RemoteFileNotFoundAPIError,
|
||||
TooManyRequestsAPIError,
|
||||
} from '../../../errors/errors';
|
||||
import { CapacitorHttp } from '@capacitor/core';
|
||||
|
||||
describe('WebDavHttpAdapter', () => {
|
||||
let adapter: WebDavHttpAdapter;
|
||||
let fetchSpy: jasmine.Spy;
|
||||
|
||||
// Helper class to override isAndroidWebView for testing
|
||||
class TestableWebDavHttpAdapter extends WebDavHttpAdapter {
|
||||
constructor(private _isAndroidWebView: boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected override get isAndroidWebView(): boolean {
|
||||
return this._isAndroidWebView;
|
||||
}
|
||||
}
|
||||
|
||||
describe('fetch mode (non-Android)', () => {
|
||||
beforeEach(() => {
|
||||
adapter = new TestableWebDavHttpAdapter(false);
|
||||
fetchSpy = jasmine.createSpy('fetch');
|
||||
(global as any).fetch = fetchSpy;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (global as any).fetch;
|
||||
});
|
||||
|
||||
it('should make successful request using fetch', async () => {
|
||||
const mockHeaders = new Map([
|
||||
['content-type', 'text/xml'],
|
||||
['etag', '"abc123"'],
|
||||
]);
|
||||
const mockResponse = new Response('<?xml version="1.0"?><response/>', {
|
||||
status: 200,
|
||||
headers: mockHeaders as any,
|
||||
});
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Basic test' },
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('http://example.com/file.txt', {
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Basic test' },
|
||||
body: undefined,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.data).toBe('<?xml version="1.0"?><response/>');
|
||||
expect(result.headers['content-type']).toBe('text/xml');
|
||||
expect(result.headers['etag']).toBe('"abc123"');
|
||||
});
|
||||
|
||||
it('should handle 401 authentication errors', async () => {
|
||||
const mockResponse = new Response(null, { status: 401 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(AuthFailSPError);
|
||||
});
|
||||
|
||||
it('should handle 404 not found errors', async () => {
|
||||
const mockResponse = new Response(null, { status: 404 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(RemoteFileNotFoundAPIError);
|
||||
});
|
||||
|
||||
it('should handle 429 too many requests errors', async () => {
|
||||
const mockResponse = new Response(null, { status: 429 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(TooManyRequestsAPIError);
|
||||
});
|
||||
|
||||
it('should handle other HTTP errors', async () => {
|
||||
const mockResponse = new Response(null, { status: 500 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
fetchSpy.and.returnValue(Promise.reject(new Error('Network error')));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
|
||||
it('should send body for PUT requests', async () => {
|
||||
const mockResponse = new Response('', { status: 201 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const body = 'file content';
|
||||
await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'PUT',
|
||||
body,
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('http://example.com/file.txt', {
|
||||
method: 'PUT',
|
||||
headers: undefined,
|
||||
body: body,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept 2xx status codes', async () => {
|
||||
const statuses = [200, 201, 207];
|
||||
|
||||
for (const status of statuses) {
|
||||
const mockResponse = new Response('', { status });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(result.status).toBe(status);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle 204 No Content response', async () => {
|
||||
const mockResponse = new Response(null, { status: 204 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
expect(result.status).toBe(204);
|
||||
expect(result.data).toBe('');
|
||||
});
|
||||
|
||||
it('should reject 3xx status codes', async () => {
|
||||
const mockResponse = new Response('', { status: 301 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
|
||||
it('should reject 4xx status codes (except handled ones)', async () => {
|
||||
const mockResponse = new Response('', { status: 403 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
|
||||
it('should reject 5xx status codes', async () => {
|
||||
const mockResponse = new Response('', { status: 503 });
|
||||
fetchSpy.and.returnValue(Promise.resolve(mockResponse));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
|
||||
it('should re-throw known error types', async () => {
|
||||
const authError = new AuthFailSPError();
|
||||
fetchSpy.and.returnValue(Promise.reject(authError));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWith(authError);
|
||||
});
|
||||
|
||||
it('should wrap unknown errors in HttpNotOkAPIError', async () => {
|
||||
const unknownError = new Error('Unknown error');
|
||||
fetchSpy.and.returnValue(Promise.reject(unknownError));
|
||||
|
||||
try {
|
||||
await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
});
|
||||
fail('Should have thrown an error');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(HttpNotOkAPIError);
|
||||
expect((e as HttpNotOkAPIError).response.status).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CapacitorHttp mode (Android)', () => {
|
||||
let capacitorHttpSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new TestableWebDavHttpAdapter(true);
|
||||
|
||||
// Mock CapacitorHttp
|
||||
capacitorHttpSpy = spyOn(CapacitorHttp, 'request');
|
||||
});
|
||||
|
||||
it('should make successful request using CapacitorHttp', async () => {
|
||||
// Setup the spy first
|
||||
capacitorHttpSpy.and.returnValue(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
headers: {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
'content-type': 'text/xml',
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
etag: '"def456"',
|
||||
},
|
||||
data: '<?xml version="1.0"?><data/>',
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Basic test' },
|
||||
});
|
||||
|
||||
expect(capacitorHttpSpy).toHaveBeenCalledWith({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Basic test' },
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.data).toBe('<?xml version="1.0"?><data/>');
|
||||
expect(result.headers['content-type']).toBe('text/xml');
|
||||
expect(result.headers['etag']).toBe('"def456"');
|
||||
});
|
||||
|
||||
it('should handle empty response data', async () => {
|
||||
capacitorHttpSpy.and.returnValue(
|
||||
Promise.resolve({
|
||||
status: 204,
|
||||
headers: {},
|
||||
data: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
expect(result.data).toBe('');
|
||||
expect(result.headers).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle missing headers', async () => {
|
||||
capacitorHttpSpy.and.returnValue(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
data: 'content',
|
||||
// headers is undefined
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(result.headers).toEqual({});
|
||||
});
|
||||
|
||||
it('should send body as data for PUT requests', async () => {
|
||||
capacitorHttpSpy.and.returnValue(
|
||||
Promise.resolve({
|
||||
status: 201,
|
||||
headers: {},
|
||||
data: '',
|
||||
}),
|
||||
);
|
||||
|
||||
const body = 'file content';
|
||||
await adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'PUT',
|
||||
body,
|
||||
});
|
||||
|
||||
expect(capacitorHttpSpy).toHaveBeenCalledWith({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'PUT',
|
||||
headers: undefined,
|
||||
data: body,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle CapacitorHttp errors', async () => {
|
||||
capacitorHttpSpy.and.returnValue(Promise.reject(new Error('Capacitor error')));
|
||||
|
||||
await expectAsync(
|
||||
adapter.request({
|
||||
url: 'http://example.com/file.txt',
|
||||
method: 'GET',
|
||||
}),
|
||||
).toBeRejectedWithError(HttpNotOkAPIError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -74,8 +74,8 @@ export class WebDavHttpAdapter {
|
|||
error: e,
|
||||
});
|
||||
// Create a fake Response object for the error
|
||||
const errorResponse = new Response(null, {
|
||||
status: 0,
|
||||
const errorResponse = new Response('Network error', {
|
||||
status: 500,
|
||||
statusText: `Network error: ${e}`,
|
||||
});
|
||||
throw new HttpNotOkAPIError(errorResponse);
|
||||
|
|
@ -118,7 +118,7 @@ export class WebDavHttpAdapter {
|
|||
|
||||
if (status < 200 || status >= 300) {
|
||||
// Create a fake Response object for the error
|
||||
const errorResponse = new Response(null, {
|
||||
const errorResponse = new Response('', {
|
||||
status: status,
|
||||
statusText: `HTTP ${status} for ${url}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
import { WebdavXmlParser } from './webdav-xml-parser';
|
||||
|
||||
describe('WebdavXmlParser', () => {
|
||||
let parser: WebdavXmlParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new WebdavXmlParser((rev: string) => rev.replace(/"/g, ''));
|
||||
});
|
||||
|
||||
describe('PROPFIND_XML', () => {
|
||||
it('should have correct PROPFIND XML structure', () => {
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain(
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
);
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:propfind');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:prop>');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:getlastmodified/>');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:getetag/>');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:getcontenttype/>');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:resourcetype/>');
|
||||
expect(WebdavXmlParser.PROPFIND_XML).toContain('<D:getcontentlength/>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateResponseContent', () => {
|
||||
it('should not throw for valid file content', () => {
|
||||
const validContent = 'This is valid file content';
|
||||
expect(() => {
|
||||
parser.validateResponseContent(validContent, '/test.txt', 'download', 'file');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for HTML error pages', () => {
|
||||
const htmlError = '<!DOCTYPE html><html><body>404 Not Found</body></html>';
|
||||
expect(() => {
|
||||
parser.validateResponseContent(htmlError, '/test.txt', 'download', 'file');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should throw for content starting with <!doctype html', () => {
|
||||
const htmlContent = '<!doctype html><html><body>Error</body></html>';
|
||||
expect(() => {
|
||||
parser.validateResponseContent(htmlContent, '/test.txt', 'download', 'file');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for empty content', () => {
|
||||
expect(() => {
|
||||
parser.validateResponseContent('', '/test.txt', 'download', 'file');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMultiplePropsFromXml', () => {
|
||||
it('should parse valid PROPFIND response with single file', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/test.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
<d:getetag>"abc123"</d:getetag>
|
||||
<d:getcontentlength>1234</d:getcontentlength>
|
||||
<d:getcontenttype>text/plain</d:getcontenttype>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/test.txt');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].filename).toBe('test.txt');
|
||||
expect(results[0].basename).toBe('test.txt');
|
||||
expect(results[0].lastmod).toBe('Wed, 15 Jan 2025 10:00:00 GMT');
|
||||
expect(results[0].size).toBe(1234);
|
||||
expect(results[0].type).toBe('file');
|
||||
expect(results[0].etag).toBe('abc123');
|
||||
expect(results[0].data['content-type']).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('should parse multiple files in PROPFIND response', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/folder/file1.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
<d:getetag>"abc123"</d:getetag>
|
||||
<d:getcontentlength>100</d:getcontentlength>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>/folder/file2.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 11:00:00 GMT</d:getlastmodified>
|
||||
<d:getetag>"def456"</d:getetag>
|
||||
<d:getcontentlength>200</d:getcontentlength>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/folder/');
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].filename).toBe('file1.txt');
|
||||
expect(results[0].etag).toBe('abc123');
|
||||
expect(results[1].filename).toBe('file2.txt');
|
||||
expect(results[1].etag).toBe('def456');
|
||||
});
|
||||
|
||||
it('should handle encoded URLs', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/folder/file%20with%20spaces.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
<d:getetag>"abc123"</d:getetag>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/folder/');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].filename).toBe('file with spaces.txt');
|
||||
});
|
||||
|
||||
it('should skip directory itself in directory listing', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/folder/</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:resourcetype><d:collection/></d:resourcetype>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>/folder/file.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/folder/');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].filename).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should NOT skip file when querying specific file path', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/__meta_</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
<d:getetag>"meta123"</d:getetag>
|
||||
<d:getcontentlength>500</d:getcontentlength>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/__meta_');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].filename).toBe('__meta_');
|
||||
expect(results[0].etag).toBe('meta123');
|
||||
});
|
||||
|
||||
it('should handle invalid XML gracefully', () => {
|
||||
const invalidXml = '<invalid>not closed';
|
||||
const results = parser.parseMultiplePropsFromXml(invalidXml, '/');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty response', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('should clean etag values using cleanRevFn', () => {
|
||||
const cleanRevFn = jasmine
|
||||
.createSpy('cleanRevFn')
|
||||
.and.callFake((rev: string) => rev.replace(/"/g, '').toUpperCase());
|
||||
const customParser = new WebdavXmlParser(cleanRevFn);
|
||||
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/test.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getetag>"abc123"</d:getetag>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = customParser.parseMultiplePropsFromXml(xml, '/test.txt');
|
||||
expect(cleanRevFn).toHaveBeenCalledWith('"abc123"');
|
||||
expect(results[0].etag).toBe('ABC123');
|
||||
});
|
||||
|
||||
it('should handle missing properties gracefully', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/test.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/test.txt');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].etag).toBe('Wed, 15 Jan 2025 10:00:00 GMT'); // Falls back to lastmod when no etag
|
||||
expect(results[0].size).toBe(0);
|
||||
expect(results[0].type).toBe('file');
|
||||
});
|
||||
|
||||
it('should identify directories correctly', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>/folder/subfolder/</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
<d:prop>
|
||||
<d:resourcetype><d:collection/></d:resourcetype>
|
||||
<d:getlastmodified>Wed, 15 Jan 2025 10:00:00 GMT</d:getlastmodified>
|
||||
</d:prop>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>`;
|
||||
|
||||
const results = parser.parseMultiplePropsFromXml(xml, '/folder/');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].type).toBe('directory');
|
||||
expect(results[0].filename).toBe(''); // displayname is empty, and href ends with /
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseXmlResponseElement', () => {
|
||||
it('should return null for response without href', () => {
|
||||
const doc = new DOMParser().parseFromString(
|
||||
'<d:response xmlns:d="DAV:"></d:response>',
|
||||
'text/xml',
|
||||
);
|
||||
const response = doc.querySelector('response')!;
|
||||
const result = parser.parseXmlResponseElement(response, '/test');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-200 status', () => {
|
||||
const xml = `<d:response xmlns:d="DAV:">
|
||||
<d:href>/test.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||
</d:propstat>
|
||||
</d:response>`;
|
||||
const doc = new DOMParser().parseFromString(xml, 'text/xml');
|
||||
const response = doc.querySelector('response')!;
|
||||
const result = parser.parseXmlResponseElement(response, '/test.txt');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -234,7 +234,7 @@ export const PFAPI_SYNC_PROVIDERS = [
|
|||
appKey: DROPBOX_APP_KEY,
|
||||
basePath: environment.production ? `/` : `/DEV/`,
|
||||
}),
|
||||
new Webdav(environment.production ? undefined : `/DEV`),
|
||||
new Webdav(environment.production ? undefined : undefined),
|
||||
...(IS_ELECTRON ? [fileSyncElectron] : []),
|
||||
...(IS_ANDROID_WEB_VIEW ? [fileSyncDroid] : []),
|
||||
];
|
||||
|
|
|
|||
39
webdav-code-issues.md
Normal file
39
webdav-code-issues.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# WebDAV Code Issues Found
|
||||
|
||||
## Issues in webdav-http-adapter.ts
|
||||
|
||||
1. **Invalid Response status in error handling** (Lines 77-78, 121-122)
|
||||
- Creating Response objects with status 0 is invalid (must be 200-599)
|
||||
- Should use a valid error status like 500 for network errors
|
||||
|
||||
# WebDAV Code Issues Found
|
||||
|
||||
## Issues in webdav-api.ts
|
||||
|
||||
1. **Unused parameter `useGetFallback`** (Line 27)
|
||||
|
||||
- The parameter is defined but never used in the method
|
||||
- The HEAD fallback code is commented out (lines 57-59)
|
||||
- This parameter was intended to enable fallback to HEAD request when PROPFIND fails
|
||||
|
||||
2. **Unused method `_getFileMetaViaHead`** (Line 346)
|
||||
|
||||
- Private method that implements HEAD request fallback
|
||||
- Not currently being used because the fallback logic is commented out
|
||||
- Should either be removed or the fallback logic should be implemented
|
||||
|
||||
3. **Exception caught locally warnings** (Lines 61, 180, 206)
|
||||
- These are eslint warnings about throwing exceptions within try-catch blocks
|
||||
- This is intentional error handling pattern and can be ignored
|
||||
|
||||
## Issues in webdav.ts
|
||||
|
||||
1. **Inconsistent revision handling in `getFileRev`**
|
||||
- After reverting changes, the method now only returns `meta.etag`
|
||||
- Should handle cases where etag might be missing and fall back to `meta.lastmod`
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. Either implement the HEAD fallback functionality or remove the unused parameter and method
|
||||
2. Add proper error handling for missing etag values
|
||||
3. Consider adding more comprehensive logging for debugging sync issues
|
||||
Loading…
Add table
Add a link
Reference in a new issue