mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(docsync): a paused binding no longer resets its own settings
PATCH links validated its body with a schema derived from the create contract through .partial(). In Zod 4 a .default() still fires inside an optional field, so a patch naming one setting arrived with every other one filled in: pausing a pull-only binding switched it to two-way, reset both policies and blanked its label. The update contract is now written out without defaults, and an e2e test drives a patch through the real pipe, which the controller unit tests never see. Also, the cosmetic leftovers from the review: - POST connections/:connectionId/scopes no longer wants the connection in the body as well. It was required, never read, and let one request name two connections; a client that still sends it has the key stripped rather than refused. - createLink loses the scope argument every caller passed as null. - The controller's owner check is now assertCanManage, because it lets instance admins through just as canManageDocSync does on the client. Who may manage a binding is unchanged. - document_provider_fields drops settings_key and payload_key. Nothing read them: the connect form takes its fields from /docsync/providers and the admin shelf sends none for document providers. - The e2e test imports the provider classes under their new names.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { docsyncLinkInputSchema, docsyncLinkUpdateSchema, docsyncScopeCreateSchema } from './docsync.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('docsyncScopeCreateSchema', () => {
|
||||
it('takes only a name, because the connection is in the path', () => {
|
||||
expect(docsyncScopeCreateSchema.parse({ name: ' Norway ' })).toEqual({ name: 'Norway' });
|
||||
});
|
||||
|
||||
it('strips a connection id sent in the body instead of refusing the request', () => {
|
||||
expect(docsyncScopeCreateSchema.parse({ connectionId: 5, name: 'Norway' })).toEqual({ name: 'Norway' });
|
||||
});
|
||||
|
||||
it('rejects a blank or oversized name', () => {
|
||||
expect(docsyncScopeCreateSchema.safeParse({ name: ' ' }).success).toBe(false);
|
||||
expect(docsyncScopeCreateSchema.safeParse({ name: 'x'.repeat(201) }).success).toBe(false);
|
||||
expect(docsyncScopeCreateSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsyncLinkInputSchema', () => {
|
||||
it('needs the connection in the body, since POST /links has no connection in its path', () => {
|
||||
expect(docsyncLinkInputSchema.safeParse({ scopeKey: 'tag:1' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('fills the safe defaults for everything the picker leaves out', () => {
|
||||
expect(docsyncLinkInputSchema.parse({ connectionId: 3, scopeKey: 'tag:1' })).toEqual({
|
||||
connectionId: 3,
|
||||
scopeKey: 'tag:1',
|
||||
remoteLabel: '',
|
||||
direction: 'both',
|
||||
deletePolicy: 'unlink',
|
||||
conflictPolicy: 'manual',
|
||||
syncEnabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsyncLinkUpdateSchema', () => {
|
||||
it('passes on only the fields the patch named, with no defaults filled in', () => {
|
||||
// Defaults here would reach updateLink as real values: pausing a pull-only
|
||||
// binding used to switch it to two-way and blank its label.
|
||||
expect(docsyncLinkUpdateSchema.parse({ syncEnabled: false })).toEqual({ syncEnabled: false });
|
||||
expect(docsyncLinkUpdateSchema.parse({})).toEqual({});
|
||||
});
|
||||
|
||||
it('still checks the values it is given', () => {
|
||||
expect(docsyncLinkUpdateSchema.safeParse({ direction: 'sideways' }).success).toBe(false);
|
||||
expect(docsyncLinkUpdateSchema.safeParse({ syncEnabled: 'no' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('drops the connection and the anchor, which are fixed once a binding exists', () => {
|
||||
const parsed = docsyncLinkUpdateSchema.parse({
|
||||
connectionId: 9,
|
||||
scopeKey: 'tag:2',
|
||||
remoteRootId: '2',
|
||||
remoteRootPath: '/elsewhere',
|
||||
deletePolicy: 'trash',
|
||||
});
|
||||
expect(parsed).toEqual({ deletePolicy: 'trash' });
|
||||
});
|
||||
});
|
||||
@@ -105,9 +105,13 @@ export type DocsyncScopeOption = z.infer<typeof docsyncScopeOptionSchema>;
|
||||
* Creating a scope rather than picking one. Only providers with a container
|
||||
* concept accept this (a WebDAV folder, an OpenCloud space, a Paperless tag,
|
||||
* a Papra tag); the adapter reports whether it can.
|
||||
*
|
||||
* The connection comes from the path (`connections/:connectionId/scopes`) and
|
||||
* is not repeated here. A copy in the body was never read and only let one
|
||||
* request name two different connections; a client that still sends it has
|
||||
* the key stripped rather than refused.
|
||||
*/
|
||||
export const docsyncScopeCreateSchema = z.object({
|
||||
connectionId: idSchema,
|
||||
/** Name for the new folder, tag or space. */
|
||||
name: z.string().trim().min(1).max(200),
|
||||
});
|
||||
@@ -155,9 +159,22 @@ export type DocsyncLinkInput = z.infer<typeof docsyncLinkInputSchema>;
|
||||
* it would leave them all pointing somewhere else. The root fields are left out
|
||||
* rather than ignored — they were accepted and silently dropped, so a client
|
||||
* sending one was told the move had happened.
|
||||
*
|
||||
* Spelled out instead of derived from the input schema with `.partial()`. In
|
||||
* Zod 4 a `.default()` still fires inside an optional field, so the derived
|
||||
* version filled in every default the patch left out: `{ syncEnabled: false }`
|
||||
* arrived as a full set of settings and reset direction, both policies and the
|
||||
* label with it. A pull-only binding turned two-way the moment somebody paused
|
||||
* it.
|
||||
*/
|
||||
export const docsyncLinkUpdateSchema = docsyncLinkInputSchema
|
||||
.omit({ connectionId: true, scopeKey: true, remoteRootId: true, remoteRootPath: true })
|
||||
export const docsyncLinkUpdateSchema = z
|
||||
.object({
|
||||
remoteLabel: z.string().max(512),
|
||||
direction: docsyncDirectionSchema,
|
||||
deletePolicy: docsyncDeletePolicySchema,
|
||||
conflictPolicy: docsyncConflictPolicySchema,
|
||||
syncEnabled: z.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
// ── Sync state ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user