Unit tests & docs #34

Merged
decentral1se merged 1 commits from unit-tests into main 2026-11-09 18:09:43 +00:00
15 changed files with 349 additions and 7 deletions
+7
View File
@@ -14,6 +14,13 @@ steps:
depends_on:
- go build
- name: unit tests
image: golang:1.26
commands:
- go test -timeout 60s -v ./...
depends_on:
- go build
trigger:
action:
exclude:
+2
View File
@@ -1,7 +1,9 @@
package iroh_ffi
// Application-Layer Protocol Negotiation
type Alpn []byte
// List of ALPNs
func Alpns(alpns ...Alpn) [][]byte {
var a [][]byte
+2
View File
@@ -6,6 +6,7 @@ import (
"golang.org/x/sync/errgroup"
)
// Accept implements the receiving end of a endpoint.
func (e *Endpoint) Accept(ctx context.Context, a Accepter) error {
g, gCtx := errgroup.WithContext(ctx)
@@ -39,6 +40,7 @@ func (e *Endpoint) Accept(ctx context.Context, a Accepter) error {
return g.Wait()
}
// Open implements the remote connection of an endpoint.
func (e *Endpoint) Open(ctx context.Context, addr *EndpointAddr, alpn Alpn, o Opener) error {
conn, err := e.Connect(addr, alpn)
if err != nil {
+24
View File
@@ -0,0 +1,24 @@
package endpoint_test
import (
"testing"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
)
func TestEndpoint(t *testing.T) {
alpn := iroh.Alpn("foo/0")
e, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyMinimal(),
)
if err != nil {
t.Errorf("want endpoint creation, got: %s", iroh.As(err))
}
if err := e.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
}
+127
View File
@@ -0,0 +1,127 @@
package iroh_ffi_test
import (
"context"
"sync"
"testing"
"time"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
"github.com/magiconair/properties/assert"
)
type TestAcceptOpener struct {
ExpectedOutput string
ReceivedOutput string
}
func (t *TestAcceptOpener) Open(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.OpenBi()
if err != nil {
return err
}
if err = stream.Ready(); err != nil {
return err
}
recv := stream.Recv()
frame, err := recv.ReadToEnd(1000)
if err != nil {
return err
}
t.ReceivedOutput += string(frame)
if err := conn.Close(0, []byte("eof")); err != nil {
return err
}
return nil
}
func (t *TestAcceptOpener) Accept(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.AcceptBi()
if err := iroh.As(err); err != nil {
return err
}
send := stream.Send()
if err := send.WriteAll([]byte(t.ExpectedOutput)); err != nil {
return err
}
if err := send.Finish(); err != nil {
return err
}
conn.Closed()
return nil
}
func TestEndpoint(t *testing.T) {
alpn := iroh.Alpn("iroh-go-endpoint-test/0")
e1, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
)
if err := iroh.As(err); err != nil {
t.Errorf("want endpoint create, got: %s", err)
}
e1.Online()
var wg sync.WaitGroup
c := make(chan struct{})
tao := &TestAcceptOpener{ExpectedOutput: "ping"}
ctx := context.Background()
wg.Go(func() {
if err := e1.Accept(ctx, tao); err != nil {
t.Errorf("want endpoint accept, got: %s", iroh.As(err))
}
if err := e1.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
})
e2, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
)
if err := iroh.As(err); err != nil {
t.Errorf("want endpoint create, got: %s", err)
}
e2.Online()
wg.Go(func() {
if err := e2.Open(ctx, e1.Addr(), alpn, tao); err != nil {
t.Errorf("want endpoint open, got: %s", iroh.As(err))
}
if err := e2.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
})
go func() {
wg.Wait()
c <- struct{}{}
}()
timeout := time.Duration(10) * time.Second
select {
case <-c:
assert.Equal(t, tao.ReceivedOutput, tao.ExpectedOutput)
case <-time.After(timeout):
t.Errorf("want graceful close, got: timeout of 10 seconds")
}
}
+4 -2
View File
@@ -77,9 +77,11 @@ func send(e *iroh.Endpoint, id string) {
}
// mark send stream as done
p.Finish()
exit(conn)
if err := p.Finish(); err != nil {
panic(iroh.As(err))
}
exit(conn)
}
func exit(conn *iroh.Connection) {
+4 -1
View File
@@ -2,4 +2,7 @@ module git.coopcloud.tech/decentral1se/iroh-go
go 1.26.1
require golang.org/x/sync v0.24.0
require (
github.com/magiconair/properties v1.18.12
golang.org/x/sync v0.24.0
)
+2
View File
@@ -1,2 +1,4 @@
github.com/magiconair/properties v1.18.12 h1:sT9zQpvTB3B4gzrX0tmZNTEaGyg8Zw55MFYRE32Mr9I=
github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
+5
View File
@@ -2,14 +2,19 @@ package iroh_ffi
import "context"
// Accepter implements the incoming side of a BiStream. This is still
// peer-to-peer and the incoming side can send and receive.
type Accepter interface {
Accept(context.Context, *Connection) error
}
// Opener implements the connecting side of a BiStream. This is still
// peer-to-peer and the connecting side can send and receive.
type Opener interface {
Open(context.Context, *Connection) error
}
// AcceptOpener combines both the "incoming" and "outgoing" interfaces.
type AcceptOpener interface {
Accepter
Opener
+2
View File
@@ -5,4 +5,6 @@ import (
"os"
)
// Log is the library-wide internal logger. It can be configured using the
// `endpoint.New(...)` options pattern. See `WithDebug` for more.
var Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
+9 -1
View File
@@ -5,14 +5,16 @@ import (
"io"
)
// Pipe is a BiStream which supports the Reader/Writer interface.
type Pipe struct {
stream *BiStream
recv *RecvStream
send *SendStream
totalBytesRead int
finished bool
finished bool
}
// NewPipe creates a new Pipe.
func NewPipe(stream *BiStream) *Pipe {
return &Pipe{
stream: stream,
@@ -21,6 +23,7 @@ func NewPipe(stream *BiStream) *Pipe {
}
}
// Ready ensures the other side of the Pipe knows that it is open.
func (p *Pipe) Ready() error {
if err := p.stream.Ready(); err != nil {
return err
@@ -28,6 +31,7 @@ func (p *Pipe) Ready() error {
return nil
}
// Read implements the Reader interface.
func (p *Pipe) Read(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, nil
@@ -56,6 +60,7 @@ func (p *Pipe) Read(buf []byte) (int, error) {
return len(retbuf), err
}
// Write implements the Writer interface.
func (p *Pipe) Write(buf []byte) (int, error) {
if p.finished {
return 0, errors.New("Pipe.Write: Write called after Finish()")
@@ -71,6 +76,9 @@ func (p *Pipe) Write(buf []byte) (int, error) {
return n, err
}
// Finish called on the send stream signals that we will not send anything
// further, which makes the receive side of the Pipe on the other end
// terminate.
func (p *Pipe) Finish() error {
p.finished = true
return p.send.Finish()
+130
View File
@@ -0,0 +1,130 @@
package iroh_ffi_test
import (
"bytes"
"context"
"io"
"sync"
"testing"
"time"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
"github.com/magiconair/properties/assert"
)
type TestAcceptOpenerPipe struct {
ExpectedOutput bytes.Buffer
ReceivedOutput bytes.Buffer
}
func (t *TestAcceptOpenerPipe) Open(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.OpenBi()
if err != nil {
return err
}
p := iroh.NewPipe(stream)
if err := p.Ready(); err != nil {
return err
}
_, err = io.Copy(p, &t.ExpectedOutput)
if err = iroh.As(err); err != nil {
return err
}
if err := p.Finish(); err != nil {
return err
}
conn.Closed()
if err := conn.Close(0, []byte("eof")); err != nil {
return err
}
return nil
}
func (t *TestAcceptOpenerPipe) Accept(ctx context.Context, conn *iroh.Connection) error {
stream, err := conn.AcceptBi()
if err := iroh.As(err); err != nil {
return err
}
p := iroh.NewPipe(stream)
_, err = io.Copy(p, &t.ReceivedOutput)
if err = iroh.As(err); err != nil {
return err
}
return nil
}
func TestPipe(t *testing.T) {
alpn := iroh.Alpn("iroh-go-pipe-test/0")
e1, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
)
if err := iroh.As(err); err != nil {
panic(err)
}
e1.Online()
var wg sync.WaitGroup
c := make(chan struct{})
tao := &TestAcceptOpenerPipe{
ExpectedOutput: *bytes.NewBufferString("ping"),
}
ctx := context.Background()
wg.Go(func() {
if err := e1.Accept(ctx, tao); err != nil {
t.Errorf("want endpoint accept, got: %s", iroh.As(err))
}
if err := e1.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
})
e2, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
)
if err := iroh.As(err); err != nil {
panic(err)
}
e2.Online()
wg.Go(func() {
if err := e2.Open(ctx, e1.Addr(), alpn, tao); err != nil {
t.Errorf("want endpoint open, got: %s", iroh.As(err))
}
if err := e2.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
})
go func() {
wg.Wait()
c <- struct{}{}
}()
timeout := time.Duration(10) * time.Second
select {
case <-c:
assert.Equal(t, tao.ReceivedOutput.String(), tao.ExpectedOutput.String())
case <-time.After(timeout):
t.Errorf("want graceful close, got: timeout of 10 seconds")
}
}
+1
View File
@@ -1,5 +1,6 @@
package iroh_ffi
// Ready ensures the other side of the BiStream knows that the stream is open.
func (s *BiStream) Ready() error {
if err := s.Send().WriteAll([]byte{1}); err != nil {
return err
+4 -3
View File
@@ -4,8 +4,9 @@ import (
"crypto/sha256"
)
// SecretBytesFromString creates secret bytes from a string.
func SecretBytesFromString(str string) []byte {
// TODO (2026-11-09): securely clear all arrays to the extent possible
digest := sha256.Sum256([]byte(str))
return digest[:] // copy out bytes from strict [32]byte type to []byte used by everything else + iroh
// TODO (2026-11-09): securely clear all arrays to the extent possible
digest := sha256.Sum256([]byte(str))
return digest[:] // copy out bytes from strict [32]byte type to []byte used by everything else + iroh
}
+26
View File
@@ -0,0 +1,26 @@
package util_test
import (
"testing"
iroh "git.coopcloud.tech/decentral1se/iroh-go"
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
"git.coopcloud.tech/decentral1se/iroh-go/util"
)
func TestSecretBytesFromString(t *testing.T) {
alpn := iroh.Alpn("iroh-go-util-test/0")
e, err := endpoint.New(
endpoint.WithAlpns(alpn),
endpoint.WithApplyN0(),
endpoint.WithSecretKey(util.SecretBytesFromString("hello-secret-bytes")),
)
if err := iroh.As(err); err != nil {
t.Errorf("want endpoint create, got: %s", err)
}
if err := e.Close(); err != nil {
t.Errorf("want endpoint close, got: %s", iroh.As(err))
}
}