From 06e6d457da2cc50cb5f32a45473b0b87017cc114 Mon Sep 17 00:00:00 2001 From: mstar Date: Mon, 14 Apr 2025 16:59:59 +0200 Subject: [PATCH] Add testing --- Code-Style.md | 2 + shared/fswrapper.go | 13 ++-- shared/fswrapper_test.go | 29 +++++++++ shared/isWritable.go | 2 + shared/isWritable_windows.go | 3 + shared/signing_test.go | 108 ++++++++++++++++++++++++++++++++++ shared/tagsFromString.go | 2 +- shared/tagsFromString_test.go | 34 +++++++++++ shared/tracing.go | 4 ++ shared/zerologGormAdapter.go | 15 +++++ shared/zerologPasskey.go | 3 + web/shared/Note.go | 3 + web/shared/User.go | 3 + web/shared/client_test.go | 57 ++++++++++++++++++ 14 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 shared/fswrapper_test.go create mode 100644 shared/signing_test.go create mode 100644 shared/tagsFromString_test.go create mode 100644 web/shared/client_test.go diff --git a/Code-Style.md b/Code-Style.md index 5343484..5cf2118 100644 --- a/Code-Style.md +++ b/Code-Style.md @@ -18,6 +18,8 @@ _or_ return an error defined (as public variable) in that package. Example: `other.Error("auth", "failed to do something important", originalError)` - Returned http errors must use `git.mstar.dev/mstar/goutils/webutils.ProblemDetails(StatusOnly)`. +- Every function must either have a test or a doc comment explaining + why it doesn't have a test ## JS/TS diff --git a/shared/fswrapper.go b/shared/fswrapper.go index d500142..b04c144 100644 --- a/shared/fswrapper.go +++ b/shared/fswrapper.go @@ -14,6 +14,10 @@ type FSWrapper struct { log bool } +// Compile time interface implementation assertion +var _ fs.FS = &FSWrapper{} + +// No test, as no processing happens, only wrapping the arguments in a struct func NewFSWrapper(wraps fs.FS, appends string, logAccess bool) *FSWrapper { return &FSWrapper{ wrapped: wraps, @@ -22,11 +26,12 @@ func NewFSWrapper(wraps fs.FS, appends string, logAccess bool) *FSWrapper { } } -func (fs *FSWrapper) Open(name string) (fs.File, error) { - res, err := fs.wrapped.Open(fs.toAdd + name) - if fs.log { +// Pretty sure this can't be reliably tested +func (fw *FSWrapper) Open(name string) (fs.File, error) { + res, err := fw.wrapped.Open(fw.toAdd + name) + if fw.log { log.Debug(). - Str("prefix", fs.toAdd). + Str("prefix", fw.toAdd). Str("filename", name). Err(err). Msg("fswrapper: File access result") diff --git a/shared/fswrapper_test.go b/shared/fswrapper_test.go new file mode 100644 index 0000000..049c49e --- /dev/null +++ b/shared/fswrapper_test.go @@ -0,0 +1,29 @@ +package shared + +import ( + "io" + "os" + "reflect" + "testing" +) + +func TestFSWrapper_Open(t *testing.T) { + rootFs := os.DirFS("/") + wrapper := NewFSWrapper(rootFs, "etc/", false) + f, err := wrapper.Open("hostname") + if err != nil { + t.Fatalf("failed to open /etc/hostname: %v", err) + } + defer f.Close() + data, err := os.ReadFile("/etc/hostname") + if err != nil { + t.Fatalf("failed to read with full path: %v", err) + } + wrappedData, err := io.ReadAll(f) + if err != nil { + t.Fatalf("failed to read from wrapped file: %v", err) + } + if !reflect.DeepEqual(wrappedData, data) { + t.Fatal("file contents are different") + } +} diff --git a/shared/isWritable.go b/shared/isWritable.go index fab473f..7b06003 100644 --- a/shared/isWritable.go +++ b/shared/isWritable.go @@ -6,6 +6,8 @@ package shared import "golang.org/x/sys/unix" // Copied from https://stackoverflow.com/a/20026945 and https://stackoverflow.com/a/49148866 +// Not testable as host environment could be just about anything +// and this function is for testing the host environment func IsWritable(path string) bool { return unix.Access(path, unix.W_OK) == nil } diff --git a/shared/isWritable_windows.go b/shared/isWritable_windows.go index 7a088c6..b84c2fd 100644 --- a/shared/isWritable_windows.go +++ b/shared/isWritable_windows.go @@ -3,6 +3,9 @@ package shared import "os" // Copied from https://stackoverflow.com/a/20026945 and https://stackoverflow.com/a/49148866 +// Not testable as host environment could be just about anything +// and this function is for testing the host environment. +// Also, Windows is not supported. It may work, it may not, doesn't matter func IsWritable(path string) bool { info, err := os.Stat(path) if err != nil { diff --git a/shared/signing_test.go b/shared/signing_test.go new file mode 100644 index 0000000..1b24585 --- /dev/null +++ b/shared/signing_test.go @@ -0,0 +1,108 @@ +package shared + +import ( + "crypto" + "crypto/ed25519" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "reflect" + "testing" +) + +const testStringToSign = "some random text that I made up at work and definitely has no other purpose" + +func TestGenerateKeypairRSA(t *testing.T) { + publicBytes, privateBytes, err := GenerateKeypair(false) + if err != nil { + t.Fatalf("generation failed: %v", err) + } + publicUntyped, err := x509.ParsePKIXPublicKey(publicBytes) + if err != nil { + t.Fatalf("parsing public bytes into key failed: %v", err) + } + public, ok := publicUntyped.(*rsa.PublicKey) + if !ok { + t.Fatal("asserting public key as rsa.PublicKey failed") + } + private, err := x509.ParsePKCS1PrivateKey(privateBytes) + if err != nil { + t.Fatalf("parsing private bytes into key failed: %v", err) + } + if err := private.Validate(); err != nil { + t.Fatalf("validation of private key failed: %v", err) + } + genPublicRaw := private.Public() + genPublic, ok := genPublicRaw.(*rsa.PublicKey) + if !reflect.DeepEqual(*public, *genPublic) { + t.Fatal("public from generator and from private are different") + } +} + +func TestGenerateKeypairED(t *testing.T) { + publicBytes, privateBytes, err := GenerateKeypair(true) + if err != nil { + t.Fatalf("generation failed: %v", err) + } + publicUntyped, err := x509.ParsePKIXPublicKey(publicBytes) + if err != nil { + t.Fatalf("parsing public bytes into key failed: %v", err) + } + public, ok := publicUntyped.(ed25519.PublicKey) + if !ok { + t.Fatal("asserting public key as ed25519.PublicKey failed") + } + private := ed25519.PrivateKey(privateBytes) + pubGenRaw := private.Public() + pubGen, ok := pubGenRaw.(ed25519.PublicKey) + if !ok { + t.Fatal("public key generated by private key is not of type ed25519.PublicKey") + } + if !reflect.DeepEqual(pubGen, public) { + t.Fatal("public from generator and from private are different") + } +} + +func TestSignRSA(t *testing.T) { + publicBytes, privateBytes, err := GenerateKeypair(false) + if err != nil { + t.Fatalf("failed to generate RSA keypair: %v", err) + } + pubRaw, err := x509.ParsePKIXPublicKey(publicBytes) + if err != nil { + t.Fatalf("failed to parse public rsa key: %v", err) + } + pub, ok := pubRaw.(*rsa.PublicKey) + if !ok { + t.Fatal("parsed public key is not *rsa.PublicKey") + } + hash := sha256.Sum256([]byte(testStringToSign)) + signed, err := Sign(testStringToSign, privateBytes, true) + if err != nil { + t.Fatalf("failed to sign test string: %v", err) + } + + if rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], signed) != nil { + t.Fatal("failed to verify") + } +} + +func TestKeyBytesToPem(t *testing.T) { + type args struct { + bytes []byte + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := KeyBytesToPem(tt.args.bytes, true); got != tt.want { + t.Errorf("KeyBytesToPem() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/shared/tagsFromString.go b/shared/tagsFromString.go index 8190d5f..b3a0960 100644 --- a/shared/tagsFromString.go +++ b/shared/tagsFromString.go @@ -7,7 +7,7 @@ import ( "git.mstar.dev/mstar/goutils/sliceutils" ) -var tagRegex = regexp.MustCompile(`#(\w+)`) +var tagRegex = regexp.MustCompile(`#([a-zA-Z0-9\-_]+)`) func TagsFromText(text string) []string { matches := tagRegex.FindAllString(text, -1) diff --git a/shared/tagsFromString_test.go b/shared/tagsFromString_test.go new file mode 100644 index 0000000..d3a59fa --- /dev/null +++ b/shared/tagsFromString_test.go @@ -0,0 +1,34 @@ +package shared + +import ( + "reflect" + "testing" +) + +func TestTagsFromText(t *testing.T) { + type args struct { + text string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "No tags", + args: args{"Text with no tags whatsoever"}, + want: []string{}, + }, { + name: "one tag", + args: args{"Text with one simple #tag"}, + want: []string{"tag"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := TagsFromText(tt.args.text); !reflect.DeepEqual(got, tt.want) { + t.Errorf("TagsFromText() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/shared/tracing.go b/shared/tracing.go index 6c0c37a..d945459 100644 --- a/shared/tracing.go +++ b/shared/tracing.go @@ -4,6 +4,8 @@ import ( "github.com/rs/zerolog" ) +// Not testable since it focuses entirely on the side effect of +// sending a log out func Trace(l *zerolog.Logger) *zerolog.Logger { if e := l.Trace(); e.Enabled() { e.Caller(2). @@ -12,6 +14,8 @@ func Trace(l *zerolog.Logger) *zerolog.Logger { return l } +// Not testable since it focuses entirely on the side effect of +// sending a log out func Untrace(l *zerolog.Logger) { if e := l.Trace(); e.Enabled() { e.Caller(2). diff --git a/shared/zerologGormAdapter.go b/shared/zerologGormAdapter.go index 74ea749..4021285 100644 --- a/shared/zerologGormAdapter.go +++ b/shared/zerologGormAdapter.go @@ -12,10 +12,16 @@ type ZerologGormAdapter struct { logger zerolog.Logger } +// Compile time interface implementation enforcement +var _ logger.Interface = &ZerologGormAdapter{} + +// Not worth testing as just a wrapper for putting into a struct func NewGormLogger(zerologger zerolog.Logger) *ZerologGormAdapter { return &ZerologGormAdapter{zerologger} } +// Not testable since it focuses entirely on the side effect of +// configuring loglevel func (g *ZerologGormAdapter) LogMode(newLevel logger.LogLevel) logger.Interface { switch newLevel { case logger.Error: @@ -29,16 +35,23 @@ func (g *ZerologGormAdapter) LogMode(newLevel logger.LogLevel) logger.Interface } return g } + +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) Info(ctx context.Context, format string, args ...any) { g.logger.Info().Ctx(ctx).Msgf(format, args...) } + +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) Warn(ctx context.Context, format string, args ...any) { g.logger.Warn().Ctx(ctx).Msgf(format, args...) } + +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) Error(ctx context.Context, format string, args ...any) { g.logger.Error().Ctx(ctx).Msgf(format, args...) } +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) Trace( ctx context.Context, begin time.Time, @@ -55,10 +68,12 @@ func (g *ZerologGormAdapter) Trace( Send() } +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) OverwriteLoggingLevel(new zerolog.Level) { g.logger = g.logger.Level(new) } +// Not worth testing since only a wrapper around another function call func (g *ZerologGormAdapter) OverwriteLogger(new zerolog.Logger) { g.logger = new } diff --git a/shared/zerologPasskey.go b/shared/zerologPasskey.go index f9d0b89..6d4badb 100644 --- a/shared/zerologPasskey.go +++ b/shared/zerologPasskey.go @@ -2,6 +2,9 @@ package shared import "github.com/rs/zerolog/log" +// Stuff here is not actually used anymore in the new implementation +// thus also no tests needed + type ZerologWrapper struct{} func (z *ZerologWrapper) Errorf(format string, args ...any) { diff --git a/web/shared/Note.go b/web/shared/Note.go index 1e7548b..208f814 100644 --- a/web/shared/Note.go +++ b/web/shared/Note.go @@ -24,14 +24,17 @@ type Note struct { var _ shared.Clonable = &Note{} var _ shared.Sanitisable = &Note{} +// No test, does nothing currently func (note *Note) Sanitize() { } +// No test, no data processing, only copy func (note *Note) Clone() shared.Clonable { tmp := *note return &tmp } +// No test, no data processing, only copy func (n *Note) FromModel(m *models.Note) { n.ID = m.ID n.CreatedAt = m.CreatedAt diff --git a/web/shared/User.go b/web/shared/User.go index ab097c2..05b8abe 100644 --- a/web/shared/User.go +++ b/web/shared/User.go @@ -44,11 +44,13 @@ type User struct { var _ shared.Sanitisable = &User{} var _ shared.Clonable = &User{} +// No test due to no fancy processing, just setting values to constants func (u *User) Sanitize() { u.Verified = nil u.FinishedRegistration = nil } +// No test, no data processing, only copy func (u *User) Clone() shared.Clonable { user := *u if u.IconId != nil { @@ -83,6 +85,7 @@ func (u *User) Clone() shared.Clonable { return &user } +// No test, no data processing, only copy func (u *User) FromModel(m *models.User) { u.ID = m.ID u.CreatedAt = m.CreatedAt diff --git a/web/shared/client_test.go b/web/shared/client_test.go new file mode 100644 index 0000000..27acbe4 --- /dev/null +++ b/web/shared/client_test.go @@ -0,0 +1,57 @@ +package webshared + +import ( + "io" + "net/http" + "reflect" + "testing" +) + +const testBody = `{ + "key": "value", + "more-key": 21 +}` + +var testBodyHash = []byte("22a5173da554010ea25f7d2ae34032a434c3a883a55d65f34c3413fdc555bed3") + +func TestSignRequest(t *testing.T) { + // TODO: Implement tests +} + +func Test_applyBodyHash_WithBody(t *testing.T) { + var headers = make(http.Header, 0) + digest := "SHA-256=" + string(testBodyHash) + applyBodyHash(headers, []byte(testBody)) + headerDigest := headers.Get("Digest") + if headerDigest != digest { + t.Fatalf("digests didn't match: header \"%v\" != precalc \"%v\"", headerDigest, digest) + } +} + +func TestNewRequest(t *testing.T) { + type args struct { + method string + url string + body io.Reader + } + tests := []struct { + name string + args args + want *http.Request + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewRequest(tt.args.method, tt.args.url, tt.args.body) + if (err != nil) != tt.wantErr { + t.Errorf("NewRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewRequest() = %v, want %v", got, tt.want) + } + }) + } +}