diff --git a/README.md b/README.md index 4f17e36..37d465f 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,17 @@ To use the hosted version set `api_url = https://wakapi.dev/api/heartbeat`. Howe **Note:** By default, the application is running in dev mode. However, it is recommended to set `ENV=production` for enhanced performance and security. To still be able to log in when using production mode, you either have to run Wakapi behind a reverse proxy, that enables for HTTPS encryption (see [best practices](#best-practices)) or set `security.insecure_cookies` to `true` in `config.yml`. ### Run with Docker -``` +```bash docker run -d -p 3000:3000 --name wakapi n1try/wakapi ``` By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options. +### Running tests +```bash +CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test ./... +``` + ## 🔧 Configuration You can specify configuration options either via a config file (default: `config.yml`, customziable through the `-c` argument) or via environment variables. Here is an overview of all options. diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..44583d1 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,66 @@ +package config + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestConfig_IsDev(t *testing.T) { + assert.True(t, IsDev("dev")) + assert.True(t, IsDev("development")) + assert.False(t, IsDev("prod")) + assert.False(t, IsDev("production")) + assert.False(t, IsDev("anything else")) +} + +func Test_mysqlConnectionString(t *testing.T) { + c := &dbConfig{ + Host: "test_host", + Port: 9999, + User: "test_user", + Password: "test_password", + Name: "test_name", + Dialect: "mysql", + MaxConn: 10, + } + + assert.Equal(t, fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES", + c.User, + c.Password, + c.Host, + c.Port, + c.Name, + "Local", + ), mysqlConnectionString(c)) +} + +func Test_postgresConnectionString(t *testing.T) { + c := &dbConfig{ + Host: "test_host", + Port: 9999, + User: "test_user", + Password: "test_password", + Name: "test_name", + Dialect: "postgres", + MaxConn: 10, + } + + assert.Equal(t, fmt.Sprintf( + "host=%s port=%d user=%s dbname=%s password=%s sslmode=disable", + c.Host, + c.Port, + c.User, + c.Name, + c.Password, + ), postgresConnectionString(c)) +} + +func Test_sqliteConnectionString(t *testing.T) { + c := &dbConfig{ + Name: "test_name", + Dialect: "sqlite3", + } + assert.Equal(t, c.Name, sqliteConnectionString(c)) +} diff --git a/go.mod b/go.mod index 8897ffc..27dc2bb 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc github.com/satori/go.uuid v1.2.0 + github.com/stretchr/testify v1.6.1 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.50.0 diff --git a/go.sum b/go.sum index 6e0c243..842d467 100644 --- a/go.sum +++ b/go.sum @@ -382,12 +382,15 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -554,6 +557,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg= gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= gorm.io/driver/postgres v1.0.5 h1:raX6ezL/ciUmaYTvOq48jq1GE95aMC0CmxQYbxQ4Ufw= diff --git a/main.go b/main.go index dd02bd0..2f4a939 100644 --- a/main.go +++ b/main.go @@ -102,6 +102,8 @@ func main() { // TODO: move endpoint registration to the respective routes files + routes.Init() + // Handlers summaryHandler := routes.NewSummaryHandler(summaryService) healthHandler := routes.NewHealthHandler(db) diff --git a/middlewares/authenticate_test.go b/middlewares/authenticate_test.go new file mode 100644 index 0000000..cc42b2e --- /dev/null +++ b/middlewares/authenticate_test.go @@ -0,0 +1,81 @@ +package middlewares + +import ( + "encoding/base64" + "fmt" + "github.com/muety/wakapi/mocks" + "github.com/muety/wakapi/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "net/http" + "testing" +) + +func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) { + testApiKey := "z5uig69cn9ut93n" + testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey)) + testUser := &models.User{ApiKey: testApiKey} + + mockRequest := &http.Request{ + Header: http.Header{ + "Authorization": []string{fmt.Sprintf("Basic %s", testToken)}, + }, + } + + userServiceMock := new(mocks.UserServiceMock) + userServiceMock.On("GetUserByKey", testApiKey).Return(testUser, nil) + + sut := NewAuthenticateMiddleware(userServiceMock, []string{}) + + result, err := sut.tryGetUserByApiKey(mockRequest) + + assert.Nil(t, err) + assert.Equal(t, testUser, result) +} + +func TestAuthenticateMiddleware_tryGetUserByApiKey_GetFromCache(t *testing.T) { + testApiKey := "z5uig69cn9ut93n" + testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey)) + testUser := &models.User{ApiKey: testApiKey} + + mockRequest := &http.Request{ + Header: http.Header{ + "Authorization": []string{fmt.Sprintf("Basic %s", testToken)}, + }, + } + + userServiceMock := new(mocks.UserServiceMock) + userServiceMock.On("GetUserByKey", testApiKey).Return(testUser, nil) + + sut := NewAuthenticateMiddleware(userServiceMock, []string{}) + sut.cache.SetDefault(testApiKey, testUser) + + result, err := sut.tryGetUserByApiKey(mockRequest) + + assert.Nil(t, err) + assert.Equal(t, testUser, result) + userServiceMock.AssertNotCalled(t, "GetUserByKey", mock.Anything) +} + +func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) { + testApiKey := "z5uig69cn9ut93n" + testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey)) + + mockRequest := &http.Request{ + Header: http.Header{ + // 'Basic' prefix missing here + "Authorization": []string{fmt.Sprintf("%s", testToken)}, + }, + } + + userServiceMock := new(mocks.UserServiceMock) + + sut := NewAuthenticateMiddleware(userServiceMock, []string{}) + + result, err := sut.tryGetUserByApiKey(mockRequest) + + assert.Error(t, err) + assert.Nil(t, result) +} + +// TODO: somehow test cookie auth function diff --git a/migrations/common/custom_post.go b/migrations/common/custom_post.go index 531720d..8c67384 100644 --- a/migrations/common/custom_post.go +++ b/migrations/common/custom_post.go @@ -22,7 +22,7 @@ func init() { func RunCustomPostMigrations(db *gorm.DB, cfg *config.Config) { for _, m := range customPostMigrations { - log.Printf("running migration '%s'\n", m.name) + log.Printf("potentially running migration '%s'\n", m.name) if err := m.f(db, cfg); err != nil { log.Fatalf("migration '%s' failed – %v\n", m.name, err) } diff --git a/migrations/common/custom_pre.go b/migrations/common/custom_pre.go index 8274256..b3d3c81 100644 --- a/migrations/common/custom_pre.go +++ b/migrations/common/custom_pre.go @@ -100,7 +100,7 @@ func init() { func RunCustomPreMigrations(db *gorm.DB, cfg *config.Config) { for _, m := range customPreMigrations { - log.Printf("running migration '%s'\n", m.name) + log.Printf("potentially running migration '%s'\n", m.name) if err := m.f(db, cfg); err != nil { log.Fatalf("migration '%s' failed – %v\n", m.name, err) } diff --git a/mocks/alias_repository_mock.go b/mocks/alias_repository_mock.go new file mode 100644 index 0000000..1ccd800 --- /dev/null +++ b/mocks/alias_repository_mock.go @@ -0,0 +1,15 @@ +package mocks + +import ( + "github.com/muety/wakapi/models" + "github.com/stretchr/testify/mock" +) + +type AliasRepositoryMock struct { + mock.Mock +} + +func (m *AliasRepositoryMock) GetByUser(s string) ([]*models.Alias, error) { + args := m.Called(s) + return args.Get(0).([]*models.Alias), args.Error(1) +} diff --git a/mocks/user_service_mock.go b/mocks/user_service_mock.go new file mode 100644 index 0000000..a91d2f9 --- /dev/null +++ b/mocks/user_service_mock.go @@ -0,0 +1,50 @@ +package mocks + +import ( + "github.com/muety/wakapi/models" + "github.com/stretchr/testify/mock" +) + +type UserServiceMock struct { + mock.Mock +} + +func (m *UserServiceMock) GetUserById(s string) (*models.User, error) { + args := m.Called(s) + return args.Get(0).(*models.User), args.Error(1) +} + +func (m *UserServiceMock) GetUserByKey(s string) (*models.User, error) { + args := m.Called(s) + return args.Get(0).(*models.User), args.Error(1) +} + +func (m *UserServiceMock) GetAll() ([]*models.User, error) { + args := m.Called() + return args.Get(0).([]*models.User), args.Error(1) +} + +func (m *UserServiceMock) CreateOrGet(signup *models.Signup) (*models.User, bool, error) { + args := m.Called(signup) + return args.Get(0).(*models.User), args.Bool(1), args.Error(2) +} + +func (m *UserServiceMock) Update(user *models.User) (*models.User, error) { + args := m.Called(user) + return args.Get(0).(*models.User), args.Error(1) +} + +func (m *UserServiceMock) ResetApiKey(user *models.User) (*models.User, error) { + args := m.Called(user) + return args.Get(0).(*models.User), args.Error(1) +} + +func (m *UserServiceMock) ToggleBadges(user *models.User) (*models.User, error) { + args := m.Called(user) + return args.Get(0).(*models.User), args.Error(1) +} + +func (m *UserServiceMock) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) { + args := m.Called(user, login) + return args.Get(0).(*models.User), args.Error(1) +} diff --git a/models/heartbeat.go b/models/heartbeat.go index 0eb05f1..e2bf2bc 100644 --- a/models/heartbeat.go +++ b/models/heartbeat.go @@ -24,7 +24,7 @@ type Heartbeat struct { } func (h *Heartbeat) Valid() bool { - return h.User != nil && h.UserID != "" && h.Time != CustomTime(time.Time{}) + return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{}) } func (h *Heartbeat) Augment(languageMappings map[string]string) { diff --git a/models/heartbeat_test.go b/models/heartbeat_test.go new file mode 100644 index 0000000..b97ad65 --- /dev/null +++ b/models/heartbeat_test.go @@ -0,0 +1,53 @@ +package models + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestHeartbeat_Valid_Success(t *testing.T) { + sut := &Heartbeat{ + User: &User{ + ID: "johndoe@example.org", + }, + UserID: "johndoe@example.org", + Time: CustomTime(time.Now()), + } + assert.True(t, sut.Valid()) +} + +func TestHeartbeat_Valid_MissingUser(t *testing.T) { + sut := &Heartbeat{ + Time: CustomTime(time.Now()), + } + assert.False(t, sut.Valid()) +} + +func TestHeartbeat_Augment(t *testing.T) { + testMappings := map[string]string{ + "py": "Python3", + } + + sut := &Heartbeat{ + Entity: "~/dev/file.py", + Language: "Python", + } + + sut.Augment(testMappings) + + assert.Equal(t, "Python3", sut.Language) +} + +func TestHeartbeat_GetKey(t *testing.T) { + sut := &Heartbeat{ + Project: "wakapi", + } + + assert.Equal(t, "wakapi", sut.GetKey(SummaryProject)) + assert.Equal(t, UnknownSummaryKey, sut.GetKey(SummaryOS)) + assert.Equal(t, UnknownSummaryKey, sut.GetKey(SummaryMachine)) + assert.Equal(t, UnknownSummaryKey, sut.GetKey(SummaryLanguage)) + assert.Equal(t, UnknownSummaryKey, sut.GetKey(SummaryEditor)) + assert.Equal(t, UnknownSummaryKey, sut.GetKey(255)) +} diff --git a/models/summary_test.go b/models/summary_test.go new file mode 100644 index 0000000..07284a4 --- /dev/null +++ b/models/summary_test.go @@ -0,0 +1,165 @@ +package models + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSummary_FillUnknown(t *testing.T) { + testDuration := 10 * time.Minute + + sut := &Summary{ + Projects: []*SummaryItem{ + { + Type: SummaryProject, + Key: "wakapi", + // hack to work around the issue that the total time of a summary item is mistakenly represented in seconds + Total: testDuration / time.Second, + }, + }, + } + + sut.FillUnknown() + + itemLists := [][]*SummaryItem{ + sut.Machines, + sut.OperatingSystems, + sut.Languages, + sut.Editors, + } + for _, l := range itemLists { + assert.Len(t, l, 1) + assert.Equal(t, UnknownSummaryKey, l[0].Key) + assert.Equal(t, testDuration, l[0].Total) + } +} + +func TestSummary_TotalTimeBy(t *testing.T) { + testDuration1, testDuration2, testDuration3 := 10*time.Minute, 5*time.Minute, 20*time.Minute + + sut := &Summary{ + Projects: []*SummaryItem{ + { + Type: SummaryProject, + Key: "wakapi", + // hack to work around the issue that the total time of a summary item is mistakenly represented in seconds + Total: testDuration1 / time.Second, + }, + { + Type: SummaryProject, + Key: "anchr", + Total: testDuration2 / time.Second, + }, + }, + Languages: []*SummaryItem{ + { + Type: SummaryLanguage, + Key: "Go", + Total: testDuration3 / time.Second, + }, + }, + } + + assert.Equal(t, testDuration1+testDuration2, sut.TotalTimeBy(SummaryProject)) + assert.Equal(t, testDuration3, sut.TotalTimeBy(SummaryLanguage)) + assert.Zero(t, sut.TotalTimeBy(SummaryEditor)) + assert.Zero(t, sut.TotalTimeBy(SummaryMachine)) + assert.Zero(t, sut.TotalTimeBy(SummaryOS)) +} + +func TestSummary_TotalTimeByFilters(t *testing.T) { + testDuration1, testDuration2, testDuration3 := 10*time.Minute, 5*time.Minute, 20*time.Minute + + sut := &Summary{ + Projects: []*SummaryItem{ + { + Type: SummaryProject, + Key: "wakapi", + // hack to work around the issue that the total time of a summary item is mistakenly represented in seconds + Total: testDuration1 / time.Second, + }, + { + Type: SummaryProject, + Key: "anchr", + Total: testDuration2 / time.Second, + }, + }, + Languages: []*SummaryItem{ + { + Type: SummaryLanguage, + Key: "Go", + Total: testDuration3 / time.Second, + }, + }, + } + + filters1 := &Filters{Project: "wakapi"} + filters2 := &Filters{Project: "wakapi", Language: "Go"} // filters have OR logic + filters3 := &Filters{} + + assert.Equal(t, testDuration1, sut.TotalTimeByFilters(filters1)) + assert.Equal(t, testDuration1+testDuration3, sut.TotalTimeByFilters(filters2)) + assert.Zero(t, sut.TotalTimeByFilters(filters3)) +} + +func TestSummary_WithResolvedAliases(t *testing.T) { + testDuration1, testDuration2, testDuration3, testDuration4 := 10*time.Minute, 5*time.Minute, 1*time.Minute, 20*time.Minute + + var resolver AliasResolver = func(t uint8, k string) string { + switch t { + case SummaryProject: + switch k { + case "wakapi-mobile": + return "wakapi" + } + case SummaryLanguage: + switch k { + case "Java 8": + return "Java" + } + } + return k + } + + sut := &Summary{ + Projects: []*SummaryItem{ + { + Type: SummaryProject, + Key: "wakapi", + Total: testDuration1 / time.Second, + }, + { + Type: SummaryProject, + Key: "wakapi-mobile", + Total: testDuration2 / time.Second, + }, + { + Type: SummaryProject, + Key: "anchr", + Total: testDuration3 / time.Second, + }, + }, + Languages: []*SummaryItem{ + { + Type: SummaryLanguage, + Key: "Java 8", + Total: testDuration4 / time.Second, + }, + }, + } + + sut = sut.WithResolvedAliases(resolver) + + assert.Equal(t, testDuration1+testDuration2, sut.TotalTimeByKey(SummaryProject, "wakapi")) + assert.Zero(t, sut.TotalTimeByKey(SummaryProject, "wakapi-mobile")) + assert.Equal(t, testDuration3, sut.TotalTimeByKey(SummaryProject, "anchr")) + assert.Equal(t, testDuration4, sut.TotalTimeByKey(SummaryLanguage, "Java")) + assert.Zero(t, sut.TotalTimeByKey(SummaryLanguage, "wakapi")) + assert.Zero(t, sut.TotalTimeByKey(SummaryProject, "Java 8")) + assert.Len(t, sut.Projects, 2) + assert.Len(t, sut.Languages, 1) + assert.Empty(t, sut.Editors) + assert.Empty(t, sut.OperatingSystems) + assert.Empty(t, sut.Machines) +} diff --git a/routes/routes.go b/routes/routes.go index e3f42d5..c32bd03 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -10,7 +10,7 @@ import ( "strings" ) -func init() { +func Init() { loadTemplates() } diff --git a/services/alias_test.go b/services/alias_test.go new file mode 100644 index 0000000..317c841 --- /dev/null +++ b/services/alias_test.go @@ -0,0 +1,63 @@ +package services + +import ( + "github.com/muety/wakapi/mocks" + "github.com/muety/wakapi/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "testing" +) + +type AliasServiceTestSuite struct { + suite.Suite + TestUserId string + AliasRepoMock *mocks.AliasRepositoryMock +} + +func (suite *AliasServiceTestSuite) SetupSuite() { + suite.TestUserId = "johndoe@example.org" + + aliases := []*models.Alias{ + { + Type: models.SummaryProject, + UserID: suite.TestUserId, + Key: "wakapi", + Value: "wakapi-mobile", + }, + } + + aliasRepoMock := new(mocks.AliasRepositoryMock) + aliasRepoMock.On("GetByUser", suite.TestUserId).Return(aliases, nil) + aliasRepoMock.On("GetByUser", mock.AnythingOfType("string")).Return([]*models.Alias{}, assert.AnError) + + suite.AliasRepoMock = aliasRepoMock +} + +func TestAliasServiceTestSuite(t *testing.T) { + suite.Run(t, new(AliasServiceTestSuite)) +} + +func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault() { + sut := NewAliasService(suite.AliasRepoMock) + + result1, err1 := sut.GetAliasOrDefault(suite.TestUserId, models.SummaryProject, "wakapi-mobile") + result2, err2 := sut.GetAliasOrDefault(suite.TestUserId, models.SummaryProject, "wakapi") + result3, err3 := sut.GetAliasOrDefault(suite.TestUserId, models.SummaryProject, "anchr") + + assert.Equal(suite.T(), "wakapi", result1) + assert.Nil(suite.T(), err1) + assert.Equal(suite.T(), "wakapi", result2) + assert.Nil(suite.T(), err2) + assert.Equal(suite.T(), "anchr", result3) + assert.Nil(suite.T(), err3) +} + +func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault_ErrorOnNonExistingUser() { + sut := NewAliasService(suite.AliasRepoMock) + + result, err := sut.GetAliasOrDefault("nonexisting", models.SummaryProject, "wakapi-mobile") + + assert.Empty(suite.T(), result) + assert.Error(suite.T(), err) +} diff --git a/utils/common_test.go b/utils/common_test.go index f5a5cf5..d1807fc 100644 --- a/utils/common_test.go +++ b/utils/common_test.go @@ -2,10 +2,11 @@ package utils import ( "errors" + "github.com/stretchr/testify/assert" "testing" ) -func TestParseUserAgent(t *testing.T) { +func TestCommon_ParseUserAgent(t *testing.T) { tests := []struct { in string outOs string @@ -38,10 +39,11 @@ func TestParseUserAgent(t *testing.T) { }, } - for i, test := range tests { - if os, editor, err := ParseUserAgent(test.in); os != test.outOs || editor != test.outEditor || !checkErr(test.outError, err) { - t.Errorf("[%d] Unexpected result of parsing '%s'; got '%v', '%v', '%v'", i, test.in, os, editor, err) - } + for _, test := range tests { + os, editor, err := ParseUserAgent(test.in) + assert.True(t, checkErr(err, test.outError)) + assert.Equal(t, test.outOs, os) + assert.Equal(t, test.outEditor, editor) } } diff --git a/version.txt b/version.txt index 4a02d2c..c807441 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.16.2 +1.16.3