From 3edac8b1ea1f8a2fe523839b056e3a1df99c9055 Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Mon, 25 May 2026 02:36:08 +0700 Subject: [PATCH] test: cover number converter service --- frontend/src/test/setup.js | 18 ++ go.mod | 6 +- go.sum | 18 +- internal/numberconverter/service_test.go | 211 +++++++++++++++++++++++ 4 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 internal/numberconverter/service_test.go diff --git a/frontend/src/test/setup.js b/frontend/src/test/setup.js index bcb2601..7ea3e01 100644 --- a/frontend/src/test/setup.js +++ b/frontend/src/test/setup.js @@ -5,6 +5,24 @@ import * as matchers from '@testing-library/jest-dom/matchers'; // Extend Vitest's expect with jest-dom matchers expect.extend(matchers); +if (typeof window !== 'undefined' && !window.localStorage) { + const storage = new Map(); + + Object.defineProperty(window, 'localStorage', { + configurable: true, + value: { + clear: () => storage.clear(), + getItem: (key) => storage.get(String(key)) ?? null, + key: (index) => Array.from(storage.keys())[index] ?? null, + removeItem: (key) => storage.delete(String(key)), + setItem: (key, value) => storage.set(String(key), String(value)), + get length() { + return storage.size; + }, + }, + }); +} + // Cleanup after each test afterEach(() => { cleanup(); diff --git a/go.mod b/go.mod index f7fe587..ae124c2 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/pelletier/go-toml/v2 v2.3.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3 + github.com/wailsapp/wails/v3 v3.0.0-alpha.95 golang.design/x/hotkey v0.4.1 golang.org/x/crypto v0.51.0 golang.org/x/net v0.54.0 @@ -30,6 +30,7 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coder/websocket v1.8.14 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect @@ -61,8 +62,7 @@ require ( github.com/skeema/knownhosts v1.3.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect - github.com/wailsapp/go-webview2 v1.0.18 // indirect - github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/wailsapp/wails/webview2 v1.0.24 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect diff --git a/go.sum b/go.sum index e1fa5e6..bdfad67 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -69,6 +71,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -130,8 +134,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -191,12 +196,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/wailsapp/go-webview2 v1.0.18 h1:SSSCoLA+MYikSp1U0WmvELF/4c3x5kH8Vi31TKyZ4yk= -github.com/wailsapp/go-webview2 v1.0.18/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= -github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3 h1:9aCL0IXD60A5iscQ/ps6f3ti3IlaoG6LQe0RZ9JkueU= -github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3/go.mod h1:9Ca1goy5oqxmy8Oetb8Tchkezcx4tK03DK+SqYByu5Y= +github.com/wailsapp/wails/v3 v3.0.0-alpha.95 h1:Rve8djRSldn6381q2l8gw8XEnzPX/4So6VsRM6bc7Vs= +github.com/wailsapp/wails/v3 v3.0.0-alpha.95/go.mod h1:3euiK0wb6vnXvxiHysRYYbukCa060bLSsfrvN7sZg4k= +github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs= +github.com/wailsapp/wails/webview2 v1.0.24/go.mod h1:sdf+s0nAdxlzVWf9SCxC15XaxnQPJeY+uU1Ucn3jHQM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= @@ -219,7 +222,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= diff --git a/internal/numberconverter/service_test.go b/internal/numberconverter/service_test.go new file mode 100644 index 0000000..31ad7fb --- /dev/null +++ b/internal/numberconverter/service_test.go @@ -0,0 +1,211 @@ +package numberconverter + +import ( + "reflect" + "strings" + "testing" +) + +func TestConvertDecimalBuildsCoreRepresentationsAndViews(t *testing.T) { + svc := NewNumberConverterService() + + got := svc.Convert(ConvertRequest{Value: "255", Base: "decimal"}) + + if got.Error != "" { + t.Fatalf("Convert returned unexpected error: %s", got.Error) + } + if got.Binary != "11111111" { + t.Fatalf("Binary = %q, want %q", got.Binary, "11111111") + } + if got.Decimal != "255" { + t.Fatalf("Decimal = %q, want %q", got.Decimal, "255") + } + if got.Hex != "0xFF" { + t.Fatalf("Hex = %q, want %q", got.Hex, "0xFF") + } + if got.Octal != "377" { + t.Fatalf("Octal = %q, want %q", got.Octal, "377") + } + + wantBits := []int{1, 1, 1, 1, 1, 1, 1, 1} + if !reflect.DeepEqual(got.Bits, wantBits) { + t.Fatalf("Bits = %v, want %v", got.Bits, wantBits) + } + wantBitValues := []int{128, 64, 32, 16, 8, 4, 2, 1} + if !reflect.DeepEqual(got.BitValues, wantBitValues) { + t.Fatalf("BitValues = %v, want %v", got.BitValues, wantBitValues) + } + wantBytes := []string{"00", "00", "00", "FF"} + if !reflect.DeepEqual(got.Bytes.BigEndian, wantBytes) { + t.Fatalf("Bytes.BigEndian = %v, want %v", got.Bytes.BigEndian, wantBytes) + } + if got.Bytes.Highlighted != 3 { + t.Fatalf("Bytes.Highlighted = %d, want 3", got.Bytes.Highlighted) + } + + if got.ASCII.Code != 255 || got.ASCII.Printable { + t.Fatalf("ASCII = %+v, want code 255 and non-printable", got.ASCII) + } + if got.Color.Hex != "#0000FF" || !got.Color.Valid { + t.Fatalf("Color = %+v, want valid #0000FF", got.Color) + } + if got.IPv4.Address != "0.0.0.255" || got.IPv4.Type != "broadcast" { + t.Fatalf("IPv4 = %+v, want broadcast 0.0.0.255", got.IPv4) + } + if got.Percentage != 100 { + t.Fatalf("Percentage = %d, want 100", got.Percentage) + } + if got.FileSize.Bytes != 255 || got.FileSize.Human != "255 bytes" { + t.Fatalf("FileSize = %+v, want 255 bytes", got.FileSize) + } + if got.Timestamp.DateTime != "1970-01-01 00:04:15" || got.Timestamp.Duration != "4m 15s" { + t.Fatalf("Timestamp = %+v, want 1970-01-01 00:04:15 and 4m 15s", got.Timestamp) + } +} + +func TestConvertParsesSupportedBaseAliases(t *testing.T) { + svc := NewNumberConverterService() + + tests := []struct { + name string + req ConvertRequest + want string + bits []int + bytes []string + }{ + { + name: "binary alias", + req: ConvertRequest{Value: "10101010", Base: "bin"}, + want: "170", + bits: []int{1, 0, 1, 0, 1, 0, 1, 0}, + bytes: []string{"00", "00", "00", "AA"}, + }, + { + name: "octal alias", + req: ConvertRequest{Value: "755", Base: "oct"}, + want: "493", + bits: []int{1, 1, 1, 0, 1, 1, 0, 1}, + bytes: []string{"00", "00", "01", "ED"}, + }, + { + name: "hexadecimal prefix", + req: ConvertRequest{Value: "0xDEADBEEF", Base: "hex"}, + want: "3735928559", + bits: []int{1, 1, 1, 0, 1, 1, 1, 1}, + bytes: []string{"DE", "AD", "BE", "EF"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := svc.Convert(tt.req) + if got.Error != "" { + t.Fatalf("Convert returned unexpected error: %s", got.Error) + } + if got.Decimal != tt.want { + t.Fatalf("Decimal = %q, want %q", got.Decimal, tt.want) + } + if !reflect.DeepEqual(got.Bits, tt.bits) { + t.Fatalf("Bits = %v, want %v", got.Bits, tt.bits) + } + if !reflect.DeepEqual(got.Bytes.BigEndian, tt.bytes) { + t.Fatalf("Bytes.BigEndian = %v, want %v", got.Bytes.BigEndian, tt.bytes) + } + }) + } +} + +func TestConvertReportsInvalidInputs(t *testing.T) { + svc := NewNumberConverterService() + + tests := []struct { + name string + req ConvertRequest + wantErrorPart string + }{ + { + name: "unsupported base", + req: ConvertRequest{Value: "10", Base: "base36"}, + wantErrorPart: "unsupported base: base36", + }, + { + name: "invalid digit", + req: ConvertRequest{Value: "102", Base: "binary"}, + wantErrorPart: "invalid number:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := svc.Convert(tt.req) + if got.Error == "" { + t.Fatal("Convert returned no error") + } + if !strings.Contains(got.Error, tt.wantErrorPart) { + t.Fatalf("Error = %q, want to contain %q", got.Error, tt.wantErrorPart) + } + }) + } +} + +func TestInterpretNumberOmitsByteOnlyViewsOutsideByteRange(t *testing.T) { + svc := NewNumberConverterService() + + got := svc.Convert(ConvertRequest{Value: "256", Base: "decimal"}) + + if got.Error != "" { + t.Fatalf("Convert returned unexpected error: %s", got.Error) + } + if got.Color.Valid { + t.Fatalf("Color.Valid = true, want false for values outside 0-255") + } + if got.ASCII.Code != 0 || got.ASCII.Char != "" || got.ASCII.Printable { + t.Fatalf("ASCII = %+v, want zero value for values outside 0-255", got.ASCII) + } + if got.Percentage != 0 { + t.Fatalf("Percentage = %d, want zero value for values outside 0-255", got.Percentage) + } + if got.IPv4.Address != "" || got.IPv4.Type != "" { + t.Fatalf("IPv4 = %+v, want zero value for values outside 0-255", got.IPv4) + } +} + +func TestFormatFileSize(t *testing.T) { + svc := NewNumberConverterService() + + tests := []struct { + bytes int64 + want string + }{ + {bytes: 1023, want: "1023 bytes"}, + {bytes: 1024, want: "1.00 KB"}, + {bytes: 1024 * 1024, want: "1.00 MB"}, + {bytes: 1024 * 1024 * 1024, want: "1.00 GB"}, + } + + for _, tt := range tests { + if got := svc.formatFileSize(tt.bytes); got != tt.want { + t.Fatalf("formatFileSize(%d) = %q, want %q", tt.bytes, got, tt.want) + } + } +} + +func TestFormatDuration(t *testing.T) { + svc := NewNumberConverterService() + + tests := []struct { + seconds int64 + want string + }{ + {seconds: 59, want: "59s"}, + {seconds: 61, want: "1m 1s"}, + {seconds: 3660, want: "1h 1m"}, + {seconds: 172800, want: "2d"}, + } + + for _, tt := range tests { + if got := svc.formatDuration(tt.seconds); got != tt.want { + t.Fatalf("formatDuration(%d) = %q, want %q", tt.seconds, got, tt.want) + } + } +}