diff --git a/cli/promote_test.go b/cli/promote_test.go index 2e1c32a..02daf18 100644 --- a/cli/promote_test.go +++ b/cli/promote_test.go @@ -8,6 +8,7 @@ import ( "github.com/database-playground/backend-v2/ent/group" "github.com/database-playground/backend-v2/ent/user" "github.com/database-playground/backend-v2/internal/testhelper" + "github.com/database-playground/backend-v2/internal/useraccount" _ "github.com/mattn/go-sqlite3" ) @@ -24,17 +25,17 @@ func TestPromoteAdmin(t *testing.T) { t.Fatalf("Setup failed: %v", err) } - // Get the new-user group to assign to the user initially - newUserGroup, err := entClient.Group.Query().Where(group.Name("new-user")).Only(ctx) + // Get the student group to assign to the user initially + studentGroup, err := entClient.Group.Query().Where(group.Name(useraccount.StudentGroupSlug)).Only(ctx) if err != nil { - t.Fatalf("Failed to get new-user group: %v", err) + t.Fatalf("Failed to get student group: %v", err) } // Create a test user testUser, err := entClient.User.Create(). SetEmail("test@example.com"). SetName("Test User"). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(ctx) if err != nil { t.Fatalf("Failed to create test user: %v", err) @@ -148,17 +149,17 @@ func TestPromoteAdmin(t *testing.T) { t.Fatalf("Setup failed: %v", err) } - // Get the new-user group to assign to the user initially - newUserGroup, err := entClient.Group.Query().Where(group.Name("new-user")).Only(ctx) + // Get the student group to assign to the user initially + studentGroup, err := entClient.Group.Query().Where(group.Name(useraccount.StudentGroupSlug)).Only(ctx) if err != nil { - t.Fatalf("Failed to get new-user group: %v", err) + t.Fatalf("Failed to get student group: %v", err) } // Create a test user _, err = entClient.User.Create(). SetEmail("test@example.com"). SetName("Test User"). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(ctx) if err != nil { t.Fatalf("Failed to create test user: %v", err) @@ -188,17 +189,17 @@ func TestPromoteAdmin(t *testing.T) { t.Fatalf("Setup failed: %v", err) } - // Get the new-user group to assign to users initially - newUserGroup, err := entClient.Group.Query().Where(group.Name("new-user")).Only(ctx) + // Get the student group to assign to users initially + studentGroup, err := entClient.Group.Query().Where(group.Name(useraccount.StudentGroupSlug)).Only(ctx) if err != nil { - t.Fatalf("Failed to get new-user group: %v", err) + t.Fatalf("Failed to get student group: %v", err) } // Create multiple test users user1, err := entClient.User.Create(). SetEmail("user1@example.com"). SetName("User 1"). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(ctx) if err != nil { t.Fatalf("Failed to create user1: %v", err) @@ -207,7 +208,7 @@ func TestPromoteAdmin(t *testing.T) { user2, err := entClient.User.Create(). SetEmail("user2@example.com"). SetName("User 2"). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(ctx) if err != nil { t.Fatalf("Failed to create user2: %v", err) @@ -271,17 +272,17 @@ func TestPromoteAdmin(t *testing.T) { t.Fatalf("Setup failed: %v", err) } - // Get the new-user group to assign to the user initially - newUserGroup, err := entClient.Group.Query().Where(group.Name("new-user")).Only(ctx) + // Get the student group to assign to the user initially + studentGroup, err := entClient.Group.Query().Where(group.Name(useraccount.StudentGroupSlug)).Only(ctx) if err != nil { - t.Fatalf("Failed to get new-user group: %v", err) + t.Fatalf("Failed to get student group: %v", err) } // Create a test user with lowercase email _, err = entClient.User.Create(). SetEmail("test@example.com"). SetName("Test User"). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(ctx) if err != nil { t.Fatalf("Failed to create test user: %v", err) diff --git a/cli/setup_test.go b/cli/setup_test.go deleted file mode 100644 index ed47e58..0000000 --- a/cli/setup_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package cli_test - -import ( - "context" - "testing" - - "github.com/database-playground/backend-v2/cli" - "github.com/database-playground/backend-v2/ent/group" - "github.com/database-playground/backend-v2/ent/scopeset" - "github.com/database-playground/backend-v2/internal/testhelper" - "github.com/database-playground/backend-v2/internal/useraccount" - - _ "github.com/mattn/go-sqlite3" -) - -func TestSetup(t *testing.T) { - t.Run("should create all required entities on first run", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - ctx := context.Background() - cliCtx := cli.NewContext(entClient) - - // Run setup - result, err := cliCtx.Setup(ctx) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify admin scope set was created - if result.AdminScopeSet == nil { - t.Fatal("AdminScopeSet should not be nil") - } - if result.AdminScopeSet.Slug != useraccount.AdminScopeSetSlug { - t.Errorf("Expected admin scope set slug to be 'admin', got %s", result.AdminScopeSet.Slug) - } - if result.AdminScopeSet.Description != "Administrator" { - t.Errorf("Expected admin scope set description to be 'Administrator', got %s", result.AdminScopeSet.Description) - } - if len(result.AdminScopeSet.Scopes) != 1 || result.AdminScopeSet.Scopes[0] != "*" { - t.Errorf("Expected admin scope set scopes to be ['*'], got %v", result.AdminScopeSet.Scopes) - } - - // Verify new-user scope set was created - if result.NewUserScopeSet == nil { - t.Fatal("NewUserScopeSet should not be nil") - } - if result.NewUserScopeSet.Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user scope set slug to be 'new-user', got %s", result.NewUserScopeSet.Slug) - } - if result.NewUserScopeSet.Description != "New users can only read and write their own data." { - t.Errorf("Expected new-user scope set description to be 'New users can only read and write their own data.', got %s", result.NewUserScopeSet.Description) - } - if len(result.NewUserScopeSet.Scopes) != 1 || result.NewUserScopeSet.Scopes[0] != "me:*" { - t.Errorf("Expected new-user scope set scopes to be ['me:*'], got %v", result.NewUserScopeSet.Scopes) - } - - // Verify unverified scope set was created - if result.UnverifiedScopeSet == nil { - t.Fatal("UnverifiedScopeSet should not be nil") - } - if result.UnverifiedScopeSet.Slug != useraccount.UnverifiedScopeSetSlug { - t.Errorf("Expected unverified scope set slug to be 'unverified', got %s", result.UnverifiedScopeSet.Slug) - } - if result.UnverifiedScopeSet.Description != "Unverified users can only verify their account and read their own initial data." { - t.Errorf("Expected unverified scope set description to be 'Unverified users can only verify their account and read their own initial data.', got %s", result.UnverifiedScopeSet.Description) - } - if len(result.UnverifiedScopeSet.Scopes) != 2 || result.UnverifiedScopeSet.Scopes[0] != "verification:*" || result.UnverifiedScopeSet.Scopes[1] != "me:read" { - t.Errorf("Expected unverified scope set scopes to be ['verification:*', 'me:read'], got %v", result.UnverifiedScopeSet.Scopes) - } - - // Verify admin group was created - if result.AdminGroup == nil { - t.Fatal("AdminGroup should not be nil") - } - if result.AdminGroup.Name != useraccount.AdminGroupSlug { - t.Errorf("Expected admin group name to be 'admin', got %s", result.AdminGroup.Name) - } - if result.AdminGroup.Description != "Administrator" { - t.Errorf("Expected admin group description to be 'Administrator', got %s", result.AdminGroup.Description) - } - - // Verify new-user group was created - if result.NewUserGroup == nil { - t.Fatal("NewUserGroup should not be nil") - } - if result.NewUserGroup.Name != useraccount.NewUserGroupSlug { - t.Errorf("Expected new-user group name to be 'new-user', got %s", result.NewUserGroup.Name) - } - if result.NewUserGroup.Description != "New users that is not yet verified." { - t.Errorf("Expected new-user group description to be 'New users that is not yet verified.', got %s", result.NewUserGroup.Description) - } - - // Verify unverified group was created - if result.UnverifiedGroup == nil { - t.Fatal("UnverifiedGroup should not be nil") - } - if result.UnverifiedGroup.Name != useraccount.UnverifiedGroupSlug { - t.Errorf("Expected unverified group name to be 'unverified', got %s", result.UnverifiedGroup.Name) - } - if result.UnverifiedGroup.Description != "Unverified users that is not yet verified." { - t.Errorf("Expected unverified group description to be 'Unverified users that is not yet verified.', got %s", result.UnverifiedGroup.Description) - } - - // Verify the groups are linked to the correct scope sets - adminGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.AdminGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query admin group with scope sets: %v", err) - } - if len(adminGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected admin group to have 1 scope set, got %d", len(adminGroupWithScopes.Edges.ScopeSets)) - } - if adminGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.AdminScopeSetSlug { - t.Errorf("Expected admin group to be linked to admin scope set, got %s", adminGroupWithScopes.Edges.ScopeSets[0].Slug) - } - - newUserGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.NewUserGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query new-user group with scope sets: %v", err) - } - if len(newUserGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected new-user group to have 1 scope set, got %d", len(newUserGroupWithScopes.Edges.ScopeSets)) - } - if newUserGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user group to be linked to new-user scope set, got %s", newUserGroupWithScopes.Edges.ScopeSets[0].Slug) - } - - unverifiedGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.UnverifiedGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query unverified group with scope sets: %v", err) - } - if len(unverifiedGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected unverified group to have 1 scope set, got %d", len(unverifiedGroupWithScopes.Edges.ScopeSets)) - } - if unverifiedGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.UnverifiedScopeSetSlug { - t.Errorf("Expected unverified group to be linked to unverified scope set, got %s", unverifiedGroupWithScopes.Edges.ScopeSets[0].Slug) - } - }) - - t.Run("should be idempotent - second run should not create duplicates", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - ctx := context.Background() - cliCtx := cli.NewContext(entClient) - - // Run setup first time - result1, err := cliCtx.Setup(ctx) - if err != nil { - t.Fatalf("First setup failed: %v", err) - } - - // Run setup second time - result2, err := cliCtx.Setup(ctx) - if err != nil { - t.Fatalf("Second setup failed: %v", err) - } - - // Verify that the same entities are returned (same IDs) - if result1.AdminScopeSet.ID != result2.AdminScopeSet.ID { - t.Errorf("Admin scope set IDs should be the same, got %d and %d", result1.AdminScopeSet.ID, result2.AdminScopeSet.ID) - } - if result1.NewUserScopeSet.ID != result2.NewUserScopeSet.ID { - t.Errorf("New-user scope set IDs should be the same, got %d and %d", result1.NewUserScopeSet.ID, result2.NewUserScopeSet.ID) - } - if result1.UnverifiedScopeSet.ID != result2.UnverifiedScopeSet.ID { - t.Errorf("Unverified scope set IDs should be the same, got %d and %d", result1.UnverifiedScopeSet.ID, result2.UnverifiedScopeSet.ID) - } - if result1.AdminGroup.ID != result2.AdminGroup.ID { - t.Errorf("Admin group IDs should be the same, got %d and %d", result1.AdminGroup.ID, result2.AdminGroup.ID) - } - if result1.NewUserGroup.ID != result2.NewUserGroup.ID { - t.Errorf("New-user group IDs should be the same, got %d and %d", result1.NewUserGroup.ID, result2.NewUserGroup.ID) - } - if result1.UnverifiedGroup.ID != result2.UnverifiedGroup.ID { - t.Errorf("Unverified group IDs should be the same, got %d and %d", result1.UnverifiedGroup.ID, result2.UnverifiedGroup.ID) - } - - // Verify that only one of each entity exists in the database - adminScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.AdminScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query admin scope sets: %v", err) - } - if len(adminScopeSets) != 1 { - t.Errorf("Expected exactly 1 admin scope set, got %d", len(adminScopeSets)) - } - - newUserScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.NewUserScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query new-user scope sets: %v", err) - } - if len(newUserScopeSets) != 1 { - t.Errorf("Expected exactly 1 new-user scope set, got %d", len(newUserScopeSets)) - } - - unverifiedScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.UnverifiedScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query unverified scope sets: %v", err) - } - if len(unverifiedScopeSets) != 1 { - t.Errorf("Expected exactly 1 unverified scope set, got %d", len(unverifiedScopeSets)) - } - - adminGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.AdminGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query admin groups: %v", err) - } - if len(adminGroups) != 1 { - t.Errorf("Expected exactly 1 admin group, got %d", len(adminGroups)) - } - - newUserGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.NewUserGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query new-user groups: %v", err) - } - if len(newUserGroups) != 1 { - t.Errorf("Expected exactly 1 new-user group, got %d", len(newUserGroups)) - } - - unverifiedGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.UnverifiedGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query unverified groups: %v", err) - } - if len(unverifiedGroups) != 1 { - t.Errorf("Expected exactly 1 unverified group, got %d", len(unverifiedGroups)) - } - }) - - t.Run("should handle partial existing data", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - ctx := context.Background() - cliCtx := cli.NewContext(entClient) - - // Create admin scope set manually before running setup - existingAdminScopeSet, err := entClient.ScopeSet.Create(). - SetSlug(useraccount.AdminScopeSetSlug). - SetDescription("Administrator"). - SetScopes([]string{"*"}). - Save(ctx) - if err != nil { - t.Fatalf("Failed to create existing admin scope set: %v", err) - } - - // Create unverified scope set manually before running setup - existingUnverifiedScopeSet, err := entClient.ScopeSet.Create(). - SetSlug(useraccount.UnverifiedScopeSetSlug). - SetDescription("Unverified users can only verify their account and read their own initial data."). - SetScopes([]string{"verification:*", "me:read"}). - Save(ctx) - if err != nil { - t.Fatalf("Failed to create existing unverified scope set: %v", err) - } - - // Run setup - result, err := cliCtx.Setup(ctx) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify that the existing admin scope set is returned - if result.AdminScopeSet.ID != existingAdminScopeSet.ID { - t.Errorf("Expected to return existing admin scope set with ID %d, got %d", existingAdminScopeSet.ID, result.AdminScopeSet.ID) - } - - // Verify that the existing unverified scope set is returned - if result.UnverifiedScopeSet.ID != existingUnverifiedScopeSet.ID { - t.Errorf("Expected to return existing unverified scope set with ID %d, got %d", existingUnverifiedScopeSet.ID, result.UnverifiedScopeSet.ID) - } - - // Verify that new-user scope set was created - if result.NewUserScopeSet == nil { - t.Fatal("NewUserScopeSet should not be nil") - } - if result.NewUserScopeSet.Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user scope set slug to be 'new-user', got %s", result.NewUserScopeSet.Slug) - } - - // Verify that all groups were created - if result.AdminGroup == nil { - t.Fatal("AdminGroup should not be nil") - } - if result.NewUserGroup == nil { - t.Fatal("NewUserGroup should not be nil") - } - if result.UnverifiedGroup == nil { - t.Fatal("UnverifiedGroup should not be nil") - } - }) -} - -func TestSetupResult(t *testing.T) { - t.Run("SetupResult should have all required fields", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - ctx := context.Background() - cliCtx := cli.NewContext(entClient) - - result, err := cliCtx.Setup(ctx) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify all fields are populated - if result.AdminScopeSet == nil { - t.Error("AdminScopeSet should not be nil") - } - if result.NewUserScopeSet == nil { - t.Error("NewUserScopeSet should not be nil") - } - if result.UnverifiedScopeSet == nil { - t.Error("UnverifiedScopeSet should not be nil") - } - if result.AdminGroup == nil { - t.Error("AdminGroup should not be nil") - } - if result.NewUserGroup == nil { - t.Error("NewUserGroup should not be nil") - } - if result.UnverifiedGroup == nil { - t.Error("UnverifiedGroup should not be nil") - } - }) -} diff --git a/cmd/admin-cli/commands.go b/cmd/admin-cli/commands.go index 3784d08..ef4efbf 100644 --- a/cmd/admin-cli/commands.go +++ b/cmd/admin-cli/commands.go @@ -38,12 +38,7 @@ func newSetupCommand(clictx *dpcli.Context) *cli.Command { fmt.Println("✅ Setup complete!") fmt.Println() - fmt.Printf("We have created a default administrator scope set (%s) with all scopes accessible,\n", result.AdminScopeSet.Slug) - fmt.Printf("a default new-user scope set (%s) with the minimal scope set,\n", result.NewUserScopeSet.Slug) - fmt.Printf("and a default unverified scope set (%s) with the minimal scope set.\n", result.UnverifiedScopeSet.Slug) - fmt.Printf("Besides, we also created a default administrator group (%s, ID #%d) with the admin scope set,\n", result.AdminGroup.Name, result.AdminGroup.ID) - fmt.Printf("a default new-user group (%s, ID #%d) with the minimal scope set,\n", result.NewUserGroup.Name, result.NewUserGroup.ID) - fmt.Printf("and a default unverified group (%s, ID #%d) with the minimal scope set.\n", result.UnverifiedGroup.Name, result.UnverifiedGroup.ID) + fmt.Printf("%+v\n", result) fmt.Println() fmt.Println("You can then use the following commands to complete the setup:") fmt.Println(" - \"promote-admin\" to promote a user to an administrator account after registration.") diff --git a/docs/scope.md b/docs/scope.md index d3d27d8..f60d026 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -9,6 +9,7 @@ - `scopeset`:範圍集合操作 - `database`:題庫對應資料庫的操作 - `question`:題庫操作 + - `answer`:解答(只有 `read` 動作,`answer:write` 被 `question:write` 涵蓋) - `submission`:提交紀錄操作(做題) ## 動作 @@ -20,5 +21,4 @@ - `user:impersonate`:給定任意使用者的 ID,允許假冒其身分操作。 - `me:delete`:刪除自己的帳號。 -- `verification:write`:執行帳號認證。 - `ai`:使用 AI 的權限(目前在前端判斷)。 diff --git a/graph/ent.resolvers.go b/graph/ent.resolvers.go index afbe3ef..9be0531 100644 --- a/graph/ent.resolvers.go +++ b/graph/ent.resolvers.go @@ -94,9 +94,7 @@ func (r *Resolver) Question() QuestionResolver { return &questionResolver{r} } // User returns UserResolver implementation. func (r *Resolver) User() UserResolver { return &userResolver{r} } -type ( - databaseResolver struct{ *Resolver } - queryResolver struct{ *Resolver } - questionResolver struct{ *Resolver } - userResolver struct{ *Resolver } -) +type databaseResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } +type questionResolver struct{ *Resolver } +type userResolver struct{ *Resolver } diff --git a/graph/question.graphqls b/graph/question.graphqls index 0d9865b..5e7f48f 100644 --- a/graph/question.graphqls +++ b/graph/question.graphqls @@ -61,7 +61,7 @@ extend type Mutation { } extend type Question { - referenceAnswerResult: SQLExecutionResult! @scope(scope: "question:read") + referenceAnswerResult: SQLExecutionResult! """ List of your submissions for this question, ordered by submitted at descending. diff --git a/graph/user.graphqls b/graph/user.graphqls index d83d54c..96f6a06 100644 --- a/graph/user.graphqls +++ b/graph/user.graphqls @@ -105,9 +105,4 @@ extend type Mutation { Logout from all the devices of the current user. """ logoutAll: Boolean! @scope(scope: "me:write") - - """ - Verify the registration of this user. - """ - verifyRegistration: Boolean! @scope(scope: "verification:write") } diff --git a/graph/user.resolvers.go b/graph/user.resolvers.go index 00c32ec..5b460c2 100644 --- a/graph/user.resolvers.go +++ b/graph/user.resolvers.go @@ -209,26 +209,6 @@ func (r *mutationResolver) LogoutAll(ctx context.Context) (bool, error) { return r.LogoutUser(ctx, user.UserID) } -// VerifyRegistration is the resolver for the verifyRegistration field. -func (r *mutationResolver) VerifyRegistration(ctx context.Context) (bool, error) { - tokenInfo, ok := auth.GetUser(ctx) - if !ok { - // this should never happen since we have set proper scope - return false, defs.ErrUnauthorized - } - - err := r.useraccount.Verify(ctx, tokenInfo.UserID) - if err != nil { - if errors.Is(err, useraccount.ErrUserVerified) { - return false, defs.ErrVerified - } - - return false, err - } - - return true, nil -} - // Me is the resolver for the me field. func (r *queryResolver) Me(ctx context.Context) (*ent.User, error) { tokenInfo, ok := auth.GetUser(ctx) diff --git a/graph/user.resolvers_test.go b/graph/user.resolvers_test.go index d21b150..162bf86 100644 --- a/graph/user.resolvers_test.go +++ b/graph/user.resolvers_test.go @@ -10,12 +10,10 @@ import ( "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/database-playground/backend-v2/ent" - "github.com/database-playground/backend-v2/ent/group" "github.com/database-playground/backend-v2/graph/defs" "github.com/database-playground/backend-v2/graph/directive" "github.com/database-playground/backend-v2/internal/auth" "github.com/database-playground/backend-v2/internal/events" - "github.com/database-playground/backend-v2/internal/setup" "github.com/database-playground/backend-v2/internal/submission" "github.com/database-playground/backend-v2/internal/testhelper" "github.com/database-playground/backend-v2/internal/useraccount" @@ -1191,202 +1189,6 @@ func TestMutationResolver_UpdateMe(t *testing.T) { }) } -func TestMutationResolver_VerifyRegistration(t *testing.T) { - t.Run("success", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - // Setup database with proper groups and scopes - _, err := setup.Setup(context.Background(), entClient) - require.NoError(t, err) - - // Get the unverified group - unverifiedGroup, err := entClient.Group.Query().Where(group.NameEQ(useraccount.UnverifiedGroupSlug)).Only(context.Background()) - require.NoError(t, err) - - // Create an unverified user - user, err := entClient.User.Create(). - SetName("testuser"). - SetEmail("test@example.com"). - SetGroup(unverifiedGroup). - Save(context.Background()) - require.NoError(t, err) - - resolver := NewTestResolver(t, entClient, &mockAuthStorage{}) - - // Create test server with scope directive - cfg := Config{ - Resolvers: resolver, - Directives: DirectiveRoot{Scope: directive.ScopeDirective}, - } - srv := handler.New(NewExecutableSchema(cfg)) - srv.AddTransport(transport.POST{}) - c := client.New(srv) - - // Execute mutation - var resp struct { - VerifyRegistration bool - } - err = c.Post(`mutation { verifyRegistration }`, &resp, func(bd *client.Request) { - bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{ - UserID: user.ID, - Scopes: []string{"verification:write"}, - })) - }) - - // Verify response - require.NoError(t, err) - require.True(t, resp.VerifyRegistration) - - // Verify user was actually verified (moved to new-user group) - updatedUser, err := entClient.User.Get(context.Background(), user.ID) - require.NoError(t, err) - - updatedGroup, err := updatedUser.QueryGroup().Only(context.Background()) - require.NoError(t, err) - require.Equal(t, useraccount.NewUserGroupSlug, updatedGroup.Name) - }) - - t.Run("unauthenticated", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - resolver := NewTestResolver(t, entClient, &mockAuthStorage{}) - - // Create test server with scope directive - cfg := Config{ - Resolvers: resolver, - Directives: DirectiveRoot{Scope: directive.ScopeDirective}, - } - srv := handler.New(NewExecutableSchema(cfg)) - srv.AddTransport(transport.POST{}) - c := client.New(srv) - - // Execute mutation with no auth context - var resp struct { - VerifyRegistration bool - } - err := c.Post(`mutation { verifyRegistration }`, &resp) - - // Verify error - require.Error(t, err) - require.Contains(t, err.Error(), defs.CodeUnauthorized) - }) - - t.Run("insufficient scope", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - resolver := NewTestResolver(t, entClient, &mockAuthStorage{}) - - // Create test server with scope directive - cfg := Config{ - Resolvers: resolver, - Directives: DirectiveRoot{Scope: directive.ScopeDirective}, - } - srv := handler.New(NewExecutableSchema(cfg)) - srv.AddTransport(transport.POST{}) - c := client.New(srv) - - // Create context with authenticated user but wrong scope - ctx := auth.WithUser(context.Background(), auth.TokenInfo{ - UserID: 1, - Scopes: []string{"user:read"}, - }) - - // Execute mutation - var resp struct { - VerifyRegistration bool - } - err := c.Post(`mutation { verifyRegistration }`, &resp, func(bd *client.Request) { - bd.HTTP = bd.HTTP.WithContext(ctx) - }) - - // Verify error - require.Error(t, err) - require.Contains(t, err.Error(), defs.NewErrNoSufficientScope("verification:write").Error()) - }) - - t.Run("user already verified", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - // Setup database with proper groups and scopes - _, err := setup.Setup(context.Background(), entClient) - require.NoError(t, err) - - // Get the new-user group (verified users) - newUserGroup, err := entClient.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context.Background()) - require.NoError(t, err) - - // Create a verified user (in new-user group) - verifiedUser, err := entClient.User.Create(). - SetName("verifieduser"). - SetEmail("verified@example.com"). - SetGroup(newUserGroup). - Save(context.Background()) - require.NoError(t, err) - - resolver := NewTestResolver(t, entClient, &mockAuthStorage{}) - - // Create test server with scope directive - cfg := Config{ - Resolvers: resolver, - Directives: DirectiveRoot{Scope: directive.ScopeDirective}, - } - srv := handler.New(NewExecutableSchema(cfg)) - srv.AddTransport(transport.POST{}) - c := client.New(srv) - - // Execute mutation - var resp struct { - VerifyRegistration bool - } - err = c.Post(`mutation { verifyRegistration }`, &resp, func(bd *client.Request) { - bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{ - UserID: verifiedUser.ID, - Scopes: []string{"verification:write"}, - })) - }) - - // Verify error - require.Error(t, err) - require.Contains(t, err.Error(), defs.ErrVerified.Error()) - }) - - t.Run("user not found", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - // Setup database with proper groups and scopes - _, err := setup.Setup(context.Background(), entClient) - require.NoError(t, err) - - resolver := NewTestResolver(t, entClient, &mockAuthStorage{}) - - // Create test server with scope directive - cfg := Config{ - Resolvers: resolver, - Directives: DirectiveRoot{Scope: directive.ScopeDirective}, - } - srv := handler.New(NewExecutableSchema(cfg)) - srv.AddTransport(transport.POST{}) - c := client.New(srv) - - // Create context with authenticated user but non-existent user ID - ctx := auth.WithUser(context.Background(), auth.TokenInfo{ - UserID: 999, // Non-existent user ID - Scopes: []string{"verification:write"}, - }) - - // Execute mutation - var resp struct { - VerifyRegistration bool - } - err = c.Post(`mutation { verifyRegistration }`, &resp, func(bd *client.Request) { - bd.HTTP = bd.HTTP.WithContext(ctx) - }) - - // Verify error - require.Error(t, err) - require.Contains(t, err.Error(), "get user") - }) -} - func TestUserResolver_TotalPoints(t *testing.T) { t.Run("user with no points", func(t *testing.T) { entClient := testhelper.NewEntSqliteClient(t) diff --git a/internal/events/points_test.go b/internal/events/points_test.go index c32ed31..9a0a718 100644 --- a/internal/events/points_test.go +++ b/internal/events/points_test.go @@ -31,7 +31,7 @@ func setupTestData(t *testing.T, client *ent.Client) int { user, err := client.User.Create(). SetName("Test User"). SetEmail("test@example.com"). - SetGroup(setupResult.NewUserGroup). + SetGroup(setupResult.StudentGroup). Save(ctx) require.NoError(t, err) @@ -798,7 +798,7 @@ func TestGrantFirstPlacePoints_NotFirstPlace(t *testing.T) { user2, err := client.User.Create(). SetName("Test User 2"). SetEmail("test2@example.com"). - SetGroup(setupResult.NewUserGroup). + SetGroup(setupResult.StudentGroup). Save(ctx) require.NoError(t, err) userID2 := user2.ID diff --git a/internal/setup/README.md b/internal/setup/README.md index 64c8731..c9665f3 100644 --- a/internal/setup/README.md +++ b/internal/setup/README.md @@ -12,8 +12,8 @@ setup 放置資料庫的初始化 (seeding) 共用程式碼。 ## 初始化項目 - `admin` scopeset (`*`) 和 `admin` 群組 -- `new-user` scopeset (`me:*`) 和 `new-user` 群組。 -- `unverified` scopeset (`[verification:*, me:read]`) 和 `unverified` 群組 +- `student` scopeset (`me:*`, `question:read`, `database:read`, `ai`) 和 `student` 群組。 +- `unverified` scopeset (`me:read`) 和 `unverified` 群組 > [!INFO] > Scope 的具體定義,請參考 [scope 文件](../../docs/scope.md)。Wildcard 的意涵請參考 [scope 套件的實作](../scope/README.md) diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 66e23dd..307410e 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -13,9 +13,9 @@ import ( // SetupResult is the result of the setup process. type SetupResult struct { AdminScopeSet *ent.ScopeSet - NewUserScopeSet *ent.ScopeSet AdminGroup *ent.Group - NewUserGroup *ent.Group + StudentScopeSet *ent.ScopeSet + StudentGroup *ent.Group UnverifiedScopeSet *ent.ScopeSet UnverifiedGroup *ent.Group } @@ -54,26 +54,26 @@ func Setup(ctx context.Context, entClient *ent.Client) (*SetupResult, error) { log.Println("[*] Admin scope set already exists, skipping creation") } - // Check if new-user scope set already exists - newUserScopeSet, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.NewUserScopeSetSlug)). + // Check if the student scope set already exists + studentScopeSet, err := entClient.ScopeSet.Query(). + Where(scopeset.SlugEQ(useraccount.StudentScopeSetSlug)). Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, err } - if newUserScopeSet == nil { - log.Println("[*] Creating the 'new-user' scope set…") - newUserScopeSet, err = entClient.ScopeSet.Create(). - SetSlug(useraccount.NewUserScopeSetSlug). - SetDescription("New users can only read and write their own data."). - SetScopes([]string{"me:*"}). + if studentScopeSet == nil { + log.Println("[*] Creating the 'student' scope set…") + studentScopeSet, err = entClient.ScopeSet.Create(). + SetSlug(useraccount.StudentScopeSetSlug). + SetDescription("The necessary permissions for using the main app"). + SetScopes([]string{"me:*", "question:read", "database:read", "ai"}). Save(ctx) if err != nil { return nil, err } } else { - log.Println("[*] New-user scope set already exists, skipping creation") + log.Println("[*] Student scope set already exists, skipping creation") } // Check if unverified scope set already exists @@ -87,8 +87,8 @@ func Setup(ctx context.Context, entClient *ent.Client) (*SetupResult, error) { log.Println("[*] Creating the 'unverified' scope set…") unverifiedScopeSet, err = entClient.ScopeSet.Create(). SetSlug(useraccount.UnverifiedScopeSetSlug). - SetDescription("Unverified users can only verify their account and read their own initial data."). - SetScopes([]string{"verification:*", "me:read"}). + SetDescription("Unverified users can only read their own initial data, and must be manually verified by an administrator."). + SetScopes([]string{"me:read"}). Save(ctx) if err != nil { return nil, err @@ -119,26 +119,26 @@ func Setup(ctx context.Context, entClient *ent.Client) (*SetupResult, error) { log.Println("[*] Admin group already exists, skipping creation") } - // Check if new-user group already exists - newUserGroup, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.NewUserGroupSlug)). + // Check if student group already exists + studentGroup, err := entClient.Group.Query(). + Where(group.NameEQ(useraccount.StudentGroupSlug)). Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, err } - if newUserGroup == nil { - log.Println("[*] Creating the 'new-user' group…") - newUserGroup, err = entClient.Group.Create(). - SetName(useraccount.NewUserGroupSlug). - SetDescription("New users that is not yet verified."). - AddScopeSetIDs(newUserScopeSet.ID). + if studentGroup == nil { + log.Println("[*] Creating the 'student' group…") + studentGroup, err = entClient.Group.Create(). + SetName(useraccount.StudentGroupSlug). + SetDescription("The group that has the minimum permissions for accessing this system."). + AddScopeSetIDs(studentScopeSet.ID). Save(ctx) if err != nil { return nil, err } } else { - log.Println("[*] New-user group already exists, skipping creation") + log.Println("[*] Student group already exists, skipping creation") } // Check if unverified group already exists @@ -165,9 +165,9 @@ func Setup(ctx context.Context, entClient *ent.Client) (*SetupResult, error) { return &SetupResult{ AdminScopeSet: adminScopeSet, - NewUserScopeSet: newUserScopeSet, AdminGroup: adminGroup, - NewUserGroup: newUserGroup, + StudentScopeSet: studentScopeSet, + StudentGroup: studentGroup, UnverifiedScopeSet: unverifiedScopeSet, UnverifiedGroup: unverifiedGroup, }, nil diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go deleted file mode 100644 index 36a28b0..0000000 --- a/internal/setup/setup_test.go +++ /dev/null @@ -1,338 +0,0 @@ -package setup_test - -import ( - "context" - "testing" - - "github.com/database-playground/backend-v2/ent/group" - "github.com/database-playground/backend-v2/ent/scopeset" - "github.com/database-playground/backend-v2/internal/setup" - "github.com/database-playground/backend-v2/internal/testhelper" - "github.com/database-playground/backend-v2/internal/useraccount" - - _ "github.com/mattn/go-sqlite3" -) - -func TestSetup(t *testing.T) { - t.Run("should create all required entities on first run", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - ctx := context.Background() - - // Run setup - result, err := setup.Setup(ctx, entClient) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify admin scope set was created - if result.AdminScopeSet == nil { - t.Fatal("AdminScopeSet should not be nil") - } - if result.AdminScopeSet.Slug != useraccount.AdminScopeSetSlug { - t.Errorf("Expected admin scope set slug to be 'admin', got %s", result.AdminScopeSet.Slug) - } - if result.AdminScopeSet.Description != "Administrator" { - t.Errorf("Expected admin scope set description to be 'Administrator', got %s", result.AdminScopeSet.Description) - } - if len(result.AdminScopeSet.Scopes) != 1 || result.AdminScopeSet.Scopes[0] != "*" { - t.Errorf("Expected admin scope set scopes to be ['*'], got %v", result.AdminScopeSet.Scopes) - } - - // Verify new-user scope set was created - if result.NewUserScopeSet == nil { - t.Fatal("NewUserScopeSet should not be nil") - } - if result.NewUserScopeSet.Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user scope set slug to be 'new-user', got %s", result.NewUserScopeSet.Slug) - } - if result.NewUserScopeSet.Description != "New users can only read and write their own data." { - t.Errorf("Expected new-user scope set description to be 'New users can only read and write their own data.', got %s", result.NewUserScopeSet.Description) - } - if len(result.NewUserScopeSet.Scopes) != 1 || result.NewUserScopeSet.Scopes[0] != "me:*" { - t.Errorf("Expected new-user scope set scopes to be ['me:*'], got %v", result.NewUserScopeSet.Scopes) - } - - // Verify unverified scope set was created - if result.UnverifiedScopeSet == nil { - t.Fatal("UnverifiedScopeSet should not be nil") - } - if result.UnverifiedScopeSet.Slug != useraccount.UnverifiedScopeSetSlug { - t.Errorf("Expected unverified scope set slug to be 'unverified', got %s", result.UnverifiedScopeSet.Slug) - } - if result.UnverifiedScopeSet.Description != "Unverified users can only verify their account and read their own initial data." { - t.Errorf("Expected unverified scope set description to be 'Unverified users can only verify their account and read their own initial data.', got %s", result.UnverifiedScopeSet.Description) - } - if len(result.UnverifiedScopeSet.Scopes) != 2 || result.UnverifiedScopeSet.Scopes[0] != "verification:*" || result.UnverifiedScopeSet.Scopes[1] != "me:read" { - t.Errorf("Expected unverified scope set scopes to be ['verification:*', 'me:read'], got %v", result.UnverifiedScopeSet.Scopes) - } - - // Verify admin group was created - if result.AdminGroup == nil { - t.Fatal("AdminGroup should not be nil") - } - if result.AdminGroup.Name != useraccount.AdminGroupSlug { - t.Errorf("Expected admin group name to be 'admin', got %s", result.AdminGroup.Name) - } - if result.AdminGroup.Description != "Administrator" { - t.Errorf("Expected admin group description to be 'Administrator', got %s", result.AdminGroup.Description) - } - - // Verify new-user group was created - if result.NewUserGroup == nil { - t.Fatal("NewUserGroup should not be nil") - } - if result.NewUserGroup.Name != useraccount.NewUserGroupSlug { - t.Errorf("Expected new-user group name to be 'new-user', got %s", result.NewUserGroup.Name) - } - if result.NewUserGroup.Description != "New users that is not yet verified." { - t.Errorf("Expected new-user group description to be 'New users that is not yet verified.', got %s", result.NewUserGroup.Description) - } - - // Verify unverified group was created - if result.UnverifiedGroup == nil { - t.Fatal("UnverifiedGroup should not be nil") - } - if result.UnverifiedGroup.Name != useraccount.UnverifiedGroupSlug { - t.Errorf("Expected unverified group name to be 'unverified', got %s", result.UnverifiedGroup.Name) - } - if result.UnverifiedGroup.Description != "Unverified users that is not yet verified." { - t.Errorf("Expected unverified group description to be 'Unverified users that is not yet verified.', got %s", result.UnverifiedGroup.Description) - } - - // Verify the groups are linked to the correct scope sets - adminGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.AdminGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query admin group with scope sets: %v", err) - } - if len(adminGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected admin group to have 1 scope set, got %d", len(adminGroupWithScopes.Edges.ScopeSets)) - } - if adminGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.AdminScopeSetSlug { - t.Errorf("Expected admin group to be linked to admin scope set, got %s", adminGroupWithScopes.Edges.ScopeSets[0].Slug) - } - - newUserGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.NewUserGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query new-user group with scope sets: %v", err) - } - if len(newUserGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected new-user group to have 1 scope set, got %d", len(newUserGroupWithScopes.Edges.ScopeSets)) - } - if newUserGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user group to be linked to new-user scope set, got %s", newUserGroupWithScopes.Edges.ScopeSets[0].Slug) - } - - unverifiedGroupWithScopes, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.UnverifiedGroupSlug)). - WithScopeSets(). - Only(ctx) - if err != nil { - t.Fatalf("Failed to query unverified group with scope sets: %v", err) - } - if len(unverifiedGroupWithScopes.Edges.ScopeSets) != 1 { - t.Errorf("Expected unverified group to have 1 scope set, got %d", len(unverifiedGroupWithScopes.Edges.ScopeSets)) - } - if unverifiedGroupWithScopes.Edges.ScopeSets[0].Slug != useraccount.UnverifiedScopeSetSlug { - t.Errorf("Expected unverified group to be linked to unverified scope set, got %s", unverifiedGroupWithScopes.Edges.ScopeSets[0].Slug) - } - }) - - t.Run("should be idempotent - second run should not create duplicates", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - ctx := context.Background() - - // Run setup first time - result1, err := setup.Setup(ctx, entClient) - if err != nil { - t.Fatalf("First setup failed: %v", err) - } - - // Run setup second time - result2, err := setup.Setup(ctx, entClient) - if err != nil { - t.Fatalf("Second setup failed: %v", err) - } - - // Verify that the same entities are returned (same IDs) - if result1.AdminScopeSet.ID != result2.AdminScopeSet.ID { - t.Errorf("Admin scope set IDs should be the same, got %d and %d", result1.AdminScopeSet.ID, result2.AdminScopeSet.ID) - } - if result1.NewUserScopeSet.ID != result2.NewUserScopeSet.ID { - t.Errorf("New-user scope set IDs should be the same, got %d and %d", result1.NewUserScopeSet.ID, result2.NewUserScopeSet.ID) - } - if result1.UnverifiedScopeSet.ID != result2.UnverifiedScopeSet.ID { - t.Errorf("Unverified scope set IDs should be the same, got %d and %d", result1.UnverifiedScopeSet.ID, result2.UnverifiedScopeSet.ID) - } - if result1.AdminGroup.ID != result2.AdminGroup.ID { - t.Errorf("Admin group IDs should be the same, got %d and %d", result1.AdminGroup.ID, result2.AdminGroup.ID) - } - if result1.NewUserGroup.ID != result2.NewUserGroup.ID { - t.Errorf("New-user group IDs should be the same, got %d and %d", result1.NewUserGroup.ID, result2.NewUserGroup.ID) - } - if result1.UnverifiedGroup.ID != result2.UnverifiedGroup.ID { - t.Errorf("Unverified group IDs should be the same, got %d and %d", result1.UnverifiedGroup.ID, result2.UnverifiedGroup.ID) - } - - // Verify that only one of each entity exists in the database - adminScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.AdminScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query admin scope sets: %v", err) - } - if len(adminScopeSets) != 1 { - t.Errorf("Expected exactly 1 admin scope set, got %d", len(adminScopeSets)) - } - - newUserScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.NewUserScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query new-user scope sets: %v", err) - } - if len(newUserScopeSets) != 1 { - t.Errorf("Expected exactly 1 new-user scope set, got %d", len(newUserScopeSets)) - } - - unverifiedScopeSets, err := entClient.ScopeSet.Query(). - Where(scopeset.SlugEQ(useraccount.UnverifiedScopeSetSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query unverified scope sets: %v", err) - } - if len(unverifiedScopeSets) != 1 { - t.Errorf("Expected exactly 1 unverified scope set, got %d", len(unverifiedScopeSets)) - } - - adminGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.AdminGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query admin groups: %v", err) - } - if len(adminGroups) != 1 { - t.Errorf("Expected exactly 1 admin group, got %d", len(adminGroups)) - } - - newUserGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.NewUserGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query new-user groups: %v", err) - } - if len(newUserGroups) != 1 { - t.Errorf("Expected exactly 1 new-user group, got %d", len(newUserGroups)) - } - - unverifiedGroups, err := entClient.Group.Query(). - Where(group.NameEQ(useraccount.UnverifiedGroupSlug)). - All(ctx) - if err != nil { - t.Fatalf("Failed to query unverified groups: %v", err) - } - if len(unverifiedGroups) != 1 { - t.Errorf("Expected exactly 1 unverified group, got %d", len(unverifiedGroups)) - } - }) - - t.Run("should handle partial existing data", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - ctx := context.Background() - - // Create admin scope set manually before running setup - existingAdminScopeSet, err := entClient.ScopeSet.Create(). - SetSlug(useraccount.AdminScopeSetSlug). - SetDescription("Administrator"). - SetScopes([]string{"*"}). - Save(ctx) - if err != nil { - t.Fatalf("Failed to create existing admin scope set: %v", err) - } - - // Create unverified scope set manually before running setup - existingUnverifiedScopeSet, err := entClient.ScopeSet.Create(). - SetSlug(useraccount.UnverifiedScopeSetSlug). - SetDescription("Unverified users can only verify their account and read their own initial data."). - SetScopes([]string{"verification:*", "me:read"}). - Save(ctx) - if err != nil { - t.Fatalf("Failed to create existing unverified scope set: %v", err) - } - - // Run setup - result, err := setup.Setup(ctx, entClient) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify that the existing admin scope set is returned - if result.AdminScopeSet.ID != existingAdminScopeSet.ID { - t.Errorf("Expected to return existing admin scope set with ID %d, got %d", existingAdminScopeSet.ID, result.AdminScopeSet.ID) - } - - // Verify that the existing unverified scope set is returned - if result.UnverifiedScopeSet.ID != existingUnverifiedScopeSet.ID { - t.Errorf("Expected to return existing unverified scope set with ID %d, got %d", existingUnverifiedScopeSet.ID, result.UnverifiedScopeSet.ID) - } - - // Verify that new-user scope set was created - if result.NewUserScopeSet == nil { - t.Fatal("NewUserScopeSet should not be nil") - } - if result.NewUserScopeSet.Slug != useraccount.NewUserScopeSetSlug { - t.Errorf("Expected new-user scope set slug to be 'new-user', got %s", result.NewUserScopeSet.Slug) - } - - // Verify that all groups were created - if result.AdminGroup == nil { - t.Fatal("AdminGroup should not be nil") - } - if result.NewUserGroup == nil { - t.Fatal("NewUserGroup should not be nil") - } - if result.UnverifiedGroup == nil { - t.Fatal("UnverifiedGroup should not be nil") - } - }) -} - -func TestSetupResult(t *testing.T) { - t.Run("SetupResult should have all required fields", func(t *testing.T) { - entClient := testhelper.NewEntSqliteClient(t) - - ctx := context.Background() - - result, err := setup.Setup(ctx, entClient) - if err != nil { - t.Fatalf("Setup failed: %v", err) - } - - // Verify all fields are populated - if result.AdminScopeSet == nil { - t.Error("AdminScopeSet should not be nil") - } - if result.NewUserScopeSet == nil { - t.Error("NewUserScopeSet should not be nil") - } - if result.UnverifiedScopeSet == nil { - t.Error("UnverifiedScopeSet should not be nil") - } - if result.AdminGroup == nil { - t.Error("AdminGroup should not be nil") - } - if result.NewUserGroup == nil { - t.Error("NewUserGroup should not be nil") - } - if result.UnverifiedGroup == nil { - t.Error("UnverifiedGroup should not be nil") - } - }) -} diff --git a/internal/submission/submission_test.go b/internal/submission/submission_test.go index 34ce7e8..ad2bffd 100644 --- a/internal/submission/submission_test.go +++ b/internal/submission/submission_test.go @@ -39,7 +39,7 @@ func setupTestData(t *testing.T, client *ent.Client) (userID, questionID, databa testUser, err := client.User.Create(). SetName("Test User"). SetEmail("test@example.com"). - SetGroup(setupResult.NewUserGroup). + SetGroup(setupResult.StudentGroup). Save(ctx) require.NoError(t, err) @@ -173,7 +173,7 @@ func TestSubmitAnswer_Failed_ReferenceQueryError(t *testing.T) { testUser, err := client.User.Create(). SetName("Test User"). SetEmail("test@example.com"). - SetGroup(setupResult.NewUserGroup). + SetGroup(setupResult.StudentGroup). Save(ctx) require.NoError(t, err) @@ -337,7 +337,7 @@ func TestSubmitAnswer_Integration_MultipleSubmissions(t *testing.T) { testUser, err := client.User.Create(). SetName("Test User"). SetEmail("test@example.com"). - SetGroup(setupResult.NewUserGroup). + SetGroup(setupResult.StudentGroup). Save(ctx) require.NoError(t, err) diff --git a/internal/useraccount/README.md b/internal/useraccount/README.md index fa6c5aa..d98b2ff 100644 --- a/internal/useraccount/README.md +++ b/internal/useraccount/README.md @@ -4,11 +4,8 @@ useraccount 套件處理使用者的預定義執行流程。 ## 註冊 -![register flow](./docs/register-flow.png) - - 使用者使用 Google 帳號或其他 OAuth 方式登入。 - 確認是否已經有這個使用者,如果沒有,建立一個 `unverified` 群組的使用者。 -- `unverified` 群組授予 `verification:write` scope,引導使用者執行驗證。 -- 使用者驗證完成後,授予 `new-user` 群組。 +- 可以在後台手動授予使用者更明確的群組(如 `student`)。 - 如果使用者放棄驗證,則直接走登出流程。 - Cronjob 會在每個小時定期清除 unverified users。 diff --git a/internal/useraccount/models.go b/internal/useraccount/models.go index 2e884bf..cc33f70 100644 --- a/internal/useraccount/models.go +++ b/internal/useraccount/models.go @@ -3,15 +3,15 @@ package useraccount const ( // AdminScopeSetSlug is the slug of the admin scope set. AdminScopeSetSlug = "admin" - // NewUserScopeSetSlug is the slug of the new user scope set. - NewUserScopeSetSlug = "new-user" + // StudentScopeSetSlug is the slug of the student scope set. + StudentScopeSetSlug = "student" // UnverifiedScopeSetSlug is the slug of the unverified scope set. UnverifiedScopeSetSlug = "unverified" // AdminGroupSlug is the slug of the admin group. AdminGroupSlug = "admin" - // NewUserGroupSlug is the slug of the new user group. - NewUserGroupSlug = "new-user" + // StudentGroupSlug is the slug of the student group. + StudentGroupSlug = "student" // UnverifiedGroupSlug is the slug of the unverified group. UnverifiedGroupSlug = "unverified" ) diff --git a/internal/useraccount/register_flow.go b/internal/useraccount/register_flow.go index ebc23a5..e51ab2c 100644 --- a/internal/useraccount/register_flow.go +++ b/internal/useraccount/register_flow.go @@ -76,7 +76,7 @@ func (c *Context) Verify(ctx context.Context, userID int) error { return ErrUserVerified } - newUserGroup, err := c.entClient.Group.Query().Where(group.NameEQ(NewUserGroupSlug)).Only(ctx) + studentGroup, err := c.entClient.Group.Query().Where(group.NameEQ(StudentGroupSlug)).Only(ctx) if err != nil { if ent.IsNotFound(err) { return ErrIncompleteSetup @@ -86,7 +86,7 @@ func (c *Context) Verify(ctx context.Context, userID int) error { } // update user's group to the new user group - if _, err := user.Update().SetGroup(newUserGroup).Save(ctx); err != nil { + if _, err := user.Update().SetGroup(studentGroup).Save(ctx); err != nil { return fmt.Errorf("update user group: %w", err) } diff --git a/internal/useraccount/register_flow_test.go b/internal/useraccount/register_flow_test.go index cf89b59..3dd1133 100644 --- a/internal/useraccount/register_flow_test.go +++ b/internal/useraccount/register_flow_test.go @@ -37,11 +37,11 @@ func TestGetOrRegister_NewUser(t *testing.T) { require.NoError(t, err) assert.Equal(t, useraccount.UnverifiedGroupSlug, group.Name) - // Verify user has verification:* scope + // Verify user has me:read scope scopeSets, err := user.QueryGroup().QueryScopeSets().All(context) require.NoError(t, err) require.Len(t, scopeSets, 1) - assert.Contains(t, scopeSets[0].Scopes, "verification:*") + assert.Contains(t, scopeSets[0].Scopes, "me:read") } func TestGetOrRegister_ExistingUser(t *testing.T) { @@ -133,8 +133,8 @@ func TestGetOrRegister_UpdateNameAndAvatar_VerifiedUser(t *testing.T) { ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() - // Create an existing verified user (in new-user group) - newUserGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context) + // Create an existing verified user (in student group) + studentGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.StudentGroupSlug)).Only(context) require.NoError(t, err) originalName := "Verified User Original" @@ -143,7 +143,7 @@ func TestGetOrRegister_UpdateNameAndAvatar_VerifiedUser(t *testing.T) { SetName(originalName). SetEmail("verified-update@example.com"). SetAvatar(originalAvatar). - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(context) require.NoError(t, err) @@ -168,7 +168,7 @@ func TestGetOrRegister_UpdateNameAndAvatar_VerifiedUser(t *testing.T) { // Verify user is still in the verified group group, err := user.QueryGroup().Only(context) require.NoError(t, err) - assert.Equal(t, useraccount.NewUserGroupSlug, group.Name, "group should not change") + assert.Equal(t, useraccount.StudentGroupSlug, group.Name, "group should not change") } func TestGetOrRegister_UpdateWithEmptyAvatar(t *testing.T) { @@ -247,15 +247,15 @@ func TestVerify_Success(t *testing.T) { err = ctx.Verify(context, user.ID) require.NoError(t, err) - // Check that user is now in new-user group + // Check that user is now in student group updatedUser, err := client.User.Get(context, user.ID) require.NoError(t, err) group, err := updatedUser.QueryGroup().Only(context) require.NoError(t, err) - assert.Equal(t, useraccount.NewUserGroupSlug, group.Name) + assert.Equal(t, useraccount.StudentGroupSlug, group.Name) - // Verify user has new-user scopes + // Verify user has student scopes scopeSets, err := updatedUser.QueryGroup().QueryScopeSets().All(context) require.NoError(t, err) require.Len(t, scopeSets, 1) @@ -281,14 +281,14 @@ func TestVerify_UserAlreadyVerified(t *testing.T) { ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() - // Create a user in new-user group (already verified) - newUserGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context) + // Create a user in student group (already verified) + studentGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.StudentGroupSlug)).Only(context) require.NoError(t, err) user, err := client.User.Create(). SetName("Verified User"). SetEmail("verified5@example.com"). // Unique email - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(context) require.NoError(t, err) @@ -298,7 +298,7 @@ func TestVerify_UserAlreadyVerified(t *testing.T) { assert.ErrorIs(t, err, useraccount.ErrUserVerified) } -func TestVerify_MissingNewUserGroup(t *testing.T) { +func TestVerify_MissingStudentGroup(t *testing.T) { // Create a fresh database without setup client := testhelper.NewEntSqliteClient(t) authStorage := newMockAuthStorage() @@ -320,7 +320,7 @@ func TestVerify_MissingNewUserGroup(t *testing.T) { Save(context) require.NoError(t, err) - // Try to verify - should fail due to missing new-user group + // Try to verify - should fail due to missing student group err = ctx.Verify(context, user.ID) require.Error(t, err) assert.ErrorIs(t, err, useraccount.ErrIncompleteSetup) @@ -353,19 +353,19 @@ func TestRegistrationFlow_Complete(t *testing.T) { tokenInfo, err := authStorage.Get(context, token) require.NoError(t, err) - assert.Contains(t, tokenInfo.Scopes, "verification:*") + assert.Contains(t, tokenInfo.Scopes, "me:read") // Step 3: Verify the user err = ctx.Verify(context, user.ID) require.NoError(t, err) - // Step 4: Verify user is now in new-user group + // Step 4: Verify user is now in student group updatedUser, err := client.User.Get(context, user.ID) require.NoError(t, err) updatedGroup, err := updatedUser.QueryGroup().Only(context) require.NoError(t, err) - assert.Equal(t, useraccount.NewUserGroupSlug, updatedGroup.Name) + assert.Equal(t, useraccount.StudentGroupSlug, updatedGroup.Name) // Step 5: Grant token for verified user newToken, err := ctx.GrantToken(context, updatedUser, "web", useraccount.WithFlow("login")) @@ -384,13 +384,13 @@ func TestRegistrationFlow_ExistingUser(t *testing.T) { context := context.Background() // Create an existing verified user - newUserGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context) + studentGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.StudentGroupSlug)).Only(context) require.NoError(t, err) existingUser, err := client.User.Create(). SetName("Existing User"). SetEmail("existing11@example.com"). // Unique email - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(context) require.NoError(t, err) @@ -407,7 +407,7 @@ func TestRegistrationFlow_ExistingUser(t *testing.T) { assert.Equal(t, existingUser.ID, user.ID) assert.Equal(t, req.Name, user.Name) // Name updated to match OAuth info - // Grant token - should have new-user scopes + // Grant token - should have student scopes token, err := ctx.GrantToken(context, user, "web", useraccount.WithFlow("login")) require.NoError(t, err) @@ -443,13 +443,13 @@ func TestRegistrationFlow_ErrorCases(t *testing.T) { context := context.Background() // Create verified user - newUserGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context) + studentGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.StudentGroupSlug)).Only(context) require.NoError(t, err) user, err := client.User.Create(). SetName("Verified User"). SetEmail("verified13@example.com"). // Unique email - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(context) require.NoError(t, err) diff --git a/internal/useraccount/token_test.go b/internal/useraccount/token_test.go index b913ea8..7013135 100644 --- a/internal/useraccount/token_test.go +++ b/internal/useraccount/token_test.go @@ -47,7 +47,7 @@ func TestGrantToken_Success(t *testing.T) { assert.Equal(t, user.ID, tokenInfo.UserID) assert.Equal(t, user.Email, tokenInfo.UserEmail) assert.Equal(t, "test-machine", tokenInfo.Machine) - assert.Contains(t, tokenInfo.Scopes, "verification:*") + assert.Contains(t, tokenInfo.Scopes, "me:read") assert.Equal(t, "registration", tokenInfo.Meta[useraccount.MetaInitiateFromFlow]) assert.Empty(t, tokenInfo.Meta[useraccount.MetaImpersonation]) } @@ -82,7 +82,7 @@ func TestGrantToken_Impersonation(t *testing.T) { assert.Equal(t, user.ID, tokenInfo.UserID) assert.Equal(t, user.Email, tokenInfo.UserEmail) assert.Equal(t, "test-machine", tokenInfo.Machine) - assert.Contains(t, tokenInfo.Scopes, "verification:*") + assert.Contains(t, tokenInfo.Scopes, "me:read") assert.Equal(t, "registration", tokenInfo.Meta[useraccount.MetaInitiateFromFlow]) assert.Equal(t, strconv.Itoa(user.ID), tokenInfo.Meta[useraccount.MetaImpersonation]) } @@ -123,14 +123,14 @@ func TestGrantToken_NewUserScopes(t *testing.T) { ctx := useraccount.NewContext(client, authStorage, eventService) context := context.Background() - // Create a user in new-user group - newUserGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context) + // Create a user in student group + studentGroup, err := client.Group.Query().Where(group.NameEQ(useraccount.StudentGroupSlug)).Only(context) require.NoError(t, err) user, err := client.User.Create(). SetName("Verified User"). SetEmail("verified8@example.com"). // Unique email - SetGroup(newUserGroup). + SetGroup(studentGroup). Save(context) require.NoError(t, err) @@ -142,7 +142,7 @@ func TestGrantToken_NewUserScopes(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, token) - // Verify token has new-user scopes + // Verify token has student scopes tokenInfo, err := authStorage.Get(context, token) require.NoError(t, err) assert.Contains(t, tokenInfo.Scopes, "me:*")