Artifact bc63f1a6aa2baedb9526c723944d111a1f6f58da83e8eb948126f7b7bca9da1e:
- File
conformance/sync_refusal_test.go
— part of check-in
[161d6ab68f]
at
2026-10-13 05:26:26
on branch main
— fix(sync): cover the sync-protocol-01 coverage rows (#394)
Check the remaining partial and uncovered sync rows, and fix the client private-gimme answer, the unversioned-file NAME encoding, the check-in lock table, the uv-hash name form and the uvgimme round limit they exposed. Tracked in #392. (user: [email protected], size: 5834) [annotate] [blame] [check-ins using]
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"), 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"), 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"), 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")), }, { // A1 is named by SHA3 and the client predates it. Case: Case{ID: "sync.5.3.errors-skips-sha3-refusal", Req: "SP-5.3-4"}, body: []string{"{CV0}", pullCard, "gimme {A1}", "{R}"}, want: all(completed, onlyError(sha3Refused), 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, }} }