LCOV - code coverage report
Current view: top level - usecase/auth - interactor_upsert_user_from_firebase.go Coverage Total Hit
Test: coverage.lcov Lines: 0.0 % 96 0
Test Date: 2026-04-14 06:42:22 Functions: - 0 0

            Line data    Source code
       1              : // Package auth は、認証ユースケース(Usecase)の実装を提供します。
       2              : // 本ファイルでは「Firebase IDトークン検証済みのユーザー情報を元に、
       3              : // users / auth_identities を upsert し、レスポンスDTOを返す」ユースケースを実装します。
       4              : package auth
       5              : 
       6              : import (
       7              :         "context"
       8              :         "fmt"
       9              : 
      10              :         "resume/internal/shared/apperr"
      11              :         "resume/internal/shared/util"
      12              : )
      13              : 
      14              : // UpsertUserFromFirebase は、VerifyIDToken 済みの入力(UID / プロバイダ配列 等)を受け取り、
      15              : // - users テーブルの作成 or 更新(LastLoginAt をサーバ時刻で更新)
      16              : // - auth_identities テーブルの upsert(provider, provider_user_id で一意)
      17              : // を **1トランザクション** で実行します。
      18              : // 成功時は、最新のユーザー情報(紐付くプロバイダ一覧を含む)とトークンを返します。
      19              : //
      20              : // エラーポリシー:
      21              : // - 入力不足(IDToken/UID 無し)は ErrInvalidInput を返す
      22              : // - リポジトリ層のDBエラーは mapInfraToUCError で UC語彙(ErrConflict/ErrNotFound 等)に正規化しつつ伝播
      23              : // - トランザクション内のいずれかで失敗したらその場で中断・ロールバック
      24            0 : func (u *interactor) UpsertUserFromFirebase(ctx context.Context, in UpsertUserFromFirebaseInput) (AuthResultOutput, error) {
      25            0 :         // --- 0) 入力チェック -------------------------------------------------------
      26            0 :         // ここでは必須だけを軽く確認(詳細バリデーションは上位レイヤで実施しても良い)。
      27            0 :         if in.IDToken == "" || in.UID == "" {
      28            0 :                 return AuthResultOutput{}, apperr.New(
      29            0 :                         apperr.CodeUnprocessable,
      30            0 :                         "validation failed",
      31            0 :                         map[string]any{"fields": map[string]string{
      32            0 :                                 "idToken": "required",
      33            0 :                                 "uid":     "required",
      34            0 :                         }},
      35            0 :                 )
      36            0 :         }
      37              : 
      38              :         // UCに注入された Clock を用いて現在時刻(UTC想定)を取得。
      39              :         // これを LastLoginAt / CreatedAt / UpdatedAt に用いる。
      40            0 :         now := u.clock.Now()
      41            0 : 
      42            0 :         // 以降で使う変数を先に宣言。
      43            0 :         var (
      44            0 :                 isNew   bool   // 今回作成された新規ユーザーかどうか
      45            0 :                 userEnt *User  // 処理の最終的な対象ユーザー(作成 or 更新後)
      46            0 :                 userID  uint64 // auth_identities 側のFKに合わせた int64 のID
      47            0 :         )
      48            0 : 
      49            0 :         // --- 1) トランザクション境界 ------------------------------------------------
      50            0 :         // TxRunner.Do は ctx に *gorm.db(tx) を差し込み、repo が FromCtxOrDB(...) で
      51            0 :         // その tx を取得して同一トランザクションに乗れるようにする。
      52            0 :         if err := u.tx.Do(ctx, func(txCtx context.Context) error {
      53            0 :                 // --- 2) users の upsert ------------------------------------------------
      54            0 :                 // UID は Firebase プロジェクト内で一意。まず既存のユーザーを探す。
      55            0 :                 found, err := u.users.FindByUID(txCtx, in.UID)
      56            0 :                 if err != nil {
      57            0 :                         // DBエラーはそのまま返し、Tx もロールバック。
      58            0 :                         return fmt.Errorf("find user by uid: %w", err)
      59            0 :                 }
      60              : 
      61            0 :                 if found == nil {
      62            0 :                         // 2-a) 見つからなければ新規作成
      63            0 :                         isNew = true
      64            0 :                         userEnt = &User{
      65            0 :                                 UID: in.UID,
      66            0 :                                 // util.Clone は *T をシャローコピー。nil 安全に上書きしたい時に使う。
      67            0 :                                 Email:         util.Clone(in.Email),
      68            0 :                                 EmailVerified: in.EmailVerified,
      69            0 :                                 DisplayName:   util.Clone(in.DisplayName),
      70            0 :                                 PhotoURL:      util.Clone(in.PhotoURL),
      71            0 :                                 LastLoginAt:   &now,
      72            0 :                                 CreatedAt:     now,
      73            0 :                                 UpdatedAt:     now,
      74            0 :                         }
      75            0 :                         newID, err := u.users.Create(txCtx, userEnt)
      76            0 :                         if err != nil {
      77            0 :                                 // DB一意制約などは repo 側で整形 → ここで UC 語彙に正規化
      78            0 :                                 return mapInfraToUCError(fmt.Errorf("create user: %w", err))
      79            0 :                         }
      80              :                         // domain/entity.User は ID が uint64 の想定。FK用に int64 も保持。
      81            0 :                         userEnt.ID = newID
      82            0 :                         userID = newID
      83            0 :                 } else {
      84            0 :                         // 2-b) 見つかったのでプロフィール同期+最終ログイン更新
      85            0 :                         found.Email = util.Clone(in.Email)
      86            0 :                         found.EmailVerified = in.EmailVerified
      87            0 :                         found.DisplayName = util.Clone(in.DisplayName)
      88            0 :                         found.PhotoURL = util.Clone(in.PhotoURL)
      89            0 :                         found.LastLoginAt = &now
      90            0 :                         found.UpdatedAt = now
      91            0 : 
      92            0 :                         // プロフィールのログイン時更新はリポジトリ側でカラム限定 Updates。
      93            0 :                         if err := u.users.UpdateProfileOnLogin(txCtx, found); err != nil {
      94            0 :                                 return mapInfraToUCError(fmt.Errorf("update user on login: %w", err))
      95            0 :                         }
      96            0 :                         userEnt = found
      97            0 :                         userID = found.ID
      98              :                 }
      99              : 
     100              :                 // --- 3) auth_identities の upsert -------------------------------------
     101              :                 // Providers は 0..n を許容。password/anonymous なども来うる。
     102              :                 // provider, provider_user_id の組で一意にし、ON CONFLICT DO UPDATE の想定。
     103            0 :                 for _, p := range in.Providers {
     104            0 :                         ident := &Identity{
     105            0 :                                 UserID:              userID,
     106            0 :                                 Provider:            p.Provider,
     107            0 :                                 ProviderUserID:      p.ProviderUserID,
     108            0 :                                 ProviderDisplayName: p.ProviderDisplayName, // NOT NULL 前提。ハンドラでFallback済みを想定。
     109            0 :                                 EmailAtSignup:       util.Clone(p.EmailAtSignup),
     110            0 :                                 CreatedAt:           now, // 新規時採用。既存更新時は repo 実装が保持。
     111            0 :                         }
     112            0 :                         if _, err := u.idents.Upsert(txCtx, ident); err != nil {
     113            0 :                                 // (provider, provider_user_id) の重複やFK不整合などをUC語彙へマップ
     114            0 :                                 return mapInfraToUCError(fmt.Errorf("upsert identity (%s/%s): %w", p.Provider, p.ProviderUserID, err))
     115            0 :                         }
     116              :                 }
     117            0 :                 return nil
     118            0 :         }); err != nil {
     119            0 :                 // Tx 内で発生したエラーはここに伝播。HTTPハンドラは標準のエラーフォーマットで返す。
     120            0 :                 return AuthResultOutput{}, err
     121            0 :         }
     122              : 
     123              :         // --- 4) 返却用のプロバイダ一覧を再読込 --------------------------------------
     124              :         // Tx外で読み出してよい(副作用なし)。一覧を DTO に詰め替えて返す。
     125            0 :         idents, err := u.idents.ListByUserID(ctx, userID)
     126            0 :         if err != nil {
     127            0 :                 return AuthResultOutput{}, mapInfraToUCError(fmt.Errorf("list providers: %w", err))
     128            0 :         }
     129              : 
     130              :         // --- 5) 出力DTOを返却 ------------------------------------------------------
     131              :         // Token は今回は受け取った IDToken をそのまま返す想定。
     132            0 :         return AuthResultOutput{
     133            0 :                 Token:     in.IDToken,
     134            0 :                 User:      toUserOutput(userEnt, idents),
     135            0 :                 IsNewUser: isNew,
     136            0 :         }, nil
     137              : }
        

Generated by: LCOV version 2.3.1-1