feat: implement Iteration 0 foundation (backend + Flutter client)
Backend (Go): - Project structure with chi router, pgxpool, goose migrations - JWT auth (access/refresh tokens) with Firebase token verification - NoopTokenVerifier for local dev without Firebase credentials - PostgreSQL user repository with atomic profile updates (transactions) - Mifflin-St Jeor calorie calculation based on profile data - REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health - Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id - Unit tests (51 passing) and integration tests (testcontainers) - Docker Compose setup with postgres healthcheck and graceful shutdown Flutter client: - Riverpod state management with GoRouter navigation - Firebase Auth (email/password + Google sign-in with web popup support) - Platform-aware API URLs (web/Android/iOS) - Dio HTTP client with JWT auth interceptor and concurrent refresh handling - Secure token storage - Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile) - Unit tests (17 passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
213
backend/internal/auth/service_test.go
Normal file
213
backend/internal/auth/service_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/food-ai/backend/internal/auth/mocks"
|
||||
"github.com/food-ai/backend/internal/user"
|
||||
umocks "github.com/food-ai/backend/internal/user/mocks"
|
||||
)
|
||||
|
||||
func newTestService(verifier *mocks.MockTokenVerifier, repo *umocks.MockUserRepository) *Service {
|
||||
jm := NewJWTManager("test-secret", 15*time.Minute, 720*time.Hour)
|
||||
return NewService(verifier, repo, jm)
|
||||
}
|
||||
|
||||
func TestLogin_Success(t *testing.T) {
|
||||
verifier := &mocks.MockTokenVerifier{
|
||||
VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) {
|
||||
return "fb-uid", "test@example.com", "Test User", "https://avatar.url", nil
|
||||
},
|
||||
}
|
||||
repo := &umocks.MockUserRepository{
|
||||
UpsertByFirebaseUIDFn: func(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) {
|
||||
return &user.User{ID: "user-1", Email: email, Name: name, Plan: "free"}, nil
|
||||
},
|
||||
SetRefreshTokenFn: func(ctx context.Context, id, token string, expiresAt time.Time) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
resp, err := svc.Login(context.Background(), "firebase-token")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.AccessToken == "" {
|
||||
t.Error("expected non-empty access token")
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
t.Error("expected non-empty refresh token")
|
||||
}
|
||||
if resp.User.ID != "user-1" {
|
||||
t.Errorf("expected user ID 'user-1', got %q", resp.User.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_InvalidFirebaseToken(t *testing.T) {
|
||||
verifier := &mocks.MockTokenVerifier{
|
||||
VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) {
|
||||
return "", "", "", "", fmt.Errorf("invalid token")
|
||||
},
|
||||
}
|
||||
repo := &umocks.MockUserRepository{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
_, err := svc.Login(context.Background(), "bad-token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid firebase token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_UpsertError(t *testing.T) {
|
||||
verifier := &mocks.MockTokenVerifier{
|
||||
VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) {
|
||||
return "fb-uid", "test@example.com", "Test", "", nil
|
||||
},
|
||||
}
|
||||
repo := &umocks.MockUserRepository{
|
||||
UpsertByFirebaseUIDFn: func(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) {
|
||||
return nil, fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
_, err := svc.Login(context.Background(), "token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for upsert failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_SetRefreshTokenError(t *testing.T) {
|
||||
verifier := &mocks.MockTokenVerifier{
|
||||
VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) {
|
||||
return "fb-uid", "test@example.com", "Test", "", nil
|
||||
},
|
||||
}
|
||||
repo := &umocks.MockUserRepository{
|
||||
UpsertByFirebaseUIDFn: func(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) {
|
||||
return &user.User{ID: "user-1", Plan: "free"}, nil
|
||||
},
|
||||
SetRefreshTokenFn: func(ctx context.Context, id, token string, expiresAt time.Time) error {
|
||||
return fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
_, err := svc.Login(context.Background(), "token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for set refresh token failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh_Success(t *testing.T) {
|
||||
repo := &umocks.MockUserRepository{
|
||||
FindByRefreshTokenFn: func(ctx context.Context, token string) (*user.User, error) {
|
||||
return &user.User{ID: "user-1", Plan: "free"}, nil
|
||||
},
|
||||
SetRefreshTokenFn: func(ctx context.Context, id, token string, expiresAt time.Time) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
verifier := &mocks.MockTokenVerifier{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
resp, err := svc.Refresh(context.Background(), "valid-refresh-token")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.AccessToken == "" {
|
||||
t.Error("expected non-empty access token")
|
||||
}
|
||||
if resp.RefreshToken == "" {
|
||||
t.Error("expected non-empty refresh token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh_InvalidToken(t *testing.T) {
|
||||
repo := &umocks.MockUserRepository{
|
||||
FindByRefreshTokenFn: func(ctx context.Context, token string) (*user.User, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
}
|
||||
verifier := &mocks.MockTokenVerifier{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
_, err := svc.Refresh(context.Background(), "bad-token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid refresh token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh_SetRefreshTokenError(t *testing.T) {
|
||||
repo := &umocks.MockUserRepository{
|
||||
FindByRefreshTokenFn: func(ctx context.Context, token string) (*user.User, error) {
|
||||
return &user.User{ID: "user-1", Plan: "free"}, nil
|
||||
},
|
||||
SetRefreshTokenFn: func(ctx context.Context, id, token string, expiresAt time.Time) error {
|
||||
return fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
verifier := &mocks.MockTokenVerifier{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
_, err := svc.Refresh(context.Background(), "valid-token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout_Success(t *testing.T) {
|
||||
repo := &umocks.MockUserRepository{
|
||||
ClearRefreshTokenFn: func(ctx context.Context, id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
verifier := &mocks.MockTokenVerifier{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
err := svc.Logout(context.Background(), "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout_Error(t *testing.T) {
|
||||
repo := &umocks.MockUserRepository{
|
||||
ClearRefreshTokenFn: func(ctx context.Context, id string) error {
|
||||
return fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
verifier := &mocks.MockTokenVerifier{}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
err := svc.Logout(context.Background(), "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin_ExpiresIn(t *testing.T) {
|
||||
verifier := &mocks.MockTokenVerifier{
|
||||
VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) {
|
||||
return "fb-uid", "test@example.com", "Test", "", nil
|
||||
},
|
||||
}
|
||||
repo := &umocks.MockUserRepository{
|
||||
UpsertByFirebaseUIDFn: func(ctx context.Context, uid, email, name, avatarURL string) (*user.User, error) {
|
||||
return &user.User{ID: "user-1", Plan: "free"}, nil
|
||||
},
|
||||
SetRefreshTokenFn: func(ctx context.Context, id, token string, expiresAt time.Time) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := newTestService(verifier, repo)
|
||||
resp, _ := svc.Login(context.Background(), "token")
|
||||
if resp.ExpiresIn != 900 {
|
||||
t.Errorf("expected expires_in 900, got %d", resp.ExpiresIn)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user