mirror of
https://github.com/colanode/colanode.git
synced 2026-02-24 11:59:53 +01:00
193 lines
4.8 KiB
TypeScript
193 lines
4.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
import { ApiErrorCode, FileStatus } from '@colanode/core';
|
|
import { database } from '@colanode/server/data/database';
|
|
import { updateNode } from '@colanode/server/lib/nodes';
|
|
import { buildTestApp } from '../helpers/app';
|
|
import {
|
|
buildAuthHeader,
|
|
createAccount,
|
|
createDevice,
|
|
createFileNode,
|
|
createSpaceNode,
|
|
createUser,
|
|
createWorkspace,
|
|
} from '../helpers/seed';
|
|
|
|
const app = buildTestApp();
|
|
|
|
beforeAll(async () => {
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
const extractPath = (location?: string) => {
|
|
if (!location) {
|
|
throw new Error('Missing Location header');
|
|
}
|
|
|
|
try {
|
|
const url = new URL(location);
|
|
return url.pathname;
|
|
} catch {
|
|
return location;
|
|
}
|
|
};
|
|
|
|
describe('file uploads', () => {
|
|
it('marks file Ready and sets uploaded_at after TUS completion', async () => {
|
|
const account = await createAccount({
|
|
email: 'upload@example.com',
|
|
});
|
|
|
|
const workspace = await createWorkspace({
|
|
createdBy: account.id,
|
|
});
|
|
|
|
const user = await createUser({
|
|
workspaceId: workspace.id,
|
|
account,
|
|
role: 'owner',
|
|
});
|
|
|
|
const { token } = await createDevice({ accountId: account.id });
|
|
|
|
const rootId = await createSpaceNode({
|
|
workspaceId: workspace.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
const payload = Buffer.from('hello world');
|
|
const fileId = await createFileNode({
|
|
workspaceId: workspace.id,
|
|
userId: user.id,
|
|
parentId: rootId,
|
|
rootId,
|
|
size: payload.length,
|
|
name: 'upload.txt',
|
|
extension: '.txt',
|
|
});
|
|
|
|
const createResponse = await app.inject({
|
|
method: 'POST',
|
|
url: `/client/v1/workspaces/${workspace.id}/files/${fileId}/tus`,
|
|
headers: {
|
|
...buildAuthHeader(token),
|
|
'Tus-Resumable': '1.0.0',
|
|
'Upload-Length': payload.length.toString(),
|
|
},
|
|
});
|
|
|
|
expect([201, 204]).toContain(createResponse.statusCode);
|
|
|
|
const location = createResponse.headers.location as string | undefined;
|
|
const uploadPath = extractPath(location);
|
|
|
|
const patchResponse = await app.inject({
|
|
method: 'PATCH',
|
|
url: uploadPath,
|
|
headers: {
|
|
...buildAuthHeader(token),
|
|
'Tus-Resumable': '1.0.0',
|
|
'Upload-Offset': '0',
|
|
'Content-Type': 'application/offset+octet-stream',
|
|
'Content-Length': payload.length.toString(),
|
|
},
|
|
payload,
|
|
});
|
|
|
|
expect([200, 204]).toContain(patchResponse.statusCode);
|
|
|
|
const upload = await database
|
|
.selectFrom('uploads')
|
|
.selectAll()
|
|
.where('file_id', '=', fileId)
|
|
.executeTakeFirst();
|
|
|
|
expect(upload?.uploaded_at).not.toBeNull();
|
|
|
|
const node = await database
|
|
.selectFrom('nodes')
|
|
.selectAll()
|
|
.where('id', '=', fileId)
|
|
.executeTakeFirst();
|
|
|
|
expect(node).not.toBeNull();
|
|
const attributes = node?.attributes as { status?: number } | null;
|
|
expect(attributes?.status).toBe(FileStatus.Ready);
|
|
});
|
|
});
|
|
|
|
describe('file download guards', () => {
|
|
it('rejects download when file is not ready or upload missing', async () => {
|
|
const account = await createAccount({
|
|
email: 'download@example.com',
|
|
});
|
|
|
|
const workspace = await createWorkspace({
|
|
createdBy: account.id,
|
|
});
|
|
|
|
const user = await createUser({
|
|
workspaceId: workspace.id,
|
|
account,
|
|
role: 'owner',
|
|
});
|
|
|
|
const { token } = await createDevice({ accountId: account.id });
|
|
|
|
const rootId = await createSpaceNode({
|
|
workspaceId: workspace.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
const fileId = await createFileNode({
|
|
workspaceId: workspace.id,
|
|
userId: user.id,
|
|
parentId: rootId,
|
|
rootId,
|
|
size: 10,
|
|
});
|
|
|
|
const notReadyResponse = await app.inject({
|
|
method: 'GET',
|
|
url: `/client/v1/workspaces/${workspace.id}/files/${fileId}`,
|
|
headers: buildAuthHeader(token),
|
|
});
|
|
|
|
expect(notReadyResponse.statusCode).toBe(400);
|
|
expect(notReadyResponse.json()).toMatchObject({
|
|
code: ApiErrorCode.FileNotReady,
|
|
});
|
|
|
|
const updated = await updateNode({
|
|
nodeId: fileId,
|
|
userId: user.id,
|
|
workspaceId: workspace.id,
|
|
updater(attributes) {
|
|
if (attributes.type !== 'file') {
|
|
throw new Error('Node is not a file');
|
|
}
|
|
attributes.status = FileStatus.Ready;
|
|
return attributes;
|
|
},
|
|
});
|
|
|
|
expect(updated).toBe(true);
|
|
|
|
const missingUploadResponse = await app.inject({
|
|
method: 'GET',
|
|
url: `/client/v1/workspaces/${workspace.id}/files/${fileId}`,
|
|
headers: buildAuthHeader(token),
|
|
});
|
|
|
|
expect(missingUploadResponse.statusCode).toBe(400);
|
|
expect(missingUploadResponse.json()).toMatchObject({
|
|
code: ApiErrorCode.FileUploadNotFound,
|
|
});
|
|
});
|
|
});
|