Artifact 6c8283841cdece47153ad8ca27048ad06bcdc4bd1c2b5d654cc4049396415dc9:
- File
conformance/sync_refusal_test.go
— part of check-in
[8cc3d7c5e4]
at
2026-10-13 02:23:50
on branch test/conformance-01-drafts
— test(conformance): check the -01 drafts of artifact, repo-state and sync
Re-pin the conformance suite to draft-fossil-artifact-format-01, draft-fossil-repo-state-01 and draft-fossil-sync-protocol-01, copied byte for byte from zit check-in 100351e26c. The delta format has no -01 and stays on -00. The -00 errata are folded into -01 and are no longer pinned; the six -01 errata are pinned for the fossil 2.28 behavior they record. conformance/rfcs/README.md records where each copy came from.
The requirement tables are re-read against -01, and new cases cover the date tag's event time (repo-state ยง4.2 step 6), the Julian-day grammar, a delta manifest naming a delta, dropped reserved F cards, the trailing-punctuation retry, truncated payloads, errors N, a file card after a refused push, the end of a request after a failed login, and the answer to a gimme for private content.
No library behavior changes. Where -01 departs from fossil 2.28 and go-libfossil follows fossil (a cancelled, unparsable or 0 date tag), the cases are marked as deviations. The non-conformances the re-pin exposes are marked as gaps, tracked in #370, #371, #372, #373, #374 and #375. (user: [email protected], size: 5623) [annotate] [blame] [check-ins using] [more...]
package conformance // Cases for what draft-fossil-sync-protocol-01 adds on refusals and their // bookkeeping: a payload cut short (Section 5.1), the error count of the // closing comment (5.3), the end of a request after a failed login (6.4), a // refused push (6.6) and a gimme for private content outside a private sync // (8.6). The reference behavior is fossil(1) 2.28 and the -01 errata. // readOnly gives nobody Read alone, so a pull is accepted and a push is not. var readOnly = []string{"UPDATE user SET cap='o' WHERE login='nobody'"} // privateA1 marks A1 private on the server. var privateA1 = []string{"INSERT INTO private(rid) SELECT rid FROM blob WHERE uuid='{A1}'"} // pullOnlyMessage is the push refusal that follows an accepted pull. const pullOnlyMessage = `message pull\sonly\s-\snot\sauthorized\sto\spush` // errorsCount checks the closing comment's error count. func errorsCount(n string) replyCheck { return matches("# timestamp ", ` errors `+n+`$`) } func serverRefusalCases() []syncServerCase { return append(serverErrorCountCases(), []syncServerCase{{ // The body ends inside the payload: the card is malformed, and // nothing is stored, though the bytes present are N1 in full. Case: Case{ID: "sync.5.1.truncated-payload-server", Req: "SP-5.1-3"}, body: []string{"{CV}", pushCard, "file {N1} 100000", "{N1.data}"}, want: all(completed, onlyError("malformed+file+line", "malformed file line"), held("N1", 0)), fossil: "fossil(1) 2.28 stores the bytes present, which hash to N1 (erratum " + "sync-protocol-01-s5.1-truncated-payload-receiver)", }, { // The login failure ends card parsing, but the pull before it // still gets its igot inventory and the reply its closing comment. Case: Case{ID: "sync.6.4.failure-end-of-request", Req: "SP-6.4-8"}, body: []string{"{CV}", pullCard, "{LOGIN alice wrong}", "{R}"}, want: all(completed, onlyError("login failed"), withCard("igot {A1}")), }, { // A1 is private and the request is not a private sync: a peer dated // 2020-04-13 or later is told the artifact exists and is private. Case: Case{ID: "sync.8.6.private-gimme-igot", Req: "SP-8.6-2"}, setup: privateA1, body: []string{"{CV}", pullCard, "gimme {A1}", "{R}"}, want: all(completed, noErrors, withCard("igot {A1} 1"), noCard("file {A1}")), }, { Case: Case{ID: "sync.8.6.private-gimme-old-peer", Req: "SP-8.6-2"}, setup: privateA1, body: []string{"pragma client-version 21000 20200101 000000", pullCard, "gimme {A1}", "{R}"}, want: all(completed, noErrors, noCard("igot {A1} 1"), noCard("file {A1}")), }, { Case: Case{ID: "sync.6.6.pull-only-message", Req: "SP-6.6-2"}, setup: readOnly, body: []string{"{CV}", pullCard, pushCard, "{R}"}, want: all(completed, noErrors, withCard(pullOnlyMessage), withCard("igot {A1}")), }, { // Without a pull before it, the refusal resets and is an error, // and the pull and gimme after it are still processed. Case: Case{ID: "sync.6.6.push-refused-continues", Req: "SP-6.6-2"}, setup: readOnly, body: []string{"{CV}", pushCard, pullCard, "gimme {A1}", "{R}"}, want: all(completed, onlyError("not authorized to write", "push denied"), withCard("file {A1} {A1.size}")), }, { // The file card after the refused push ends card processing: the // gimme after it is not answered, but the pull's inventory is sent. Case: Case{ID: "sync.6.6.file-after-refused-push", Req: "SP-6.6-3"}, setup: readOnly, body: []string{"{CV}", pullCard, pushCard, "file {N1} {N1.size}", "{N1.data}", "gimme {A1}", "{R}"}, want: all(completed, onlyError("not authorized to write", "push denied"), held("N1", 0), noCard("file {A1}"), withCard("igot {A1}")), }}...) } // serverErrorCountCases are Section 5.3's N: errors raised, not error cards // present, and three refusals that are not counted. func serverErrorCountCases() []syncServerCase { return []syncServerCase{{ // The refused file card resets the push refusal's card away, but // both errors are counted. Case: Case{ID: "sync.5.3.errors-counts-reset", Req: "SP-5.3-4"}, setup: closed, body: []string{"{CV}", pushCard, "file {N1} {N1.size}", "{N1.data}", "{R}"}, want: all(completed, onlyError("not authorized to write", "push denied"), errorsCount("2")), }, { Case: Case{ID: "sync.5.3.errors-skips-bad-command", Req: "SP-5.3-4"}, body: []string{"{CV}", pullCard, "frobnicate", "{R}"}, want: all(completed, onlyError("bad command"), errorsCount("0")), }, { Case: Case{ID: "sync.5.3.errors-skips-private-refusal", Req: "SP-5.3-4"}, body: []string{"{CV}", pullCard, "pragma send-private", "{R}"}, want: all(completed, onlyError("not authorized to sync private content"), errorsCount("0")), }} } // clientRefusalCases are the client's side of Sections 5.1 and 5.3. func clientRefusalCases() []syncClientCase { return []syncClientCase{{ // The reply ends inside N3's payload: N1 before it is kept, the // short N3 is not stored. Case: Case{ID: "sync.5.1.truncated-payload", Req: "SP-5.1-4"}, op: opPull, replies: [][]string{{"{SV}", "file {N1} {N1.size}", "{N1.data}", "file {N3} 100000", "{N3.data}"}}, want: every(stored("N1"), notStored("N3")), fc: true, fossil: "fossil(1) 2.28 stores the bytes present, which hash to N3 (erratum " + "sync-protocol-01-s5.1-truncated-payload-receiver)", }, { Case: Case{ID: "sync.5.3.errors-not-acted-on", Req: "SP-5.3-5"}, op: opPull, replies: [][]string{{"{SV}", "file {N1} {N1.size}", "{N1.data}", "# timestamp 2026-01-01T00:00:00 errors 7"}}, want: every(noFailure, stored("N1")), fc: true, }} }