Add testing

This commit is contained in:
Melody Becker 2025-04-14 16:59:59 +02:00
parent 3233f8c27f
commit 06e6d457da
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
14 changed files with 273 additions and 5 deletions

View file

@ -18,6 +18,8 @@
_or_ return an error defined (as public variable) in that package. _or_ return an error defined (as public variable) in that package.
Example: `other.Error("auth", "failed to do something important", originalError)` Example: `other.Error("auth", "failed to do something important", originalError)`
- Returned http errors must use `git.mstar.dev/mstar/goutils/webutils.ProblemDetails(StatusOnly)`. - 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 ## JS/TS

View file

@ -14,6 +14,10 @@ type FSWrapper struct {
log bool 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 { func NewFSWrapper(wraps fs.FS, appends string, logAccess bool) *FSWrapper {
return &FSWrapper{ return &FSWrapper{
wrapped: wraps, 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) { // Pretty sure this can't be reliably tested
res, err := fs.wrapped.Open(fs.toAdd + name) func (fw *FSWrapper) Open(name string) (fs.File, error) {
if fs.log { res, err := fw.wrapped.Open(fw.toAdd + name)
if fw.log {
log.Debug(). log.Debug().
Str("prefix", fs.toAdd). Str("prefix", fw.toAdd).
Str("filename", name). Str("filename", name).
Err(err). Err(err).
Msg("fswrapper: File access result") Msg("fswrapper: File access result")

29
shared/fswrapper_test.go Normal file
View file

@ -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")
}
}

View file

@ -6,6 +6,8 @@ package shared
import "golang.org/x/sys/unix" import "golang.org/x/sys/unix"
// Copied from https://stackoverflow.com/a/20026945 and https://stackoverflow.com/a/49148866 // 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 { func IsWritable(path string) bool {
return unix.Access(path, unix.W_OK) == nil return unix.Access(path, unix.W_OK) == nil
} }

View file

@ -3,6 +3,9 @@ package shared
import "os" import "os"
// Copied from https://stackoverflow.com/a/20026945 and https://stackoverflow.com/a/49148866 // 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 { func IsWritable(path string) bool {
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {

108
shared/signing_test.go Normal file
View file

@ -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)
}
})
}
}

View file

@ -7,7 +7,7 @@ import (
"git.mstar.dev/mstar/goutils/sliceutils" "git.mstar.dev/mstar/goutils/sliceutils"
) )
var tagRegex = regexp.MustCompile(`#(\w+)`) var tagRegex = regexp.MustCompile(`#([a-zA-Z0-9\-_]+)`)
func TagsFromText(text string) []string { func TagsFromText(text string) []string {
matches := tagRegex.FindAllString(text, -1) matches := tagRegex.FindAllString(text, -1)

View file

@ -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)
}
})
}
}

View file

