diff options
| author | Nikolay Govorov <me@govorov.online> | 2026-04-01 20:30:52 +0100 |
|---|---|---|
| committer | Nikolay Govorov <me@govorov.online> | 2026-04-01 20:37:11 +0100 |
| commit | de6dc17b4ec92dfdf66b992e429e186f3f2c279e (patch) | |
| tree | d9d154922790096481a395b32565bc6c15557c10 | |
| parent | e704a47d81e86e4815fa051af46ab1fdb162f5c7 (diff) | |
| download | tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip | |
Unit tests for hmac and version parsing
Diffstat
| -rw-r--r-- | .github/workflows/build.yml | 3 | +3 −0 |
| -rw-r--r-- | Taskfile.yml | 12 | +8 −4 |
| -rw-r--r-- | internal/protocol/backoff.go | 2 | +1 −1 |
| -rw-r--r-- | internal/protocol/backoff_test.go | 74 | +74 −0 |
| -rw-r--r-- | internal/protocol/hmac_test.go | 87 | +87 −0 |
| -rw-r--r-- | internal/protocol/version_test.go | 45 | +45 −0 |
6 files changed, 218 insertions, 5 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a7119e..20aa471 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,9 @@ jobs: - name: Check formatting run: test -z "$(gofmt -l .)" + - name: Test + run: task test + - name: Determine version id: version run: | diff --git a/Taskfile.yml b/Taskfile.yml index 1fc37eb..ba31fe4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,22 +18,26 @@ tasks: cmds: - protoc --go_out=. --go-grpc_out=. proto/mirum.proto + test: + desc: Run all tests + deps: [proto] + cmds: + - go test -race -count=1 ./... + build: desc: "Build binaries (override GOOS/GOARCH for cross-compilation)" deps: [proto] vars: GOOS: { sh: "echo ${GOOS:-$(go env GOOS)}" } GOARCH: { sh: "echo ${GOARCH:-$(go env GOARCH)}" } - VERSION: { sh: cat VERSION } - LDFLAGS: "-X mrdimidium/mirum/internal/protocol.raw={{.VERSION}}" env: CGO_ENABLED: "0" GOOS: "{{.GOOS}}" GOARCH: "{{.GOARCH}}" cmds: - mkdir -p {{.BUILD_DIR}} - - go build -ldflags '{{.LDFLAGS}}' -o {{.BUILD_DIR}}/mirumd-{{.GOOS}}-{{.GOARCH}} ./cmd/mirumd - - go build -ldflags '{{.LDFLAGS}}' -o {{.BUILD_DIR}}/mirumw-{{.GOOS}}-{{.GOARCH}} ./cmd/mirumw + - go build -o {{.BUILD_DIR}}/mirumd-{{.GOOS}}-{{.GOARCH}} ./cmd/mirumd + - go build -o {{.BUILD_DIR}}/mirumw-{{.GOOS}}-{{.GOARCH}} ./cmd/mirumw - cp {{.BUILD_DIR}}/mirumd-{{.GOOS}}-{{.GOARCH}} {{.BUILD_DIR}}/mirumd - cp {{.BUILD_DIR}}/mirumw-{{.GOOS}}-{{.GOARCH}} {{.BUILD_DIR}}/mirumw diff --git a/internal/protocol/backoff.go b/internal/protocol/backoff.go index ca5e193..dc53e60 100644 --- a/internal/protocol/backoff.go +++ b/internal/protocol/backoff.go @@ -24,7 +24,7 @@ func (b *Backoff) Reset() { // Wait sleeps with exponential backoff + jitter. Returns false if ctx is cancelled. func (b *Backoff) Wait(ctx context.Context) bool { - d := max(b.Max, b.Min<<b.attempt) + d := min(b.Max, b.Min<<b.attempt) // Add jitter: 50%-100% of the computed duration d = d/2 + time.Duration(rand.Int64N(int64(d/2))) diff --git a/internal/protocol/backoff_test.go b/internal/protocol/backoff_test.go new file mode 100644 --- /dev/null +++ b/internal/protocol/backoff_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +package protocol + +import ( + "context" + "testing" + "time" +) + +func TestBackoff_ExponentialGrowth(t *testing.T) { + b := &Backoff{Min: 10 * time.Millisecond, Max: 200 * time.Millisecond} + + var durations []time.Duration + for range 5 { + start := time.Now() + b.Wait(context.Background()) + durations = append(durations, time.Since(start)) + } + + if durations[4] < durations[0] { + t.Errorf("no exponential growth: first=%v last=%v", durations[0], durations[4]) + } +} + +func TestBackoff_NeverExceedsMax(t *testing.T) { + b := &Backoff{Min: time.Millisecond, Max: 50 * time.Millisecond} + + for range 20 { + start := time.Now() + b.Wait(context.Background()) + d := time.Since(start) + if d > 80*time.Millisecond { + t.Fatalf("wait %v exceeded max %v", d, b.Max) + } + } +} + +func TestBackoff_Reset(t *testing.T) { + b := &Backoff{Min: time.Millisecond, Max: time.Second} + + for range 10 { + b.Wait(context.Background()) + } + + b.Reset() + + start := time.Now() + b.Wait(context.Background()) + d := time.Since(start) + + if d > 10*time.Millisecond { + t.Fatalf("after Reset, wait %v is too long", d) + } +} + +func TestBackoff_CancelledContext(t *testing.T) { + b := &Backoff{Min: time.Hour, Max: time.Hour} + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + start := time.Now() + ok := b.Wait(ctx) + d := time.Since(start) + + if ok { + t.Fatal("Wait returned true on cancelled context") + } + if d > 10*time.Millisecond { + t.Fatalf("Wait took %v on cancelled context", d) + } +} diff --git a/internal/protocol/hmac_test.go b/internal/protocol/hmac_test.go new file mode 100644 --- /dev/null +++ b/internal/protocol/hmac_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +package protocol + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "testing" +) + +func TestGenerateNonce(t *testing.T) { + nonce, err := GenerateNonce() + if err != nil { + t.Fatal(err) + } + if len(nonce) != NonceSize { + t.Fatalf("len = %d, want %d", len(nonce), NonceSize) + } + + nonce2, _ := GenerateNonce() + if bytes.Equal(nonce, nonce2) { + t.Fatal("two nonces are identical") + } +} + +func TestComputeProof(t *testing.T) { + secret := []byte("secret") + first := []byte("first") + second := []byte("second") + + proof := ComputeProof(secret, first, second) + + // Golden value: HMAC-SHA256("secret", "first" || "second") + mac := hmac.New(sha256.New, secret) + mac.Write(first) + mac.Write(second) + want := mac.Sum(nil) + + if !bytes.Equal(proof, want) { + t.Fatalf("proof mismatch") + } + + // Different secret → different proof + other := ComputeProof([]byte("other"), first, second) + if bytes.Equal(proof, other) { + t.Fatal("different secrets produced same proof") + } + + // Order matters: (first, second) != (second, first) + reversed := ComputeProof(secret, second, first) + if bytes.Equal(proof, reversed) { + t.Fatal("argument order did not affect proof") + } +} + +func TestVerifyProof(t *testing.T) { + secret := []byte("secret") + first := []byte("first") + second := []byte("second") + proof := ComputeProof(secret, first, second) + + tests := []struct { + name string + secret []byte + first []byte + second []byte + proof []byte + want bool + }{ + {"valid", secret, first, second, proof, true}, + {"wrong proof", secret, first, second, []byte("wrong"), false}, + {"wrong secret", []byte("wrong"), first, second, proof, false}, + {"swapped args", secret, second, first, proof, false}, + {"empty proof", secret, first, second, nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := VerifyProof(tt.secret, tt.first, tt.second, tt.proof) + if got != tt.want { + t.Fatalf("VerifyProof = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/protocol/version_test.go b/internal/protocol/version_test.go new file mode 100644 --- /dev/null +++ b/internal/protocol/version_test.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +package protocol + +import ( + "errors" + "testing" +) + +func TestParseVersion(t *testing.T) { + tests := []struct { + name string + in string + major, minor, patch uint32 + wantErr bool + }{ + {"valid", "0.1.0", 0, 1, 0, false}, + {"large", "10.20.30", 10, 20, 30, false}, + {"whitespace", " 1.2.3 ", 1, 2, 3, false}, + {"empty", "", 0, 0, 0, true}, + {"two parts", "1.2", 0, 0, 0, true}, + {"one part", "1", 0, 0, 0, true}, + {"letters", "a.b.c", 0, 0, 0, true}, + {"negative", "-1.0.0", 0, 0, 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + major, minor, patch, err := ParseVersion(tt.in) + if tt.wantErr { + if !errors.Is(err, ErrInvalidVersion) { + t.Fatalf("ParseVersion(%q) err = %v, want ErrInvalidVersion", tt.in, err) + } + return + } + if err != nil { + t.Fatalf("ParseVersion(%q) unexpected error: %v", tt.in, err) + } + if major != tt.major || minor != tt.minor || patch != tt.patch { + t.Fatalf("ParseVersion(%q) = %d.%d.%d, want %d.%d.%d", + tt.in, major, minor, patch, tt.major, tt.minor, tt.patch) + } + }) + } +} |
