package usbip import ( "bytes" "encoding/binary" "io" "net" "testing" "time" "escpos-emulator/internal/escpos" ) func startServer(t *testing.T) (*Server, *escpos.Printer) { t.Helper() prof, _ := escpos.FindProfile("80mm") p, err := escpos.NewPrinter(prof) if err != nil { t.Fatal(err) } s, err := Listen("127.0.0.1:0", DeviceFromProfile(prof), p) if err != nil { t.Fatal(err) } t.Cleanup(func() { s.Close() }) return s, p } func dial(t *testing.T, s *Server) net.Conn { t.Helper() c, err := net.Dial("tcp", s.Addr().String()) if err != nil { t.Fatal(err) } _ = c.SetDeadline(time.Now().Add(5 * time.Second)) t.Cleanup(func() { c.Close() }) return c } func readN(t *testing.T, c net.Conn, n int) []byte { t.Helper() b := make([]byte, n) if _, err := io.ReadFull(c, b); err != nil { t.Fatalf("leyendo %d bytes: %v", n, err) } return b } func TestDevlist(t *testing.T) { s, _ := startServer(t) c := dial(t, s) c.Write([]byte{0x01, 0x11, 0x80, 0x05, 0, 0, 0, 0}) h := readN(t, c, 12) if binary.BigEndian.Uint16(h[2:]) != opRepDevlist || binary.BigEndian.Uint32(h[8:]) != 1 { t.Fatalf("cabecera DEVLIST inesperada: % X", h) } dev := readN(t, c, 312+4) if got := string(bytes.TrimRight(dev[256:288], "\x00")); got != BusID { t.Fatalf("busid = %q", got) } if !bytes.Equal(dev[312:], []byte{7, 1, 2, 0}) { t.Fatalf("interfaz = % X, want clase impresora 07 01 02", dev[312:]) } } type client struct { t *testing.T c net.Conn seq uint32 isIn map[uint32]bool } func attach(t *testing.T, s *Server) *client { c := dial(t, s) req := []byte{0x01, 0x11, 0x80, 0x03, 0, 0, 0, 0} bus := make([]byte, 32) copy(bus, BusID) c.Write(append(req, bus...)) h := readN(t, c, 8) if binary.BigEndian.Uint32(h[4:]) != 0 { t.Fatalf("IMPORT rechazado: % X", h) } readN(t, c, 312) return &client{t: t, c: c, isIn: map[uint32]bool{}} } func (cl *client) submit(dir, ep uint32, length int, setup []byte, data []byte) uint32 { cl.seq++ cl.isIn[cl.seq] = dir == dirIn h := make([]byte, 48) binary.BigEndian.PutUint32(h[0:], cmdSubmit) binary.BigEndian.PutUint32(h[4:], cl.seq) binary.BigEndian.PutUint32(h[8:], 0x00010002) binary.BigEndian.PutUint32(h[12:], dir) binary.BigEndian.PutUint32(h[16:], ep) binary.BigEndian.PutUint32(h[24:], uint32(length)) binary.BigEndian.PutUint32(h[32:], 0xFFFFFFFF) copy(h[40:], setup) cl.c.Write(append(h, data...)) return cl.seq } func (cl *client) ret() (seq uint32, status int32, data []byte) { h := readN(cl.t, cl.c, 48) seq = binary.BigEndian.Uint32(h[4:]) status = int32(binary.BigEndian.Uint32(h[20:])) if n := int(binary.BigEndian.Uint32(h[24:])); cl.isIn[seq] && n > 0 { data = readN(cl.t, cl.c, n) } return } func TestControlAndBulk(t *testing.T) { s, p := startServer(t) cl := attach(t, s) // GET_DESCRIPTOR(DEVICE) cl.submit(dirIn, 0, 18, []byte{0x80, 6, 0, 1, 0, 0, 18, 0}, nil) _, st, d := cl.ret() if st != 0 || len(d) != 18 || d[1] != 1 { t.Fatalf("descriptor de dispositivo: status %d, % X", st, d) } if vid := binary.LittleEndian.Uint16(d[8:]); vid != 0x1209 { t.Fatalf("VID = %04X", vid) } // GET_DESCRIPTOR(CONFIGURATION) con wLength 9: solo la cabecera cl.submit(dirIn, 0, 9, []byte{0x80, 6, 0, 2, 0, 0, 9, 0}, nil) _, _, d = cl.ret() if len(d) != 9 || d[2] != 32 { t.Fatalf("configuración: % X", d) } // GET_DEVICE_ID de la clase impresora cl.submit(dirIn, 0, 1024, []byte{0xA1, 0, 0, 0, 0, 0, 0, 4}, nil) _, _, d = cl.ret() if !bytes.Contains(d, []byte("CMD:ESC/POS")) { t.Fatalf("GET_DEVICE_ID = %q", d) } // DEVICE_QUALIFIER debe dar STALL cl.submit(dirIn, 0, 10, []byte{0x80, 6, 0, 6, 0, 0, 10, 0}, nil) if _, st, _ = cl.ret(); st != errPipe { t.Fatalf("DEVICE_QUALIFIER status = %d, want %d", st, errPipe) } // Lectura bulk IN pendiente antes de que haya datos inSeq := cl.submit(dirIn, epBulkIn, 64, nil, nil) // Bulk OUT: texto + DLE EOT 1 payload := []byte("\x1b@Hola USB\n\x10\x04\x01") outSeq := cl.submit(dirOut, epBulkOut, len(payload), nil, payload) got := map[uint32][]byte{} for i := 0; i < 2; i++ { seq, st, data := cl.ret() if st != 0 { t.Fatalf("seq %d status %d", seq, st) } got[seq] = data } if !bytes.Equal(got[inSeq], []byte{0x12}) { t.Fatalf("bulk IN = % X, want 12", got[inSeq]) } if _, ok := got[outSeq]; !ok { t.Fatal("falta la respuesta del bulk OUT") } // UNLINK de una lectura pendiente pending := cl.submit(dirIn, epBulkIn, 64, nil, nil) cl.seq++ u := make([]byte, 48) binary.BigEndian.PutUint32(u[0:], cmdUnlink) binary.BigEndian.PutUint32(u[4:], cl.seq) binary.BigEndian.PutUint32(u[20:], pending) cl.c.Write(u) h := readN(t, cl.c, 48) if binary.BigEndian.Uint32(h[0:]) != retUnlink || int32(binary.BigEndian.Uint32(h[20:])) != errConnReset { t.Fatalf("RET_UNLINK inesperado: % X", h) } if len(p.Snapshot(0).Connections) != 1 { t.Fatal("debería haber una conexión USB activa") } }