@ -4,6 +4,8 @@ import (
"github.com/rs/zerolog" "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 { func Trace(l *zerolog.Logger) *zerolog.Logger {
if e := l.Trace(); e.Enabled() { if e := l.Trace(); e.Enabled() {
e.Caller(2). e.Caller(2).
@ -12,6 +14,8 @@ func Trace(l *zerolog.Logger) *zerolog.Logger {
return l return l
} }
// Not testable since it focuses entirely on the side effect of
// sending a log out
func Untrace(l *zerolog.Logger) { func Untrace(l *zerolog.Logger) {
if e := l.Trace(); e.Enabled() { if e := l.Trace(); e.Enabled() {
e.Caller(2). e.Caller(2).

View file

@ -12,10 +12,16 @@ type ZerologGormAdapter struct {
logger zerolog.Logger 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 { func NewGormLogger(zerologger zerolog.Logger) *ZerologGormAdapter {
return &ZerologGormAdapter{zerologger} 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 { func (g *ZerologGormAdapter) LogMode(newLevel logger.LogLevel) logger.Interface {
switch newLevel { switch newLevel {
case logger.Error: case logger.Error:
@ -29,16 +35,23 @@ func (g *ZerologGormAdapter) LogMode(newLevel logger.LogLevel) logger.Interface
} }
return g return g
} }
// Not worth testing since only a wrapper around another function call
func (g *ZerologGormAdapter) Info(ctx context.Context, format string, args ...any) { func (g *ZerologGormAdapter) Info(ctx context.Context, format string, args ...any) {
g.logger.Info().Ctx(ctx).Msgf(format, args...) 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) { func (g *ZerologGormAdapter) Warn(ctx context.Context, format string, args ...any) {
g.logger.Warn().Ctx(ctx).Msgf(format, args...) 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) { func (g *ZerologGormAdapter) Error(ctx context.Context, format string, args ...any) {
g.logger.Error().Ctx(ctx).Msgf(format, args...) g.logger.Error().Ctx(ctx).Msgf(format, args...)
} }
// Not worth testing since only a wrapper around another function call
func (g *ZerologGormAdapter) Trace( func (g *ZerologGormAdapter) Trace(
ctx context.Context, ctx context.Context,
begin time.Time, begin time.Time,
@ -55,10 +68,12 @@ func (g *ZerologGormAdapter) Trace(
Send() Send()
} }
// Not worth testing since only a wrapper around another function call
func (g *ZerologGormAdapter) OverwriteLoggingLevel(new zerolog.Level) { func (g *ZerologGormAdapter) OverwriteLoggingLevel(new zerolog.Level) {
g.logger = g.logger.Level(new) g.logger = g.logger.Level(new)
} }
// Not worth testing since only a wrapper around another function call
func (g *ZerologGormAdapter) OverwriteLogger(new zerolog.Logger) { func (g *ZerologGormAdapter) OverwriteLogger(new zerolog.Logger) {
g.logger = new g.logger = new
} }

View file

@ -2,6 +2,9 @@ package shared
import "github.com/rs/zerolog/log" 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{} type ZerologWrapper struct{}
func (z *ZerologWrapper) Errorf(format string, args ...any) { func (z *ZerologWrapper) Errorf(format string, args ...any) {

View file

@ -24,14 +24,17 @@ type Note struct {
var _ shared.Clonable = &Note{} var _ shared.Clonable = &Note{}
var _ shared.Sanitisable = &Note{} var _ shared.Sanitisable = &Note{}
// No test, does nothing currently
func (note *Note) Sanitize() { func (note *Note) Sanitize() {
} }
// No test, no data processing, only copy
func (note *Note) Clone() shared.Clonable { func (note *Note) Clone() shared.Clonable {
tmp := *note tmp := *note
return &tmp return &tmp
} }
// No test, no data processing, only copy
func (n *Note) FromModel(m *models.Note) { func (n *Note) FromModel(m *models.Note) {
n.ID = m.ID n.ID = m.ID
n.CreatedAt = m.CreatedAt n.CreatedAt = m.CreatedAt

View file

@ -44,11 +44,13 @@ type User struct {
var _ shared.Sanitisable = &User{} var _ shared.Sanitisable = &User{}
var _ shared.Clonable = &User{} var _ shared.Clonable = &User{}
// No test due to no fancy processing, just setting values to constants
func (u *User) Sanitize() { func (u *User) Sanitize() {
u.Verified = nil u.Verified = nil
u.FinishedRegistration = nil u.FinishedRegistration = nil
} }
// No test, no data processing, only copy
func (u *User) Clone() shared.Clonable { func (u *User) Clone() shared.Clonable {
user := *u user := *u
if u.IconId != nil { if u.IconId != nil {
@ -83,6 +85,7 @@ func (u *User) Clone() shared.Clonable {
return &user return &user
} }
// No test, no data processing, only copy
func (u *User) FromModel(m *models.User) { func (u *User) FromModel(m *models.User) {
u.ID = m.ID u.ID = m.ID
u.CreatedAt = m.CreatedAt u.CreatedAt = m.CreatedAt

57
web/shared/client_test.go Normal file
View file

@ -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)
}
})
}
}