08512ad58b
Two review threads from PR 4, and the answer to the third. The routine status was a bare string with its legal set in a comment. Nothing caught a typo at compile time, nothing enumerated the set for a test, and a bad value surfaced as a /routines row that neither accepts nor dismisses. It is a RoutineStatus now, with the three constants, a RoutineStatuses slice as the single source of truth, and Valid(). Listing by an unknown status is refused with ErrRoutineStatus instead of answering "no rows", which is what a correct query says about an empty table. A round-trip test moves a routine into each state and reads it back, so a constant that drifts from the inline SQL fails loudly. The hand-rolled framing stays, and frame.go now says why: ninety lines, readable with socat, and every standard replacement brings schema machinery this boundary does not want. What was wrong was inheriting it untested. frame_test.go covers the paths a real socket produces and the round-trip test never does — truncated header, truncated body, one byte per Read, two frames back to back, and a non-JSON body. Empty input is the only EOF. The unanswered question in the same file is answered in place: a routine object stays a local string, not a Nexus ref, because nothing acts on it. It is the word he used, replayed back to him, compared only against itself for the UNIQUE key. Canonical refs arrive if a routine ever drives a Hexis call, which is V-272. The mood enum has the same shape and is not done here: it is spelled in the GBNF grammar, three prompts and the parse, so it is its own change.
143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package ipc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
// The framing is hand-rolled, and it is the protocol all nine daemons depend
|
|
// on, so a bug in it is a bug everywhere (Vikunja #410). The review asked
|
|
// whether to replace it with something standard. It stays: length-prefixed
|
|
// JSON over a unix socket is ninety lines, it is readable with socat, and the
|
|
// alternatives (net/rpc, gRPC, a codec library) all buy schema machinery this
|
|
// boundary does not want. What was wrong was inheriting it untested. These
|
|
// are the paths a real socket produces that the round-trip test never does.
|
|
|
|
// A short header is not EOF. EOF means the peer closed cleanly between
|
|
// frames, which the server treats as a normal disconnect; a header that stops
|
|
// halfway is a truncated frame and must be reported as an error, or a peer
|
|
// that dies mid-write looks like one that hung up politely.
|
|
func TestReadFrameTruncatedHeader(t *testing.T) {
|
|
var v any
|
|
err := readFrame(bytes.NewReader([]byte{0, 0, 4}), &v)
|
|
if err == nil {
|
|
t.Fatal("a three-byte header must fail")
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
t.Fatalf("err = %v, want a truncation error, not EOF", err)
|
|
}
|
|
}
|
|
|
|
// A header promising more body than follows. Same reasoning: the frame never
|
|
// arrived, so it must not decode into a zero value the caller then trusts.
|
|
func TestReadFrameTruncatedBody(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
var hdr [4]byte
|
|
binary.BigEndian.PutUint32(hdr[:], 32)
|
|
buf.Write(hdr[:])
|
|
buf.WriteString(`{"m":"pi`)
|
|
|
|
var got map[string]string
|
|
if err := readFrame(&buf, &got); err == nil {
|
|
t.Fatal("a body shorter than its prefix must fail")
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("decoded %v from a truncated frame", got)
|
|
}
|
|
}
|
|
|
|
// Nothing at all is EOF, and only this is.
|
|
func TestReadFrameEmptyIsEOF(t *testing.T) {
|
|
var v any
|
|
if err := readFrame(bytes.NewReader(nil), &v); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("err = %v, want io.EOF", err)
|
|
}
|
|
}
|
|
|
|
// byteAtATime returns one byte per Read, which is what a socket is allowed to
|
|
// do and what a bytes.Reader never does. readFrame uses io.ReadFull for both
|
|
// the header and the body; this is the test that would fail if either turned
|
|
// into a bare Read.
|
|
type byteAtATime struct {
|
|
b []byte
|
|
i int
|
|
}
|
|
|
|
func (r *byteAtATime) Read(p []byte) (int, error) {
|
|
if r.i >= len(r.b) {
|
|
return 0, io.EOF
|
|
}
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
p[0] = r.b[r.i]
|
|
r.i++
|
|
return 1, nil
|
|
}
|
|
|
|
func TestReadFrameReassemblesPartialReads(t *testing.T) {
|
|
type payload struct {
|
|
Msg string `json:"m"`
|
|
N int `json:"n"`
|
|
}
|
|
want := payload{Msg: "привет", N: 7}
|
|
var buf bytes.Buffer
|
|
if err := writeFrame(&buf, want); err != nil {
|
|
t.Fatalf("writeFrame: %v", err)
|
|
}
|
|
|
|
var got payload
|
|
if err := readFrame(&byteAtATime{b: buf.Bytes()}, &got); err != nil {
|
|
t.Fatalf("readFrame: %v", err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
// Two frames written back to back must come back as two frames. A reader that
|
|
// consumed more than one frame's body would desynchronize the connection, and
|
|
// the symptom would be a reply attributed to the wrong request.
|
|
func TestReadFrameStopsAtTheFrameBoundary(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
for _, m := range []string{"first", "second"} {
|
|
if err := writeFrame(&buf, map[string]string{"m": m}); err != nil {
|
|
t.Fatalf("writeFrame: %v", err)
|
|
}
|
|
}
|
|
r := &byteAtATime{b: buf.Bytes()}
|
|
for _, want := range []string{"first", "second"} {
|
|
var got map[string]string
|
|
if err := readFrame(r, &got); err != nil {
|
|
t.Fatalf("readFrame(%s): %v", want, err)
|
|
}
|
|
if got["m"] != want {
|
|
t.Fatalf("got %q, want %q", got["m"], want)
|
|
}
|
|
}
|
|
var extra map[string]string
|
|
if err := readFrame(r, &extra); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("after two frames: err = %v, want io.EOF", err)
|
|
}
|
|
}
|
|
|
|
// A body that is not JSON is an error, not a zero value. The peer is either
|
|
// broken or not speaking this protocol; either way the caller must not read
|
|
// on as though it decoded.
|
|
func TestReadFrameRejectsNonJSONBody(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
var hdr [4]byte
|
|
body := []byte("not json at all")
|
|
binary.BigEndian.PutUint32(hdr[:], uint32(len(body)))
|
|
buf.Write(hdr[:])
|
|
buf.Write(body)
|
|
|
|
var got map[string]string
|
|
if err := readFrame(&buf, &got); err == nil {
|
|
t.Fatal("a non-JSON body must fail")
|
|
}
|
|
}
|