Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions workspace-server/src/__tests__/services/DocsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('DocsService', () => {
let docsService: DocsService;
let mockAuthManager: jest.Mocked<AuthManager>;
let mockDocsAPI: any;
let mockDriveAPI: any;

beforeEach(() => {
// Clear all mocks before each test
Expand All @@ -43,8 +44,17 @@ describe('DocsService', () => {
},
};

// Create mock Drive API (used by the Markdown import tools)
mockDriveAPI = {
files: {
create: jest.fn(),
update: jest.fn(),
},
};

// Mock the google constructors
(google.docs as jest.Mock) = jest.fn().mockReturnValue(mockDocsAPI);
(google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI);

// Create DocsService instance
docsService = new DocsService(mockAuthManager);
Expand All @@ -59,6 +69,119 @@ describe('DocsService', () => {
jest.restoreAllMocks();
});

describe('createFromMarkdown', () => {
it('creates a Google Doc from markdown via Drive conversion', async () => {
mockDriveAPI.files.create.mockResolvedValue({
data: {
id: 'new-doc-id',
name: 'My Doc',
webViewLink: 'https://docs.google.com/document/d/new-doc-id/edit',
},
});

const result = await docsService.createFromMarkdown({
markdown: '# Title\n\n- a\n- b',
name: 'My Doc',
parentId: 'folder-1',
});

expect(mockDriveAPI.files.create).toHaveBeenCalledTimes(1);
const arg = mockDriveAPI.files.create.mock.calls[0][0];
// Destination mimeType must be a Google Doc to trigger conversion...
expect(arg.requestBody.mimeType).toBe(
'application/vnd.google-apps.document',
);
expect(arg.requestBody.name).toBe('My Doc');
expect(arg.requestBody.parents).toEqual(['folder-1']);
// ...and the source media must be text/markdown.
expect(arg.media.mimeType).toBe('text/markdown');

const payload = JSON.parse(result.content[0].text);
expect(payload).toMatchObject({
documentId: 'new-doc-id',
title: 'My Doc',
webViewLink: 'https://docs.google.com/document/d/new-doc-id/edit',
});
expect(result.isError).toBeUndefined();
});

it('omits parents when parentId is not provided', async () => {
mockDriveAPI.files.create.mockResolvedValue({ data: { id: 'x' } });

await docsService.createFromMarkdown({ markdown: '# Hi', name: 'Doc' });

const arg = mockDriveAPI.files.create.mock.calls[0][0];
expect(arg.requestBody.parents).toBeUndefined();
});

it('returns an error result when the Drive API fails', async () => {
mockDriveAPI.files.create.mockRejectedValue(new Error('boom'));

const result = await docsService.createFromMarkdown({
markdown: '# Hi',
name: 'Doc',
});

expect(result.isError).toBe(true);
expect(JSON.parse(result.content[0].text).error).toBe('boom');
});
});

describe('updateFromMarkdown', () => {
it('replaces an existing doc content in place, keeping the same id', async () => {
mockDriveAPI.files.update.mockResolvedValue({
data: {
id: 'doc-123',
name: 'Doc',
webViewLink: 'https://docs.google.com/document/d/doc-123/edit',
modifiedTime: '2026-07-01T00:00:00Z',
},
});

const result = await docsService.updateFromMarkdown({
documentId: 'doc-123',
markdown: '# Updated',
});

expect(mockDriveAPI.files.update).toHaveBeenCalledTimes(1);
const arg = mockDriveAPI.files.update.mock.calls[0][0];
expect(arg.fileId).toBe('doc-123');
expect(arg.media.mimeType).toBe('text/markdown');

const payload = JSON.parse(result.content[0].text);
expect(payload).toMatchObject({
documentId: 'doc-123',
modifiedTime: '2026-07-01T00:00:00Z',
});
expect(result.isError).toBeUndefined();
});

it('extracts the doc id from a full Docs URL', async () => {
mockDriveAPI.files.update.mockResolvedValue({ data: { id: 'doc-123' } });

await docsService.updateFromMarkdown({
documentId:
'https://docs.google.com/document/d/doc-123/edit?tab=t.0',
markdown: '# Updated',
});

const arg = mockDriveAPI.files.update.mock.calls[0][0];
expect(arg.fileId).toBe('doc-123');
});

it('returns an error result when the Drive API fails', async () => {
mockDriveAPI.files.update.mockRejectedValue(new Error('nope'));

const result = await docsService.updateFromMarkdown({
documentId: 'doc-123',
markdown: '# x',
});

expect(result.isError).toBe(true);
expect(JSON.parse(result.content[0].text).error).toBe('nope');
});
});

describe('create', () => {
it('should create a blank document', async () => {
const mockDoc = {
Expand Down
135 changes: 135 additions & 0 deletions workspace-server/src/__tests__/services/DriveService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jest.mock('node:fs', () => {
existsSync: jest.fn(),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
createReadStream: jest.fn(),
};
});
jest.mock('node:path', () => {
Expand Down Expand Up @@ -76,6 +77,11 @@ describe('DriveService', () => {
comments: {
list: jest.fn(),
},
permissions: {
create: jest.fn(),
list: jest.fn(),
delete: jest.fn(),
},
};

// Mock the google.drive constructor
Expand Down Expand Up @@ -1439,4 +1445,133 @@ describe('DriveService', () => {
);
});
});

describe('uploadFile', () => {
it('should error (isError) when the local file does not exist', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);

const result = await driveService.uploadFile({
localPath: '/does/not/exist.png',
});
const response = JSON.parse(result.content[0].text);

expect('isError' in result && result.isError).toBe(true);
expect(response.error).toContain('File not found');
expect(mockDriveAPI.files.create).not.toHaveBeenCalled();
});

it('should upload a private file and return its id', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
mockDriveAPI.files.create.mockResolvedValue({
data: { id: 'uploaded-id', name: 'pic.png', webViewLink: 'http://x' },
});

const result = await driveService.uploadFile({
localPath: '/tmp/pic.png',
mimeType: 'image/png',
});
const response = JSON.parse(result.content[0].text);

expect(response.id).toBe('uploaded-id');
// No sharing is granted on upload.
expect(mockDriveAPI.permissions.create).not.toHaveBeenCalled();
});
});

describe('addPublicAccess', () => {
it('should grant anyone:reader and return an lh3 image URL', async () => {
mockDriveAPI.permissions.create.mockResolvedValue({
data: { id: 'perm-1' },
});

const result = await driveService.addPublicAccess({ fileId: 'file123' });
const response = JSON.parse(result.content[0].text);

expect(mockDriveAPI.permissions.create).toHaveBeenCalledWith(
expect.objectContaining({
requestBody: { role: 'reader', type: 'anyone' },
}),
);
expect(response.imageUrl).toBe(
'https://lh3.googleusercontent.com/d/file123',
);
});

it('should surface a structured hint when org policy blocks sharing', async () => {
mockDriveAPI.permissions.create.mockRejectedValue({
message: 'Insufficient permissions',
errors: [{ reason: 'publishOutNotPermitted' }],
});

const result = await driveService.addPublicAccess({ fileId: 'file123' });
const response = JSON.parse(result.content[0].text);

expect('isError' in result && result.isError).toBe(true);
expect(response.reason).toBe('publishOutNotPermitted');
expect(response.hint).toContain('publicly reachable URL');
});
});

describe('removePublicAccess', () => {
it('should revoke only anyone-type permissions', async () => {
mockDriveAPI.permissions.list.mockResolvedValue({
data: {
permissions: [
{ id: 'p-anyone', type: 'anyone', role: 'reader' },
{ id: 'p-user', type: 'user', role: 'writer' },
],
},
});
mockDriveAPI.permissions.delete.mockResolvedValue({});

const result = await driveService.removePublicAccess({
fileId: 'file123',
});
const response = JSON.parse(result.content[0].text);

expect(mockDriveAPI.permissions.delete).toHaveBeenCalledTimes(1);
expect(mockDriveAPI.permissions.delete).toHaveBeenCalledWith(
expect.objectContaining({ permissionId: 'p-anyone' }),
);
expect(response.removed).toEqual(['p-anyone']);
expect(response.failed).toEqual([]);
});

it('should report partial failures without throwing', async () => {
mockDriveAPI.permissions.list.mockResolvedValue({
data: {
permissions: [
{ id: 'p-1', type: 'anyone' },
{ id: 'p-2', type: 'anyone' },
],
},
});
mockDriveAPI.permissions.delete
.mockResolvedValueOnce({})
.mockRejectedValueOnce(new Error('revoked auth'));

const result = await driveService.removePublicAccess({
fileId: 'file123',
});
const response = JSON.parse(result.content[0].text);

expect(response.removed).toEqual(['p-1']);
expect(response.failed).toHaveLength(1);
expect(response.failed[0].permissionId).toBe('p-2');
});

it('should be a no-op when there are no public permissions', async () => {
mockDriveAPI.permissions.list.mockResolvedValue({
data: { permissions: [{ id: 'p-user', type: 'user' }] },
});

const result = await driveService.removePublicAccess({
fileId: 'file123',
});
const response = JSON.parse(result.content[0].text);

expect(mockDriveAPI.permissions.delete).not.toHaveBeenCalled();
expect(response.removed).toEqual([]);
});
});
});
Loading