From ab116d39fbf79b93df6d855299ece1e3dacf3c86 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 14:04:59 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=82=92=E3=83=88=E3=83=A9=E3=83=B3=E3=82=B6?= =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E5=8C=96=E3=81=97E2E?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 6 + api/drivers/server/server.go | 2 +- api/externals/handler/mail_auth_handler.go | 6 +- .../repository/mail_auth_repository.go | 11 +- .../repository/session_repository.go | 19 ++- api/externals/repository/user_repository.go | 63 +++++++-- api/generated/openapi_gen.go | 30 ++++- api/internals/di/wire_gen.go | 4 +- api/internals/domain/mail_auth.go | 1 + api/internals/usecase/mail_auth_usecase.go | 58 ++++++-- api/internals/usecase/user_usecase.go | 4 +- api/test/signup_transaction_test.go | 124 ++++++++++++++++++ compose.e2e.yml | 124 ++++++++++++++++++ mysql/e2e_seed.sql | 7 + openapi/openapi.yaml | 16 ++- view/next-project/e2e/package.json | 10 ++ view/next-project/e2e/playwright.config.ts | 17 +++ view/next-project/e2e/tests/signup.spec.ts | 59 +++++++++ view/next-project/next.config.js | 4 + .../src/components/auth/SignUpView.tsx | 31 ++--- .../model/postMailAuthSignupParams.ts | 12 +- view/next-project/src/utils/api/signUp.ts | 15 ++- 22 files changed, 553 insertions(+), 70 deletions(-) create mode 100644 api/test/signup_transaction_test.go create mode 100644 compose.e2e.yml create mode 100644 mysql/e2e_seed.sql create mode 100644 view/next-project/e2e/package.json create mode 100644 view/next-project/e2e/playwright.config.ts create mode 100644 view/next-project/e2e/tests/signup.spec.ts diff --git a/Makefile b/Makefile index f951177aa..3c9d9eca6 100644 --- a/Makefile +++ b/Makefile @@ -216,6 +216,12 @@ run-test: ## APIテスト実行 run-eslint: ## ESLint実行 docker compose exec view pnpm run lint +run-e2e: ## E2Eテスト実行 (DB/API/View/PlaywrightをDocker内で完結) + docker compose -f compose.e2e.yml down --volumes --remove-orphans + docker compose -f compose.e2e.yml up --build -d db minio migrate seed api view + docker compose -f compose.e2e.yml run --rm --no-deps e2e + docker compose -f compose.e2e.yml down --volumes --remove-orphans + ##@ クリーンアップ del-vol: ## アプリコンテナボリューム削除 docker compose down -v diff --git a/api/drivers/server/server.go b/api/drivers/server/server.go index 7d06b625e..76113c9b2 100644 --- a/api/drivers/server/server.go +++ b/api/drivers/server/server.go @@ -28,7 +28,7 @@ func RunServer(server *handler.Handler) *echo.Echo { // CORS対策 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"http://localhost:3000", "127.0.0.1:3000", "http://localhost:3001", "127.0.0.1:3001", "http://localhost:8000", "127.0.0.1:8000", "https://finansu.nutfes.net", "https://stg-finansu.nutfes.net"}, // ドメイン + AllowOrigins: []string{"http://localhost:3000", "127.0.0.1:3000", "http://view:3000", "http://localhost:3001", "127.0.0.1:3001", "http://localhost:8000", "127.0.0.1:8000", "https://finansu.nutfes.net", "https://stg-finansu.nutfes.net"}, // ドメイン AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}, })) diff --git a/api/externals/handler/mail_auth_handler.go b/api/externals/handler/mail_auth_handler.go index 682b50c99..1bcc90f5c 100644 --- a/api/externals/handler/mail_auth_handler.go +++ b/api/externals/handler/mail_auth_handler.go @@ -48,8 +48,10 @@ func (h *Handler) DeleteMailAuthSignout(c echo.Context, params generated.DeleteM func (h *Handler) PostMailAuthSignup(c echo.Context, params generated.PostMailAuthSignupParams) error { email := params.Email password := params.Password - userID := strconv.Itoa(params.UserId) - token, err := h.mailAuthUseCase.SignUp(c.Request().Context(), email, password, userID) + name := params.Name + bureauID := strconv.Itoa(params.BureauId) + roleID := strconv.Itoa(params.RoleId) + token, err := h.mailAuthUseCase.SignUp(c.Request().Context(), email, password, name, bureauID, roleID) if err != nil { return err } diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 8533a2ed9..98f5dce1a 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -16,6 +16,7 @@ type mailAuthRepository struct { type MailAuthRepository interface { CreateMailAuth(context.Context, string, string, string) (int64, error) + CreateMailAuthWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) FindMailAuthByEmail(context.Context, string) *sql.Row FindMailAuthByID(context.Context, string) *sql.Row ChangePasswordByUserID(context.Context, string, string) error @@ -27,7 +28,7 @@ func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthReposit // 作成 func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, password string, userID string) (int64, error) { - result, err := r.client.DB().ExecContext(c, "insert into mail_auth (email, password, user_id) values ('"+email+"','"+password+"',"+userID+")") + result, err := r.client.DB().ExecContext(c, "INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, password, userID) if err != nil { return 0, err } @@ -35,6 +36,14 @@ func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, pas return lastInsertID, err } +func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, email string, password string, userID string) (int64, error) { + result, err := tx.ExecContext(c, "INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, password, userID) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + // メールアドレスからmail_authを探してくる func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) *sql.Row { query := "select * from mail_auth where email= '" + email + "'" diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index e53386846..75c860f7c 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -3,10 +3,10 @@ package repository import ( "context" "database/sql" - "github.com/NUTFes/FinanSu/api/drivers/db" "fmt" -) + "github.com/NUTFes/FinanSu/api/drivers/db" +) type sessionRepository struct { client db.Client @@ -14,6 +14,7 @@ type sessionRepository struct { type SessionRepository interface { Create(context.Context, string, string, string) error + CreateWithTx(context.Context, *sql.Tx, string, string, string) error Destroy(context.Context, string) error FindSessionByAccessToken(context.Context, string) *sql.Row DestroyByUserID(context.Context, string) error @@ -25,8 +26,18 @@ func NewSessionRepository(client db.Client) SessionRepository { // 作成 func (r *sessionRepository) Create(c context.Context, authID string, userID string, accessToken string) error { - query := "insert into session (auth_id, user_id, access_token) values (" + authID + ", " + userID + ", '" + accessToken + "')" - _, err := r.client.DB().ExecContext(c, query) + query := "insert into session (auth_id, user_id, access_token) values (?, ?, ?)" + _, err := r.client.DB().ExecContext(c, query, authID, userID, accessToken) + if err != nil { + return err + } + fmt.Printf("\x1b[36m%s\n", query) + return nil +} + +func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID string, userID string, accessToken string) error { + query := "insert into session (auth_id, user_id, access_token) values (?, ?, ?)" + _, err := tx.ExecContext(c, query, authID, userID, accessToken) if err != nil { return err } diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index 9ffb13c28..9501f9fbc 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -19,10 +19,13 @@ type UserRepository interface { All(context.Context) (*sql.Rows, error) Find(context.Context, string) (*sql.Row, error) FindByIDs(context.Context, []int) (*sql.Rows, error) - Create(context.Context, string, string, string) error + Create(context.Context, string, string, string) (int64, error) + CreateWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) Update(context.Context, string, string, string, string) error Destroy(context.Context, string) error + DestroyWithTx(context.Context, *sql.Tx, string) error MultiDestroy(context.Context, []int) error + MultiDestroyWithTx(context.Context, *sql.Tx, []int) error FindNewRecord(context.Context) (*sql.Row, error) FindByEmail(context.Context, string) (*sql.Row, error) } @@ -59,12 +62,20 @@ func (ur *userRepository) FindByIDs(c context.Context, ids []int) (*sql.Rows, er } // 作成 -func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) error { - query := ` - INSERT INTO - users (name, bureau_id, role_id) - VALUES ('` + name + "', " + bureauID + ", " + roleID + ")" - return ur.crud.UpdateDB(c, query) +func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) (int64, error) { + result, err := ur.client.DB().ExecContext(c, "INSERT INTO users (name, bureau_id, role_id) VALUES (?, ?, ?)", name, bureauID, roleID) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name string, bureauID string, roleID string) (int64, error) { + result, err := tx.ExecContext(c, "INSERT INTO users (name, bureau_id, role_id) VALUES (?, ?, ?)", name, bureauID, roleID) + if err != nil { + return 0, err + } + return result.LastInsertId() } // 編集 @@ -82,21 +93,45 @@ func (ur *userRepository) Update(c context.Context, id string, name string, bure // 削除 func (ur *userRepository) Destroy(c context.Context, id string) error { - query := "UPDATE users SET is_deleted = TRUE WHERE id =" + id - - err := ur.crud.UpdateDB(c, query) + tx, err := ur.client.DB().BeginTx(c, nil) if err != nil { return err } + if err = ur.DestroyWithTx(c, tx, id); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} - query = "UPDATE mail_auth SET email = NULL WHERE user_id =" + id - err = ur.crud.UpdateDB(c, query) +func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string) error { + _, err := tx.ExecContext(c, "UPDATE users SET is_deleted = TRUE WHERE id = ?", id) + if err != nil { + return err + } + _, err = tx.ExecContext(c, "UPDATE mail_auth SET email = NULL WHERE user_id = ?", id) return err } // 複数削除 func (ur *userRepository) MultiDestroy(c context.Context, ids []int) error { + tx, err := ur.client.DB().BeginTx(c, nil) + if err != nil { + return err + } + if err = ur.MultiDestroyWithTx(c, tx, ids); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +func (ur *userRepository) MultiDestroyWithTx(c context.Context, tx *sql.Tx, ids []int) error { + if len(ids) == 0 { + return nil + } + query := "UPDATE users SET is_deleted = TRUE WHERE " query2 := "UPDATE mail_auth SET email = NULL WHERE " for index, id := range ids { @@ -110,12 +145,12 @@ func (ur *userRepository) MultiDestroy(c context.Context, ids []int) error { } - err := ur.crud.UpdateDB(c, query) + _, err := tx.ExecContext(c, query) if err != nil { return err } - err = ur.crud.UpdateDB(c, query2) + _, err = tx.ExecContext(c, query2) return err } diff --git a/api/generated/openapi_gen.go b/api/generated/openapi_gen.go index 3d1050202..09707fc17 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -812,8 +812,14 @@ type PostMailAuthSignupParams struct { // Password password Password string `form:"password" json:"password"` - // UserId user_id - UserId int `form:"user_id" json:"user_id"` + // Name name + Name string `form:"name" json:"name"` + + // BureauId bureau_id + BureauId int `form:"bureau_id" json:"bureau_id"` + + // RoleId role_id + RoleId int `form:"role_id" json:"role_id"` } // PostPasswordResetRequestParams defines parameters for PostPasswordResetRequest. @@ -2760,11 +2766,25 @@ func (w *ServerInterfaceWrapper) PostMailAuthSignup(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter password: %s", err)) } - // ------------- Required query parameter "user_id" ------------- + // ------------- Required query parameter "name" ------------- - err = runtime.BindQueryParameter("form", true, true, "user_id", ctx.QueryParams(), ¶ms.UserId) + err = runtime.BindQueryParameter("form", true, true, "name", ctx.QueryParams(), ¶ms.Name) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter user_id: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter name: %s", err)) + } + + // ------------- Required query parameter "bureau_id" ------------- + + err = runtime.BindQueryParameter("form", true, true, "bureau_id", ctx.QueryParams(), ¶ms.BureauId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter bureau_id: %s", err)) + } + + // ------------- Required query parameter "role_id" ------------- + + err = runtime.BindQueryParameter("form", true, true, "role_id", ctx.QueryParams(), ¶ms.RoleId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter role_id: %s", err)) } // Invoke the callback with all the unmarshaled arguments diff --git a/api/internals/di/wire_gen.go b/api/internals/di/wire_gen.go index 7ab63596e..690811cab 100644 --- a/api/internals/di/wire_gen.go +++ b/api/internals/di/wire_gen.go @@ -58,10 +58,10 @@ func InitializeServer() (*ServerComponents, error) { incomeExpenditureManagementUseCase := usecase.NewIncomeExpenditureManagementUseCase(incomeExpenditureManagementRepository) mailAuthRepository := repository.NewMailAuthRepository(client, crud) sessionRepository := repository.NewSessionRepository(client) - mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository) + userRepository := repository.NewUserRepository(client, crud) + mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository, userRepository, transactionRepository) objectUploadUseCase := usecase.NewObjectUploadUseCase(objectHandleRepository) passwordResetTokenRepository := repository.NewPasswordResetTokenRepository(client, crud) - userRepository := repository.NewUserRepository(client, crud) passwordResetTokenUseCase := usecase.NewPasswordResetTokenUseCase(passwordResetTokenRepository, userRepository, mailAuthRepository) sponsorRepository := repository.NewSponsorRepository(client, crud) sponsorUseCase := usecase.NewSponsorUseCase(sponsorRepository) diff --git a/api/internals/domain/mail_auth.go b/api/internals/domain/mail_auth.go index fb430915e..08c5223fd 100644 --- a/api/internals/domain/mail_auth.go +++ b/api/internals/domain/mail_auth.go @@ -15,6 +15,7 @@ type MailAuth struct { type Token struct { AccessToken string `json:"accessToken"` + UserID int `json:"userID,omitempty"` } type IsSignIn struct { diff --git a/api/internals/usecase/mail_auth_usecase.go b/api/internals/usecase/mail_auth_usecase.go index ca32c61fd..01ed02b13 100644 --- a/api/internals/usecase/mail_auth_usecase.go +++ b/api/internals/usecase/mail_auth_usecase.go @@ -12,41 +12,81 @@ import ( ) type mailAuthUseCase struct { - mailAuthRep rep.MailAuthRepository - sessionRep rep.SessionRepository + mailAuthRep rep.MailAuthRepository + sessionRep rep.SessionRepository + userRep rep.UserRepository + transactionRep rep.TransactionRepository } type MailAuthUseCase interface { - SignUp(context.Context, string, string, string) (domain.Token, error) + SignUp(context.Context, string, string, string, string, string) (domain.Token, error) SignIn(context.Context, string, string) (domain.Token, error) SignOut(context.Context, string) error IsSignIn(context.Context, string) (domain.IsSignIn, error) } -func NewMailAuthUseCase(mailAuthRep rep.MailAuthRepository, sessionRep rep.SessionRepository) MailAuthUseCase { - return &mailAuthUseCase{mailAuthRep: mailAuthRep, sessionRep: sessionRep} +func NewMailAuthUseCase( + mailAuthRep rep.MailAuthRepository, + sessionRep rep.SessionRepository, + userRep rep.UserRepository, + transactionRep rep.TransactionRepository, +) MailAuthUseCase { + return &mailAuthUseCase{ + mailAuthRep: mailAuthRep, + sessionRep: sessionRep, + userRep: userRep, + transactionRep: transactionRep, + } } -func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, userID string) (domain.Token, error) { +func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, name string, bureauID string, roleID string) (domain.Token, error) { var token domain.Token + + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return token, err + } + // パスワードをハッシュ化 - hashed, _ := bcrypt.GenerateFromPassword([]byte(password), 10) - mailAuthID, err := u.mailAuthRep.CreateMailAuth(c, email, string(hashed), userID) + hashed, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + + userID, err := u.userRep.CreateWithTx(c, tx, name, bureauID, roleID) + if err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + userIDStr := strconv.FormatInt(userID, 10) + + mailAuthID, err := u.mailAuthRep.CreateMailAuthWithTx(c, tx, email, string(hashed), userIDStr) if err != nil { + _ = u.transactionRep.RollBack(c, tx) return token, err } // トークン発行 accessToken, err := _makeRandomStr(10) if err != nil { + _ = u.transactionRep.RollBack(c, tx) return token, err } // ログイン(セッション開始) - err = u.sessionRep.Create(c, strconv.FormatInt(mailAuthID, 10), userID, accessToken) + err = u.sessionRep.CreateWithTx(c, tx, strconv.FormatInt(mailAuthID, 10), userIDStr, accessToken) if err != nil { + _ = u.transactionRep.RollBack(c, tx) return token, err } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + token.AccessToken = accessToken + token.UserID = int(userID) return token, nil } diff --git a/api/internals/usecase/user_usecase.go b/api/internals/usecase/user_usecase.go index b441a06de..b749ecac4 100644 --- a/api/internals/usecase/user_usecase.go +++ b/api/internals/usecase/user_usecase.go @@ -102,12 +102,12 @@ func (u *userUseCase) GetUserByID(c context.Context, id string) (domain.User, er func (u *userUseCase) CreateUser(c context.Context, name string, bureauID string, roleID string) (domain.User, error) { latastUser := domain.User{} - err := u.userRep.Create(c, name, bureauID, roleID) + userID, err := u.userRep.Create(c, name, bureauID, roleID) if err != nil { return latastUser, err } - row, err := u.userRep.FindNewRecord(c) + row, err := u.userRep.Find(c, strconv.FormatInt(userID, 10)) if err != nil { return latastUser, err } diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go new file mode 100644 index 000000000..dfadadb87 --- /dev/null +++ b/api/test/signup_transaction_test.go @@ -0,0 +1,124 @@ +package test + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/NUTFes/FinanSu/api/internals/di" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type signupResponse struct { + AccessToken string `json:"accessToken"` + UserID int `json:"userID"` +} + +func signupURL(t *testing.T, baseURL string, values map[string]string) string { + t.Helper() + + u, err := url.Parse(baseURL + "/mail_auth/signup") + require.NoError(t, err) + + q := u.Query() + for key, value := range values { + q.Set(key, value) + } + u.RawQuery = q.Encode() + return u.String() +} + +func countRows(t *testing.T, query string, args ...any) int { + t.Helper() + + var count int + err := db.QueryRow(query, args...).Scan(&count) + require.NoError(t, err) + return count +} + +func TestSignupCreatesUserMailAuthSessionAndCurrentUser(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-success@example.com" + name := "Signup Success User" + r, err := http.Post(signupURL(t, testServer.URL, map[string]string{ + "email": email, + "password": "password123", + "name": name, + "bureau_id": "1", + "role_id": "1", + }), "application/json", nil) + require.NoError(t, err) + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, r.StatusCode) + + var res signupResponse + require.NoError(t, json.Unmarshal(body, &res)) + require.NotEmpty(t, res.AccessToken) + require.NotZero(t, res.UserID) + + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM users WHERE id = ? AND name = ? AND is_deleted IS FALSE", res.UserID, name)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ? AND user_id = ?", email, res.UserID)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM session WHERE access_token = ? AND user_id = ?", res.AccessToken, res.UserID)) + + currentUserURL := testServer.URL + "/current_user" + req, err := http.NewRequest(http.MethodGet, currentUserURL, nil) + require.NoError(t, err) + req.Header.Set("access-token", res.AccessToken) + + currentUserRes, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer currentUserRes.Body.Close() + + assert.Equal(t, http.StatusOK, currentUserRes.StatusCode) +} + +func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-duplicate@example.com" + _, err = db.Exec("INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, "hashed-password", 1) + require.NoError(t, err) + + name := "Rollback Target User" + r, err := http.Post(signupURL(t, testServer.URL, map[string]string{ + "email": email, + "password": "password123", + "name": name, + "bureau_id": "1", + "role_id": "1", + }), "application/json", nil) + require.NoError(t, err) + defer r.Body.Close() + + assert.NotEqual(t, http.StatusOK, r.StatusCode) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM users WHERE name = ?", name)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) +} diff --git a/compose.e2e.yml b/compose.e2e.yml new file mode 100644 index 000000000..61b253584 --- /dev/null +++ b/compose.e2e.yml @@ -0,0 +1,124 @@ +services: + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: finansu_db + MYSQL_USER: finansu + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: root + TZ: Asia/Tokyo + volumes: + - e2e-db-data:/var/lib/mysql + - ./my.cnf:/etc/mysql/conf.d/my.cnf + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 20 + + migrate: + image: migrate/migrate:latest + volumes: + - ./mysql/migrations:/migrations + command: + [ + "-path", + "/migrations", + "-database", + "mysql://finansu:password@tcp(db:3306)/finansu_db", + "up", + ] + depends_on: + db: + condition: service_healthy + + seed: + image: mysql:8.0 + volumes: + - ./mysql/e2e_seed.sql:/e2e_seed.sql + command: > + bash -lc ' + mysql -h db -u finansu -ppassword finansu_db < /e2e_seed.sql + ' + depends_on: + migrate: + condition: service_completed_successfully + + minio: + image: minio/minio:latest + command: "server /data --console-address :9001" + environment: + MINIO_ROOT_USER: user + MINIO_ROOT_PASSWORD: password + + api: + build: + context: ./api + dockerfile: dev.Dockerfile + user: root + volumes: + - ./api:/app + - ./openapi:/openapi + - e2e-go-cache:/go/cache + - e2e-go-mod-cache:/go/pkg/mod + environment: + NUTMEG_DB_USER: finansu + NUTMEG_DB_PASSWORD: password + NUTMEG_DB_HOST: db + NUTMEG_DB_PORT: 3306 + NUTMEG_DB_NAME: finansu_db + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: user + MINIO_SECRET_KEY: password + MINIO_USE_SSL: "false" + RESET_PASSWORD_URL: "http://view:3000/reset_password" + command: go run main.go + depends_on: + seed: + condition: service_completed_successfully + minio: + condition: service_started + + view: + build: ./view + user: root + volumes: + - ./view:/app + - ./openapi:/openapi + - e2e-pnpm-store:/app/.pnpm-store + environment: + NEXT_PUBLIC_APP_ENV: e2e + NEXT_PUBLIC_ENDPOINT: minio + NEXT_PUBLIC_PORT: 9000 + NEXT_PUBLIC_BUCKET_NAME: finansu + NEXT_PUBLIC_MINIO_ENDPONT: http://minio:9000 + NEXT_PUBLIC_ACCESS_KEY: user + NEXT_PUBLIC_SECRET_KEY: password + RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: false + command: sh -c "pnpm install && pnpm exec next dev -H 0.0.0.0" + depends_on: + api: + condition: service_started + + e2e: + image: mcr.microsoft.com/playwright:v1.57.0-noble + working_dir: /workspace/view/next-project/e2e + volumes: + - .:/workspace + - e2e-node-modules:/workspace/view/next-project/e2e/node_modules + environment: + BASE_URL: http://view:3000 + API_URL: http://api:1323 + command: bash -lc "npm install --no-package-lock && npx playwright test" + depends_on: + view: + condition: service_started + api: + condition: service_started + +volumes: + e2e-db-data: + e2e-go-cache: + e2e-go-mod-cache: + e2e-pnpm-store: + e2e-node-modules: diff --git a/mysql/e2e_seed.sql b/mysql/e2e_seed.sql new file mode 100644 index 000000000..632606c50 --- /dev/null +++ b/mysql/e2e_seed.sql @@ -0,0 +1,7 @@ +USE finansu_db; + +INSERT INTO years (id, year) +VALUES (1, 2025); + +INSERT INTO year_periods (year_id, started_at, ended_at) +VALUES (1, '2024-11-15 00:00:00', '2025-11-15 00:00:00'); diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 624aa0b48..8e2ecdd6a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1567,9 +1567,21 @@ paths: required: true schema: type: string - - name: user_id + - name: name in: query - description: user_id + description: name + required: true + schema: + type: string + - name: bureau_id + in: query + description: bureau_id + required: true + schema: + type: integer + - name: role_id + in: query + description: role_id required: true schema: type: integer diff --git a/view/next-project/e2e/package.json b/view/next-project/e2e/package.json new file mode 100644 index 000000000..efa150592 --- /dev/null +++ b/view/next-project/e2e/package.json @@ -0,0 +1,10 @@ +{ + "name": "finansu-e2e", + "private": true, + "scripts": { + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "1.57.0" + } +} diff --git a/view/next-project/e2e/playwright.config.ts b/view/next-project/e2e/playwright.config.ts new file mode 100644 index 000000000..275823b82 --- /dev/null +++ b/view/next-project/e2e/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 60_000, + expect: { + timeout: 10_000, + }, + reporter: [['list']], + use: { + baseURL: process.env.BASE_URL || 'http://view:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + retries: 1, + workers: 1, +}); diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts new file mode 100644 index 000000000..9d61322ae --- /dev/null +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; + +const apiURL = process.env.API_URL || 'http://api:1323'; + +async function waitForService(url: string) { + const deadline = Date.now() + 60_000; + let lastError: unknown; + + while (Date.now() < deadline) { + try { + const response = await fetch(url); + if (response.ok) return; + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + + throw new Error(`Service did not become ready: ${url}. Last error: ${String(lastError)}`); +} + +test.beforeAll(async () => { + await waitForService(`${apiURL}/`); +}); + +test('新規登録後に current_user が 404 にならず My Page に遷移する', async ({ page }) => { + const currentUserStatuses: number[] = []; + const page404Responses: string[] = []; + + page.on('response', (response) => { + const url = response.url(); + if (url.includes('/current_user')) { + currentUserStatuses.push(response.status()); + } + if (response.status() === 404) { + page404Responses.push(url); + } + }); + + await page.goto('/'); + await page.getByRole('button', { name: '新規登録' }).click(); + + const timestamp = Date.now(); + const email = `e2e-signup-${timestamp}@example.com`; + const name = `E2E Signup ${timestamp}`; + + await page.locator('input[type="text"]').nth(0).fill(name); + await page.locator('input[type="text"]').nth(1).fill(email); + await page.locator('input[type="password"]').nth(0).fill('password123'); + await page.locator('input[type="password"]').nth(1).fill('password123'); + await page.getByRole('button', { name: '登録' }).click(); + + await expect(page).toHaveURL(/\/my_page/); + await expect(page.getByRole('heading', { name: 'My Page' })).toBeVisible(); + + expect(currentUserStatuses).toContain(200); + expect(currentUserStatuses).not.toContain(404); + expect(page404Responses).toEqual([]); +}); diff --git a/view/next-project/next.config.js b/view/next-project/next.config.js index 46f0e6723..fd2ec825f 100644 --- a/view/next-project/next.config.js +++ b/view/next-project/next.config.js @@ -17,6 +17,10 @@ const apiConfig = { SSR_API_URI: 'http://nutfes-finansu-api:1323', CSR_API_URI: 'http://localhost:1323', }, + e2e: { + SSR_API_URI: 'http://api:1323', + CSR_API_URI: 'http://api:1323', + }, }; // 現在の環境に合った設定を展開(該当しない場合はdevelopment設定を利用) diff --git a/view/next-project/src/components/auth/SignUpView.tsx b/view/next-project/src/components/auth/SignUpView.tsx index b5a38111a..dda419c27 100644 --- a/view/next-project/src/components/auth/SignUpView.tsx +++ b/view/next-project/src/components/auth/SignUpView.tsx @@ -5,7 +5,6 @@ import { useForm } from 'react-hook-form'; import { BUREAUS } from '@/constants/bureaus'; import { useAuthStore, useUserStore } from '@/store'; import { signUp } from '@api/signUp'; -import { post } from '@api/user'; import { PrimaryButton } from '@components/common'; import LoadingButton from '@components/common/LoadingButton'; import { SignUp, User } from '@type/common'; @@ -41,27 +40,17 @@ export default function SignUpView() { const postUser = async (data: SignUp) => { setIsSignUpNow(true); - const userUrl: string = process.env.CSR_API_URI + '/users'; const signUpUrl: string = process.env.CSR_API_URI + '/mail_auth/signup'; - // signIn には登録したuserのIDが必要なので先にUserをpost - const newUser = await post(userUrl, postUserData); - if (!newUser || !newUser.id) { - alert('ユーザーの作成に失敗しました。'); - setIsSignUpNow(false); - return; - } - const userID: number = newUser.id; - // signUp - const req = await signUp(signUpUrl, data, userID); - const res = await req.json(); - // state用のuserのデータ - const userData: User = { - id: userID, - name: postUserData.name, - bureauID: Number(postUserData.bureauID), - roleID: postUserData.roleID, - }; + const req = await signUp(signUpUrl, data, postUserData); if (req.status === 200) { + const res = await req.json(); + // state用のuserのデータ + const userData: User = { + id: res.userID, + name: postUserData.name, + bureauID: Number(postUserData.bureauID), + roleID: Number(postUserData.roleID), + }; // state用のauthのデータ const authData = { isSignIn: true, @@ -69,7 +58,7 @@ export default function SignUpView() { }; setAuth(authData); setUser(userData); - Router.push('/purchaseorders'); + Router.push('/my_page'); } else { alert( '新規登録に失敗しました。メールアドレスもしくはパスワードがすでに登録されている可能性があります', diff --git a/view/next-project/src/generated/model/postMailAuthSignupParams.ts b/view/next-project/src/generated/model/postMailAuthSignupParams.ts index 4fe580e95..36bcc8bae 100644 --- a/view/next-project/src/generated/model/postMailAuthSignupParams.ts +++ b/view/next-project/src/generated/model/postMailAuthSignupParams.ts @@ -16,7 +16,15 @@ export type PostMailAuthSignupParams = { */ password: string; /** - * user_id + * name */ - user_id: number; + name: string; + /** + * bureau_id + */ + bureau_id: number; + /** + * role_id + */ + role_id: number; }; diff --git a/view/next-project/src/utils/api/signUp.ts b/view/next-project/src/utils/api/signUp.ts index 156f8a989..d0cd096bb 100644 --- a/view/next-project/src/utils/api/signUp.ts +++ b/view/next-project/src/utils/api/signUp.ts @@ -1,9 +1,14 @@ -import { SignUp } from '@type/common'; +import { SignUp, User } from '@type/common'; -export const signUp = async (url: string, data: SignUp, userID: number) => { - const email = data.email; - const password = data.password; - const postUrl = url + '?email=' + email + '&password=' + password + '&user_id=' + userID; +export const signUp = async (url: string, data: SignUp, user: User) => { + const params = new URLSearchParams({ + email: data.email, + password: data.password, + name: user.name, + bureau_id: String(user.bureauID), + role_id: String(user.roleID), + }); + const postUrl = url + '?' + params.toString(); const res = await fetch(postUrl, { method: 'POST', mode: 'cors', From 287990a2d0c3837998744e86e4da0702ebfe8afe Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 14:11:41 +0900 Subject: [PATCH 02/22] =?UTF-8?q?E2E=E3=82=92=E3=83=95=E3=83=AD=E3=83=B3?= =?UTF-8?q?=E3=83=88=E6=9C=AC=E4=BD=93=E3=81=AE=E5=9E=8B=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=E5=AF=BE=E8=B1=A1=E3=81=8B=E3=82=89=E9=99=A4?= =?UTF-8?q?=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- view/next-project/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view/next-project/tsconfig.json b/view/next-project/tsconfig.json index b2e6b5180..0dc36cfcb 100644 --- a/view/next-project/tsconfig.json +++ b/view/next-project/tsconfig.json @@ -19,5 +19,5 @@ }, "extends": "./tsconfig.paths.json", "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "e2e"] } From 74375ef994b2338f4181734ac493bad0496cf8c9 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 16:13:24 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=83=B3=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=81=AE=E6=A9=9F=E5=AF=86=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=82=92=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=83=9C?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=81=A7=E9=80=81=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/handler/mail_auth_handler.go | 17 +++-- api/generated/openapi_gen.go | 64 ++++------------- api/test/signup_transaction_test.go | 33 ++++----- openapi/openapi.yaml | 69 ++++++++++--------- view/next-project/e2e/tests/signup.spec.ts | 18 +++++ .../src/components/auth/SignUpView.tsx | 19 +++-- view/next-project/src/generated/hooks.ts | 59 ++++++---------- .../next-project/src/generated/model/index.ts | 2 +- .../generated/model/postMailAuthSignup200.ts | 7 +- ...nupParams.ts => postMailAuthSignupBody.ts} | 22 ++---- view/next-project/src/utils/api/signUp.ts | 21 ------ 11 files changed, 144 insertions(+), 187 deletions(-) rename view/next-project/src/generated/model/{postMailAuthSignupParams.ts => postMailAuthSignupBody.ts} (60%) delete mode 100644 view/next-project/src/utils/api/signUp.ts diff --git a/api/externals/handler/mail_auth_handler.go b/api/externals/handler/mail_auth_handler.go index 1bcc90f5c..89c89324a 100644 --- a/api/externals/handler/mail_auth_handler.go +++ b/api/externals/handler/mail_auth_handler.go @@ -45,12 +45,17 @@ func (h *Handler) DeleteMailAuthSignout(c echo.Context, params generated.DeleteM } // router.POST(baseURL+"/mail_auth/signup", wrapper.PostMailAuthSignup) -func (h *Handler) PostMailAuthSignup(c echo.Context, params generated.PostMailAuthSignupParams) error { - email := params.Email - password := params.Password - name := params.Name - bureauID := strconv.Itoa(params.BureauId) - roleID := strconv.Itoa(params.RoleId) +func (h *Handler) PostMailAuthSignup(c echo.Context) error { + var request generated.PostMailAuthSignupJSONRequestBody + if err := c.Bind(&request); err != nil { + return err + } + + email := request.Email + password := request.Password + name := request.Name + bureauID := strconv.Itoa(request.BureauId) + roleID := strconv.Itoa(request.RoleId) token, err := h.mailAuthUseCase.SignUp(c.Request().Context(), email, password, name, bureauID, roleID) if err != nil { return err diff --git a/api/generated/openapi_gen.go b/api/generated/openapi_gen.go index 09707fc17..b2fcacab1 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -804,22 +804,22 @@ type DeleteMailAuthSignoutParams struct { AccessToken *string `json:"Access-Token,omitempty"` } -// PostMailAuthSignupParams defines parameters for PostMailAuthSignup. -type PostMailAuthSignupParams struct { - // Email email - Email string `form:"email" json:"email"` +// PostMailAuthSignupJSONBody defines parameters for PostMailAuthSignup. +type PostMailAuthSignupJSONBody struct { + // BureauId bureau_id + BureauId int `json:"bureau_id"` - // Password password - Password string `form:"password" json:"password"` + // Email email + Email string `json:"email"` // Name name - Name string `form:"name" json:"name"` + Name string `json:"name"` - // BureauId bureau_id - BureauId int `form:"bureau_id" json:"bureau_id"` + // Password password + Password string `json:"password"` // RoleId role_id - RoleId int `form:"role_id" json:"role_id"` + RoleId int `json:"role_id"` } // PostPasswordResetRequestParams defines parameters for PostPasswordResetRequest. @@ -1029,6 +1029,9 @@ type PostIncomesJSONRequestBody = Income // PutIncomesIdJSONRequestBody defines body for PutIncomesId for application/json ContentType. type PutIncomesIdJSONRequestBody = Income +// PostMailAuthSignupJSONRequestBody defines body for PostMailAuthSignup for application/json ContentType. +type PostMailAuthSignupJSONRequestBody PostMailAuthSignupJSONBody + // PostPasswordResetIdJSONRequestBody defines body for PostPasswordResetId for application/json ContentType. type PostPasswordResetIdJSONRequestBody = PasswordResetData @@ -1291,7 +1294,7 @@ type ServerInterface interface { DeleteMailAuthSignout(ctx echo.Context, params DeleteMailAuthSignoutParams) error // (POST /mail_auth/signup) - PostMailAuthSignup(ctx echo.Context, params PostMailAuthSignupParams) error + PostMailAuthSignup(ctx echo.Context) error // (POST /password_reset/request) PostPasswordResetRequest(ctx echo.Context, params PostPasswordResetRequestParams) error @@ -2750,45 +2753,8 @@ func (w *ServerInterfaceWrapper) DeleteMailAuthSignout(ctx echo.Context) error { func (w *ServerInterfaceWrapper) PostMailAuthSignup(ctx echo.Context) error { var err error - // Parameter object where we will unmarshal all parameters from the context - var params PostMailAuthSignupParams - // ------------- Required query parameter "email" ------------- - - err = runtime.BindQueryParameter("form", true, true, "email", ctx.QueryParams(), ¶ms.Email) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter email: %s", err)) - } - - // ------------- Required query parameter "password" ------------- - - err = runtime.BindQueryParameter("form", true, true, "password", ctx.QueryParams(), ¶ms.Password) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter password: %s", err)) - } - - // ------------- Required query parameter "name" ------------- - - err = runtime.BindQueryParameter("form", true, true, "name", ctx.QueryParams(), ¶ms.Name) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter name: %s", err)) - } - - // ------------- Required query parameter "bureau_id" ------------- - - err = runtime.BindQueryParameter("form", true, true, "bureau_id", ctx.QueryParams(), ¶ms.BureauId) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter bureau_id: %s", err)) - } - - // ------------- Required query parameter "role_id" ------------- - - err = runtime.BindQueryParameter("form", true, true, "role_id", ctx.QueryParams(), ¶ms.RoleId) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter role_id: %s", err)) - } - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.PostMailAuthSignup(ctx, params) + err = w.Handler.PostMailAuthSignup(ctx) return err } diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index dfadadb87..011f3b9dc 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -1,11 +1,11 @@ package test import ( + "bytes" "encoding/json" "io" "net/http" "net/http/httptest" - "net/url" "testing" "github.com/NUTFes/FinanSu/api/internals/di" @@ -18,18 +18,15 @@ type signupResponse struct { UserID int `json:"userID"` } -func signupURL(t *testing.T, baseURL string, values map[string]string) string { +func postSignup(t *testing.T, baseURL string, values map[string]any) *http.Response { t.Helper() - u, err := url.Parse(baseURL + "/mail_auth/signup") + body, err := json.Marshal(values) require.NoError(t, err) - q := u.Query() - for key, value := range values { - q.Set(key, value) - } - u.RawQuery = q.Encode() - return u.String() + r, err := http.Post(baseURL+"/mail_auth/signup", "application/json", bytes.NewReader(body)) + require.NoError(t, err) + return r } func countRows(t *testing.T, query string, args ...any) int { @@ -55,14 +52,13 @@ func TestSignupCreatesUserMailAuthSessionAndCurrentUser(t *testing.T) { email := "signup-success@example.com" name := "Signup Success User" - r, err := http.Post(signupURL(t, testServer.URL, map[string]string{ + r := postSignup(t, testServer.URL, map[string]any{ "email": email, "password": "password123", "name": name, - "bureau_id": "1", - "role_id": "1", - }), "application/json", nil) - require.NoError(t, err) + "bureau_id": 1, + "role_id": 1, + }) defer r.Body.Close() body, err := io.ReadAll(r.Body) @@ -108,14 +104,13 @@ func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { require.NoError(t, err) name := "Rollback Target User" - r, err := http.Post(signupURL(t, testServer.URL, map[string]string{ + r := postSignup(t, testServer.URL, map[string]any{ "email": email, "password": "password123", "name": name, - "bureau_id": "1", - "role_id": "1", - }), "application/json", nil) - require.NoError(t, err) + "bureau_id": 1, + "role_id": 1, + }) defer r.Body.Close() assert.NotEqual(t, http.StatusOK, r.StatusCode) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 8e2ecdd6a..5354e2742 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1554,37 +1554,34 @@ paths: tags: - mail_auth description: ユーザー登録 - parameters: - - name: email - in: query - description: email - required: true - schema: - type: string - - name: password - in: query - description: password - required: true - schema: - type: string - - name: name - in: query - description: name - required: true - schema: - type: string - - name: bureau_id - in: query - description: bureau_id - required: true - schema: - type: integer - - name: role_id - in: query - description: role_id - required: true - schema: - type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - password + - name + - bureau_id + - role_id + properties: + email: + type: string + description: email + password: + type: string + description: password + name: + type: string + description: name + bureau_id: + type: integer + description: bureau_id + role_id: + type: integer + description: role_id responses: "200": description: ユーザー登録完了 @@ -1592,6 +1589,16 @@ paths: application/json: schema: type: object + required: + - accessToken + - userID + properties: + accessToken: + type: string + description: アクセストークン + userID: + type: integer + description: ユーザーID x-codegen-request-body-name: mailAuthSignUp /mail_auth/signin: post: diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts index 9d61322ae..155568374 100644 --- a/view/next-project/e2e/tests/signup.spec.ts +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -48,8 +48,26 @@ test('新規登録後に current_user が 404 にならず My Page に遷移す await page.locator('input[type="text"]').nth(1).fill(email); await page.locator('input[type="password"]').nth(0).fill('password123'); await page.locator('input[type="password"]').nth(1).fill('password123'); + + const signupRequestPromise = page.waitForRequest( + (request) => request.url().includes('/mail_auth/signup') && request.method() === 'POST', + ); + await page.getByRole('button', { name: '登録' }).click(); + const signupRequest = await signupRequestPromise; + const signupRequestURL = new URL(signupRequest.url()); + const signupRequestBody = JSON.parse(signupRequest.postData() ?? '{}'); + + expect(signupRequestURL.search).toBe(''); + expect(signupRequestBody).toMatchObject({ + email, + password: 'password123', + name, + bureau_id: 1, + role_id: 1, + }); + await expect(page).toHaveURL(/\/my_page/); await expect(page.getByRole('heading', { name: 'My Page' })).toBeVisible(); diff --git a/view/next-project/src/components/auth/SignUpView.tsx b/view/next-project/src/components/auth/SignUpView.tsx index dda419c27..1a4e723dd 100644 --- a/view/next-project/src/components/auth/SignUpView.tsx +++ b/view/next-project/src/components/auth/SignUpView.tsx @@ -3,8 +3,8 @@ import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import { BUREAUS } from '@/constants/bureaus'; +import { postMailAuthSignup } from '@/generated/hooks'; import { useAuthStore, useUserStore } from '@/store'; -import { signUp } from '@api/signUp'; import { PrimaryButton } from '@components/common'; import LoadingButton from '@components/common/LoadingButton'; import { SignUp, User } from '@type/common'; @@ -40,10 +40,17 @@ export default function SignUpView() { const postUser = async (data: SignUp) => { setIsSignUpNow(true); - const signUpUrl: string = process.env.CSR_API_URI + '/mail_auth/signup'; - const req = await signUp(signUpUrl, data, postUserData); - if (req.status === 200) { - const res = await req.json(); + + try { + const req = await postMailAuthSignup({ + email: data.email, + password: data.password, + name: postUserData.name, + bureau_id: Number(postUserData.bureauID), + role_id: Number(postUserData.roleID), + }); + const res = req.data; + // state用のuserのデータ const userData: User = { id: res.userID, @@ -59,7 +66,7 @@ export default function SignUpView() { setAuth(authData); setUser(userData); Router.push('/my_page'); - } else { + } catch { alert( '新規登録に失敗しました。メールアドレスもしくはパスワードがすでに登録されている可能性があります', ); diff --git a/view/next-project/src/generated/hooks.ts b/view/next-project/src/generated/hooks.ts index 750fd8993..cb3a46f01 100644 --- a/view/next-project/src/generated/hooks.ts +++ b/view/next-project/src/generated/hooks.ts @@ -111,7 +111,7 @@ import type { PostMailAuthSignin200, PostMailAuthSigninParams, PostMailAuthSignup200, - PostMailAuthSignupParams, + PostMailAuthSignupBody, PostPasswordResetId200, PostPasswordResetIdValid200, PostPasswordResetIdValidParams, @@ -4952,65 +4952,50 @@ export type postMailAuthSignupResponseSuccess = postMailAuthSignupResponse200 & }; export type postMailAuthSignupResponse = postMailAuthSignupResponseSuccess; -export const getPostMailAuthSignupUrl = (params: PostMailAuthSignupParams) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/mail_auth/signup?${stringifiedParams}` - : `/mail_auth/signup`; +export const getPostMailAuthSignupUrl = () => { + return `/mail_auth/signup`; }; export const postMailAuthSignup = async ( - params: PostMailAuthSignupParams, + postMailAuthSignupBody: PostMailAuthSignupBody, options?: RequestInit, ): Promise => { - return customFetch(getPostMailAuthSignupUrl(params), { + return customFetch(getPostMailAuthSignupUrl(), { ...options, method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(postMailAuthSignupBody), }); }; export const getPostMailAuthSignupMutationFetcher = ( - params: PostMailAuthSignupParams, options?: SecondParameter, ) => { - return (_: Key, __: { arg: Arguments }) => { - return postMailAuthSignup(params, options); + return (_: Key, { arg }: { arg: PostMailAuthSignupBody }) => { + return postMailAuthSignup(arg, options); }; }; -export const getPostMailAuthSignupMutationKey = (params: PostMailAuthSignupParams) => - [`/mail_auth/signup`, ...(params ? [params] : [])] as const; +export const getPostMailAuthSignupMutationKey = () => [`/mail_auth/signup`] as const; export type PostMailAuthSignupMutationResult = NonNullable< Awaited> >; export type PostMailAuthSignupMutationError = unknown; -export const usePostMailAuthSignup = ( - params: PostMailAuthSignupParams, - options?: { - swr?: SWRMutationConfiguration< - Awaited>, - TError, - Key, - Arguments, - Awaited> - > & { swrKey?: string }; - request?: SecondParameter; - }, -) => { +export const usePostMailAuthSignup = (options?: { + swr?: SWRMutationConfiguration< + Awaited>, + TError, + Key, + PostMailAuthSignupBody, + Awaited> + > & { swrKey?: string }; + request?: SecondParameter; +}) => { const { swr: swrOptions, request: requestOptions } = options ?? {}; - const swrKey = swrOptions?.swrKey ?? getPostMailAuthSignupMutationKey(params); - const swrFn = getPostMailAuthSignupMutationFetcher(params, requestOptions); + const swrKey = swrOptions?.swrKey ?? getPostMailAuthSignupMutationKey(); + const swrFn = getPostMailAuthSignupMutationFetcher(requestOptions); const query = useSWRMutation(swrKey, swrFn, swrOptions); diff --git a/view/next-project/src/generated/model/index.ts b/view/next-project/src/generated/model/index.ts index a6313678b..7446e2376 100644 --- a/view/next-project/src/generated/model/index.ts +++ b/view/next-project/src/generated/model/index.ts @@ -163,7 +163,7 @@ export * from './postIncomeExpenditureManagementsCheckIdBody'; export * from './postMailAuthSignin200'; export * from './postMailAuthSigninParams'; export * from './postMailAuthSignup200'; -export * from './postMailAuthSignupParams'; +export * from './postMailAuthSignupBody'; export * from './postPasswordResetId200'; export * from './postPasswordResetIdValid200'; export * from './postPasswordResetIdValidParams'; diff --git a/view/next-project/src/generated/model/postMailAuthSignup200.ts b/view/next-project/src/generated/model/postMailAuthSignup200.ts index b3d2d26c4..54afda63f 100644 --- a/view/next-project/src/generated/model/postMailAuthSignup200.ts +++ b/view/next-project/src/generated/model/postMailAuthSignup200.ts @@ -6,4 +6,9 @@ * OpenAPI spec version: 2.0.0 */ -export type PostMailAuthSignup200 = { [key: string]: unknown }; +export type PostMailAuthSignup200 = { + /** アクセストークン */ + accessToken: string; + /** ユーザーID */ + userID: number; +}; diff --git a/view/next-project/src/generated/model/postMailAuthSignupParams.ts b/view/next-project/src/generated/model/postMailAuthSignupBody.ts similarity index 60% rename from view/next-project/src/generated/model/postMailAuthSignupParams.ts rename to view/next-project/src/generated/model/postMailAuthSignupBody.ts index 36bcc8bae..e9bcc4b88 100644 --- a/view/next-project/src/generated/model/postMailAuthSignupParams.ts +++ b/view/next-project/src/generated/model/postMailAuthSignupBody.ts @@ -6,25 +6,15 @@ * OpenAPI spec version: 2.0.0 */ -export type PostMailAuthSignupParams = { - /** - * email - */ +export type PostMailAuthSignupBody = { + /** email */ email: string; - /** - * password - */ + /** password */ password: string; - /** - * name - */ + /** name */ name: string; - /** - * bureau_id - */ + /** bureau_id */ bureau_id: number; - /** - * role_id - */ + /** role_id */ role_id: number; }; diff --git a/view/next-project/src/utils/api/signUp.ts b/view/next-project/src/utils/api/signUp.ts deleted file mode 100644 index d0cd096bb..000000000 --- a/view/next-project/src/utils/api/signUp.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SignUp, User } from '@type/common'; - -export const signUp = async (url: string, data: SignUp, user: User) => { - const params = new URLSearchParams({ - email: data.email, - password: data.password, - name: user.name, - bureau_id: String(user.bureauID), - role_id: String(user.roleID), - }); - const postUrl = url + '?' + params.toString(); - const res = await fetch(postUrl, { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - return await res; -}; From f41e7767f5e9440d6823ea576e52f3b2ae972781 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 10:10:47 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=81=AE=E8=AA=8D=E8=A8=BC=E9=96=A2=E9=80=A3=E3=82=AF?= =?UTF-8?q?=E3=82=A8=E3=83=AA=E3=82=92goqu=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/mail_auth_repository.go | 56 +++++- .../repository/session_repository.go | 60 +++++-- api/externals/repository/user_repository.go | 159 ++++++++++++++---- 3 files changed, 221 insertions(+), 54 deletions(-) diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 98f5dce1a..2553ea7b1 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -7,6 +7,7 @@ import ( "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" + goqu "github.com/doug-martin/goqu/v9" ) type mailAuthRepository struct { @@ -28,7 +29,19 @@ func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthReposit // 作成 func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, password string, userID string) (int64, error) { - result, err := r.client.DB().ExecContext(c, "INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, password, userID) + query, args, err := dialect.Insert("mail_auth"). + Prepared(true). + Rows(goqu.Record{ + "email": email, + "password": password, + "user_id": userID, + }). + ToSQL() + if err != nil { + return 0, err + } + + result, err := r.client.DB().ExecContext(c, query, args...) if err != nil { return 0, err } @@ -37,7 +50,19 @@ func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, pas } func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, email string, password string, userID string) (int64, error) { - result, err := tx.ExecContext(c, "INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, password, userID) + query, args, err := dialect.Insert("mail_auth"). + Prepared(true). + Rows(goqu.Record{ + "email": email, + "password": password, + "user_id": userID, + }). + ToSQL() + if err != nil { + return 0, err + } + + result, err := tx.ExecContext(c, query, args...) if err != nil { return 0, err } @@ -46,22 +71,37 @@ func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, // メールアドレスからmail_authを探してくる func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) *sql.Row { - query := "select * from mail_auth where email= '" + email + "'" - row := r.client.DB().QueryRowContext(c, query) + query, args, _ := dialect.From("mail_auth"). + Prepared(true). + Where(goqu.Ex{"email": email}). + ToSQL() + row := r.client.DB().QueryRowContext(c, query, args...) fmt.Printf("\x1b[36m%s\n", query) return row } // mail_auth_idからmail_authを探してくる func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) *sql.Row { - query := "select * from mail_auth where id= " + id - row := r.client.DB().QueryRowContext(c, query) + query, args, _ := dialect.From("mail_auth"). + Prepared(true). + Where(goqu.Ex{"id": id}). + ToSQL() + row := r.client.DB().QueryRowContext(c, query, args...) fmt.Printf("\x1b[36m%s\n", query) return row } // パスワードの変更 func (r *mailAuthRepository) ChangePasswordByUserID(c context.Context, userID string, password string) error { - query := "UPDATE mail_auth SET password = '" + password + "' WHERE user_id = " + userID - return r.crud.UpdateDB(c, query) + query, args, err := dialect.Update("mail_auth"). + Prepared(true). + Set(goqu.Record{"password": password}). + Where(goqu.Ex{"user_id": userID}). + ToSQL() + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) + return err } diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index 75c860f7c..e3a03ccc8 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/NUTFes/FinanSu/api/drivers/db" + goqu "github.com/doug-martin/goqu/v9" ) type sessionRepository struct { @@ -26,8 +27,19 @@ func NewSessionRepository(client db.Client) SessionRepository { // 作成 func (r *sessionRepository) Create(c context.Context, authID string, userID string, accessToken string) error { - query := "insert into session (auth_id, user_id, access_token) values (?, ?, ?)" - _, err := r.client.DB().ExecContext(c, query, authID, userID, accessToken) + query, args, err := dialect.Insert("session"). + Prepared(true). + Rows(goqu.Record{ + "auth_id": authID, + "user_id": userID, + "access_token": accessToken, + }). + ToSQL() + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) if err != nil { return err } @@ -36,8 +48,19 @@ func (r *sessionRepository) Create(c context.Context, authID string, userID stri } func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID string, userID string, accessToken string) error { - query := "insert into session (auth_id, user_id, access_token) values (?, ?, ?)" - _, err := tx.ExecContext(c, query, authID, userID, accessToken) + query, args, err := dialect.Insert("session"). + Prepared(true). + Rows(goqu.Record{ + "auth_id": authID, + "user_id": userID, + "access_token": accessToken, + }). + ToSQL() + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) if err != nil { return err } @@ -48,8 +71,15 @@ func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID s // 削除 func (r *sessionRepository) Destroy(c context.Context, accessToken string) error { // access tokenで該当のsessionを削除 - query := "delete from session where access_token = '" + accessToken + "'" - _, err := r.client.DB().ExecContext(c, query) + query, args, err := dialect.Delete("session"). + Prepared(true). + Where(goqu.Ex{"access_token": accessToken}). + ToSQL() + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) if err != nil { return err } @@ -59,16 +89,26 @@ func (r *sessionRepository) Destroy(c context.Context, accessToken string) error // アクセストークンからセッションを取得 func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessToken string) *sql.Row { - query := "select * from session where access_token = '" + accessToken + "'" - row := r.client.DB().QueryRowContext(c, query) + query, args, _ := dialect.From("session"). + Prepared(true). + Where(goqu.Ex{"access_token": accessToken}). + ToSQL() + row := r.client.DB().QueryRowContext(c, query, args...) fmt.Printf("\x1b[36m%s\n", query) return row } // user_idからsessionを削除する func (r *sessionRepository) DestroyByUserID(c context.Context, userID string) error { - query := "delete from session where user_id = " + userID - _, err := r.client.DB().ExecContext(c, query) + query, args, err := dialect.Delete("session"). + Prepared(true). + Where(goqu.Ex{"user_id": userID}). + ToSQL() + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) if err != nil { return err } diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index 9501f9fbc..bb106e981 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -3,7 +3,6 @@ package repository import ( "context" "database/sql" - "strconv" "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" @@ -36,34 +35,61 @@ func NewUserRepository(c db.Client, ac abstract.Crud) UserRepository { // 全件取得 func (ur *userRepository) All(c context.Context) (*sql.Rows, error) { - query := "SELECT * FROM users WHERE is_deleted IS FALSE" - return ur.crud.Read(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"is_deleted": false}). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryContext(c, query, args...) } // 1件取得 func (ur *userRepository) Find(c context.Context, id string) (*sql.Row, error) { - query := "SELECT * FROM users WHERE id = " + id - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"id": id}). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil } // 複数件取得 func (ur *userRepository) FindByIDs(c context.Context, ids []int) (*sql.Rows, error) { ds := dialect. From("users"). + Prepared(true). Select("users.*"). Where(goqu.I("users.id").In(ids)) - query, _, err := ds.ToSQL() + query, args, err := ds.ToSQL() if err != nil { return nil, err } - return ur.crud.Read(c, query) + return ur.client.DB().QueryContext(c, query, args...) } // 作成 func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) (int64, error) { - result, err := ur.client.DB().ExecContext(c, "INSERT INTO users (name, bureau_id, role_id) VALUES (?, ?, ?)", name, bureauID, roleID) + query, args, err := dialect.Insert("users"). + Prepared(true). + Rows(goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }). + ToSQL() + if err != nil { + return 0, err + } + + result, err := ur.client.DB().ExecContext(c, query, args...) if err != nil { return 0, err } @@ -71,7 +97,19 @@ func (ur *userRepository) Create(c context.Context, name string, bureauID string } func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name string, bureauID string, roleID string) (int64, error) { - result, err := tx.ExecContext(c, "INSERT INTO users (name, bureau_id, role_id) VALUES (?, ?, ?)", name, bureauID, roleID) + query, args, err := dialect.Insert("users"). + Prepared(true). + Rows(goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }). + ToSQL() + if err != nil { + return 0, err + } + + result, err := tx.ExecContext(c, query, args...) if err != nil { return 0, err } @@ -80,15 +118,21 @@ func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name strin // 編集 func (ur *userRepository) Update(c context.Context, id string, name string, bureauID string, roleID string) error { - query := ` - UPDATE - users - SET - name = '` + name + - "', bureau_id = " + bureauID + - ", role_id = " + roleID + - " WHERE id = " + id - return ur.crud.UpdateDB(c, query) + query, args, err := dialect.Update("users"). + Prepared(true). + Set(goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }). + Where(goqu.Ex{"id": id}). + ToSQL() + if err != nil { + return err + } + + _, err = ur.client.DB().ExecContext(c, query, args...) + return err } // 削除 @@ -105,12 +149,30 @@ func (ur *userRepository) Destroy(c context.Context, id string) error { } func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string) error { - _, err := tx.ExecContext(c, "UPDATE users SET is_deleted = TRUE WHERE id = ?", id) + userQuery, userArgs, err := dialect.Update("users"). + Prepared(true). + Set(goqu.Record{"is_deleted": true}). + Where(goqu.Ex{"id": id}). + ToSQL() if err != nil { return err } - _, err = tx.ExecContext(c, "UPDATE mail_auth SET email = NULL WHERE user_id = ?", id) + _, err = tx.ExecContext(c, userQuery, userArgs...) + if err != nil { + return err + } + + mailAuthQuery, mailAuthArgs, err := dialect.Update("mail_auth"). + Prepared(true). + Set(goqu.Record{"email": nil}). + Where(goqu.Ex{"user_id": id}). + ToSQL() + if err != nil { + return err + } + + _, err = tx.ExecContext(c, mailAuthQuery, mailAuthArgs...) return err } @@ -132,36 +194,61 @@ func (ur *userRepository) MultiDestroyWithTx(c context.Context, tx *sql.Tx, ids return nil } - query := "UPDATE users SET is_deleted = TRUE WHERE " - query2 := "UPDATE mail_auth SET email = NULL WHERE " - for index, id := range ids { - query += "id = " + strconv.Itoa(id) - query2 += "user_id = " + strconv.Itoa(id) - - if index != len(ids)-1 { - query += " OR " - query2 += " OR " - } + userQuery, userArgs, err := dialect.Update("users"). + Prepared(true). + Set(goqu.Record{"is_deleted": true}). + Where(goqu.I("id").In(ids)). + ToSQL() + if err != nil { + return err + } + _, err = tx.ExecContext(c, userQuery, userArgs...) + if err != nil { + return err } - _, err := tx.ExecContext(c, query) + mailAuthQuery, mailAuthArgs, err := dialect.Update("mail_auth"). + Prepared(true). + Set(goqu.Record{"email": nil}). + Where(goqu.I("user_id").In(ids)). + ToSQL() if err != nil { return err } - _, err = tx.ExecContext(c, query2) + _, err = tx.ExecContext(c, mailAuthQuery, mailAuthArgs...) return err } func (ur *userRepository) FindNewRecord(c context.Context) (*sql.Row, error) { - query := "SELECT * FROM users WHERE is_deleted IS FALSE ORDER BY id DESC LIMIT 1" - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"is_deleted": false}). + Order(goqu.I("id").Desc()). + Limit(1). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil } // 1件取得 func (ur *userRepository) FindByEmail(c context.Context, email string) (*sql.Row, error) { - query := "SELECT * FROM users INNER JOIN mail_auth ON users.id = mail_auth.user_id WHERE is_deleted IS FALSE AND email = '" + email + "'" - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Join(goqu.I("mail_auth"), goqu.On(goqu.I("users.id").Eq(goqu.I("mail_auth.user_id")))). + Where( + goqu.Ex{"users.is_deleted": false}, + goqu.Ex{"mail_auth.email": email}, + ). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil } From 53fb33e6736d209e25a9526e9599b0219e4363c8 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 10:55:24 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E3=83=AA=E3=83=9D?= =?UTF-8?q?=E3=82=B8=E3=83=88=E3=83=AA=E3=81=AE=E3=83=87=E3=83=90=E3=83=83?= =?UTF-8?q?=E3=82=B0=E5=87=BA=E5=8A=9B=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/repository/mail_auth_repository.go | 3 --- api/externals/repository/session_repository.go | 6 ------ 2 files changed, 9 deletions(-) diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 2553ea7b1..b42635902 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -3,7 +3,6 @@ package repository import ( "context" "database/sql" - "fmt" "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" @@ -76,7 +75,6 @@ func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string Where(goqu.Ex{"email": email}). ToSQL() row := r.client.DB().QueryRowContext(c, query, args...) - fmt.Printf("\x1b[36m%s\n", query) return row } @@ -87,7 +85,6 @@ func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) *sql Where(goqu.Ex{"id": id}). ToSQL() row := r.client.DB().QueryRowContext(c, query, args...) - fmt.Printf("\x1b[36m%s\n", query) return row } diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index e3a03ccc8..25ec69e15 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -3,7 +3,6 @@ package repository import ( "context" "database/sql" - "fmt" "github.com/NUTFes/FinanSu/api/drivers/db" goqu "github.com/doug-martin/goqu/v9" @@ -43,7 +42,6 @@ func (r *sessionRepository) Create(c context.Context, authID string, userID stri if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } @@ -64,7 +62,6 @@ func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID s if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } @@ -83,7 +80,6 @@ func (r *sessionRepository) Destroy(c context.Context, accessToken string) error if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } @@ -94,7 +90,6 @@ func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessTo Where(goqu.Ex{"access_token": accessToken}). ToSQL() row := r.client.DB().QueryRowContext(c, query, args...) - fmt.Printf("\x1b[36m%s\n", query) return row } @@ -112,6 +107,5 @@ func (r *sessionRepository) DestroyByUserID(c context.Context, userID string) er if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } From 4fdcb108cdba4c6f06130501c050cd4b5f44ec44 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 11:55:02 +0900 Subject: [PATCH 06/22] =?UTF-8?q?E2E=E3=82=92CI=E3=81=A7=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=AF=E3=83=BC=E3=82=AF=E3=83=95=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e.yml | 39 ++++++++++++++++++++++++++++++++++++ view/next-project/.gitignore | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..da66d0349 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,39 @@ +name: E2E + +on: + workflow_dispatch: + pull_request: + branches: + - develop + paths: + - ".github/workflows/e2e.yml" + - "Makefile" + - "compose.e2e.yml" + - "api/**" + - "mysql/**" + - "openapi/**" + - "view/**" + - "my.cnf" + +concurrency: + group: e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + signup-e2e: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Prepare env file + run: touch finansu.env + + - name: Run E2E + run: make run-e2e + + - name: Cleanup E2E containers + if: always() + run: docker compose -f compose.e2e.yml down --volumes --remove-orphans diff --git a/view/next-project/.gitignore b/view/next-project/.gitignore index 6317051f0..fdce9b17d 100644 --- a/view/next-project/.gitignore +++ b/view/next-project/.gitignore @@ -7,6 +7,8 @@ # testing /coverage +/e2e/test-results/ +/e2e/playwright-report/ # next.js /.next/ @@ -35,4 +37,3 @@ yarn-error.log* # typescript *.tsbuildinfo - From 3c022b2b0bd86a27cd23db054c1de49e44d4b47d Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 13:13:14 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E3=81=AB=E5=9F=BA=E3=81=A5=E3=81=8Dsignup?= =?UTF-8?q?=E6=A4=9C=E8=A8=BC=E3=81=A8=E3=82=A8=E3=83=A9=E3=83=BC=E4=BC=9D?= =?UTF-8?q?=E6=92=AD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/handler/mail_auth_handler.go | 11 +++++++- .../repository/mail_auth_repository.go | 26 ++++++++++++------- .../repository/session_repository.go | 13 ++++++---- api/internals/usecase/mail_auth_usecase.go | 21 ++++++++++----- api/internals/usecase/user_usecase.go | 6 ++++- api/test/signup_transaction_test.go | 26 +++++++++++++++++++ 6 files changed, 79 insertions(+), 24 deletions(-) diff --git a/api/externals/handler/mail_auth_handler.go b/api/externals/handler/mail_auth_handler.go index 89c89324a..83fcd5f8e 100644 --- a/api/externals/handler/mail_auth_handler.go +++ b/api/externals/handler/mail_auth_handler.go @@ -3,6 +3,7 @@ package handler import ( "net/http" "strconv" + "strings" "github.com/NUTFes/FinanSu/api/generated" "github.com/labstack/echo/v4" @@ -48,7 +49,15 @@ func (h *Handler) DeleteMailAuthSignout(c echo.Context, params generated.DeleteM func (h *Handler) PostMailAuthSignup(c echo.Context) error { var request generated.PostMailAuthSignupJSONRequestBody if err := c.Bind(&request); err != nil { - return err + return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") + } + + if strings.TrimSpace(request.Email) == "" || + strings.TrimSpace(request.Password) == "" || + strings.TrimSpace(request.Name) == "" || + request.BureauId <= 0 || + request.RoleId <= 0 { + return echo.NewHTTPError(http.StatusBadRequest, "email, password, name, bureau_id and role_id are required") } email := request.Email diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index b42635902..e3b16fc52 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -17,8 +17,8 @@ type mailAuthRepository struct { type MailAuthRepository interface { CreateMailAuth(context.Context, string, string, string) (int64, error) CreateMailAuthWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) - FindMailAuthByEmail(context.Context, string) *sql.Row - FindMailAuthByID(context.Context, string) *sql.Row + FindMailAuthByEmail(context.Context, string) (*sql.Row, error) + FindMailAuthByID(context.Context, string) (*sql.Row, error) ChangePasswordByUserID(context.Context, string, string) error } @@ -69,23 +69,29 @@ func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, } // メールアドレスからmail_authを探してくる -func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) *sql.Row { - query, args, _ := dialect.From("mail_auth"). +func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) (*sql.Row, error) { + query, args, err := dialect.From("mail_auth"). Prepared(true). Where(goqu.Ex{"email": email}). ToSQL() - row := r.client.DB().QueryRowContext(c, query, args...) - return row + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // mail_auth_idからmail_authを探してくる -func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) *sql.Row { - query, args, _ := dialect.From("mail_auth"). +func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) (*sql.Row, error) { + query, args, err := dialect.From("mail_auth"). Prepared(true). Where(goqu.Ex{"id": id}). ToSQL() - row := r.client.DB().QueryRowContext(c, query, args...) - return row + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // パスワードの変更 diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index 25ec69e15..1e6fcf0e5 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -16,7 +16,7 @@ type SessionRepository interface { Create(context.Context, string, string, string) error CreateWithTx(context.Context, *sql.Tx, string, string, string) error Destroy(context.Context, string) error - FindSessionByAccessToken(context.Context, string) *sql.Row + FindSessionByAccessToken(context.Context, string) (*sql.Row, error) DestroyByUserID(context.Context, string) error } @@ -84,13 +84,16 @@ func (r *sessionRepository) Destroy(c context.Context, accessToken string) error } // アクセストークンからセッションを取得 -func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessToken string) *sql.Row { - query, args, _ := dialect.From("session"). +func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessToken string) (*sql.Row, error) { + query, args, err := dialect.From("session"). Prepared(true). Where(goqu.Ex{"access_token": accessToken}). ToSQL() - row := r.client.DB().QueryRowContext(c, query, args...) - return row + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // user_idからsessionを削除する diff --git a/api/internals/usecase/mail_auth_usecase.go b/api/internals/usecase/mail_auth_usecase.go index 01ed02b13..201f3b815 100644 --- a/api/internals/usecase/mail_auth_usecase.go +++ b/api/internals/usecase/mail_auth_usecase.go @@ -42,15 +42,14 @@ func NewMailAuthUseCase( func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, name string, bureauID string, roleID string) (domain.Token, error) { var token domain.Token - tx, err := u.transactionRep.StartTransaction(c) + // パスワードをハッシュ化 + hashed, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { return token, err } - // パスワードをハッシュ化 - hashed, err := bcrypt.GenerateFromPassword([]byte(password), 10) + tx, err := u.transactionRep.StartTransaction(c) if err != nil { - _ = u.transactionRep.RollBack(c, tx) return token, err } @@ -94,8 +93,12 @@ func (u *mailAuthUseCase) SignIn(c context.Context, email string, password strin var mailAuth = domain.MailAuth{} var token domain.Token // メールアドレスの存在確認 - row := u.mailAuthRep.FindMailAuthByEmail(c, email) - err := row.Scan( + row, err := u.mailAuthRep.FindMailAuthByEmail(c, email) + if err != nil { + return token, err + } + + err = row.Scan( &mailAuth.ID, &mailAuth.Email, &mailAuth.Password, @@ -140,7 +143,11 @@ func (u *mailAuthUseCase) SignOut(c context.Context, accessToken string) error { func (u *mailAuthUseCase) IsSignIn(c context.Context, accessToken string) (domain.IsSignIn, error) { var session = domain.Session{} var isSignIn domain.IsSignIn - row := u.sessionRep.FindSessionByAccessToken(c, accessToken) + row, err := u.sessionRep.FindSessionByAccessToken(c, accessToken) + if err != nil { + return isSignIn, err + } + _ = row.Scan( &session.ID, &session.AuthID, diff --git a/api/internals/usecase/user_usecase.go b/api/internals/usecase/user_usecase.go index b749ecac4..5a4bd6c21 100644 --- a/api/internals/usecase/user_usecase.go +++ b/api/internals/usecase/user_usecase.go @@ -169,7 +169,11 @@ func (u *userUseCase) GetCurrentUser(c context.Context, accessToken string) (dom var row *sql.Row var err error // アクセストークンからmail_authを取得 - row = u.sessionRep.FindSessionByAccessToken(c, accessToken) + row, err = u.sessionRep.FindSessionByAccessToken(c, accessToken) + if err != nil { + return user, err + } + err = row.Scan( &session.ID, &session.AuthID, diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index 011f3b9dc..6f4ecbdac 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -117,3 +117,29 @@ func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM users WHERE name = ?", name)) assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) } + +func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-missing-fields@example.com" + r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "role_id": 1, + }) + defer r.Body.Close() + + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM users WHERE name = ''")) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) +} From 2b75be9ba14ee41d3b151413e9a6f304f8414174 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 14:06:18 +0900 Subject: [PATCH 08/22] =?UTF-8?q?CI=E5=90=91=E3=81=91=E3=81=ABE2E=E3=81=AE?= =?UTF-8?q?env=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E3=82=92=E7=B7=A9=E5=92=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e.yml | 3 --- Makefile | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index da66d0349..3da55fefe 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -28,9 +28,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Prepare env file - run: touch finansu.env - - name: Run E2E run: make run-e2e diff --git a/Makefile b/Makefile index 3c9d9eca6..2762d8a61 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # アプリコンテナ=view,api、DBコンテナ=db,minio -include finansu.env +-include finansu.env # 配色 SHELL := /bin/bash From 33638ce7e0a0ad4c634b09e8fd97bd2aa236f5dd Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 5 Jun 2026 12:39:57 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=A7TransactionRepository=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/repository/user_repository.go | 28 -------------- api/internals/di/wire_gen.go | 2 +- api/internals/usecase/user_usecase.go | 43 +++++++++++++++++---- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index bb106e981..5cba3ad1c 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -21,9 +21,7 @@ type UserRepository interface { Create(context.Context, string, string, string) (int64, error) CreateWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) Update(context.Context, string, string, string, string) error - Destroy(context.Context, string) error DestroyWithTx(context.Context, *sql.Tx, string) error - MultiDestroy(context.Context, []int) error MultiDestroyWithTx(context.Context, *sql.Tx, []int) error FindNewRecord(context.Context) (*sql.Row, error) FindByEmail(context.Context, string) (*sql.Row, error) @@ -135,19 +133,6 @@ func (ur *userRepository) Update(c context.Context, id string, name string, bure return err } -// 削除 -func (ur *userRepository) Destroy(c context.Context, id string) error { - tx, err := ur.client.DB().BeginTx(c, nil) - if err != nil { - return err - } - if err = ur.DestroyWithTx(c, tx, id); err != nil { - _ = tx.Rollback() - return err - } - return tx.Commit() -} - func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string) error { userQuery, userArgs, err := dialect.Update("users"). Prepared(true). @@ -176,19 +161,6 @@ func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string return err } -// 複数削除 -func (ur *userRepository) MultiDestroy(c context.Context, ids []int) error { - tx, err := ur.client.DB().BeginTx(c, nil) - if err != nil { - return err - } - if err = ur.MultiDestroyWithTx(c, tx, ids); err != nil { - _ = tx.Rollback() - return err - } - return tx.Commit() -} - func (ur *userRepository) MultiDestroyWithTx(c context.Context, tx *sql.Tx, ids []int) error { if len(ids) == 0 { return nil diff --git a/api/internals/di/wire_gen.go b/api/internals/di/wire_gen.go index 690811cab..2b9ad5bf4 100644 --- a/api/internals/di/wire_gen.go +++ b/api/internals/di/wire_gen.go @@ -69,7 +69,7 @@ func InitializeServer() (*ServerComponents, error) { sponsorStyleUseCase := usecase.NewSponsorStyleUseCase(sponsorStyleRepository) teacherRepository := repository.NewTeacherRepository(client, crud) teacherUseCase := usecase.NewTeacherUseCase(teacherRepository) - userUseCase := usecase.NewUserUseCase(userRepository, sessionRepository) + userUseCase := usecase.NewUserUseCase(userRepository, sessionRepository, transactionRepository) yearRepository := repository.NewYearRepository(client, crud) yearUseCase := usecase.NewYearUseCase(yearRepository) sponsorshipActivityRepository := repository.NewSponsorshipActivityRepository(client, crud) diff --git a/api/internals/usecase/user_usecase.go b/api/internals/usecase/user_usecase.go index 5a4bd6c21..832ff139a 100644 --- a/api/internals/usecase/user_usecase.go +++ b/api/internals/usecase/user_usecase.go @@ -12,8 +12,9 @@ import ( ) type userUseCase struct { - userRep rep.UserRepository - sessionRep rep.SessionRepository + userRep rep.UserRepository + sessionRep rep.SessionRepository + transactionRep rep.TransactionRepository } type UserUseCase interface { @@ -26,8 +27,8 @@ type UserUseCase interface { GetCurrentUser(context.Context, string) (domain.User, error) } -func NewUserUseCase(userRep rep.UserRepository, sessionRep rep.SessionRepository) UserUseCase { - return &userUseCase{userRep: userRep, sessionRep: sessionRep} +func NewUserUseCase(userRep rep.UserRepository, sessionRep rep.SessionRepository, transactionRep rep.TransactionRepository) UserUseCase { + return &userUseCase{userRep: userRep, sessionRep: sessionRep, transactionRep: transactionRep} } func (u *userUseCase) GetUsers(c context.Context, ids *[]int) ([]domain.User, error) { @@ -154,13 +155,39 @@ func (u *userUseCase) UpdateUser(c context.Context, id string, name string, bure } func (u *userUseCase) DestroyUser(c context.Context, id string) error { - err := u.userRep.Destroy(c, id) - return err + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return err + } + + if err = u.userRep.DestroyWithTx(c, tx, id); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + return nil } func (u *userUseCase) DestroyMultiUsers(c context.Context, ids []int) error { - err := u.userRep.MultiDestroy(c, ids) - return err + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return err + } + + if err = u.userRep.MultiDestroyWithTx(c, tx, ids); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + return nil } func (u *userUseCase) GetCurrentUser(c context.Context, accessToken string) (domain.User, error) { From 59b14cfb7e838c37eb74d2779eadbc2778f650d3 Mon Sep 17 00:00:00 2001 From: hikahana <106811268+hikahana@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:59:51 +0900 Subject: [PATCH 10/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- view/next-project/e2e/tests/signup.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts index 155568374..a977bbb42 100644 --- a/view/next-project/e2e/tests/signup.spec.ts +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -21,6 +21,7 @@ async function waitForService(url: string) { test.beforeAll(async () => { await waitForService(`${apiURL}/`); + await waitForService(`${process.env.BASE_URL || 'http://view:3000'}/`); }); test('新規登録後に current_user が 404 にならず My Page に遷移する', async ({ page }) => { From 943f7eaa45212dc360ff9d491f160b4bcfbdbc5b Mon Sep 17 00:00:00 2001 From: hikahana <106811268+hikahana@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:00:31 +0900 Subject: [PATCH 11/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- api/test/signup_transaction_test.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index 6f4ecbdac..6ae981c32 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -130,16 +130,17 @@ func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { serverComponents.Client.CloseDB() }) - email := "signup-missing-fields@example.com" - r := postSignup(t, testServer.URL, map[string]any{ - "email": email, - "password": "password123", - "role_id": 1, - }) - defer r.Body.Close() - - assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM users WHERE name = ''")) - assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) - assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) +email := "signup-missing-fields@example.com" +beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") +r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "role_id": 1, +}) +defer r.Body.Close() + +assert.Equal(t, http.StatusBadRequest, r.StatusCode) +assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) +assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) +assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) } From 14e404a81521807b45bb73a3a87380616e86e78b Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 12 Jun 2026 02:23:19 +0900 Subject: [PATCH 12/22] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=83=B3=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97E2E=E3=81=AE=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/test/signup_transaction_test.go | 26 +++++++++---------- view/next-project/e2e/package.json | 7 +++-- view/next-project/e2e/tests/signup.spec.ts | 5 ---- view/next-project/e2e/tsconfig.json | 13 ++++++++++ .../src/components/auth/SignUpView.tsx | 20 +++++++------- view/next-project/src/type/common.ts | 1 + 6 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 view/next-project/e2e/tsconfig.json diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index 6ae981c32..7a0b5f5b0 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -130,17 +130,17 @@ func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { serverComponents.Client.CloseDB() }) -email := "signup-missing-fields@example.com" -beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") -r := postSignup(t, testServer.URL, map[string]any{ - "email": email, - "password": "password123", - "role_id": 1, -}) -defer r.Body.Close() - -assert.Equal(t, http.StatusBadRequest, r.StatusCode) -assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) -assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) -assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) + email := "signup-missing-fields@example.com" + beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") + r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "role_id": 1, + }) + defer r.Body.Close() + + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) } diff --git a/view/next-project/e2e/package.json b/view/next-project/e2e/package.json index efa150592..b9a4c062f 100644 --- a/view/next-project/e2e/package.json +++ b/view/next-project/e2e/package.json @@ -2,9 +2,12 @@ "name": "finansu-e2e", "private": true, "scripts": { - "test": "playwright test" + "test": "playwright test", + "type-check": "tsc --noEmit -p tsconfig.json" }, "devDependencies": { - "@playwright/test": "1.57.0" + "@playwright/test": "1.57.0", + "@types/node": "20.19.0", + "typescript": "5.9.3" } } diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts index a977bbb42..2ca6a975d 100644 --- a/view/next-project/e2e/tests/signup.spec.ts +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -26,16 +26,12 @@ test.beforeAll(async () => { test('新規登録後に current_user が 404 にならず My Page に遷移する', async ({ page }) => { const currentUserStatuses: number[] = []; - const page404Responses: string[] = []; page.on('response', (response) => { const url = response.url(); if (url.includes('/current_user')) { currentUserStatuses.push(response.status()); } - if (response.status() === 404) { - page404Responses.push(url); - } }); await page.goto('/'); @@ -74,5 +70,4 @@ test('新規登録後に current_user が 404 にならず My Page に遷移す expect(currentUserStatuses).toContain(200); expect(currentUserStatuses).not.toContain(404); - expect(page404Responses).toEqual([]); }); diff --git a/view/next-project/e2e/tsconfig.json b/view/next-project/e2e/tsconfig.json new file mode 100644 index 000000000..0a2e9c5af --- /dev/null +++ b/view/next-project/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["playwright.config.ts", "tests/**/*.ts"] +} diff --git a/view/next-project/src/components/auth/SignUpView.tsx b/view/next-project/src/components/auth/SignUpView.tsx index ebd91602a..6f030af14 100644 --- a/view/next-project/src/components/auth/SignUpView.tsx +++ b/view/next-project/src/components/auth/SignUpView.tsx @@ -16,9 +16,8 @@ export default function SignUpView() { // 新規登録中フラグ const [isSignUpNow, setIsSignUpNow] = useState(false); - const [postUserData, setPostUserData] = useState({ + const [postUserData, setPostUserData] = useState>({ id: 0, - name: '', bureauID: 1, roleID: 1, }); @@ -33,8 +32,7 @@ export default function SignUpView() { }); const userDataHandler = - (input: string) => - (e: React.ChangeEvent | React.ChangeEvent) => { + (input: 'bureauID' | 'roleID') => (e: React.ChangeEvent) => { setPostUserData({ ...postUserData, [input]: e.target.value }); }; @@ -45,7 +43,7 @@ export default function SignUpView() { const req = await postMailAuthSignup({ email: data.email, password: data.password, - name: postUserData.name, + name: data.name, bureau_id: Number(postUserData.bureauID), role_id: Number(postUserData.roleID), }); @@ -54,7 +52,7 @@ export default function SignUpView() { // state用のuserのデータ const userData: User = { id: res.userID, - name: postUserData.name, + name: data.name, bureauID: Number(postUserData.bureauID), roleID: Number(postUserData.roleID), }; @@ -67,9 +65,7 @@ export default function SignUpView() { setUser(userData); Router.push('/my_page'); } catch { - alert( - '新規登録に失敗しました。メールアドレスもしくはパスワードがすでに登録されている可能性があります', - ); + alert('新規登録に失敗しました。このメールアドレスは既に登録されている可能性があります。'); setIsSignUpNow(false); } }; @@ -83,8 +79,9 @@ export default function SignUpView() {

