fe429dbe01
- Links anlegen und aendern: Passwort, Ablaufdatum und Bezeichnung nur im JSON-Koerper, Vorpruefung aus den frisch gelesenen Faehigkeiten (Passwort/Ablauf verlangt, Hochladen, mehrere Links), echte Datumspruefung, nur geaenderte Felder beim Aendern - Routen shares/mine, shares/received (angenommen plus offen, 404/405 bei offen = leer) und POST shares/:id/accept am Ende der Klasse - Fehlerzweige fuer Passwort und Ablauf mit Tests; nie Passwort in Antwort oder Fehler - Live-Test e2e-shares.sh: Abschnitte links (Pflicht-Passwort und Pflicht-Ablauf per occ) und received (eingehend, offen, annehmen, verlassen) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
994 lines
39 KiB
TypeScript
994 lines
39 KiB
TypeScript
import { Readable } from 'node:stream';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { NextcloudCallGate } from './nextcloud-call-gate';
|
|
import { type NcSession } from './nextcloud-files.types';
|
|
import { NextcloudFilesSharesService } from './nextcloud-files-shares.service';
|
|
import type { NcTransportRequest, NextcloudTransport } from './nextcloud-http';
|
|
import { NextcloudLoginGuard } from './nextcloud-login-guard';
|
|
|
|
const SESSION: NcSession = {
|
|
baseUrl: 'https://cloud.example/nc',
|
|
ncUserId: 'anna',
|
|
authorization: 'Basic YW5uYTphcHAtcHctMTIz',
|
|
credentialKey: 'k1',
|
|
};
|
|
const OCS = 'https://cloud.example/nc/ocs/v2.php';
|
|
const SHARES = `${OCS}/apps/files_sharing/api/v1/shares`;
|
|
const BY_PATH = `${SHARES}?path=%2FProjekte&reshares=true`;
|
|
const CAPS_URL = `${OCS}/cloud/capabilities`;
|
|
const DAV = 'https://cloud.example/nc/remote.php/dav/files/anna';
|
|
|
|
const NS = 'xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"';
|
|
function statXml(name: string, folder: boolean, letters: string): string {
|
|
return `<?xml version="1.0"?><d:multistatus ${NS}><d:response>
|
|
<d:href>/nc/remote.php/dav/files/anna/${name}</d:href><d:propstat><d:prop>
|
|
<d:resourcetype>${folder ? '<d:collection/>' : ''}</d:resourcetype>
|
|
<oc:fileid>7</oc:fileid><oc:permissions>${letters}</oc:permissions>
|
|
${folder ? '' : '<d:getcontentlength>5</d:getcontentlength><d:getcontenttype>text/plain</d:getcontenttype>'}
|
|
</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>`;
|
|
}
|
|
|
|
const ocs = (data: unknown, status = 200, message = 'OK') =>
|
|
JSON.stringify({ ocs: { meta: { statuscode: status, message }, data } });
|
|
|
|
const CAPS = (over: Record<string, unknown> = {}) =>
|
|
ocs({
|
|
capabilities: {
|
|
password_policy: { minLength: 10 },
|
|
files_sharing: {
|
|
api_enabled: true,
|
|
group_sharing: true,
|
|
public: { enabled: true, upload: true },
|
|
sharee: { minSearchStringLength: 0 },
|
|
...over,
|
|
},
|
|
},
|
|
});
|
|
|
|
function shareJson(over: Record<string, unknown> = {}) {
|
|
return {
|
|
id: '17',
|
|
share_type: 0,
|
|
uid_owner: 'anna',
|
|
displayname_owner: 'Anna Müller',
|
|
permissions: 15,
|
|
can_edit: true,
|
|
can_delete: true,
|
|
stime: 1791532378,
|
|
path: '/Projekte',
|
|
item_type: 'folder',
|
|
item_permissions: 31,
|
|
mimetype: 'httpd/unix-directory',
|
|
file_target: '/Projekte',
|
|
share_with: 'ben',
|
|
share_with_displayname: 'Ben Beispiel',
|
|
...over,
|
|
};
|
|
}
|
|
|
|
type Reply = { status: number; text?: string; headers?: Record<string, string> };
|
|
|
|
function setup(replies: Reply[]) {
|
|
const calls: NcTransportRequest[] = [];
|
|
const queue = [...replies];
|
|
const transport: NextcloudTransport = async (req) => {
|
|
calls.push(req);
|
|
const reply = queue.shift();
|
|
if (!reply) throw new Error(`unerwarteter Aufruf ${req.method} ${req.url}`);
|
|
return {
|
|
statusCode: reply.status,
|
|
headers: reply.headers ?? {},
|
|
body: Readable.from(reply.text !== undefined ? [Buffer.from(reply.text)] : []),
|
|
};
|
|
};
|
|
const account = {
|
|
getSession: vi.fn(async (..._a: unknown[]) => SESSION),
|
|
markExpired: vi.fn(async (..._a: unknown[]) => {}),
|
|
};
|
|
const guard = new NextcloudLoginGuard();
|
|
const clock = { t: 1_000_000 };
|
|
guard.now = () => clock.t;
|
|
const service = new NextcloudFilesSharesService(
|
|
account as never,
|
|
new NextcloudCallGate(),
|
|
transport,
|
|
guard,
|
|
);
|
|
return { service, account, calls, queue, clock, guard };
|
|
}
|
|
|
|
const codeOf = (e: unknown) => (e as { response: { code: string } }).response.code;
|
|
const bodyOf = (e: unknown) => (e as { response: Record<string, unknown> }).response;
|
|
const statusOf = (e: unknown) => (e as { getStatus(): number }).getStatus();
|
|
|
|
async function failure(promise: Promise<unknown>): Promise<unknown> {
|
|
try {
|
|
await promise;
|
|
} catch (e) {
|
|
return e;
|
|
}
|
|
throw new Error('es wurde kein Fehler geworfen');
|
|
}
|
|
|
|
/** Antworten fuer ein erfolgreiches create(): stat, Faehigkeiten, Liste des Pfads, POST. */
|
|
function createReplies(
|
|
over: { stat?: Reply; caps?: Reply; list?: Reply; post?: Reply } = {},
|
|
): Reply[] {
|
|
return [
|
|
over.stat ?? { status: 207, text: statXml('Projekte', true, 'RGDNVCK') },
|
|
over.caps ?? { status: 200, text: CAPS() },
|
|
over.list ?? { status: 200, text: ocs([]) },
|
|
over.post ?? { status: 200, text: ocs(shareJson()) },
|
|
];
|
|
}
|
|
|
|
const INPUT = { path: '/Projekte', kind: 'user', shareWith: 'ben', access: 'edit' } as const;
|
|
|
|
describe('NextcloudFilesSharesService — create', () => {
|
|
it('sendet PROPFIND, Faehigkeiten, Liste des Pfads, POST in dieser Reihenfolge und liefert die Freigabe', async () => {
|
|
const { service, account, calls } = setup(createReplies());
|
|
const share = await service.create('t1', 'u1', INPUT);
|
|
expect(account.getSession).toHaveBeenCalledWith('t1', 'u1');
|
|
expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
|
|
`PROPFIND ${DAV}/Projekte`,
|
|
`GET ${CAPS_URL}`,
|
|
`GET ${BY_PATH}`,
|
|
`POST ${SHARES}`,
|
|
]);
|
|
expect(calls[0].headers.depth).toBe('0');
|
|
expect(calls[3].headers['content-type']).toBe('application/json');
|
|
expect(calls[3].body).toBe(
|
|
'{"path":"/Projekte","shareType":0,"shareWith":"ben","permissions":15}',
|
|
);
|
|
expect(share).toMatchObject({ id: '17', kind: 'user', shareWith: 'ben', access: 'edit' });
|
|
});
|
|
|
|
it('Gruppe: shareType 1', async () => {
|
|
const { service, calls } = setup(
|
|
createReplies({
|
|
post: { status: 200, text: ocs(shareJson({ share_type: 1, share_with: 'team' })) },
|
|
}),
|
|
);
|
|
const share = await service.create('t1', 'u1', { ...INPUT, kind: 'group', shareWith: 'team' });
|
|
expect(calls[3].body).toBe(
|
|
'{"path":"/Projekte","shareType":1,"shareWith":"team","permissions":15}',
|
|
);
|
|
expect(share.kind).toBe('group');
|
|
});
|
|
|
|
it('Datei: Bearbeiten sendet 3, Ansehen sendet 1', async () => {
|
|
for (const [access, permissions] of [
|
|
['edit', 3],
|
|
['view', 1],
|
|
] as const) {
|
|
const { service, calls } = setup([
|
|
{ status: 207, text: statXml('Bericht.txt', false, 'RGDNVW') },
|
|
{ status: 200, text: CAPS() },
|
|
{ status: 200, text: ocs([]) },
|
|
{
|
|
status: 200,
|
|
text: ocs(shareJson({ path: '/Bericht.txt', item_type: 'file', permissions })),
|
|
},
|
|
]);
|
|
await service.create('t1', 'u1', { ...INPUT, path: '/Bericht.txt', access });
|
|
expect(calls[2].url).toBe(`${SHARES}?path=%2FBericht.txt&reshares=true`);
|
|
expect(calls[3].body).toBe(
|
|
`{"path":"/Bericht.txt","shareType":0,"shareWith":"ben","permissions":${permissions}}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('Hochladen fuer eine Person, oder Bearbeiten bei Buchstaben RG: 400 shareAccessInvalid, kein POST', async () => {
|
|
const upload = setup(createReplies());
|
|
const e1 = await failure(upload.service.create('t1', 'u1', { ...INPUT, access: 'upload' }));
|
|
expect(codeOf(e1)).toBe('shareAccessInvalid');
|
|
expect(statusOf(e1)).toBe(400);
|
|
expect(upload.calls.some((c) => c.method === 'POST')).toBe(false);
|
|
|
|
const readonly = setup(
|
|
createReplies({ stat: { status: 207, text: statXml('Projekte', true, 'RG') } }),
|
|
);
|
|
const e2 = await failure(readonly.service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e2)).toBe('shareAccessInvalid');
|
|
expect(readonly.calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('der Empfaenger hat schon eine Freigabe: 409 shareAlreadyExists, kein POST', async () => {
|
|
const { service, calls } = setup(
|
|
createReplies({ list: { status: 200, text: ocs([shareJson()]) } }),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe('shareAlreadyExists');
|
|
expect(statusOf(e)).toBe(409);
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('gleiche Kennung als Gruppe zaehlt nicht als vorhandene Personenfreigabe', async () => {
|
|
const { service } = setup(
|
|
createReplies({
|
|
list: { status: 200, text: ocs([shareJson({ share_type: 1, share_with: 'ben' })]) },
|
|
}),
|
|
);
|
|
await expect(service.create('t1', 'u1', INPUT)).resolves.toMatchObject({ id: '17' });
|
|
});
|
|
|
|
it('Gruppen ausgeschaltet: 409 sharingDisabled mit der Gruppenmeldung, kein POST', async () => {
|
|
const { service, calls } = setup(
|
|
createReplies({ caps: { status: 200, text: CAPS({ group_sharing: false }) } }),
|
|
);
|
|
const e = await failure(
|
|
service.create('t1', 'u1', { ...INPUT, kind: 'group', shareWith: 'team' }),
|
|
);
|
|
expect(codeOf(e)).toBe('sharingDisabled');
|
|
expect(statusOf(e)).toBe(409);
|
|
expect(bodyOf(e).message).toBe('Teilen mit Gruppen ist in Ihrer Nextcloud ausgeschaltet.');
|
|
expect(bodyOf(e).scope).toBe('groups');
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('Freigabe-Schnittstelle aus: 409 sharingDisabled', async () => {
|
|
const { service, calls } = setup(
|
|
createReplies({ caps: { status: 200, text: CAPS({ api_enabled: false }) } }),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe('sharingDisabled');
|
|
expect(bodyOf(e).message).toBe('Teilen ist in Ihrer Nextcloud ausgeschaltet.');
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('leerer Pfad oder Wurzel: 400 invalidPath ohne jeden Aufruf', async () => {
|
|
for (const path of ['', '/']) {
|
|
const { service, account, calls } = setup([]);
|
|
const e = await failure(service.create('t1', 'u1', { ...INPUT, path }));
|
|
expect(codeOf(e)).toBe('invalidPath');
|
|
expect(calls).toHaveLength(0);
|
|
expect(account.getSession).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('Eintrag nicht vorhanden: 404 notFound, danach kein Freigabe-Aufruf', async () => {
|
|
const { service, calls } = setup([{ status: 404 }]);
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe('notFound');
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
it.each([
|
|
[404, undefined, 422, 'shareRecipientInvalid'],
|
|
[403, 'You cannot share a folder that contains other shares', 422, 'shareRejected'],
|
|
[400, undefined, 422, 'shareRejected'],
|
|
[500, undefined, 502, 'nextcloudError'],
|
|
[503, undefined, 503, 'nextcloudMaintenance'],
|
|
])('POST antwortet %i -> %i %s', async (nc, message, http, code) => {
|
|
const { service } = setup(
|
|
createReplies({
|
|
post: {
|
|
status: nc,
|
|
text: JSON.stringify({
|
|
ocs: { meta: { statuscode: nc, message: message ?? 'x' }, data: [] },
|
|
}),
|
|
},
|
|
}),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe(code);
|
|
expect(statusOf(e)).toBe(http);
|
|
expect([401, 403]).not.toContain(statusOf(e));
|
|
if (code === 'shareRejected' && message) expect(bodyOf(e).ncMessage).toBe(message);
|
|
});
|
|
|
|
it('POST antwortet 401: 409 connectionExpired und die Verbindung ist als abgelaufen markiert', async () => {
|
|
const { service, account } = setup(createReplies({ post: { status: 401 } }));
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe('connectionExpired');
|
|
expect(statusOf(e)).toBe(409);
|
|
expect(account.markExpired).toHaveBeenCalledTimes(1);
|
|
expect(account.markExpired).toHaveBeenCalledWith('t1', 'u1');
|
|
});
|
|
|
|
it('POST antwortet 429: 503 nextcloudLocked', async () => {
|
|
const { service } = setup(
|
|
createReplies({ post: { status: 429, headers: { 'retry-after': '60' } } }),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', INPUT));
|
|
expect(codeOf(e)).toBe('nextcloudLocked');
|
|
expect(statusOf(e)).toBe(503);
|
|
});
|
|
});
|
|
|
|
describe('NextcloudFilesSharesService — Begrenzung neuer Freigaben', () => {
|
|
it('15 in 10 Minuten gehen durch, die 16. ist 429 tooManyShares ohne Aufruf, nach 10 Minuten geht es wieder', async () => {
|
|
const replies: Reply[] = [];
|
|
for (let i = 0; i < 15; i++) replies.push(...createReplies());
|
|
// der abgewiesene 16. Versuch verbraucht nur die Vorpruefungen, nie den POST
|
|
replies.push(...createReplies().slice(0, 3));
|
|
replies.push(...createReplies());
|
|
const { service, calls, clock } = setup(replies);
|
|
for (let i = 0; i < 15; i++) {
|
|
await service.create('t1', 'u1', { ...INPUT, shareWith: `ben${i}` });
|
|
}
|
|
const before = calls.length;
|
|
const e = await failure(service.create('t1', 'u1', { ...INPUT, shareWith: 'ben15' }));
|
|
expect(codeOf(e)).toBe('tooManyShares');
|
|
expect(statusOf(e)).toBe(429);
|
|
expect(bodyOf(e).retryAfterSeconds).toBe(600);
|
|
// Vorpruefungen laufen, aber der POST wird nicht gesendet.
|
|
expect(calls.slice(before).some((c) => c.method === 'POST')).toBe(false);
|
|
|
|
clock.t += 10 * 60 * 1000;
|
|
await expect(
|
|
service.create('t1', 'u1', { ...INPUT, shareWith: 'ben15' }),
|
|
).resolves.toBeTruthy();
|
|
});
|
|
|
|
it('eine durch Vorpruefung abgelehnte Eingabe zaehlt nicht mit', async () => {
|
|
const replies: Reply[] = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
replies.push(
|
|
...createReplies({
|
|
list: { status: 200, text: ocs([shareJson({ share_with: 'x' })]) },
|
|
}).slice(0, 3),
|
|
);
|
|
}
|
|
for (let i = 0; i < 15; i++) replies.push(...createReplies());
|
|
const { service } = setup(replies);
|
|
for (let i = 0; i < 3; i++) {
|
|
const e = await failure(service.create('t1', 'u1', { ...INPUT, shareWith: 'x' }));
|
|
expect(codeOf(e)).toBe('shareAlreadyExists');
|
|
}
|
|
for (let i = 0; i < 15; i++) await service.create('t1', 'u1', { ...INPUT, shareWith: `b${i}` });
|
|
});
|
|
});
|
|
|
|
describe('NextcloudFilesSharesService — update', () => {
|
|
const GET17 = `GET ${SHARES}/17`;
|
|
|
|
it('Ansehen: GET der Freigabe, dann PUT nur mit der neuen Berechtigung', async () => {
|
|
const { service, calls } = setup([
|
|
{ status: 200, text: ocs([shareJson()]) },
|
|
{ status: 200, text: ocs(shareJson({ permissions: 1 })) },
|
|
]);
|
|
const share = await service.update('t1', 'u1', '17', { access: 'view' });
|
|
expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([GET17, `PUT ${SHARES}/17`]);
|
|
expect(calls[1].body).toBe('{"permissions":1}');
|
|
expect(share.access).toBe('view');
|
|
});
|
|
|
|
it('Freigabe nicht aenderbar (can_edit false): 422 shareRejected ohne PUT', async () => {
|
|
const { service, calls } = setup([
|
|
{ status: 200, text: ocs([shareJson({ can_edit: false })]) },
|
|
]);
|
|
const e = await failure(service.update('t1', 'u1', '17', { access: 'view' }));
|
|
expect(codeOf(e)).toBe('shareRejected');
|
|
expect(statusOf(e)).toBe(422);
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
it('PUT 404 -> 404 shareNotFound', async () => {
|
|
const { service } = setup([
|
|
{ status: 200, text: ocs([shareJson()]) },
|
|
{ status: 404, text: ocs([], 404, 'Wrong share ID, share does not exist') },
|
|
]);
|
|
const e = await failure(service.update('t1', 'u1', '17', { access: 'view' }));
|
|
expect(codeOf(e)).toBe('shareNotFound');
|
|
expect(statusOf(e)).toBe(404);
|
|
});
|
|
|
|
it('PUT 400 und 403 -> 422 shareRejected mit Meldung der Nextcloud', async () => {
|
|
for (const nc of [400, 403]) {
|
|
const { service } = setup([
|
|
{ status: 200, text: ocs([shareJson()]) },
|
|
{ status: nc, text: ocs([], nc, 'Failed to update share.') },
|
|
]);
|
|
const e = await failure(service.update('t1', 'u1', '17', { access: 'view' }));
|
|
expect(codeOf(e)).toBe('shareRejected');
|
|
expect(statusOf(e)).toBe(422);
|
|
expect(bodyOf(e).ncMessage).toBe('Failed to update share.');
|
|
}
|
|
});
|
|
|
|
it("Kennung 'abc' und eine 21-stellige Kennung: 404 shareNotFound ohne Aufruf", async () => {
|
|
for (const id of ['abc', '1'.repeat(21), '../x', '']) {
|
|
const { service, calls } = setup([]);
|
|
const e = await failure(service.update('t1', 'u1', id, { access: 'view' }));
|
|
expect(codeOf(e)).toBe('shareNotFound');
|
|
expect(calls).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it('ohne Aenderung kommt die aktuelle Freigabe zurueck, es gibt kein PUT', async () => {
|
|
const { service, calls } = setup([{ status: 200, text: ocs([shareJson()]) }]);
|
|
const share = await service.update('t1', 'u1', '17', {});
|
|
expect(share.id).toBe('17');
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
it('gleiche Berechtigung wie bisher: kein PUT', async () => {
|
|
const { service, calls } = setup([{ status: 200, text: ocs([shareJson()]) }]);
|
|
await service.update('t1', 'u1', '17', { access: 'edit' });
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
it('Bearbeiten bei einem Eintrag ohne Schreibrecht und Hochladen sind shareAccessInvalid', async () => {
|
|
const ro = setup([
|
|
{ status: 200, text: ocs([shareJson({ permissions: 1, item_permissions: 17 })]) },
|
|
]);
|
|
expect(codeOf(await failure(ro.service.update('t1', 'u1', '17', { access: 'edit' })))).toBe(
|
|
'shareAccessInvalid',
|
|
);
|
|
const up = setup([{ status: 200, text: ocs([shareJson()]) }]);
|
|
expect(codeOf(await failure(up.service.update('t1', 'u1', '17', { access: 'upload' })))).toBe(
|
|
'shareAccessInvalid',
|
|
);
|
|
});
|
|
|
|
it('GET der Freigabe 404 -> shareNotFound; 401 markiert die Verbindung', async () => {
|
|
const gone = setup([{ status: 404, text: ocs([], 404, 'Wrong share ID') }]);
|
|
expect(codeOf(await failure(gone.service.update('t1', 'u1', '17', { access: 'view' })))).toBe(
|
|
'shareNotFound',
|
|
);
|
|
const dead = setup([{ status: 401 }]);
|
|
const e = await failure(dead.service.update('t1', 'u1', '17', { access: 'view' }));
|
|
expect(codeOf(e)).toBe('connectionExpired');
|
|
expect(dead.account.markExpired).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('NextcloudFilesSharesService — remove, lesen, Richtlinie', () => {
|
|
it('remove: DELETE auf die Kennung', async () => {
|
|
const { service, calls } = setup([{ status: 200, text: ocs([]) }]);
|
|
await expect(service.remove('t1', 'u1', '17')).resolves.toEqual({ deleted: true });
|
|
expect(calls[0].method).toBe('DELETE');
|
|
expect(calls[0].url).toBe(`${SHARES}/17`);
|
|
expect(calls[0].body).toBeNull();
|
|
});
|
|
|
|
it('remove: 404 -> shareNotFound, 403 -> 422 shareRejected, falsche Kennung ohne Aufruf', async () => {
|
|
const nf = setup([{ status: 404, text: ocs([], 404, 'Wrong share ID') }]);
|
|
expect(codeOf(await failure(nf.service.remove('t1', 'u1', '17')))).toBe('shareNotFound');
|
|
const forbidden = setup([{ status: 403, text: ocs([], 403, 'Could not delete share') }]);
|
|
const e = await failure(forbidden.service.remove('t1', 'u1', '17'));
|
|
expect(codeOf(e)).toBe('shareRejected');
|
|
expect(statusOf(e)).toBe(422);
|
|
expect(bodyOf(e).ncMessage).toBe('Could not delete share');
|
|
const bad = setup([]);
|
|
expect(codeOf(await failure(bad.service.remove('t1', 'u1', 'abc')))).toBe('shareNotFound');
|
|
expect(bad.calls).toHaveLength(0);
|
|
});
|
|
|
|
it('Freigaben eines Pfads: Pfad normalisiert, Wurzel ist invalidPath', async () => {
|
|
const { service, calls } = setup([
|
|
{ status: 200, text: ocs([shareJson(), { share_type: 4, id: '9' }]) },
|
|
]);
|
|
const out = await service.sharesForPath('t1', 'u1', '/Projekte/');
|
|
expect(calls[0].url).toBe(BY_PATH);
|
|
expect(out.path).toBe('/Projekte');
|
|
expect(out.shares.map((s) => s.id)).toEqual(['17']);
|
|
expect(out.hidden).toBe(1);
|
|
const root = setup([]);
|
|
expect(codeOf(await failure(root.service.sharesForPath('t1', 'u1', '/')))).toBe('invalidPath');
|
|
const nf = setup([{ status: 404, text: ocs([], 404, 'Wrong path') }]);
|
|
expect(codeOf(await failure(nf.service.sharesForPath('t1', 'u1', '/x')))).toBe('notFound');
|
|
});
|
|
|
|
it('Empfaengersuche: genau die erwartete Adresse; leerer Suchbegriff ruft nichts auf', async () => {
|
|
const { service, calls } = setup([
|
|
{
|
|
status: 200,
|
|
text: ocs({
|
|
users: [
|
|
{
|
|
label: 'Ben Beispiel',
|
|
value: { shareType: 0, shareWith: 'ben' },
|
|
shareWithDisplayNameUnique: 'ben',
|
|
},
|
|
],
|
|
groups: [],
|
|
}),
|
|
},
|
|
]);
|
|
const out = await service.sharees('t1', 'u1', ' ben ', 'folder');
|
|
expect(calls[0].url).toBe(
|
|
`${OCS}/apps/files_sharing/api/v1/sharees?search=ben&itemType=folder&perPage=20&shareType%5B0%5D=0&shareType%5B1%5D=1`,
|
|
);
|
|
expect(out.sharees).toEqual([
|
|
{ kind: 'user', id: 'ben', label: 'Ben Beispiel', detail: 'ben' },
|
|
]);
|
|
|
|
const empty = setup([]);
|
|
await expect(empty.service.sharees('t1', 'u1', ' ', 'file')).resolves.toEqual({ sharees: [] });
|
|
expect(empty.calls).toHaveLength(0);
|
|
});
|
|
|
|
it('Richtlinie: zwei Aufrufe -> zwei Anfragen an die Faehigkeiten (kein Zwischenspeicher)', async () => {
|
|
const { service, calls } = setup([
|
|
{ status: 200, text: CAPS() },
|
|
{ status: 200, text: CAPS({ group_sharing: false }) },
|
|
]);
|
|
const first = await service.policy('t1', 'u1');
|
|
const second = await service.policy('t1', 'u1');
|
|
expect(calls.map((c) => c.url)).toEqual([CAPS_URL, CAPS_URL]);
|
|
expect(first.groupsEnabled).toBe(true);
|
|
expect(second.groupsEnabled).toBe(false);
|
|
});
|
|
|
|
it('nie 401 oder 403 nach aussen, auch nicht bei 403 aus der Suche', async () => {
|
|
const { service } = setup([{ status: 403, text: ocs([], 403, 'nope') }]);
|
|
const e = await failure(service.sharees('t1', 'u1', 'be', 'file'));
|
|
expect([401, 403]).not.toContain(statusOf(e));
|
|
});
|
|
});
|
|
|
|
// --- Links (quick-261009-dkv, Aufgabe 2) -----------------------------------------------------------
|
|
|
|
const PW = 'Geheim-Pass-2026!';
|
|
|
|
function linkJson(over: Record<string, unknown> = {}) {
|
|
return shareJson({
|
|
id: '31',
|
|
share_type: 3,
|
|
share_with: null,
|
|
share_with_displayname: null,
|
|
permissions: 1,
|
|
token: 'AbC123',
|
|
url: 'https://cloud.example/nc/index.php/s/AbC123',
|
|
password: 'redacted',
|
|
label: 'Kunde',
|
|
...over,
|
|
});
|
|
}
|
|
|
|
/** Faehigkeiten mit Link-Regeln: `public` wird mit den angegebenen Werten ueberlagert. */
|
|
const LINK_CAPS = (pub: Record<string, unknown> = {}, over: Record<string, unknown> = {}) =>
|
|
CAPS({ public: { enabled: true, upload: true, ...pub }, ...over });
|
|
|
|
const LINK_INPUT = {
|
|
path: '/Projekte',
|
|
kind: 'link',
|
|
access: 'view',
|
|
password: PW,
|
|
expireDate: '',
|
|
label: 'Kunde',
|
|
} as const;
|
|
|
|
function linkReplies(
|
|
over: { stat?: Reply; caps?: Reply; list?: Reply; post?: Reply } = {},
|
|
withList = false,
|
|
): Reply[] {
|
|
const replies: Reply[] = [
|
|
over.stat ?? { status: 207, text: statXml('Projekte', true, 'RGDNVCK') },
|
|
over.caps ?? { status: 200, text: LINK_CAPS() },
|
|
];
|
|
if (withList) replies.push(over.list ?? { status: 200, text: ocs([]) });
|
|
replies.push(over.post ?? { status: 200, text: ocs(linkJson()) });
|
|
return replies;
|
|
}
|
|
|
|
describe('NextcloudFilesSharesService — Link anlegen', () => {
|
|
it('sendet genau den erwarteten JSON-Koerper und gibt weder Passwort noch Kennung heraus', async () => {
|
|
const { service, calls } = setup(linkReplies());
|
|
const share = await service.create('t1', 'u1', LINK_INPUT);
|
|
expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
|
|
`PROPFIND ${DAV}/Projekte`,
|
|
`GET ${CAPS_URL}`,
|
|
`POST ${SHARES}`,
|
|
]);
|
|
expect(calls[2].body).toBe(
|
|
'{"path":"/Projekte","shareType":3,"permissions":1,"password":"Geheim-Pass-2026!","expireDate":"","label":"Kunde"}',
|
|
);
|
|
expect(share).toMatchObject({
|
|
id: '31',
|
|
kind: 'link',
|
|
hasPassword: true,
|
|
url: 'https://cloud.example/nc/index.php/s/AbC123',
|
|
label: 'Kunde',
|
|
});
|
|
const text = JSON.stringify(share);
|
|
expect(text).not.toContain(PW);
|
|
expect(text).not.toContain('redacted');
|
|
expect(text).not.toContain('"token"');
|
|
});
|
|
|
|
it('ohne Passwort, Ablauf und Bezeichnung werden diese Felder nicht gesendet', async () => {
|
|
const { service, calls } = setup(linkReplies());
|
|
await service.create('t1', 'u1', { path: '/Projekte', kind: 'link', access: 'view' });
|
|
expect(calls[2].body).toBe('{"path":"/Projekte","shareType":3,"permissions":1}');
|
|
});
|
|
|
|
it('Nur hochladen bei einem Ordner sendet 4; bei einer Datei und ohne erlaubtes Hochladen: shareAccessInvalid ohne POST', async () => {
|
|
const ok = setup(linkReplies());
|
|
await ok.service.create('t1', 'u1', { ...LINK_INPUT, access: 'upload' });
|
|
expect(ok.calls[2].body).toContain('"permissions":4');
|
|
|
|
const file = setup(
|
|
linkReplies({ stat: { status: 207, text: statXml('Bericht.txt', false, 'RGDNVW') } }),
|
|
);
|
|
const e1 = await failure(
|
|
file.service.create('t1', 'u1', { ...LINK_INPUT, path: '/Bericht.txt', access: 'upload' }),
|
|
);
|
|
expect(codeOf(e1)).toBe('shareAccessInvalid');
|
|
expect(statusOf(e1)).toBe(400);
|
|
expect(file.calls.some((c) => c.method === 'POST')).toBe(false);
|
|
|
|
for (const access of ['edit', 'upload'] as const) {
|
|
const off = setup(linkReplies({ caps: { status: 200, text: LINK_CAPS({ upload: false }) } }));
|
|
const e2 = await failure(off.service.create('t1', 'u1', { ...LINK_INPUT, access }));
|
|
expect(codeOf(e2)).toBe('shareAccessInvalid');
|
|
expect(off.calls.some((c) => c.method === 'POST')).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('Ordner ohne Schreibrecht (RG): Nur hochladen und Bearbeiten sind shareAccessInvalid', async () => {
|
|
for (const access of ['edit', 'upload'] as const) {
|
|
const { service, calls } = setup(
|
|
linkReplies({ stat: { status: 207, text: statXml('Projekte', true, 'RG') } }),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', { ...LINK_INPUT, access }));
|
|
expect(codeOf(e)).toBe('shareAccessInvalid');
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('Links ausgeschaltet: 409 linkSharingDisabled', async () => {
|
|
const { service, calls } = setup(
|
|
linkReplies({ caps: { status: 200, text: CAPS({ public: { enabled: false } }) } }),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', LINK_INPUT));
|
|
expect(codeOf(e)).toBe('linkSharingDisabled');
|
|
expect(statusOf(e)).toBe(409);
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('nur ein Link erlaubt und schon einer da: 409 shareAlreadyExists; mehrere erlaubt: keine Listenabfrage', async () => {
|
|
const one = setup(
|
|
linkReplies(
|
|
{
|
|
caps: { status: 200, text: LINK_CAPS({ multiple_links: false }) },
|
|
list: { status: 200, text: ocs([linkJson()]) },
|
|
},
|
|
true,
|
|
),
|
|
);
|
|
const e = await failure(one.service.create('t1', 'u1', LINK_INPUT));
|
|
expect(codeOf(e)).toBe('shareAlreadyExists');
|
|
expect(one.calls.some((c) => c.method === 'POST')).toBe(false);
|
|
|
|
const many = setup(linkReplies());
|
|
await many.service.create('t1', 'u1', LINK_INPUT);
|
|
expect(many.calls.some((c) => c.url === BY_PATH)).toBe(false);
|
|
});
|
|
|
|
it('Passwort verlangt und keins gesendet: 400 sharePasswordRequired nach nur zwei Aufrufen', async () => {
|
|
for (const password of [undefined, '']) {
|
|
const { service, calls } = setup(
|
|
linkReplies({
|
|
caps: { status: 200, text: LINK_CAPS({ password: { enforced: true } }) },
|
|
}),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', { ...LINK_INPUT, password }));
|
|
expect(codeOf(e)).toBe('sharePasswordRequired');
|
|
expect(statusOf(e)).toBe(400);
|
|
expect(calls.map((c) => c.method)).toEqual(['PROPFIND', 'GET']);
|
|
}
|
|
});
|
|
|
|
it('Ablauf verlangt und fehlend oder leer: 400 shareExpiryRequired ohne POST', async () => {
|
|
for (const expireDate of [undefined, '']) {
|
|
const { service, calls } = setup(
|
|
linkReplies({
|
|
caps: {
|
|
status: 200,
|
|
text: LINK_CAPS({ expire_date: { enabled: true, days: '7', enforced: true } }),
|
|
},
|
|
}),
|
|
);
|
|
const e = await failure(service.create('t1', 'u1', { ...LINK_INPUT, expireDate }));
|
|
expect(codeOf(e)).toBe('shareExpiryRequired');
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('Ablauf verlangt und ein Datum gesendet: das Datum steht im Koerper', async () => {
|
|
const { service, calls } = setup(
|
|
linkReplies({
|
|
caps: {
|
|
status: 200,
|
|
text: LINK_CAPS({ expire_date: { enabled: true, days: '7', enforced: true } }),
|
|
},
|
|
post: { status: 200, text: ocs(linkJson({ expiration: '2026-12-01 00:00:00' })) },
|
|
}),
|
|
);
|
|
const share = await service.create('t1', 'u1', { ...LINK_INPUT, expireDate: '2026-12-01' });
|
|
expect(calls[2].body).toContain('"expireDate":"2026-12-01"');
|
|
expect(share.expiration).toBe('2026-12-01');
|
|
});
|
|
|
|
it("kein echtes Datum ('2026-02-30'): 400 shareExpiryInvalid ohne POST", async () => {
|
|
const { service, calls } = setup(linkReplies());
|
|
const e = await failure(
|
|
service.create('t1', 'u1', { ...LINK_INPUT, expireDate: '2026-02-30' }),
|
|
);
|
|
expect(codeOf(e)).toBe('shareExpiryInvalid');
|
|
expect(statusOf(e)).toBe(400);
|
|
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
});
|
|
|
|
it('Passwort, Ablauf und Bezeichnung werden bei Personen und Gruppen nie weitergegeben', async () => {
|
|
const { service, calls } = setup(createReplies());
|
|
await service.create('t1', 'u1', {
|
|
...INPUT,
|
|
password: PW,
|
|
expireDate: '2026-12-01',
|
|
label: 'x',
|
|
});
|
|
expect(calls[3].body).toBe(
|
|
'{"path":"/Projekte","shareType":0,"shareWith":"ben","permissions":15}',
|
|
);
|
|
});
|
|
|
|
it('Person oder Gruppe ohne Empfaenger: 422 shareRecipientInvalid ohne Aufruf', async () => {
|
|
const { service, calls } = setup([]);
|
|
const e = await failure(
|
|
service.create('t1', 'u1', { path: '/Projekte', kind: 'user', access: 'view' }),
|
|
);
|
|
expect(codeOf(e)).toBe('shareRecipientInvalid');
|
|
expect(calls).toHaveLength(0);
|
|
});
|
|
|
|
it.each([
|
|
[400, 'Password is too short', { password: PW, expireDate: '' }, 'sharePasswordRejected', 400],
|
|
[
|
|
404,
|
|
'Expiration date is in the past',
|
|
{ password: PW, expireDate: '2026-12-01' },
|
|
'shareExpiryInvalid',
|
|
400,
|
|
],
|
|
[
|
|
403,
|
|
'Passwords are enforced for link and mail shares',
|
|
{ password: PW, expireDate: '' },
|
|
'shareRejected',
|
|
422,
|
|
],
|
|
[404, 'Could not create share', { password: PW, expireDate: '' }, 'notFound', 404],
|
|
] as const)('POST antwortet %i (%s) -> %s', async (nc, message, extra, code, http) => {
|
|
const { service } = setup(linkReplies({ post: { status: nc, text: ocs([], nc, message) } }));
|
|
const e = await failure(service.create('t1', 'u1', { ...LINK_INPUT, ...extra }));
|
|
expect(codeOf(e)).toBe(code);
|
|
expect(statusOf(e)).toBe(http);
|
|
expect([401, 403]).not.toContain(statusOf(e));
|
|
if (code !== 'notFound') expect(bodyOf(e).ncMessage).toBe(message);
|
|
expect(JSON.stringify(bodyOf(e))).not.toContain(PW);
|
|
});
|
|
});
|
|
|
|
describe('NextcloudFilesSharesService — Link aendern', () => {
|
|
const PUT31 = `PUT ${SHARES}/31`;
|
|
const linkWith = (over: Record<string, unknown> = {}) => ({
|
|
status: 200,
|
|
text: ocs([linkJson(over)]),
|
|
});
|
|
const caps = (pub: Record<string, unknown> = {}): Reply => ({
|
|
status: 200,
|
|
text: LINK_CAPS(pub),
|
|
});
|
|
|
|
it('sendet nur die geaenderten Felder', async () => {
|
|
const cases: Array<[Record<string, unknown>, string]> = [
|
|
[{ password: 'Neu-Pass-2026!x' }, '{"password":"Neu-Pass-2026!x"}'],
|
|
[{ expireDate: '' }, '{"expireDate":""}'],
|
|
[{ access: 'upload' }, '{"permissions":4}'],
|
|
[{ label: 'Angebot' }, '{"label":"Angebot"}'],
|
|
];
|
|
for (const [input, body] of cases) {
|
|
const { service, calls } = setup([
|
|
linkWith({ expiration: '2026-12-01 00:00:00' }),
|
|
caps(),
|
|
{ status: 200, text: ocs(linkJson()) },
|
|
]);
|
|
await service.update('t1', 'u1', '31', input);
|
|
expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
|
|
`GET ${SHARES}/31`,
|
|
`GET ${CAPS_URL}`,
|
|
PUT31,
|
|
]);
|
|
expect(calls[2].body).toBe(body);
|
|
}
|
|
});
|
|
|
|
it('unveraenderte Werte ergeben kein PUT', async () => {
|
|
const { service, calls } = setup([
|
|
linkWith({ expiration: '2026-12-01 00:00:00', label: 'Kunde', password: null }),
|
|
caps(),
|
|
]);
|
|
await service.update('t1', 'u1', '31', {
|
|
access: 'view',
|
|
expireDate: '2026-12-01',
|
|
label: 'Kunde',
|
|
password: '',
|
|
});
|
|
expect(calls.some((c) => c.method === 'PUT')).toBe(false);
|
|
});
|
|
|
|
it("Passwort '' bei verlangtem Passwort: 400 sharePasswordRequired ohne PUT", async () => {
|
|
const { service, calls } = setup([linkWith(), caps({ password: { enforced: true } })]);
|
|
const e = await failure(service.update('t1', 'u1', '31', { password: '' }));
|
|
expect(codeOf(e)).toBe('sharePasswordRequired');
|
|
expect(calls.some((c) => c.method === 'PUT')).toBe(false);
|
|
});
|
|
|
|
it("Ablauf '' bei verlangtem Ablauf: 400 shareExpiryRequired ohne PUT", async () => {
|
|
const { service, calls } = setup([
|
|
linkWith({ expiration: '2026-12-01 00:00:00' }),
|
|
caps({ expire_date: { enabled: true, days: '7', enforced: true } }),
|
|
]);
|
|
const e = await failure(service.update('t1', 'u1', '31', { expireDate: '' }));
|
|
expect(codeOf(e)).toBe('shareExpiryRequired');
|
|
expect(calls.some((c) => c.method === 'PUT')).toBe(false);
|
|
});
|
|
|
|
it("Passwort '' ohne Pflicht entfernt das Passwort (nur wenn eins gesetzt ist)", async () => {
|
|
const { service, calls } = setup([
|
|
linkWith(),
|
|
caps(),
|
|
{ status: 200, text: ocs(linkJson({ password: null })) },
|
|
]);
|
|
const share = await service.update('t1', 'u1', '31', { password: '' });
|
|
expect(calls[2].body).toBe('{"password":""}');
|
|
expect(share.hasPassword).toBe(false);
|
|
});
|
|
|
|
it('Links ausgeschaltet: 409 linkSharingDisabled', async () => {
|
|
const { service } = setup([
|
|
linkWith(),
|
|
{ status: 200, text: CAPS({ public: { enabled: false } }) },
|
|
]);
|
|
expect(codeOf(await failure(service.update('t1', 'u1', '31', { label: 'x' })))).toBe(
|
|
'linkSharingDisabled',
|
|
);
|
|
});
|
|
|
|
it('Bearbeiten oder Hochladen ohne erlaubtes Hochladen: shareAccessInvalid', async () => {
|
|
const { service } = setup([linkWith(), caps({ upload: false })]);
|
|
expect(codeOf(await failure(service.update('t1', 'u1', '31', { access: 'edit' })))).toBe(
|
|
'shareAccessInvalid',
|
|
);
|
|
});
|
|
|
|
it('kein echtes Datum: 400 shareExpiryInvalid ohne PUT', async () => {
|
|
const { service, calls } = setup([linkWith(), caps()]);
|
|
const e = await failure(service.update('t1', 'u1', '31', { expireDate: '2026-13-01' }));
|
|
expect(codeOf(e)).toBe('shareExpiryInvalid');
|
|
expect(calls.some((c) => c.method === 'PUT')).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
[400, { password: 'schwach' }, 'sharePasswordRejected', 400],
|
|
[400, { expireDate: '2027-01-01' }, 'shareExpiryInvalid', 400],
|
|
[404, { expireDate: '2027-01-01' }, 'shareExpiryInvalid', 400],
|
|
[404, { label: 'x' }, 'shareNotFound', 404],
|
|
[400, { label: 'x' }, 'shareRejected', 422],
|
|
[403, { label: 'x' }, 'shareRejected', 422],
|
|
] as const)('PUT antwortet %i mit %j -> %s', async (nc, input, code, http) => {
|
|
const { service } = setup([
|
|
linkWith(),
|
|
caps(),
|
|
{ status: nc, text: ocs([], nc, 'Failed to update share.') },
|
|
]);
|
|
const e = await failure(service.update('t1', 'u1', '31', input));
|
|
expect(codeOf(e)).toBe(code);
|
|
expect(statusOf(e)).toBe(http);
|
|
if (code !== 'shareNotFound') expect(bodyOf(e).ncMessage).toBe('Failed to update share.');
|
|
expect(JSON.stringify(bodyOf(e))).not.toContain('schwach');
|
|
});
|
|
|
|
it('bei Personenfreigaben werden Link-Felder verworfen', async () => {
|
|
const { service, calls } = setup([{ status: 200, text: ocs([shareJson()]) }]);
|
|
await service.update('t1', 'u1', '17', { password: 'x', expireDate: '2026-12-01', label: 'y' });
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('NextcloudFilesSharesService — Uebersichten und Annehmen', () => {
|
|
const received = (over: Record<string, unknown> = {}) =>
|
|
shareJson({
|
|
id: '40',
|
|
uid_owner: 'ben',
|
|
displayname_owner: 'Ben Beispiel',
|
|
share_with: 'anna',
|
|
path: '/Ben-Ordner',
|
|
file_target: '/Ben-Ordner',
|
|
permissions: 1,
|
|
can_edit: false,
|
|
...over,
|
|
});
|
|
|
|
it('mine: GET auf die Freigaben ohne Abfrage, Arten 0/1/3, andere nur als Zahl', async () => {
|
|
const { service, calls } = setup([
|
|
{
|
|
status: 200,
|
|
text: ocs([shareJson(), linkJson(), { share_type: 4, id: '5' }]),
|
|
},
|
|
]);
|
|
const out = await service.mine('t1', 'u1');
|
|
expect(calls).toHaveLength(1);
|
|
expect(`${calls[0].method} ${calls[0].url}`).toBe(`GET ${SHARES}`);
|
|
expect(out.shares.map((s) => s.kind)).toEqual(['user', 'link']);
|
|
expect(out.hidden).toBe(1);
|
|
expect(out.truncated).toBe(false);
|
|
});
|
|
|
|
it('received: angenommene plus offene Freigaben, nur Arten 0/1, offene mit pending', async () => {
|
|
const { service, calls } = setup([
|
|
{
|
|
status: 200,
|
|
text: ocs([
|
|
received(),
|
|
received({ id: '41', share_type: 1 }),
|
|
linkJson({ uid_owner: 'ben' }),
|
|
]),
|
|
},
|
|
{ status: 200, text: ocs([received({ id: '42', file_target: '/Ben-Briefkasten' })]) },
|
|
]);
|
|
const out = await service.received('t1', 'u1');
|
|
expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
|
|
`GET ${SHARES}?shared_with_me=true`,
|
|
`GET ${SHARES}/pending`,
|
|
]);
|
|
expect(out.shares.map((s) => s.id)).toEqual(['40', '41']);
|
|
expect(out.pending.map((s) => [s.id, s.pending])).toEqual([['42', true]]);
|
|
expect(out.pending[0].name).toBe('Ben-Briefkasten');
|
|
expect(out.shares.every((s) => s.pending === undefined)).toBe(true);
|
|
expect(out.hidden).toBe(1);
|
|
});
|
|
|
|
it('received: eine offene Freigabe steht nicht doppelt bei den angenommenen', async () => {
|
|
const { service } = setup([
|
|
{ status: 200, text: ocs([received()]) },
|
|
{ status: 200, text: ocs([received()]) },
|
|
]);
|
|
const out = await service.received('t1', 'u1');
|
|
expect(out.shares).toHaveLength(0);
|
|
expect(out.pending.map((s) => s.id)).toEqual(['40']);
|
|
});
|
|
|
|
it.each([
|
|
404, 405,
|
|
])('received: die offene Liste antwortet %i -> leer, kein Fehler', async (nc) => {
|
|
const { service } = setup([
|
|
{ status: 200, text: ocs([received()]) },
|
|
{ status: nc, text: ocs([], nc, 'nope') },
|
|
]);
|
|
const out = await service.received('t1', 'u1');
|
|
expect(out.pending).toEqual([]);
|
|
expect(out.shares).toHaveLength(1);
|
|
});
|
|
|
|
it('received: 500 bei der offenen Liste ist ein Fehler, 401 markiert die Verbindung', async () => {
|
|
const boom = setup([
|
|
{ status: 200, text: ocs([]) },
|
|
{ status: 500, text: ocs([], 500, 'x') },
|
|
]);
|
|
expect(codeOf(await failure(boom.service.received('t1', 'u1')))).toBe('nextcloudError');
|
|
const dead = setup([{ status: 401 }]);
|
|
expect(codeOf(await failure(dead.service.received('t1', 'u1')))).toBe('connectionExpired');
|
|
expect(dead.account.markExpired).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('accept: POST auf pending/<Kennung>', async () => {
|
|
const { service, calls } = setup([{ status: 200, text: ocs([]) }]);
|
|
await expect(service.accept('t1', 'u1', '17')).resolves.toEqual({ accepted: true });
|
|
expect(`${calls[0].method} ${calls[0].url}`).toBe(`POST ${SHARES}/pending/17`);
|
|
expect(calls[0].body).toBeNull();
|
|
});
|
|
|
|
it('accept: 404 -> shareNotFound, falsche Kennung ohne Aufruf', async () => {
|
|
const nf = setup([{ status: 404, text: ocs([], 404, 'Wrong share ID') }]);
|
|
expect(codeOf(await failure(nf.service.accept('t1', 'u1', '17')))).toBe('shareNotFound');
|
|
const bad = setup([]);
|
|
expect(codeOf(await failure(bad.service.accept('t1', 'u1', 'x')))).toBe('shareNotFound');
|
|
expect(bad.calls).toHaveLength(0);
|
|
});
|
|
});
|