package graph import ( "encoding/json" "fmt" "strings" "testing" "github.com/99designs/gqlgen/client" "github.com/99designs/gqlgen/graphql/handler" "github.com/bradleyjkemp/cupaloy/v2" "gogs.dmsc.dev/arp/graph/testutil" "gorm.io/gorm" ) var snapshotter = cupaloy.New(cupaloy.SnapshotSubdirectory("testdata/snapshots")) type TestClient struct { client *client.Client db *gorm.DB } type IDTracker struct { Permissions map[string]string Roles map[string]string Users map[string]string TaskStatuses map[string]string Services map[string]string Tasks map[string]string Notes map[string]string Channels []string Messages []string } func NewIDTracker() *IDTracker { return &IDTracker{ Permissions: make(map[string]string), Roles: make(map[string]string), Users: make(map[string]string), TaskStatuses: make(map[string]string), Services: make(map[string]string), Tasks: make(map[string]string), Notes: make(map[string]string), Channels: make([]string, 0), Messages: make([]string, 0), } } func setupTestClient(t *testing.T) (*TestClient, *IDTracker) { db, err := testutil.SetupTestDB() if err != nil { t.Fatalf("Failed to setup test database: %v", err) } resolver := &Resolver{DB: db} schema := NewExecutableSchema(Config{Resolvers: resolver}) srv := handler.NewDefaultServer(schema) return &TestClient{client: client.New(srv), db: db}, NewIDTracker() } func normalizeJSON(jsonStr string) string { var data interface{} if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { return jsonStr } normalizeData(data) bytes, _ := json.MarshalIndent(data, "", " ") return string(bytes) } func normalizeData(data interface{}) { switch v := data.(type) { case map[string]interface{}: delete(v, "id") delete(v, "ID") delete(v, "createdAt") delete(v, "updatedAt") delete(v, "sentAt") delete(v, "createdByID") delete(v, "userId") delete(v, "serviceId") delete(v, "statusId") delete(v, "assigneeId") delete(v, "conversationId") delete(v, "senderId") for _, val := range v { normalizeData(val) } case []interface{}: for _, item := range v { normalizeData(item) } } } func snapshotResult(t *testing.T, name string, jsonStr string) { normalized := normalizeJSON(jsonStr) snapshotter.SnapshotT(t, name, normalized) } func TestIntegration_Bootstrap(t *testing.T) { tc, tracker := setupTestClient(t) seed := testutil.GetSeedData() // Phase 1: Create Permissions t.Run("CreatePermissions", func(t *testing.T) { for _, perm := range seed.Permissions { var response struct { CreatePermission struct { ID string `json:"id"` Code string `json:"code"` Description string `json:"description"` } `json:"createPermission"` } query := fmt.Sprintf(`mutation { createPermission(input: {code: "%s", description: "%s"}) { id code description } }`, perm.Code, perm.Description) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create permission %s: %v", perm.Code, err) } tracker.Permissions[perm.Code] = response.CreatePermission.ID } var permsResponse struct { Permissions []interface{} `json:"permissions"` } tc.client.Post(`query { permissions { id code description } }`, &permsResponse) jsonBytes, _ := json.MarshalIndent(permsResponse, "", " ") snapshotResult(t, "permissions", string(jsonBytes)) }) // Phase 2: Create Roles t.Run("CreateRoles", func(t *testing.T) { for _, role := range seed.Roles { permIDs := make([]string, len(role.PermissionCodes)) for i, code := range role.PermissionCodes { permIDs[i] = tracker.Permissions[code] } var response struct { CreateRole struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } `json:"createRole"` } query := fmt.Sprintf(`mutation { createRole(input: {name: "%s", description: "%s", permissions: ["%s"]}) { id name description } }`, role.Name, role.Description, strings.Join(permIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create role %s: %v", role.Name, err) } tracker.Roles[role.Name] = response.CreateRole.ID } var rolesResponse struct { Roles []interface{} `json:"roles"` } tc.client.Post(`query { roles { id name description permissions { id code } } }`, &rolesResponse) jsonBytes, _ := json.MarshalIndent(rolesResponse, "", " ") snapshotResult(t, "roles", string(jsonBytes)) }) // Phase 3: Create Users t.Run("CreateUsers", func(t *testing.T) { for _, user := range seed.Users { roleIDs := make([]string, len(user.RoleNames)) for i, name := range user.RoleNames { roleIDs[i] = tracker.Roles[name] } var response struct { CreateUser struct { ID string `json:"id"` Email string `json:"email"` } `json:"createUser"` } query := fmt.Sprintf(`mutation { createUser(input: {email: "%s", password: "%s", roles: ["%s"]}) { id email } }`, user.Email, user.Password, strings.Join(roleIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create user %s: %v", user.Email, err) } tracker.Users[user.Email] = response.CreateUser.ID } var usersResponse struct { Users []interface{} `json:"users"` } tc.client.Post(`query { users { id email roles { id name } } }`, &usersResponse) jsonBytes, _ := json.MarshalIndent(usersResponse, "", " ") snapshotResult(t, "users", string(jsonBytes)) }) // Phase 4: Create Task Statuses t.Run("CreateTaskStatuses", func(t *testing.T) { for _, status := range seed.TaskStatuses { var response struct { CreateTaskStatus struct { ID string `json:"id"` Code string `json:"code"` Label string `json:"label"` } `json:"createTaskStatus"` } query := fmt.Sprintf(`mutation { createTaskStatus(input: {code: "%s", label: "%s"}) { id code label } }`, status.Code, status.Label) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task status %s: %v", status.Code, err) } tracker.TaskStatuses[status.Code] = response.CreateTaskStatus.ID } var statusesResponse struct { TaskStatuses []interface{} `json:"taskStatuses"` } tc.client.Post(`query { taskStatuses { id code label } }`, &statusesResponse) jsonBytes, _ := json.MarshalIndent(statusesResponse, "", " ") snapshotResult(t, "taskStatuses", string(jsonBytes)) }) // Phase 5: Create Services t.Run("CreateServices", func(t *testing.T) { for _, service := range seed.Services { participantIDs := make([]string, len(service.ParticipantEmails)) for i, email := range service.ParticipantEmails { participantIDs[i] = tracker.Users[email] } var response struct { CreateService struct { ID string `json:"id"` Name string `json:"name"` } `json:"createService"` } query := fmt.Sprintf(`mutation { createService(input: {name: "%s", description: "%s", createdById: "%s", participants: ["%s"]}) { id name } }`, service.Name, service.Description, tracker.Users[service.CreatorEmail], strings.Join(participantIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create service %s: %v", service.Name, err) } tracker.Services[service.Name] = response.CreateService.ID } var servicesResponse struct { Services []interface{} `json:"services"` } tc.client.Post(`query { services { id name description } }`, &servicesResponse) jsonBytes, _ := json.MarshalIndent(servicesResponse, "", " ") snapshotResult(t, "services", string(jsonBytes)) }) // Phase 6: Create Tasks t.Run("CreateTasks", func(t *testing.T) { for _, task := range seed.Tasks { var response struct { CreateTask struct { ID string `json:"id"` Title string `json:"title"` } `json:"createTask"` } var assigneeID string if task.AssigneeEmail != "" { assigneeID = tracker.Users[task.AssigneeEmail] } statusID := tracker.TaskStatuses[task.StatusCode] if assigneeID != "" { query := fmt.Sprintf(`mutation { createTask(input: {title: "%s", content: "%s", createdById: "%s", assigneeId: "%s", statusId: "%s", priority: "%s"}) { id title } }`, task.Title, task.Content, tracker.Users[task.CreatorEmail], assigneeID, statusID, task.Priority) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task %s: %v", task.Title, err) } } else { query := fmt.Sprintf(`mutation { createTask(input: {title: "%s", content: "%s", createdById: "%s", statusId: "%s", priority: "%s"}) { id title } }`, task.Title, task.Content, tracker.Users[task.CreatorEmail], statusID, task.Priority) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task %s: %v", task.Title, err) } } tracker.Tasks[task.Title] = response.CreateTask.ID } var tasksResponse struct { Tasks []interface{} `json:"tasks"` } tc.client.Post(`query { tasks { id title content priority } }`, &tasksResponse) jsonBytes, _ := json.MarshalIndent(tasksResponse, "", " ") snapshotResult(t, "tasks", string(jsonBytes)) }) // Phase 7: Create Notes t.Run("CreateNotes", func(t *testing.T) { for _, note := range seed.Notes { var response struct { CreateNote struct { ID string `json:"id"` Title string `json:"title"` } `json:"createNote"` } query := fmt.Sprintf(`mutation { createNote(input: {title: "%s", content: "%s", userId: "%s", serviceId: "%s"}) { id title } }`, note.Title, note.Content, tracker.Users[note.UserEmail], tracker.Services[note.ServiceName]) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create note %s: %v", note.Title, err) } tracker.Notes[note.Title] = response.CreateNote.ID } var notesResponse struct { Notes []interface{} `json:"notes"` } tc.client.Post(`query { notes { id title content } }`, ¬esResponse) jsonBytes, _ := json.MarshalIndent(notesResponse, "", " ") snapshotResult(t, "notes", string(jsonBytes)) }) // Phase 8: Create Channels t.Run("CreateChannels", func(t *testing.T) { for _, channel := range seed.Channels { participantIDs := make([]string, len(channel.ParticipantEmails)) for i, email := range channel.ParticipantEmails { participantIDs[i] = tracker.Users[email] } var response struct { CreateChannel struct { ID string `json:"id"` } `json:"createChannel"` } query := fmt.Sprintf(`mutation { createChannel(input: {participants: ["%s"]}) { id } }`, strings.Join(participantIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create channel: %v", err) } tracker.Channels = append(tracker.Channels, response.CreateChannel.ID) } var channelsResponse struct { Channels []interface{} `json:"channels"` } tc.client.Post(`query { channels { id } }`, &channelsResponse) jsonBytes, _ := json.MarshalIndent(channelsResponse, "", " ") snapshotResult(t, "channels", string(jsonBytes)) }) // Phase 9: Create Messages t.Run("CreateMessages", func(t *testing.T) { for _, msg := range seed.Messages { var response struct { CreateMessage struct { ID string `json:"id"` } `json:"createMessage"` } query := fmt.Sprintf(`mutation { createMessage(input: {conversationId: "%s", senderId: "%s", content: "%s"}) { id } }`, tracker.Channels[msg.ChannelIndex], tracker.Users[msg.SenderEmail], msg.Content) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create message: %v", err) } tracker.Messages = append(tracker.Messages, response.CreateMessage.ID) } var messagesResponse struct { Messages []interface{} `json:"messages"` } tc.client.Post(`query { messages { id content } }`, &messagesResponse) jsonBytes, _ := json.MarshalIndent(messagesResponse, "", " ") snapshotResult(t, "messages", string(jsonBytes)) }) } // TestIntegration_Update tests update operations func TestIntegration_Update(t *testing.T) { tc, tracker := setupTestClient(t) seed := testutil.GetSeedData() // Bootstrap first bootstrapData(t, tc, tracker, seed) // Update a user t.Run("UpdateUser", func(t *testing.T) { userID := tracker.Users["admin@example.com"] var response struct { UpdateUser struct { ID string `json:"id"` Email string `json:"email"` } `json:"updateUser"` } query := fmt.Sprintf(`mutation { updateUser(id: "%s", input: {email: "admin-updated@example.com", password: "new-password", roles: ["%s"]}) { id email } }`, userID, tracker.Roles["admin"]) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to update user: %v", err) } var usersResponse struct { Users []interface{} `json:"users"` } tc.client.Post(`query { users { id email roles { id name } } }`, &usersResponse) jsonBytes, _ := json.MarshalIndent(usersResponse, "", " ") snapshotResult(t, "users_after_update", string(jsonBytes)) }) // Update a task t.Run("UpdateTask", func(t *testing.T) { taskID := tracker.Tasks["Setup development environment"] var response struct { UpdateTask struct { ID string `json:"id"` Title string `json:"title"` } `json:"updateTask"` } query := fmt.Sprintf(`mutation { updateTask(id: "%s", input: {title: "Setup development environment - COMPLETE", priority: "low"}) { id title } }`, taskID) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to update task: %v", err) } var tasksResponse struct { Tasks []interface{} `json:"tasks"` } tc.client.Post(`query { tasks { id title content priority } }`, &tasksResponse) jsonBytes, _ := json.MarshalIndent(tasksResponse, "", " ") snapshotResult(t, "tasks_after_update", string(jsonBytes)) }) } // TestIntegration_Delete tests delete operations func TestIntegration_Delete(t *testing.T) { tc, tracker := setupTestClient(t) seed := testutil.GetSeedData() // Bootstrap first bootstrapData(t, tc, tracker, seed) // Delete a note t.Run("DeleteNote", func(t *testing.T) { noteID := tracker.Notes["Meeting notes - Sprint 1"] var response struct { DeleteNote bool `json:"deleteNote"` } query := fmt.Sprintf(`mutation { deleteNote(id: "%s") }`, noteID) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to delete note: %v", err) } var notesResponse struct { Notes []interface{} `json:"notes"` } tc.client.Post(`query { notes { id title content } }`, ¬esResponse) jsonBytes, _ := json.MarshalIndent(notesResponse, "", " ") snapshotResult(t, "notes_after_delete", string(jsonBytes)) }) // Delete a task t.Run("DeleteTask", func(t *testing.T) { taskID := tracker.Tasks["Performance optimization"] var response struct { DeleteTask bool `json:"deleteTask"` } query := fmt.Sprintf(`mutation { deleteTask(id: "%s") }`, taskID) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to delete task: %v", err) } var tasksResponse struct { Tasks []interface{} `json:"tasks"` } tc.client.Post(`query { tasks { id title content priority } }`, &tasksResponse) jsonBytes, _ := json.MarshalIndent(tasksResponse, "", " ") snapshotResult(t, "tasks_after_delete", string(jsonBytes)) }) } // bootstrapData creates all entities for testing func bootstrapData(t *testing.T, tc *TestClient, tracker *IDTracker, seed testutil.SeedData) { // Create Permissions for _, perm := range seed.Permissions { var response struct { CreatePermission struct { ID string `json:"id"` } `json:"createPermission"` } query := fmt.Sprintf(`mutation { createPermission(input: {code: "%s", description: "%s"}) { id } }`, perm.Code, perm.Description) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create permission %s: %v", perm.Code, err) } tracker.Permissions[perm.Code] = response.CreatePermission.ID } // Create Roles for _, role := range seed.Roles { permIDs := make([]string, len(role.PermissionCodes)) for i, code := range role.PermissionCodes { permIDs[i] = tracker.Permissions[code] } var response struct { CreateRole struct { ID string `json:"id"` } `json:"createRole"` } query := fmt.Sprintf(`mutation { createRole(input: {name: "%s", description: "%s", permissions: ["%s"]}) { id } }`, role.Name, role.Description, strings.Join(permIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create role %s: %v", role.Name, err) } tracker.Roles[role.Name] = response.CreateRole.ID } // Create Users for _, user := range seed.Users { roleIDs := make([]string, len(user.RoleNames)) for i, name := range user.RoleNames { roleIDs[i] = tracker.Roles[name] } var response struct { CreateUser struct { ID string `json:"id"` } `json:"createUser"` } query := fmt.Sprintf(`mutation { createUser(input: {email: "%s", password: "%s", roles: ["%s"]}) { id } }`, user.Email, user.Password, strings.Join(roleIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create user %s: %v", user.Email, err) } tracker.Users[user.Email] = response.CreateUser.ID } // Create Task Statuses for _, status := range seed.TaskStatuses { var response struct { CreateTaskStatus struct { ID string `json:"id"` } `json:"createTaskStatus"` } query := fmt.Sprintf(`mutation { createTaskStatus(input: {code: "%s", label: "%s"}) { id } }`, status.Code, status.Label) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task status %s: %v", status.Code, err) } tracker.TaskStatuses[status.Code] = response.CreateTaskStatus.ID } // Create Services for _, service := range seed.Services { participantIDs := make([]string, len(service.ParticipantEmails)) for i, email := range service.ParticipantEmails { participantIDs[i] = tracker.Users[email] } var response struct { CreateService struct { ID string `json:"id"` } `json:"createService"` } query := fmt.Sprintf(`mutation { createService(input: {name: "%s", description: "%s", createdById: "%s", participants: ["%s"]}) { id } }`, service.Name, service.Description, tracker.Users[service.CreatorEmail], strings.Join(participantIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create service %s: %v", service.Name, err) } tracker.Services[service.Name] = response.CreateService.ID } // Create Tasks for _, task := range seed.Tasks { var response struct { CreateTask struct { ID string `json:"id"` } `json:"createTask"` } statusID := tracker.TaskStatuses[task.StatusCode] if task.AssigneeEmail != "" { query := fmt.Sprintf(`mutation { createTask(input: {title: "%s", content: "%s", createdById: "%s", assigneeId: "%s", statusId: "%s", priority: "%s"}) { id } }`, task.Title, task.Content, tracker.Users[task.CreatorEmail], tracker.Users[task.AssigneeEmail], statusID, task.Priority) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task %s: %v", task.Title, err) } } else { query := fmt.Sprintf(`mutation { createTask(input: {title: "%s", content: "%s", createdById: "%s", statusId: "%s", priority: "%s"}) { id } }`, task.Title, task.Content, tracker.Users[task.CreatorEmail], statusID, task.Priority) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create task %s: %v", task.Title, err) } } tracker.Tasks[task.Title] = response.CreateTask.ID } // Create Notes for _, note := range seed.Notes { var response struct { CreateNote struct { ID string `json:"id"` } `json:"createNote"` } query := fmt.Sprintf(`mutation { createNote(input: {title: "%s", content: "%s", userId: "%s", serviceId: "%s"}) { id } }`, note.Title, note.Content, tracker.Users[note.UserEmail], tracker.Services[note.ServiceName]) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create note %s: %v", note.Title, err) } tracker.Notes[note.Title] = response.CreateNote.ID } // Create Channels for _, channel := range seed.Channels { participantIDs := make([]string, len(channel.ParticipantEmails)) for i, email := range channel.ParticipantEmails { participantIDs[i] = tracker.Users[email] } var response struct { CreateChannel struct { ID string `json:"id"` } `json:"createChannel"` } query := fmt.Sprintf(`mutation { createChannel(input: {participants: ["%s"]}) { id } }`, strings.Join(participantIDs, `", "`)) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create channel: %v", err) } tracker.Channels = append(tracker.Channels, response.CreateChannel.ID) } // Create Messages for _, msg := range seed.Messages { var response struct { CreateMessage struct { ID string `json:"id"` } `json:"createMessage"` } query := fmt.Sprintf(`mutation { createMessage(input: {conversationId: "%s", senderId: "%s", content: "%s"}) { id } }`, tracker.Channels[msg.ChannelIndex], tracker.Users[msg.SenderEmail], msg.Content) err := tc.client.Post(query, &response) if err != nil { t.Fatalf("Failed to create message: %v", err) } tracker.Messages = append(tracker.Messages, response.CreateMessage.ID) } }