c1d0eb3891
Same discriminant as the Android client - "/api/v1/repos/" in the URL - so one address tells both ends which transport to speak, and nothing new appears in Settings beyond a placeholder and a warning. The bridge stays shared. Three things differ, and they are confined: the header is "token <PAT>" rather than Basic (Gitea answers a silent 401 to the other), a PUT carries a JSON envelope with the file base64-encoded inside it, and the connection test uses GET because the contents API does not answer HEAD. The sha does the ETag's whole job. It comes back from the pull, rides in the PUT body instead of an If-Match, and a stale one earns a 409 - handled exactly like WebDAV's 412, by re-running pull->merge->push so the other device's changes are folded in rather than clobbered. On a first push `sha` is OMITTED rather than sent empty: Gitea reads its presence as "replace that exact version". `branch` is omitted too, so the repo's default is used instead of a name we would be guessing. A failing PUT now reads its response body: Gitea explains a 409 or a 422 there, and without it the error was just a number. The test button checks the SHAPE of what came back, not only the status. A URL one path segment off answers 200 with valid JSON that is not a file - which would otherwise surface much later as "wrong sync password" and send you looking on the wrong side. Four tests cover that parsing, including the line-wrapped base64 Gitea sends and the array an accidental directory URL returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
3.6 KiB
JavaScript
76 lines
3.6 KiB
JavaScript
// Le transport Gitea : reconnaissance de l'URL, et lecture de la reponse de
|
|
// l'API des contenus.
|
|
//
|
|
// POURQUOI CES DEUX-LA. `giteaUnwrap` est le seul endroit du chemin de sync qui
|
|
// interprete du JSON venu du reseau, et il decide de ce qui sera ensuite
|
|
// dechiffre. Une URL a un segment pres rend du JSON valide qui n'est PAS un
|
|
// fichier ; s'il passait, l'erreur ne se verrait qu'au message
|
|
// « mot de passe de sync incorrect », qui envoie chercher la cause ailleurs.
|
|
//
|
|
// Le `sha` compte autant que le contenu : il repart en `sha` au push, et c'est
|
|
// lui qui fait refuser une ecriture concurrente. Le rendre vide plutot
|
|
// qu'absent ferait ecraser la version d'un autre appareil.
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const { loadApp } = require('./harness');
|
|
|
|
function b64(text) {
|
|
return Buffer.from(text, 'utf8').toString('base64');
|
|
}
|
|
|
|
test('syncIsGitea: recognises a contents-API URL, ignores WebDAV', async () => {
|
|
const { __test: t } = await loadApp();
|
|
assert.equal(t.syncIsGitea(
|
|
'https://git.example.com/api/v1/repos/Zaki/pm_sync/contents/vault-sync.json'), true);
|
|
// Casse indifferente : l'URL est saisie a la main.
|
|
assert.equal(t.syncIsGitea(
|
|
'https://git.example.com/API/V1/REPOS/Zaki/pm_sync/contents/v.json'), true);
|
|
// Une URL de depot n'est pas une URL d'API, et elle ne rendrait pas de sha.
|
|
assert.equal(t.syncIsGitea('https://git.example.com/Zaki/pm_sync'), false);
|
|
assert.equal(t.syncIsGitea('https://dav.example.com/remote.php/vault.json'), false);
|
|
assert.equal(t.syncIsGitea(''), false);
|
|
assert.equal(t.syncIsGitea(undefined), false);
|
|
});
|
|
|
|
test('giteaUnwrap: returns the file and its sha', async () => {
|
|
const { __test: t } = await loadApp();
|
|
const container = '{"version":1,"cipher":"AAAA"}';
|
|
const un = t.giteaUnwrap(JSON.stringify({
|
|
content: b64(container), encoding: 'base64', sha: 'abc123', name: 'vault-sync.json'
|
|
}));
|
|
assert.ok(un, 'devrait accepter une reponse bien formee');
|
|
assert.equal(un.container, container);
|
|
assert.equal(un.sha, 'abc123');
|
|
});
|
|
|
|
test('giteaUnwrap: accepts the line-wrapped base64 Gitea sends', async () => {
|
|
const { __test: t } = await loadApp();
|
|
const container = '{"version":1}';
|
|
// Gitea coupe son base64 en lignes ; un decodeur strict s'y casse.
|
|
const wrapped = b64(container).replace(/(.{4})/, '$1\r\n');
|
|
const un = t.giteaUnwrap(JSON.stringify({
|
|
content: wrapped, encoding: 'base64', sha: 'deadbeef'
|
|
}));
|
|
assert.ok(un, 'les retours a la ligne ne doivent pas faire echouer le decodage');
|
|
assert.equal(un.container, container);
|
|
});
|
|
|
|
test('giteaUnwrap: refuses anything that is not a file', async () => {
|
|
const { __test: t } = await loadApp();
|
|
// Ce que rend une URL a un segment pres : du JSON valide, sans fichier.
|
|
assert.equal(t.giteaUnwrap('{"message":"Not found"}'), null);
|
|
// Un repertoire : l'API rend un TABLEAU, pas un objet.
|
|
assert.equal(t.giteaUnwrap('[{"name":"vault-sync.json","sha":"x"}]'), null);
|
|
// Un sha sans contenu ne permet rien de dechiffrer.
|
|
assert.equal(t.giteaUnwrap('{"sha":"abc"}'), null);
|
|
// Un contenu sans sha interdirait l'ecriture concurrente : refuse aussi.
|
|
assert.equal(t.giteaUnwrap(JSON.stringify({ content: b64('{}') })), null);
|
|
// Un encodage que nous ne savons pas lire.
|
|
assert.equal(t.giteaUnwrap(JSON.stringify({
|
|
content: 'x', encoding: 'utf-8', sha: 'abc' })), null);
|
|
// Pas du JSON du tout - une page d'erreur HTML, par exemple.
|
|
assert.equal(t.giteaUnwrap('<html>502</html>'), null);
|
|
assert.equal(t.giteaUnwrap(''), null);
|
|
});
|