所属局

-

所属局

+ -

メールアドレス

+ -

パスワード

+ -

パスワード確認

+ Date: Mon, 15 Jun 2026 11:07:10 +0900 Subject: [PATCH 18/22] =?UTF-8?q?mail=5Fauth=E3=81=AE=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E7=84=A1=E5=8A=B9=E5=8C=96=E3=83=A1=E3=82=BD=E3=83=83?= =?UTF-8?q?=E3=83=89=E5=90=8D=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/repository/mail_auth_repository.go | 8 ++++---- api/internals/usecase/user_usecase.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index e0717c32a..7edfd47eb 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -20,8 +20,8 @@ type MailAuthRepository interface { FindMailAuthByEmail(context.Context, string) (*sql.Row, error) FindMailAuthByID(context.Context, string) (*sql.Row, error) ChangePasswordByUserID(context.Context, string, string) error - UpdateEmailNullByUserIDWithTx(context.Context, *sql.Tx, string) error - UpdateEmailNullByUserIDsWithTx(context.Context, *sql.Tx, []int) error + InvalidateEmailByUserIDWithTx(context.Context, *sql.Tx, string) error + InvalidateEmailByUserIDsWithTx(context.Context, *sql.Tx, []int) error } func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthRepository { @@ -111,7 +111,7 @@ func (r *mailAuthRepository) ChangePasswordByUserID(c context.Context, userID st return err } -func (r *mailAuthRepository) UpdateEmailNullByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { +func (r *mailAuthRepository) InvalidateEmailByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { query, args, err := dialect.Update("mail_auth"). Prepared(true). Set(goqu.Record{"email": nil}). @@ -125,7 +125,7 @@ func (r *mailAuthRepository) UpdateEmailNullByUserIDWithTx(c context.Context, tx return err } -func (r *mailAuthRepository) UpdateEmailNullByUserIDsWithTx(c context.Context, tx *sql.Tx, userIDs []int) error { +func (r *mailAuthRepository) InvalidateEmailByUserIDsWithTx(c context.Context, tx *sql.Tx, userIDs []int) error { if len(userIDs) == 0 { return nil } diff --git a/api/internals/usecase/user_usecase.go b/api/internals/usecase/user_usecase.go index 2b275b4a0..e145252b7 100644 --- a/api/internals/usecase/user_usecase.go +++ b/api/internals/usecase/user_usecase.go @@ -171,7 +171,7 @@ func (u *userUseCase) DestroyUser(c context.Context, id string) error { return err } - if err = u.mailAuthRep.UpdateEmailNullByUserIDWithTx(c, tx, id); err != nil { + if err = u.mailAuthRep.InvalidateEmailByUserIDWithTx(c, tx, id); err != nil { _ = u.transactionRep.RollBack(c, tx) return err } @@ -199,7 +199,7 @@ func (u *userUseCase) DestroyMultiUsers(c context.Context, ids []int) error { return err } - if err = u.mailAuthRep.UpdateEmailNullByUserIDsWithTx(c, tx, ids); err != nil { + if err = u.mailAuthRep.InvalidateEmailByUserIDsWithTx(c, tx, ids); err != nil { _ = u.transactionRep.RollBack(c, tx) return err } From 6c44dad6912d6e60db3948c02d5e6d09a54f2502 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:42:07 +0900 Subject: [PATCH 19/22] =?UTF-8?q?mail=5Fauth=E3=81=AE=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=83=AA=E7=94=9F=E6=88=90=E3=82=92=E5=85=B1=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/mail_auth_repository.go | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 7edfd47eb..1022782d0 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -7,6 +7,7 @@ import ( "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) type mailAuthRepository struct { @@ -30,14 +31,7 @@ func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthReposit // 作成 func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, password string, userID string) (int64, error) { - query, args, err := dialect.Insert("mail_auth"). - Prepared(true). - Rows(goqu.Record{ - "email": email, - "password": password, - "user_id": userID, - }). - ToSQL() + query, args, err := createMailAuthQuery(email, password, userID) if err != nil { return 0, err } @@ -51,14 +45,7 @@ func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, pas } func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, email string, password string, userID string) (int64, error) { - query, args, err := dialect.Insert("mail_auth"). - Prepared(true). - Rows(goqu.Record{ - "email": email, - "password": password, - "user_id": userID, - }). - ToSQL() + query, args, err := createMailAuthQuery(email, password, userID) if err != nil { return 0, err } @@ -98,11 +85,10 @@ func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) (*sq // パスワードの変更 func (r *mailAuthRepository) ChangePasswordByUserID(c context.Context, userID string, password string) error { - query, args, err := dialect.Update("mail_auth"). - Prepared(true). - Set(goqu.Record{"password": password}). - Where(goqu.Ex{"user_id": userID}). - ToSQL() + query, args, err := updateMailAuthQuery( + goqu.Record{"password": password}, + goqu.Ex{"user_id": userID}, + ) if err != nil { return err } @@ -112,11 +98,10 @@ func (r *mailAuthRepository) ChangePasswordByUserID(c context.Context, userID st } func (r *mailAuthRepository) InvalidateEmailByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { - query, args, err := dialect.Update("mail_auth"). - Prepared(true). - Set(goqu.Record{"email": nil}). - Where(goqu.Ex{"user_id": userID}). - ToSQL() + query, args, err := updateMailAuthQuery( + goqu.Record{"email": nil}, + goqu.Ex{"user_id": userID}, + ) if err != nil { return err } @@ -130,11 +115,10 @@ func (r *mailAuthRepository) InvalidateEmailByUserIDsWithTx(c context.Context, t return nil } - query, args, err := dialect.Update("mail_auth"). - Prepared(true). - Set(goqu.Record{"email": nil}). - Where(goqu.I("user_id").In(userIDs)). - ToSQL() + query, args, err := updateMailAuthQuery( + goqu.Record{"email": nil}, + goqu.I("user_id").In(userIDs), + ) if err != nil { return err } @@ -142,3 +126,22 @@ func (r *mailAuthRepository) InvalidateEmailByUserIDsWithTx(c context.Context, t _, err = tx.ExecContext(c, query, args...) return err } + +func createMailAuthQuery(email string, password string, userID string) (string, []any, error) { + return dialect.Insert("mail_auth"). + Prepared(true). + Rows(goqu.Record{ + "email": email, + "password": password, + "user_id": userID, + }). + ToSQL() +} + +func updateMailAuthQuery(record goqu.Record, where ...exp.Expression) (string, []any, error) { + return dialect.Update("mail_auth"). + Prepared(true). + Set(record). + Where(where...). + ToSQL() +} From f456ce586bb7c8689e44ee455d00eb25edd9e243 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:53:53 +0900 Subject: [PATCH 20/22] =?UTF-8?q?repository=E3=81=AE=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=83=AA=E7=94=9F=E6=88=90=E3=82=92=E5=85=B1=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/session_repository.go | 57 +++++++--------- api/externals/repository/user_repository.go | 67 ++++++++++--------- 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index 56ca326f6..dd95ce066 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -6,6 +6,7 @@ import ( "github.com/NUTFes/FinanSu/api/drivers/db" goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) type sessionRepository struct { @@ -28,14 +29,7 @@ func NewSessionRepository(client db.Client) SessionRepository { // 作成 func (r *sessionRepository) Create(c context.Context, authID string, userID string, accessToken string) error { - query, args, err := dialect.Insert("session"). - Prepared(true). - Rows(goqu.Record{ - "auth_id": authID, - "user_id": userID, - "access_token": accessToken, - }). - ToSQL() + query, args, err := createSessionQuery(authID, userID, accessToken) if err != nil { return err } @@ -48,14 +42,7 @@ func (r *sessionRepository) Create(c context.Context, authID string, userID stri } func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID string, userID string, accessToken string) error { - query, args, err := dialect.Insert("session"). - Prepared(true). - Rows(goqu.Record{ - "auth_id": authID, - "user_id": userID, - "access_token": accessToken, - }). - ToSQL() + query, args, err := createSessionQuery(authID, userID, accessToken) if err != nil { return err } @@ -70,10 +57,7 @@ func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID s // 削除 func (r *sessionRepository) Destroy(c context.Context, accessToken string) error { // access tokenで該当のsessionを削除 - query, args, err := dialect.Delete("session"). - Prepared(true). - Where(goqu.Ex{"access_token": accessToken}). - ToSQL() + query, args, err := deleteSessionQuery(goqu.Ex{"access_token": accessToken}) if err != nil { return err } @@ -100,10 +84,7 @@ func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessTo // user_idからsessionを削除する func (r *sessionRepository) DestroyByUserID(c context.Context, userID string) error { - query, args, err := dialect.Delete("session"). - Prepared(true). - Where(goqu.Ex{"user_id": userID}). - ToSQL() + query, args, err := deleteSessionQuery(goqu.Ex{"user_id": userID}) if err != nil { return err } @@ -116,10 +97,7 @@ func (r *sessionRepository) DestroyByUserID(c context.Context, userID string) er } func (r *sessionRepository) DestroyByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { - query, args, err := dialect.Delete("session"). - Prepared(true). - Where(goqu.Ex{"user_id": userID}). - ToSQL() + query, args, err := deleteSessionQuery(goqu.Ex{"user_id": userID}) if err != nil { return err } @@ -133,10 +111,7 @@ func (r *sessionRepository) DestroyByUserIDsWithTx(c context.Context, tx *sql.Tx return nil } - query, args, err := dialect.Delete("session"). - Prepared(true). - Where(goqu.I("user_id").In(userIDs)). - ToSQL() + query, args, err := deleteSessionQuery(goqu.I("user_id").In(userIDs)) if err != nil { return err } @@ -144,3 +119,21 @@ func (r *sessionRepository) DestroyByUserIDsWithTx(c context.Context, tx *sql.Tx _, err = tx.ExecContext(c, query, args...) return err } + +func createSessionQuery(authID string, userID string, accessToken string) (string, []any, error) { + return dialect.Insert("session"). + Prepared(true). + Rows(goqu.Record{ + "auth_id": authID, + "user_id": userID, + "access_token": accessToken, + }). + ToSQL() +} + +func deleteSessionQuery(where ...exp.Expression) (string, []any, error) { + return dialect.Delete("session"). + Prepared(true). + Where(where...). + ToSQL() +} diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index 9d0e80e5e..5773c0981 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -7,6 +7,7 @@ import ( "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) type userRepository struct { @@ -75,14 +76,7 @@ func (ur *userRepository) FindByIDs(c context.Context, ids []int) (*sql.Rows, er // 作成 func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) (int64, error) { - query, args, err := dialect.Insert("users"). - Prepared(true). - Rows(goqu.Record{ - "name": name, - "bureau_id": bureauID, - "role_id": roleID, - }). - ToSQL() + query, args, err := createUserQuery(name, bureauID, roleID) if err != nil { return 0, err } @@ -95,14 +89,7 @@ func (ur *userRepository) Create(c context.Context, name string, bureauID string } func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name string, bureauID string, roleID string) (int64, error) { - query, args, err := dialect.Insert("users"). - Prepared(true). - Rows(goqu.Record{ - "name": name, - "bureau_id": bureauID, - "role_id": roleID, - }). - ToSQL() + query, args, err := createUserQuery(name, bureauID, roleID) if err != nil { return 0, err } @@ -116,15 +103,14 @@ func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name strin // 編集 func (ur *userRepository) Update(c context.Context, id string, name string, bureauID string, roleID string) error { - query, args, err := dialect.Update("users"). - Prepared(true). - Set(goqu.Record{ + query, args, err := updateUserQuery( + goqu.Record{ "name": name, "bureau_id": bureauID, "role_id": roleID, - }). - Where(goqu.Ex{"id": id}). - ToSQL() + }, + goqu.Ex{"id": id}, + ) if err != nil { return err } @@ -134,11 +120,10 @@ func (ur *userRepository) Update(c context.Context, id string, name string, bure } func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string) error { - userQuery, userArgs, err := dialect.Update("users"). - Prepared(true). - Set(goqu.Record{"is_deleted": true}). - Where(goqu.Ex{"id": id}). - ToSQL() + userQuery, userArgs, err := updateUserQuery( + goqu.Record{"is_deleted": true}, + goqu.Ex{"id": id}, + ) if err != nil { return err } @@ -152,11 +137,10 @@ func (ur *userRepository) MultiDestroyWithTx(c context.Context, tx *sql.Tx, ids return nil } - userQuery, userArgs, err := dialect.Update("users"). - Prepared(true). - Set(goqu.Record{"is_deleted": true}). - Where(goqu.I("id").In(ids)). - ToSQL() + userQuery, userArgs, err := updateUserQuery( + goqu.Record{"is_deleted": true}, + goqu.I("id").In(ids), + ) if err != nil { return err } @@ -195,3 +179,22 @@ func (ur *userRepository) FindByEmail(c context.Context, email string) (*sql.Row return ur.client.DB().QueryRowContext(c, query, args...), nil } + +func createUserQuery(name string, bureauID string, roleID string) (string, []any, error) { + return dialect.Insert("users"). + Prepared(true). + Rows(goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }). + ToSQL() +} + +func updateUserQuery(record goqu.Record, where ...exp.Expression) (string, []any, error) { + return dialect.Update("users"). + Prepared(true). + Set(record). + Where(where...). + ToSQL() +} From c739eee9f6c8ec4c7a05fc27427ae5309a33ac79 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:53:57 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=B3=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/test/signup_transaction_test.go | 7 +++++++ api/test/user_delete_transaction_test.go | 2 ++ 2 files changed, 9 insertions(+) diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index 00ec7a6c0..c975f2117 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -38,6 +38,7 @@ func countRows(t *testing.T, query string, args ...any) int { return count } +// 正常系: サインアップ時に users、mail_auth、session が作成され、発行されたアクセストークンで current_user を取得できることを確認する func TestSignupCreatesUserMailAuthSessionAndCurrentUser(t *testing.T) { prepareTestDatabase(t) @@ -87,6 +88,7 @@ func TestSignupCreatesUserMailAuthSessionAndCurrentUser(t *testing.T) { assert.Equal(t, http.StatusOK, currentUserRes.StatusCode) } +// 異常系: mail_auth の作成に失敗した場合、同一トランザクション内で作成した users がロールバックされることを確認する func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { prepareTestDatabase(t) @@ -118,6 +120,7 @@ func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) } +// 異常系: 必須項目が不足しているリクエストでは BadRequest になり、users、mail_auth、session が作成されないことを確認する func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { prepareTestDatabase(t) @@ -145,6 +148,7 @@ func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) } +// 異常系: OpenAPI スキーマに違反する値では BadRequest になり、関連レコードが作成されないことを確認する func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { prepareTestDatabase(t) @@ -163,6 +167,7 @@ func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { values map[string]any }{ { + // name が空文字の場合にバリデーションエラーになることを確認する name: "empty name", email: "signup-empty-name@example.com", values: map[string]any{ @@ -174,6 +179,7 @@ func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { }, }, { + // bureau_id が 0 の場合にバリデーションエラーになることを確認する name: "zero bureau id", email: "signup-zero-bureau@example.com", values: map[string]any{ @@ -185,6 +191,7 @@ func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { }, }, { + // role_id が 0 の場合にバリデーションエラーになることを確認する name: "zero role id", email: "signup-zero-role@example.com", values: map[string]any{ diff --git a/api/test/user_delete_transaction_test.go b/api/test/user_delete_transaction_test.go index 1a274b62f..6379b7831 100644 --- a/api/test/user_delete_transaction_test.go +++ b/api/test/user_delete_transaction_test.go @@ -38,6 +38,7 @@ func nullableEmailByUserID(t *testing.T, userID int) sql.NullString { return email } +// 正常系: ユーザー単体削除時に users の論理削除、mail_auth のメール無効化、session 削除が同一トランザクションで実行されることを確認する func TestDestroyUserClearsMailAuthAndSessionInTransaction(t *testing.T) { prepareTestDatabase(t) @@ -66,6 +67,7 @@ func TestDestroyUserClearsMailAuthAndSessionInTransaction(t *testing.T) { assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session WHERE user_id = ?", userID)) } +// 正常系: ユーザー複数削除時に対象全員の users 論理削除、mail_auth のメール無効化、session 削除が同一トランザクションで実行されることを確認する func TestDestroyMultiUsersClearsMailAuthAndSessionInTransaction(t *testing.T) { prepareTestDatabase(t) From 7ff0ab61f87e2dc3c9ce0f068e774b3770eaae7e Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 00:32:12 +0900 Subject: [PATCH 22/22] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E4=BA=8B=E9=A0=85=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/externals/repository/mail_auth_repository.go | 3 ++- api/externals/repository/session_repository.go | 3 ++- api/externals/repository/user_repository.go | 3 ++- api/test/signup_transaction_test.go | 6 ++++-- view/next-project/e2e/tests/signup.spec.ts | 7 +++++-- .../next-project/src/components/auth/SignUpView.tsx | 13 ++++++------- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 1022782d0..48f1a8dc1 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -138,7 +138,8 @@ func createMailAuthQuery(email string, password string, userID string) (string, ToSQL() } -func updateMailAuthQuery(record goqu.Record, where ...exp.Expression) (string, []any, error) { +func updateMailAuthQuery(record goqu.Record, first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) return dialect.Update("mail_auth"). Prepared(true). Set(record). diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index dd95ce066..2ed472653 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -131,7 +131,8 @@ func createSessionQuery(authID string, userID string, accessToken string) (strin ToSQL() } -func deleteSessionQuery(where ...exp.Expression) (string, []any, error) { +func deleteSessionQuery(first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) return dialect.Delete("session"). Prepared(true). Where(where...). diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index 5773c0981..8a30134ba 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -191,7 +191,8 @@ func createUserQuery(name string, bureauID string, roleID string) (string, []any ToSQL() } -func updateUserQuery(record goqu.Record, where ...exp.Expression) (string, []any, error) { +func updateUserQuery(record goqu.Record, first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) return dialect.Update("users"). Prepared(true). Set(record). diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go index c975f2117..d0b4cafe3 100644 --- a/api/test/signup_transaction_test.go +++ b/api/test/signup_transaction_test.go @@ -135,6 +135,7 @@ func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { email := "signup-missing-fields@example.com" beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") + beforeSessions := countRows(t, "SELECT COUNT(*) FROM session") r := postSignup(t, testServer.URL, map[string]any{ "email": email, "password": "password123", @@ -145,7 +146,7 @@ func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { assert.Equal(t, http.StatusBadRequest, r.StatusCode) assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) - assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) + assert.Equal(t, beforeSessions, countRows(t, "SELECT COUNT(*) FROM session")) } // 異常系: OpenAPI スキーマに違反する値では BadRequest になり、関連レコードが作成されないことを確認する @@ -207,13 +208,14 @@ func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") + beforeSessions := countRows(t, "SELECT COUNT(*) FROM session") r := postSignup(t, testServer.URL, tt.values) defer r.Body.Close() assert.Equal(t, http.StatusBadRequest, r.StatusCode) assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", tt.email)) - assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session")) + assert.Equal(t, beforeSessions, countRows(t, "SELECT COUNT(*) FROM session")) }) } } diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts index cb0b00aa9..732c8090d 100644 --- a/view/next-project/e2e/tests/signup.spec.ts +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -8,8 +8,11 @@ async function waitForService(url: string) { while (Date.now() < deadline) { try { - await fetch(url); - return; + const res = await fetch(url); + if (res.ok) { + return; + } + lastError = new Error(`HTTP ${res.status}`); } catch (error) { lastError = error; } diff --git a/view/next-project/src/components/auth/SignUpView.tsx b/view/next-project/src/components/auth/SignUpView.tsx index ba95119c7..dbfceb088 100644 --- a/view/next-project/src/components/auth/SignUpView.tsx +++ b/view/next-project/src/components/auth/SignUpView.tsx @@ -16,8 +16,7 @@ export default function SignUpView() { // 新規登録中フラグ const [isSignUpNow, setIsSignUpNow] = useState(false); - const [postUserData, setPostUserData] = useState>({ - id: 0, + const [postUserData, setPostUserData] = useState>({ bureauID: 1, roleID: 1, }); @@ -33,7 +32,7 @@ export default function SignUpView() { const userDataHandler = (input: 'bureauID' | 'roleID') => (e: React.ChangeEvent) => { - setPostUserData({ ...postUserData, [input]: e.target.value }); + setPostUserData({ ...postUserData, [input]: Number(e.target.value) }); }; const postUser = async (data: SignUp) => { @@ -44,8 +43,8 @@ export default function SignUpView() { email: data.email, password: data.password, name: data.name, - bureau_id: Number(postUserData.bureauID), - role_id: Number(postUserData.roleID), + bureau_id: postUserData.bureauID, + role_id: postUserData.roleID, }); const res = req.data; @@ -53,8 +52,8 @@ export default function SignUpView() { const userData: User = { id: res.userID, name: data.name, - bureauID: Number(postUserData.bureauID), - roleID: Number(postUserData.roleID), + bureauID: postUserData.bureauID, + roleID: postUserData.roleID, }; // state用のauthのデータ const authData = {