From 451848fc6f4486bae5e3c59b40d4d4f8d407e1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20COLL?= Date: Sun, 11 Jun 2023 15:17:50 +0200 Subject: [PATCH] feat(class): add list endpoint - split auth service into auth and user service Closes #93 --- db/queries/quiz.sql | 9 + db/queries/student_class.sql | 4 + db/queries/{auth.sql => user.sql} | 0 internal/back/back.go | 7 +- internal/back/domain/authService.go | 21 +- internal/back/domain/authService_test.go | 5 +- internal/back/domain/classService.go | 41 +++ .../back/domain/mock_AuthRepository_test.go | 246 +--------------- .../back/domain/mock_ClassRepository_test.go | 145 +++++++++ .../back/domain/mock_UserRepository_test.go | 277 ++++++++++++++++++ internal/back/domain/model.go | 6 + internal/back/domain/repositories.go | 14 +- .../back/infrastructure/authRepositoryImpl.go | 101 +------ .../infrastructure/authRepositoryImpl_test.go | 51 ---- .../infrastructure/classRepositoryImpl.go | 73 +++++ internal/back/infrastructure/sqlc/quiz.sql.go | 17 ++ .../infrastructure/sqlc/student_class.sql.go | 14 +- .../sqlc/{auth.sql.go => user.sql.go} | 2 +- .../back/infrastructure/userRepositoryImpl.go | 136 +++++++++ .../infrastructure/userRepositoryImpl_test.go | 74 +++++ .../back/presentation/baseApiController.go | 52 +++- internal/back/presentation/classController.go | 42 +++ internal/back/presentation/model.go | 22 ++ internal/back/presentation/quizController.go | 38 --- 24 files changed, 941 insertions(+), 456 deletions(-) rename db/queries/{auth.sql => user.sql} (100%) create mode 100644 internal/back/domain/classService.go create mode 100644 internal/back/domain/mock_ClassRepository_test.go create mode 100644 internal/back/domain/mock_UserRepository_test.go create mode 100644 internal/back/infrastructure/classRepositoryImpl.go rename internal/back/infrastructure/sqlc/{auth.sql.go => user.sql.go} (99%) create mode 100644 internal/back/infrastructure/userRepositoryImpl.go create mode 100644 internal/back/infrastructure/userRepositoryImpl_test.go create mode 100644 internal/back/presentation/classController.go diff --git a/db/queries/quiz.sql b/db/queries/quiz.sql index 8e6f572..dada85e 100644 --- a/db/queries/quiz.sql +++ b/db/queries/quiz.sql @@ -100,3 +100,12 @@ LIMIT ? OFFSET ?; SELECT COUNT(1) FROM quiz WHERE active = 1; + +-- name: CountAllActiveQuizRestrictedToClass :one +SELECT COUNT(1) +FROM quiz q + JOIN quiz_class_visibility qcv ON q.sha1 = qcv.quiz_sha1 + JOIN student_class sc ON sc.uuid = qcv.class_uuid + JOIN user u ON sc.uuid = u.class_uuid +WHERE q.active = 1 + AND u.id = ?; diff --git a/db/queries/student_class.sql b/db/queries/student_class.sql index 3c58e16..3327715 100644 --- a/db/queries/student_class.sql +++ b/db/queries/student_class.sql @@ -7,6 +7,10 @@ SELECT * FROM student_class LIMIT ? OFFSET ?; +-- name: CountAllClasses :one +SELECT COUNT(1) +FROM student_class; + -- name: DeleteClassById :exec DELETE FROM student_class diff --git a/db/queries/auth.sql b/db/queries/user.sql similarity index 100% rename from db/queries/auth.sql rename to db/queries/user.sql diff --git a/internal/back/back.go b/internal/back/back.go index ff831f3..7759da8 100644 --- a/internal/back/back.go +++ b/internal/back/back.go @@ -42,13 +42,16 @@ func New() Module { quizRepository := infrastructure.NewQuizRepository(connection) authRepository := infrastructure.NewAuthRepository(connection) + userRepository := infrastructure.NewUserRepository(connection) + classRepository := infrastructure.NewClassRepository(connection) quizService := domain.NewQuizService(quizRepository) - authService := domain.NewAuthService(authRepository) + authService := domain.NewAuthService(authRepository, userRepository) + classService := domain.NewClassService(classRepository) return Module{ quizServ: quizService, authServ: authService, - quizCtrl: presentation.NewApiController(&quizService, &authService), + quizCtrl: presentation.NewApiController(&quizService, &authService, &classService), } } diff --git a/internal/back/domain/authService.go b/internal/back/domain/authService.go index f5918a1..a6e3ca2 100644 --- a/internal/back/domain/authService.go +++ b/internal/back/domain/authService.go @@ -28,17 +28,18 @@ import ( ) type AuthService struct { - r AuthRepository + authRepository AuthRepository + userRepository UserRepository } -func NewAuthService(r AuthRepository) AuthService { +func NewAuthService(authRepository AuthRepository, userRepository UserRepository) AuthService { adminEmail := viper.GetString("default-admin-email") if len(adminEmail) > 0 { fmt.Printf("%s Default admin email set to %s\n", color.HiYellowString("i"), color.BlueString(adminEmail)) } - return AuthService{r: r} + return AuthService{authRepository: authRepository, userRepository: userRepository} } func (s *AuthService) Login(ctx context.Context, idToken string) (*User, error) { @@ -68,7 +69,7 @@ func (s *AuthService) Login(ctx context.Context, idToken string) (*User, error) user.Role = Student } - err = s.r.CreateOrReplaceUser(ctx, user) + err = s.userRepository.CreateOrReplaceUser(ctx, user) if err != nil { return nil, err } @@ -125,7 +126,7 @@ func (s *AuthService) ValidateTokenAndGetUser(ctx context.Context, accessToken s return nil, err } - user, err := s.r.FindUserById(ctx, token.Sub) + user, err := s.userRepository.FindUserById(ctx, token.Sub) if err != nil { return nil, err } @@ -135,7 +136,7 @@ func (s *AuthService) ValidateTokenAndGetUser(ctx context.Context, accessToken s } if token.Provenance == Parse { - err := s.r.CacheToken(token) + err := s.authRepository.CacheToken(token) if err != nil { return nil, err } @@ -145,7 +146,7 @@ func (s *AuthService) ValidateTokenAndGetUser(ctx context.Context, accessToken s } func (s *AuthService) FindUserById(ctx context.Context, id string) (*User, error) { - user, err := s.r.FindUserById(ctx, id) + user, err := s.userRepository.FindUserById(ctx, id) if err != nil { return nil, err } @@ -175,13 +176,13 @@ func (role Role) CanAccess(other Role) bool { } func (s *AuthService) FindAllUser(ctx context.Context) ([]*User, error) { - return s.r.FindAllUser(ctx) + return s.userRepository.FindAllUser(ctx) } func (s *AuthService) DeactivateUser(ctx context.Context, id string) error { - return s.r.UpdateUserActive(ctx, id, false) + return s.userRepository.UpdateUserActive(ctx, id, false) } func (s *AuthService) ActivateUser(ctx context.Context, id string) error { - return s.r.UpdateUserActive(ctx, id, true) + return s.userRepository.UpdateUserActive(ctx, id, true) } diff --git a/internal/back/domain/authService_test.go b/internal/back/domain/authService_test.go index ce71b9a..418391e 100644 --- a/internal/back/domain/authService_test.go +++ b/internal/back/domain/authService_test.go @@ -25,9 +25,10 @@ import ( func TestAuthService_FindUserById(t *testing.T) { mockAuthRepository := NewMockAuthRepository(t) - service := NewAuthService(mockAuthRepository) + mockUserRepository := NewMockUserRepository(t) + service := NewAuthService(mockAuthRepository, mockUserRepository) - mockAuthRepository.On("FindUserById", context.Background(), sub).Return(nil, nil) + mockUserRepository.On("FindUserById", context.Background(), sub).Return(nil, nil) _, err := service.FindUserById(context.Background(), sub) if err != nil { diff --git a/internal/back/domain/classService.go b/internal/back/domain/classService.go new file mode 100644 index 0000000..5895763 --- /dev/null +++ b/internal/back/domain/classService.go @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Michaël COLL. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package domain + +import "context" + +type ClassService struct { + r ClassRepository +} + +func NewClassService(classRepository ClassRepository) ClassService { + return ClassService{r: classRepository} +} + +func (s *ClassService) FindAllClasses(ctx context.Context, limit uint16, offset uint16) ([]*Class, uint32, error) { + classes, err := s.r.FindAll(ctx, limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := s.r.CountAll(ctx) + if err != nil { + return nil, 0, err + } + + return classes, total, nil +} diff --git a/internal/back/domain/mock_AuthRepository_test.go b/internal/back/domain/mock_AuthRepository_test.go index ae0e8ba..2a9f318 100644 --- a/internal/back/domain/mock_AuthRepository_test.go +++ b/internal/back/domain/mock_AuthRepository_test.go @@ -2,11 +2,7 @@ package domain -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) +import mock "github.com/stretchr/testify/mock" // MockAuthRepository is an autogenerated mock type for the AuthRepository type type MockAuthRepository struct { @@ -63,103 +59,6 @@ func (_c *MockAuthRepository_CacheToken_Call) RunAndReturn(run func(*IdToken) er return _c } -// CreateOrReplaceUser provides a mock function with given fields: ctx, user -func (_m *MockAuthRepository) CreateOrReplaceUser(ctx context.Context, user *User) error { - ret := _m.Called(ctx, user) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *User) error); ok { - r0 = rf(ctx, user) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockAuthRepository_CreateOrReplaceUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrReplaceUser' -type MockAuthRepository_CreateOrReplaceUser_Call struct { - *mock.Call -} - -// CreateOrReplaceUser is a helper method to define mock.On call -// - ctx context.Context -// - user *User -func (_e *MockAuthRepository_Expecter) CreateOrReplaceUser(ctx interface{}, user interface{}) *MockAuthRepository_CreateOrReplaceUser_Call { - return &MockAuthRepository_CreateOrReplaceUser_Call{Call: _e.mock.On("CreateOrReplaceUser", ctx, user)} -} - -func (_c *MockAuthRepository_CreateOrReplaceUser_Call) Run(run func(ctx context.Context, user *User)) *MockAuthRepository_CreateOrReplaceUser_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*User)) - }) - return _c -} - -func (_c *MockAuthRepository_CreateOrReplaceUser_Call) Return(_a0 error) *MockAuthRepository_CreateOrReplaceUser_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockAuthRepository_CreateOrReplaceUser_Call) RunAndReturn(run func(context.Context, *User) error) *MockAuthRepository_CreateOrReplaceUser_Call { - _c.Call.Return(run) - return _c -} - -// FindAllUser provides a mock function with given fields: ctx -func (_m *MockAuthRepository) FindAllUser(ctx context.Context) ([]*User, error) { - ret := _m.Called(ctx) - - var r0 []*User - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*User, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []*User); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*User) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockAuthRepository_FindAllUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAllUser' -type MockAuthRepository_FindAllUser_Call struct { - *mock.Call -} - -// FindAllUser is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockAuthRepository_Expecter) FindAllUser(ctx interface{}) *MockAuthRepository_FindAllUser_Call { - return &MockAuthRepository_FindAllUser_Call{Call: _e.mock.On("FindAllUser", ctx)} -} - -func (_c *MockAuthRepository_FindAllUser_Call) Run(run func(ctx context.Context)) *MockAuthRepository_FindAllUser_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockAuthRepository_FindAllUser_Call) Return(_a0 []*User, _a1 error) *MockAuthRepository_FindAllUser_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockAuthRepository_FindAllUser_Call) RunAndReturn(run func(context.Context) ([]*User, error)) *MockAuthRepository_FindAllUser_Call { - _c.Call.Return(run) - return _c -} - // FindTokenByTokenStr provides a mock function with given fields: tokenStr func (_m *MockAuthRepository) FindTokenByTokenStr(tokenStr string) (*IdToken, error) { ret := _m.Called(tokenStr) @@ -214,149 +113,6 @@ func (_c *MockAuthRepository_FindTokenByTokenStr_Call) RunAndReturn(run func(str return _c } -// FindUserById provides a mock function with given fields: ctx, id -func (_m *MockAuthRepository) FindUserById(ctx context.Context, id string) (*User, error) { - ret := _m.Called(ctx, id) - - var r0 *User - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*User, error)); ok { - return rf(ctx, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *User); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*User) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockAuthRepository_FindUserById_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindUserById' -type MockAuthRepository_FindUserById_Call struct { - *mock.Call -} - -// FindUserById is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *MockAuthRepository_Expecter) FindUserById(ctx interface{}, id interface{}) *MockAuthRepository_FindUserById_Call { - return &MockAuthRepository_FindUserById_Call{Call: _e.mock.On("FindUserById", ctx, id)} -} - -func (_c *MockAuthRepository_FindUserById_Call) Run(run func(ctx context.Context, id string)) *MockAuthRepository_FindUserById_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *MockAuthRepository_FindUserById_Call) Return(_a0 *User, _a1 error) *MockAuthRepository_FindUserById_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockAuthRepository_FindUserById_Call) RunAndReturn(run func(context.Context, string) (*User, error)) *MockAuthRepository_FindUserById_Call { - _c.Call.Return(run) - return _c -} - -// UpdateUserActive provides a mock function with given fields: ctx, id, active -func (_m *MockAuthRepository) UpdateUserActive(ctx context.Context, id string, active bool) error { - ret := _m.Called(ctx, id, active) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { - r0 = rf(ctx, id, active) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockAuthRepository_UpdateUserActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserActive' -type MockAuthRepository_UpdateUserActive_Call struct { - *mock.Call -} - -// UpdateUserActive is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - active bool -func (_e *MockAuthRepository_Expecter) UpdateUserActive(ctx interface{}, id interface{}, active interface{}) *MockAuthRepository_UpdateUserActive_Call { - return &MockAuthRepository_UpdateUserActive_Call{Call: _e.mock.On("UpdateUserActive", ctx, id, active)} -} - -func (_c *MockAuthRepository_UpdateUserActive_Call) Run(run func(ctx context.Context, id string, active bool)) *MockAuthRepository_UpdateUserActive_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(bool)) - }) - return _c -} - -func (_c *MockAuthRepository_UpdateUserActive_Call) Return(_a0 error) *MockAuthRepository_UpdateUserActive_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockAuthRepository_UpdateUserActive_Call) RunAndReturn(run func(context.Context, string, bool) error) *MockAuthRepository_UpdateUserActive_Call { - _c.Call.Return(run) - return _c -} - -// UpdateUserRole provides a mock function with given fields: ctx, userId, role -func (_m *MockAuthRepository) UpdateUserRole(ctx context.Context, userId string, role Role) error { - ret := _m.Called(ctx, userId, role) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, Role) error); ok { - r0 = rf(ctx, userId, role) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockAuthRepository_UpdateUserRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserRole' -type MockAuthRepository_UpdateUserRole_Call struct { - *mock.Call -} - -// UpdateUserRole is a helper method to define mock.On call -// - ctx context.Context -// - userId string -// - role Role -func (_e *MockAuthRepository_Expecter) UpdateUserRole(ctx interface{}, userId interface{}, role interface{}) *MockAuthRepository_UpdateUserRole_Call { - return &MockAuthRepository_UpdateUserRole_Call{Call: _e.mock.On("UpdateUserRole", ctx, userId, role)} -} - -func (_c *MockAuthRepository_UpdateUserRole_Call) Run(run func(ctx context.Context, userId string, role Role)) *MockAuthRepository_UpdateUserRole_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(Role)) - }) - return _c -} - -func (_c *MockAuthRepository_UpdateUserRole_Call) Return(_a0 error) *MockAuthRepository_UpdateUserRole_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockAuthRepository_UpdateUserRole_Call) RunAndReturn(run func(context.Context, string, Role) error) *MockAuthRepository_UpdateUserRole_Call { - _c.Call.Return(run) - return _c -} - type mockConstructorTestingTNewMockAuthRepository interface { mock.TestingT Cleanup(func()) diff --git a/internal/back/domain/mock_ClassRepository_test.go b/internal/back/domain/mock_ClassRepository_test.go new file mode 100644 index 0000000..25c152a --- /dev/null +++ b/internal/back/domain/mock_ClassRepository_test.go @@ -0,0 +1,145 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package domain + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockClassRepository is an autogenerated mock type for the ClassRepository type +type MockClassRepository struct { + mock.Mock +} + +type MockClassRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockClassRepository) EXPECT() *MockClassRepository_Expecter { + return &MockClassRepository_Expecter{mock: &_m.Mock} +} + +// CountAll provides a mock function with given fields: ctx +func (_m *MockClassRepository) CountAll(ctx context.Context) (uint32, error) { + ret := _m.Called(ctx) + + var r0 uint32 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint32, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) uint32); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(uint32) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClassRepository_CountAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountAll' +type MockClassRepository_CountAll_Call struct { + *mock.Call +} + +// CountAll is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockClassRepository_Expecter) CountAll(ctx interface{}) *MockClassRepository_CountAll_Call { + return &MockClassRepository_CountAll_Call{Call: _e.mock.On("CountAll", ctx)} +} + +func (_c *MockClassRepository_CountAll_Call) Run(run func(ctx context.Context)) *MockClassRepository_CountAll_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockClassRepository_CountAll_Call) Return(_a0 uint32, _a1 error) *MockClassRepository_CountAll_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClassRepository_CountAll_Call) RunAndReturn(run func(context.Context) (uint32, error)) *MockClassRepository_CountAll_Call { + _c.Call.Return(run) + return _c +} + +// FindAll provides a mock function with given fields: ctx, limit, offset +func (_m *MockClassRepository) FindAll(ctx context.Context, limit uint16, offset uint16) ([]*Class, error) { + ret := _m.Called(ctx, limit, offset) + + var r0 []*Class + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint16, uint16) ([]*Class, error)); ok { + return rf(ctx, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, uint16, uint16) []*Class); ok { + r0 = rf(ctx, limit, offset) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Class) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint16, uint16) error); ok { + r1 = rf(ctx, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClassRepository_FindAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAll' +type MockClassRepository_FindAll_Call struct { + *mock.Call +} + +// FindAll is a helper method to define mock.On call +// - ctx context.Context +// - limit uint16 +// - offset uint16 +func (_e *MockClassRepository_Expecter) FindAll(ctx interface{}, limit interface{}, offset interface{}) *MockClassRepository_FindAll_Call { + return &MockClassRepository_FindAll_Call{Call: _e.mock.On("FindAll", ctx, limit, offset)} +} + +func (_c *MockClassRepository_FindAll_Call) Run(run func(ctx context.Context, limit uint16, offset uint16)) *MockClassRepository_FindAll_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint16), args[2].(uint16)) + }) + return _c +} + +func (_c *MockClassRepository_FindAll_Call) Return(_a0 []*Class, _a1 error) *MockClassRepository_FindAll_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClassRepository_FindAll_Call) RunAndReturn(run func(context.Context, uint16, uint16) ([]*Class, error)) *MockClassRepository_FindAll_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewMockClassRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockClassRepository creates a new instance of MockClassRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockClassRepository(t mockConstructorTestingTNewMockClassRepository) *MockClassRepository { + mock := &MockClassRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/back/domain/mock_UserRepository_test.go b/internal/back/domain/mock_UserRepository_test.go new file mode 100644 index 0000000..6cbae58 --- /dev/null +++ b/internal/back/domain/mock_UserRepository_test.go @@ -0,0 +1,277 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package domain + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockUserRepository is an autogenerated mock type for the UserRepository type +type MockUserRepository struct { + mock.Mock +} + +type MockUserRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUserRepository) EXPECT() *MockUserRepository_Expecter { + return &MockUserRepository_Expecter{mock: &_m.Mock} +} + +// CreateOrReplaceUser provides a mock function with given fields: ctx, user +func (_m *MockUserRepository) CreateOrReplaceUser(ctx context.Context, user *User) error { + ret := _m.Called(ctx, user) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *User) error); ok { + r0 = rf(ctx, user) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUserRepository_CreateOrReplaceUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrReplaceUser' +type MockUserRepository_CreateOrReplaceUser_Call struct { + *mock.Call +} + +// CreateOrReplaceUser is a helper method to define mock.On call +// - ctx context.Context +// - user *User +func (_e *MockUserRepository_Expecter) CreateOrReplaceUser(ctx interface{}, user interface{}) *MockUserRepository_CreateOrReplaceUser_Call { + return &MockUserRepository_CreateOrReplaceUser_Call{Call: _e.mock.On("CreateOrReplaceUser", ctx, user)} +} + +func (_c *MockUserRepository_CreateOrReplaceUser_Call) Run(run func(ctx context.Context, user *User)) *MockUserRepository_CreateOrReplaceUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*User)) + }) + return _c +} + +func (_c *MockUserRepository_CreateOrReplaceUser_Call) Return(_a0 error) *MockUserRepository_CreateOrReplaceUser_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUserRepository_CreateOrReplaceUser_Call) RunAndReturn(run func(context.Context, *User) error) *MockUserRepository_CreateOrReplaceUser_Call { + _c.Call.Return(run) + return _c +} + +// FindAllUser provides a mock function with given fields: ctx +func (_m *MockUserRepository) FindAllUser(ctx context.Context) ([]*User, error) { + ret := _m.Called(ctx) + + var r0 []*User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*User, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*User); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUserRepository_FindAllUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAllUser' +type MockUserRepository_FindAllUser_Call struct { + *mock.Call +} + +// FindAllUser is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockUserRepository_Expecter) FindAllUser(ctx interface{}) *MockUserRepository_FindAllUser_Call { + return &MockUserRepository_FindAllUser_Call{Call: _e.mock.On("FindAllUser", ctx)} +} + +func (_c *MockUserRepository_FindAllUser_Call) Run(run func(ctx context.Context)) *MockUserRepository_FindAllUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockUserRepository_FindAllUser_Call) Return(_a0 []*User, _a1 error) *MockUserRepository_FindAllUser_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUserRepository_FindAllUser_Call) RunAndReturn(run func(context.Context) ([]*User, error)) *MockUserRepository_FindAllUser_Call { + _c.Call.Return(run) + return _c +} + +// FindUserById provides a mock function with given fields: ctx, id +func (_m *MockUserRepository) FindUserById(ctx context.Context, id string) (*User, error) { + ret := _m.Called(ctx, id) + + var r0 *User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*User, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *User); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUserRepository_FindUserById_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindUserById' +type MockUserRepository_FindUserById_Call struct { + *mock.Call +} + +// FindUserById is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *MockUserRepository_Expecter) FindUserById(ctx interface{}, id interface{}) *MockUserRepository_FindUserById_Call { + return &MockUserRepository_FindUserById_Call{Call: _e.mock.On("FindUserById", ctx, id)} +} + +func (_c *MockUserRepository_FindUserById_Call) Run(run func(ctx context.Context, id string)) *MockUserRepository_FindUserById_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockUserRepository_FindUserById_Call) Return(_a0 *User, _a1 error) *MockUserRepository_FindUserById_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUserRepository_FindUserById_Call) RunAndReturn(run func(context.Context, string) (*User, error)) *MockUserRepository_FindUserById_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUserActive provides a mock function with given fields: ctx, id, active +func (_m *MockUserRepository) UpdateUserActive(ctx context.Context, id string, active bool) error { + ret := _m.Called(ctx, id, active) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { + r0 = rf(ctx, id, active) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUserRepository_UpdateUserActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserActive' +type MockUserRepository_UpdateUserActive_Call struct { + *mock.Call +} + +// UpdateUserActive is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - active bool +func (_e *MockUserRepository_Expecter) UpdateUserActive(ctx interface{}, id interface{}, active interface{}) *MockUserRepository_UpdateUserActive_Call { + return &MockUserRepository_UpdateUserActive_Call{Call: _e.mock.On("UpdateUserActive", ctx, id, active)} +} + +func (_c *MockUserRepository_UpdateUserActive_Call) Run(run func(ctx context.Context, id string, active bool)) *MockUserRepository_UpdateUserActive_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(bool)) + }) + return _c +} + +func (_c *MockUserRepository_UpdateUserActive_Call) Return(_a0 error) *MockUserRepository_UpdateUserActive_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUserRepository_UpdateUserActive_Call) RunAndReturn(run func(context.Context, string, bool) error) *MockUserRepository_UpdateUserActive_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUserRole provides a mock function with given fields: ctx, userId, role +func (_m *MockUserRepository) UpdateUserRole(ctx context.Context, userId string, role Role) error { + ret := _m.Called(ctx, userId, role) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, Role) error); ok { + r0 = rf(ctx, userId, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUserRepository_UpdateUserRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserRole' +type MockUserRepository_UpdateUserRole_Call struct { + *mock.Call +} + +// UpdateUserRole is a helper method to define mock.On call +// - ctx context.Context +// - userId string +// - role Role +func (_e *MockUserRepository_Expecter) UpdateUserRole(ctx interface{}, userId interface{}, role interface{}) *MockUserRepository_UpdateUserRole_Call { + return &MockUserRepository_UpdateUserRole_Call{Call: _e.mock.On("UpdateUserRole", ctx, userId, role)} +} + +func (_c *MockUserRepository_UpdateUserRole_Call) Run(run func(ctx context.Context, userId string, role Role)) *MockUserRepository_UpdateUserRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(Role)) + }) + return _c +} + +func (_c *MockUserRepository_UpdateUserRole_Call) Return(_a0 error) *MockUserRepository_UpdateUserRole_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUserRepository_UpdateUserRole_Call) RunAndReturn(run func(context.Context, string, Role) error) *MockUserRepository_UpdateUserRole_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewMockUserRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockUserRepository creates a new instance of MockUserRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockUserRepository(t mockConstructorTestingTNewMockUserRepository) *MockUserRepository { + mock := &MockUserRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/back/domain/model.go b/internal/back/domain/model.go index 8d53f87..b7b734d 100644 --- a/internal/back/domain/model.go +++ b/internal/back/domain/model.go @@ -69,6 +69,7 @@ type User struct { Lastname string Active bool Role Role + Class *Class } type TokenProvenance int8 @@ -105,3 +106,8 @@ type Session struct { RemainingSec int Result *SessionResult } + +type Class struct { + Id uuid.UUID + Name string +} diff --git a/internal/back/domain/repositories.go b/internal/back/domain/repositories.go index 5cda669..85f23a5 100644 --- a/internal/back/domain/repositories.go +++ b/internal/back/domain/repositories.go @@ -40,11 +40,21 @@ type QuizRepository interface { //go:generate mockery --name AuthRepository type AuthRepository interface { + CacheToken(token *IdToken) error + FindTokenByTokenStr(tokenStr string) (*IdToken, error) +} + +//go:generate mockery --name UserRepository +type UserRepository interface { FindUserById(ctx context.Context, id string) (*User, error) FindAllUser(ctx context.Context) ([]*User, error) CreateOrReplaceUser(ctx context.Context, user *User) error UpdateUserActive(ctx context.Context, id string, active bool) error UpdateUserRole(ctx context.Context, userId string, role Role) error - CacheToken(token *IdToken) error - FindTokenByTokenStr(tokenStr string) (*IdToken, error) +} + +//go:generate mockery --name ClassRepository +type ClassRepository interface { + FindAll(ctx context.Context, limit uint16, offset uint16) ([]*Class, error) + CountAll(ctx context.Context) (uint32, error) } diff --git a/internal/back/infrastructure/authRepositoryImpl.go b/internal/back/infrastructure/authRepositoryImpl.go index fcee027..f81c6ee 100644 --- a/internal/back/infrastructure/authRepositoryImpl.go +++ b/internal/back/infrastructure/authRepositoryImpl.go @@ -17,9 +17,7 @@ package infrastructure import ( - "context" "database/sql" - "errors" "time" "github.com/patrickmn/go-cache" @@ -32,63 +30,12 @@ type AuthDBRepository struct { domain.AuthRepository q *sqlc.Queries - uc *cache.Cache tc *cache.Cache } func NewAuthRepository(c *sql.DB) *AuthDBRepository { - userCache := cache.New(30*time.Minute, 10*time.Minute) tokenCache := cache.New(1*time.Hour, 1*time.Second) - return &AuthDBRepository{q: sqlc.New(c), uc: userCache, tc: tokenCache} -} - -func (r *AuthDBRepository) FindUserById(ctx context.Context, id string) (*domain.User, error) { - - if user, found := r.uc.Get(id); found { - return user.(*domain.User), nil - } - - entity, err := r.q.FindUserById(ctx, id) - if err != nil && errors.Is(err, sql.ErrNoRows) { - return nil, nil - } else if err != nil { - return nil, err - } - - user := r.toUser(entity) - - r.uc.Set(id, user, cache.DefaultExpiration) - - return user, nil -} - -func (r *AuthDBRepository) CreateOrReplaceUser(ctx context.Context, user *domain.User) error { - err := r.q.CreateOrReplaceUser(ctx, sqlc.CreateOrReplaceUserParams{ - ID: user.Id, - Email: user.Email, - Firstname: user.Firstname, - Lastname: user.Lastname, - RoleID: int64(user.Role), - }) - if err != nil { - return err - } - - return nil -} - -func (r *AuthDBRepository) UpdateUserRole(ctx context.Context, userId string, role domain.Role) error { - err := r.q.UpdateUserRole(ctx, sqlc.UpdateUserRoleParams{ - ID: userId, - RoleID: int64(role), - }) - if err != nil { - return err - } - - r.uc.Delete(userId) - - return nil + return &AuthDBRepository{q: sqlc.New(c), tc: tokenCache} } func (r *AuthDBRepository) CacheToken(token *domain.IdToken) error { @@ -109,49 +56,3 @@ func (r *AuthDBRepository) FindTokenByTokenStr(tokenStr string) (*domain.IdToken return nil, nil } - -func (r *AuthDBRepository) FindAllUser(ctx context.Context) ([]*domain.User, error) { - entities, err := r.q.FindAllUser(ctx) - if err != nil { - return nil, err - } - - users := make([]*domain.User, len(entities)) - - for i, entity := range entities { - users[i] = r.toUser(entity) - } - - return users, nil -} - -func (r *AuthDBRepository) UpdateUserActive(ctx context.Context, id string, active bool) error { - return r.q.UpdateUserActive(ctx, sqlc.UpdateUserActiveParams{ - Active: active, - ID: id, - }) -} - -func (r *AuthDBRepository) toUser(entity sqlc.User) *domain.User { - return &domain.User{ - Id: entity.ID, - Email: entity.Email, - Firstname: entity.Firstname, - Lastname: entity.Lastname, - Active: entity.Active, - Role: r.toRole(entity.RoleID), - } -} - -func (r *AuthDBRepository) toRole(entity int64) domain.Role { - switch entity { - case int64(domain.Admin): - return domain.Admin - case int64(domain.Teacher): - return domain.Teacher - case int64(domain.Student): - return domain.Student - } - - return 0 -} diff --git a/internal/back/infrastructure/authRepositoryImpl_test.go b/internal/back/infrastructure/authRepositoryImpl_test.go index d53c361..b673318 100644 --- a/internal/back/infrastructure/authRepositoryImpl_test.go +++ b/internal/back/infrastructure/authRepositoryImpl_test.go @@ -17,12 +17,9 @@ package infrastructure import ( - "context" "testing" "github.com/stretchr/testify/assert" - - "github.com/michaelcoll/quiz-app/internal/back/domain" ) func TestAuthDBRepository_FindTokenByTokenStr(t *testing.T) { @@ -39,51 +36,3 @@ func TestAuthDBRepository_FindTokenByTokenStr(t *testing.T) { assert.Nil(t, token) } - -func TestAuthDBRepository_FindUserById(t *testing.T) { - connection := getDBConnection(t, true) - defer connection.Close() - - r := NewAuthRepository(connection) - - user, err := r.FindUserById(context.Background(), "42") - if err != nil { - assert.Failf(t, "Fail to get user", "%v", err) - } - - assert.Nil(t, user) - - err = r.CreateOrReplaceUser(context.Background(), &domain.User{ - Id: sub, - Email: email, - Firstname: firstName, - Lastname: lastName, - Role: domain.Admin, - }) - if err != nil { - assert.Failf(t, "Fail to create user", "%v", err) - } - - user, err = r.FindUserById(context.Background(), sub) - if err != nil { - assert.Failf(t, "Fail to get user", "%v", err) - } - - assert.Equal(t, sub, user.Id) - assert.Equal(t, email, user.Email) - assert.Equal(t, firstName, user.Firstname) - assert.Equal(t, lastName, user.Lastname) - assert.True(t, user.Active) - - user, err = r.FindUserById(context.Background(), sub) - if err != nil { - assert.Failf(t, "Fail to get user", "%v", err) - } - - assert.Equal(t, sub, user.Id) - assert.Equal(t, email, user.Email) - assert.Equal(t, firstName, user.Firstname) - assert.Equal(t, lastName, user.Lastname) - assert.True(t, user.Active) - assert.Equal(t, user.Role, domain.Admin) -} diff --git a/internal/back/infrastructure/classRepositoryImpl.go b/internal/back/infrastructure/classRepositoryImpl.go new file mode 100644 index 0000000..8614cb2 --- /dev/null +++ b/internal/back/infrastructure/classRepositoryImpl.go @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Michaël COLL. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package infrastructure + +import ( + "context" + "database/sql" + + "github.com/michaelcoll/quiz-app/internal/back/domain" + "github.com/michaelcoll/quiz-app/internal/back/infrastructure/sqlc" +) + +type ClassDBRepository struct { + domain.ClassRepository + + q *sqlc.Queries +} + +func NewClassRepository(c *sql.DB) *ClassDBRepository { + return &ClassDBRepository{q: sqlc.New(c)} +} + +func (r *ClassDBRepository) FindAll(ctx context.Context, limit uint16, offset uint16) ([]*domain.Class, error) { + classes, err := r.q.FindAllClasses(ctx, sqlc.FindAllClassesParams{ + Limit: int64(limit), + Offset: int64(offset), + }) + if err != nil { + return nil, err + } + + return r.toClassArray(classes), nil +} + +func (r *ClassDBRepository) CountAll(ctx context.Context) (uint32, error) { + count, err := r.q.CountAllClasses(ctx) + if err != nil { + return 0, err + } + + return uint32(count), nil +} + +func (r *ClassDBRepository) toClass(entity sqlc.StudentClass) *domain.Class { + return &domain.Class{ + Id: entity.Uuid, + Name: entity.Name, + } +} + +func (r *ClassDBRepository) toClassArray(entities []sqlc.StudentClass) []*domain.Class { + domains := make([]*domain.Class, len(entities)) + + for i, entity := range entities { + domains[i] = r.toClass(entity) + } + + return domains +} diff --git a/internal/back/infrastructure/sqlc/quiz.sql.go b/internal/back/infrastructure/sqlc/quiz.sql.go index 009e52d..de715f3 100644 --- a/internal/back/infrastructure/sqlc/quiz.sql.go +++ b/internal/back/infrastructure/sqlc/quiz.sql.go @@ -41,6 +41,23 @@ func (q *Queries) CountAllActiveQuiz(ctx context.Context) (int64, error) { return count, err } +const countAllActiveQuizRestrictedToClass = `-- name: CountAllActiveQuizRestrictedToClass :one +SELECT COUNT(1) +FROM quiz q + JOIN quiz_class_visibility qcv ON q.sha1 = qcv.quiz_sha1 + JOIN student_class sc ON sc.uuid = qcv.class_uuid + JOIN user u ON sc.uuid = u.class_uuid +WHERE q.active = 1 + AND u.id = ? +` + +func (q *Queries) CountAllActiveQuizRestrictedToClass(ctx context.Context, id string) (int64, error) { + row := q.db.QueryRowContext(ctx, countAllActiveQuizRestrictedToClass, id) + var count int64 + err := row.Scan(&count) + return count, err +} + const createOrReplaceAnswer = `-- name: CreateOrReplaceAnswer :exec REPLACE INTO quiz_answer (sha1, content, valid) VALUES (?, ?, ?) diff --git a/internal/back/infrastructure/sqlc/student_class.sql.go b/internal/back/infrastructure/sqlc/student_class.sql.go index f21c162..5043cec 100644 --- a/internal/back/infrastructure/sqlc/student_class.sql.go +++ b/internal/back/infrastructure/sqlc/student_class.sql.go @@ -27,6 +27,18 @@ func (q *Queries) AssignUserToClass(ctx context.Context, arg AssignUserToClassPa return err } +const countAllClasses = `-- name: CountAllClasses :one +SELECT COUNT(1) +FROM student_class +` + +func (q *Queries) CountAllClasses(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countAllClasses) + var count int64 + err := row.Scan(&count) + return count, err +} + const createOrReplaceClass = `-- name: CreateOrReplaceClass :exec REPLACE INTO student_class (uuid, name) VALUES (?, ?) @@ -85,7 +97,7 @@ func (q *Queries) DeleteQuizClassVisibility(ctx context.Context, arg DeleteQuizC return err } -const findAllClasses = `-- name: FindAllClasses :many +const findAllClasses = `-- name: FindAll :many SELECT uuid, name FROM student_class LIMIT ? OFFSET ? diff --git a/internal/back/infrastructure/sqlc/auth.sql.go b/internal/back/infrastructure/sqlc/user.sql.go similarity index 99% rename from internal/back/infrastructure/sqlc/auth.sql.go rename to internal/back/infrastructure/sqlc/user.sql.go index 6f5ac0e..f14de79 100644 --- a/internal/back/infrastructure/sqlc/auth.sql.go +++ b/internal/back/infrastructure/sqlc/user.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.18.0 -// source: auth.sql +// source: user.sql package sqlc diff --git a/internal/back/infrastructure/userRepositoryImpl.go b/internal/back/infrastructure/userRepositoryImpl.go new file mode 100644 index 0000000..bb8acbe --- /dev/null +++ b/internal/back/infrastructure/userRepositoryImpl.go @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 Michaël COLL. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package infrastructure + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/patrickmn/go-cache" + + "github.com/michaelcoll/quiz-app/internal/back/domain" + "github.com/michaelcoll/quiz-app/internal/back/infrastructure/sqlc" +) + +type UserDBRepository struct { + domain.UserRepository + + q *sqlc.Queries + uc *cache.Cache +} + +func NewUserRepository(c *sql.DB) *UserDBRepository { + userCache := cache.New(30*time.Minute, 10*time.Minute) + return &UserDBRepository{q: sqlc.New(c), uc: userCache} +} + +func (r *UserDBRepository) FindUserById(ctx context.Context, id string) (*domain.User, error) { + + if user, found := r.uc.Get(id); found { + return user.(*domain.User), nil + } + + entity, err := r.q.FindUserById(ctx, id) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err + } + + user := r.toUser(entity) + + r.uc.Set(id, user, cache.DefaultExpiration) + + return user, nil +} + +func (r *UserDBRepository) CreateOrReplaceUser(ctx context.Context, user *domain.User) error { + err := r.q.CreateOrReplaceUser(ctx, sqlc.CreateOrReplaceUserParams{ + ID: user.Id, + Email: user.Email, + Firstname: user.Firstname, + Lastname: user.Lastname, + RoleID: int64(user.Role), + }) + if err != nil { + return err + } + + return nil +} + +func (r *UserDBRepository) UpdateUserRole(ctx context.Context, userId string, role domain.Role) error { + err := r.q.UpdateUserRole(ctx, sqlc.UpdateUserRoleParams{ + ID: userId, + RoleID: int64(role), + }) + if err != nil { + return err + } + + r.uc.Delete(userId) + + return nil +} + +func (r *UserDBRepository) FindAllUser(ctx context.Context) ([]*domain.User, error) { + entities, err := r.q.FindAllUser(ctx) + if err != nil { + return nil, err + } + + users := make([]*domain.User, len(entities)) + + for i, entity := range entities { + users[i] = r.toUser(entity) + } + + return users, nil +} + +func (r *UserDBRepository) UpdateUserActive(ctx context.Context, id string, active bool) error { + return r.q.UpdateUserActive(ctx, sqlc.UpdateUserActiveParams{ + Active: active, + ID: id, + }) +} + +func (r *UserDBRepository) toUser(entity sqlc.User) *domain.User { + return &domain.User{ + Id: entity.ID, + Email: entity.Email, + Firstname: entity.Firstname, + Lastname: entity.Lastname, + Active: entity.Active, + Role: r.toRole(entity.RoleID), + } +} + +func (r *UserDBRepository) toRole(entity int64) domain.Role { + switch entity { + case int64(domain.Admin): + return domain.Admin + case int64(domain.Teacher): + return domain.Teacher + case int64(domain.Student): + return domain.Student + } + + return 0 +} diff --git a/internal/back/infrastructure/userRepositoryImpl_test.go b/internal/back/infrastructure/userRepositoryImpl_test.go new file mode 100644 index 0000000..3289be7 --- /dev/null +++ b/internal/back/infrastructure/userRepositoryImpl_test.go @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Michaël COLL. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package infrastructure + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/michaelcoll/quiz-app/internal/back/domain" +) + +func TestUserDBRepository_FindUserById(t *testing.T) { + connection := getDBConnection(t, true) + defer connection.Close() + + r := NewUserRepository(connection) + + user, err := r.FindUserById(context.Background(), "42") + if err != nil { + assert.Failf(t, "Fail to get user", "%v", err) + } + + assert.Nil(t, user) + + err = r.CreateOrReplaceUser(context.Background(), &domain.User{ + Id: sub, + Email: email, + Firstname: firstName, + Lastname: lastName, + Role: domain.Admin, + }) + if err != nil { + assert.Failf(t, "Fail to create user", "%v", err) + } + + user, err = r.FindUserById(context.Background(), sub) + if err != nil { + assert.Failf(t, "Fail to get user", "%v", err) + } + + assert.Equal(t, sub, user.Id) + assert.Equal(t, email, user.Email) + assert.Equal(t, firstName, user.Firstname) + assert.Equal(t, lastName, user.Lastname) + assert.True(t, user.Active) + + user, err = r.FindUserById(context.Background(), sub) + if err != nil { + assert.Failf(t, "Fail to get user", "%v", err) + } + + assert.Equal(t, sub, user.Id) + assert.Equal(t, email, user.Email) + assert.Equal(t, firstName, user.Firstname) + assert.Equal(t, lastName, user.Lastname) + assert.True(t, user.Active) + assert.Equal(t, user.Role, domain.Admin) +} diff --git a/internal/back/presentation/baseApiController.go b/internal/back/presentation/baseApiController.go index 3840ecd..e4bf6e7 100644 --- a/internal/back/presentation/baseApiController.go +++ b/internal/back/presentation/baseApiController.go @@ -19,7 +19,9 @@ package presentation import ( "fmt" "log" + "net/http" "regexp" + "strconv" "github.com/fatih/color" "github.com/gin-gonic/gin" @@ -29,13 +31,19 @@ import ( const apiPort = ":8080" +var rangeRxp = regexp.MustCompile(`(?P.*)=(?P[0-9]+)-(?P[0-9]*)`) + type ApiController struct { - quizService *domain.QuizService - authService *domain.AuthService + quizService *domain.QuizService + authService *domain.AuthService + classService *domain.ClassService } -func NewApiController(quizService *domain.QuizService, authService *domain.AuthService) ApiController { - return ApiController{quizService: quizService, authService: authService} +func NewApiController( + quizService *domain.QuizService, + authService *domain.AuthService, + classService *domain.ClassService) ApiController { + return ApiController{quizService: quizService, authService: authService, classService: classService} } var pathRoleMapping = map[*endPointDef]domain.Role{} @@ -69,6 +77,8 @@ func (c *ApiController) Serve() { addPostEndpoint(private, "/session", domain.Student, c.startSession) addPostEndpoint(private, "/session/:sessionId/answer", domain.Student, c.addSessionAnswer) + addGetEndpoint(private, "/class", domain.Teacher, c.classList) + // Listen and serve on 0.0.0.0:8080 fmt.Printf("%s Listening API on http://0.0.0.0%s\n", color.GreenString("✓"), color.GreenString(apiPort)) err := router.Run(apiPort) @@ -107,3 +117,37 @@ func toRegExPath(routerGroup *gin.RouterGroup, path string) *regexp.Regexp { return regexp.MustCompile(fmt.Sprintf("^%s%s$", routerGroup.BasePath(), replacedPath)) } + +func extractRangeHeader(rangeHeader string, unit string) (uint16, uint16, error) { + r := rangeRxp.FindStringSubmatch(rangeHeader) + st := http.StatusRequestedRangeNotSatisfiable + + if len(r) < 4 { + return 0, 0, Errorf(st, "Range is not valid, supported format : %s=0-25", unit) + } + + if r[1] != unit { + return 0, 0, Errorf(st, "Unit in range is not valid, supported unit : %s", unit) + } + + start, errStart := strconv.ParseUint(r[2], 10, 16) + end, errEnd := strconv.ParseUint(r[3], 10, 16) + + if len(r[3]) == 0 { + end = 0 + } + + if errStart != nil { + return 0, 0, Errorf(st, "Start range is not valid") + } + + if len(r[3]) != 0 && errEnd != nil { + return 0, 0, Errorf(st, "End range is not valid") + } + + if end != 0 && start >= end { + return 0, 0, Errorf(st, "Range is not valid, start > end") + } + + return uint16(start), uint16(end), nil +} diff --git a/internal/back/presentation/classController.go b/internal/back/presentation/classController.go new file mode 100644 index 0000000..f7145a0 --- /dev/null +++ b/internal/back/presentation/classController.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022-2023 Michaël COLL. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package presentation + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +func (c *ApiController) classList(ctx *gin.Context) { + + start, end, err := extractRangeHeader(ctx.GetHeader("Range"), "class") + if err != nil { + handleError(ctx, err) + return + } + + classes, total, err := c.classService.FindAllClasses(ctx.Request.Context(), end-start, start) + if err != nil { + handleError(ctx, err) + return + } + + ctx.Header("Content-Range", fmt.Sprintf("%s %d-%d/%d", "class", start, int(start)+len(classes), total)) + ctx.JSON(http.StatusOK, toClassDtos(classes)) +} diff --git a/internal/back/presentation/model.go b/internal/back/presentation/model.go index ec47858..ae494d8 100644 --- a/internal/back/presentation/model.go +++ b/internal/back/presentation/model.go @@ -196,3 +196,25 @@ type SessionAnswerRequestBody struct { AnswerSha1 string `json:"answerSha1" binding:"required"` Checked bool `json:"checked" binding:"required"` } + +type Class struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` +} + +func toClassDto(domain *domain.Class) *Class { + return &Class{ + Id: domain.Id, + Name: domain.Name, + } +} + +func toClassDtos(domains []*domain.Class) []*Class { + dtos := make([]*Class, len(domains)) + + for i, d := range domains { + dtos[i] = toClassDto(d) + } + + return dtos +} diff --git a/internal/back/presentation/quizController.go b/internal/back/presentation/quizController.go index 59f3bbc..84407e2 100644 --- a/internal/back/presentation/quizController.go +++ b/internal/back/presentation/quizController.go @@ -19,15 +19,11 @@ package presentation import ( "fmt" "net/http" - "regexp" - "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" ) -var rangeRxp = regexp.MustCompile(`(?P.*)=(?P[0-9]+)-(?P[0-9]*)`) - func (c *ApiController) quizList(ctx *gin.Context) { start, end, err := extractRangeHeader(ctx.GetHeader("Range"), "quiz") @@ -46,40 +42,6 @@ func (c *ApiController) quizList(ctx *gin.Context) { ctx.JSON(http.StatusOK, toQuizDtos(quizzes)) } -func extractRangeHeader(rangeHeader string, unit string) (uint16, uint16, error) { - r := rangeRxp.FindStringSubmatch(rangeHeader) - st := http.StatusRequestedRangeNotSatisfiable - - if len(r) < 4 { - return 0, 0, Errorf(st, "Range is not valid, supported format : %s=0-25", unit) - } - - if r[1] != unit { - return 0, 0, Errorf(st, "Unit in range is not valid, supported unit : %s", unit) - } - - start, errStart := strconv.ParseUint(r[2], 10, 16) - end, errEnd := strconv.ParseUint(r[3], 10, 16) - - if len(r[3]) == 0 { - end = 0 - } - - if errStart != nil { - return 0, 0, Errorf(st, "Start range is not valid") - } - - if len(r[3]) != 0 && errEnd != nil { - return 0, 0, Errorf(st, "End range is not valid") - } - - if end != 0 && start >= end { - return 0, 0, Errorf(st, "Range is not valid, start > end") - } - - return uint16(start), uint16(end), nil -} - func (c *ApiController) quizBySha1(ctx *gin.Context) { sha1 := ctx.Param("sha1")