From 32780082abf3111d57287c5c81e97aae4a64402e Mon Sep 17 00:00:00 2001
From: Linjie Zhang <ljzhang@gitlab.cn>
Date: Wed, 30 Aug 2023 14:05:10 +0000
Subject: [PATCH] Add user_list to feature_flag api

Changelog: added
---
 app/models/operations/feature_flag.rb         |   2 +-
 doc/api/feature_flags.md                      |  39 ++++-
 .../entities/feature_flag/basic_user_list.rb  |  14 ++
 lib/api/entities/feature_flag/strategy.rb     |   1 +
 lib/api/entities/feature_flag/user_list.rb    |   6 +-
 lib/api/feature_flags.rb                      |   4 +-
 .../public_api/v4/operations/strategy.json    |   3 +-
 .../public_api/v4/operations/user_list.json   |  16 ++
 spec/requests/api/feature_flags_spec.rb       | 162 +++++++++++++++++-
 9 files changed, 234 insertions(+), 13 deletions(-)
 create mode 100644 lib/api/entities/feature_flag/basic_user_list.rb
 create mode 100644 spec/fixtures/api/schemas/public_api/v4/operations/user_list.json

diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 01db0a5cf8b23..b93537e0d1edf 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -52,7 +52,7 @@ class FeatureFlag < ApplicationRecord
 
     class << self
       def preload_relations
-        preload(strategies: :scopes)
+        preload(strategies: [:scopes, :user_list])
       end
 
       def for_unleash_client(project, environment)
diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md
index d7bf4fe826e53..06661207f8eaf 100644
--- a/doc/api/feature_flags.md
+++ b/doc/api/feature_flags.md
@@ -59,7 +59,8 @@ Example response:
               "id": 1,
               "environment_scope": "production"
             }
-          ]
+          ],
+          "user_list": null
         }
       ]
    },
@@ -81,7 +82,36 @@ Example response:
               "id": 2,
               "environment_scope": "staging"
             }
-          ]
+          ],
+          "user_list": null
+        }
+      ]
+   },
+   {
+      "name":"user_list",
+      "description":"This feature is about user list",
+      "active": true,
+      "version": "new_version_flag",
+      "created_at":"2019-11-04T08:13:10.507Z",
+      "updated_at":"2019-11-04T08:13:10.507Z",
+      "scopes":[],
+      "strategies": [
+        {
+          "id": 2,
+          "name": "gitlabUserList",
+          "parameters": {},
+          "scopes": [
+            {
+              "id": 2,
+              "environment_scope": "staging"
+            }
+          ],
+          "user_list": {
+            "id": 1,
+            "iid": 1,
+            "name": "My user list",
+            "user_xids": "user1,user2,user3"
+          }
         }
       ]
    }
@@ -126,7 +156,8 @@ Example response:
           "id": 37,
           "environment_scope": "production"
         }
-      ]
+      ],
+      "user_list": null
     }
   ]
 }
@@ -152,6 +183,7 @@ POST /projects/:id/feature_flags
 | `strategies:parameters` | JSON         | no         | The strategy parameters.                                                               |
 | `strategies:scopes` | JSON             | no         | The scopes for the strategy.                                                           |
 | `strategies:scopes:environment_scope` | string | no | The environment scope of the scope.                                                    |
+| `strategies:user_list_id` | integer/string | no     | The ID of the feature flag user list. If strategy is `gitlabUserList`.                 |
 
 ```shell
 curl "https://gitlab.example.com/api/v4/projects/1/feature_flags" \
@@ -217,6 +249,7 @@ PUT /projects/:id/feature_flags/:feature_flag_name
 | `strategies:scopes:id` | JSON          | no         | The environment scope ID.                                                                         |
 | `strategies:scopes:environment_scope` | string | no | The environment scope of the scope.                                                    |
 | `strategies:scopes:_destroy` | boolean | no | Delete the scope when true.                                                    |
+| `strategies:user_list_id` | integer/string | no     | The ID of the feature flag user list. If strategy is `gitlabUserList`.                 |
 
 ```shell
 curl "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature" \
diff --git a/lib/api/entities/feature_flag/basic_user_list.rb b/lib/api/entities/feature_flag/basic_user_list.rb
new file mode 100644
index 0000000000000..df577e9f1a4f8
--- /dev/null
+++ b/lib/api/entities/feature_flag/basic_user_list.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+  module Entities
+    class FeatureFlag < Grape::Entity
+      class BasicUserList < Grape::Entity
+        expose :id, documentation: { type: 'integer', example: 1 }
+        expose :iid, documentation: { type: 'integer', example: 1 }
+        expose :name, documentation: { type: 'string', example: 'user_list' }
+        expose :user_xids, documentation: { type: 'string', example: 'user1,user2' }
+      end
+    end
+  end
+end
diff --git a/lib/api/entities/feature_flag/strategy.rb b/lib/api/entities/feature_flag/strategy.rb
index 6217842037025..aea38f0e24a61 100644
--- a/lib/api/entities/feature_flag/strategy.rb
+++ b/lib/api/entities/feature_flag/strategy.rb
@@ -8,6 +8,7 @@ class Strategy < Grape::Entity
         expose :name, documentation: { type: 'string', example: 'userWithId' }
         expose :parameters, documentation: { type: 'string', example: '{"userIds": "user1"}' }
         expose :scopes, using: FeatureFlag::Scope
+        expose :user_list, using: FeatureFlag::BasicUserList
       end
     end
   end
diff --git a/lib/api/entities/feature_flag/user_list.rb b/lib/api/entities/feature_flag/user_list.rb
index efb3261658ac7..47f89cea4d20b 100644
--- a/lib/api/entities/feature_flag/user_list.rb
+++ b/lib/api/entities/feature_flag/user_list.rb
@@ -3,16 +3,12 @@
 module API
   module Entities
     class FeatureFlag < Grape::Entity
-      class UserList < Grape::Entity
+      class UserList < BasicUserList
         include RequestAwareEntity
 
-        expose :id, documentation: { type: 'integer', example: 1 }
-        expose :iid, documentation: { type: 'integer', example: 1 }
         expose :project_id, documentation: { type: 'integer', example: 2 }
         expose :created_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' }
         expose :updated_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' }
-        expose :name, documentation: { type: 'string', example: 'user_list' }
-        expose :user_xids, documentation: { type: 'string', example: 'user1,user2' }
 
         expose :path do |list|
           project_feature_flags_user_list_path(list.project, list)
diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb
index 1846ddf683322..4ed288ee997bd 100644
--- a/lib/api/feature_flags.rb
+++ b/lib/api/feature_flags.rb
@@ -63,7 +63,8 @@ class FeatureFlags < ::API::Base
           optional :version, type: String, desc: 'The version of the feature flag. Must be `new_version_flag`. Omit to create a Legacy feature flag.'
           optional :strategies, type: Array do
             requires :name, type: String, desc: 'The strategy name. Can be `default`, `gradualRolloutUserId`, `userWithId`, or `gitlabUserList`. In GitLab 13.5 and later, can be `flexibleRollout`'
-            requires :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' }
+            optional :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' }
+            optional :user_list_id, type: Integer, desc: "The ID of the feature flag user list. If strategy is `gitlabUserList`."
             optional :scopes, type: Array do
               requires :environment_scope, type: String, desc: 'The environment scope of the scope'
             end
@@ -131,6 +132,7 @@ class FeatureFlags < ::API::Base
             optional :id, type: Integer, desc: 'The feature flag strategy ID'
             optional :name, type: String, desc: 'The strategy name'
             optional :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' }
+            optional :user_list_id, type: Integer, desc: "The ID of the feature flag user list"
             optional :_destroy, type: Boolean, desc: 'Delete the strategy when true'
             optional :scopes, type: Array do
               optional :id, type: Integer, desc: 'The scope id'
diff --git a/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json b/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json
index f572b1a4f9bbb..a72260af14539 100644
--- a/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json
+++ b/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json
@@ -8,7 +8,8 @@
     "id": { "type": "integer" },
     "name": { "type": "string" },
     "parameters": { "type": "object" },
-    "scopes": { "type": "array", "items": { "$ref": "scope.json" } }
+    "scopes": { "type": "array", "items": { "$ref": "scope.json" } },
+    "user_list": { "type": ["object", "null"], "$ref": "user_list.json" }
   },
   "additionalProperties": false
 }
diff --git a/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json b/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json
new file mode 100644
index 0000000000000..6a9f977e37de6
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json
@@ -0,0 +1,16 @@
+{
+  "type": ["object", "null"],
+  "required": [
+    "id",
+    "iid",
+    "name",
+    "user_xids"
+  ],
+  "properties": {
+    "id": { "type": "integer" },
+    "iid": { "type": "integer" },
+    "name": { "type": "string" },
+    "user_xids": { "type": "string" }
+  },
+  "additionalProperties": false
+}
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index 69e3633de577e..4fb0dfbb0704e 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -111,7 +111,57 @@
             'scopes' => [{
               'id' => scope.id,
               'environment_scope' => 'production'
-            }]
+            }],
+            'user_list' => nil
+          }]
+        }])
+      end
+    end
+
+    context 'with user_list strategy feature flags' do
+      let!(:feature_flag) do
+        create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
+      end
+
+      let!(:user_list) do
+        create(:operations_feature_flag_user_list, project: project)
+      end
+
+      let!(:strategy) do
+        create(:operations_strategy, :gitlab_userlist, user_list: user_list, feature_flag: feature_flag, name: 'gitlabUserList', parameters: {})
+      end
+
+      let!(:scope) do
+        create(:operations_scope, strategy: strategy, environment_scope: 'production')
+      end
+
+      it 'returns the feature flags', :aggregate_failures do
+        subject
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('public_api/v4/feature_flags')
+        expect(json_response).to eq([{
+          'name' => 'feature1',
+          'description' => nil,
+          'active' => true,
+          'version' => 'new_version_flag',
+          'updated_at' => feature_flag.updated_at.as_json,
+          'created_at' => feature_flag.created_at.as_json,
+          'scopes' => [],
+          'strategies' => [{
+            'id' => strategy.id,
+            'name' => 'gitlabUserList',
+            'parameters' => {},
+            'scopes' => [{
+              'id' => scope.id,
+              'environment_scope' => 'production'
+            }],
+            'user_list' => {
+              'id' => user_list.id,
+              'iid' => user_list.iid,
+              'name' => user_list.name,
+              'user_xids' => user_list.user_xids
+            }
           }]
         }])
       end
@@ -162,7 +212,57 @@
             'scopes' => [{
               'id' => scope.id,
               'environment_scope' => 'production'
-            }]
+            }],
+            'user_list' => nil
+          }]
+        })
+      end
+    end
+
+    context 'with user_list strategy feature flag' do
+      let!(:feature_flag) do
+        create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
+      end
+
+      let(:user_list) do
+        create(:operations_feature_flag_user_list, project: project)
+      end
+
+      let!(:strategy) do
+        create(:operations_strategy, :gitlab_userlist, user_list: user_list, feature_flag: feature_flag, name: 'gitlabUserList', parameters: {})
+      end
+
+      let!(:scope) do
+        create(:operations_scope, strategy: strategy, environment_scope: 'production')
+      end
+
+      it 'returns the feature flag', :aggregate_failures do
+        subject
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('public_api/v4/feature_flag')
+        expect(json_response).to eq({
+          'name' => 'feature1',
+          'description' => nil,
+          'active' => true,
+          'version' => 'new_version_flag',
+          'updated_at' => feature_flag.updated_at.as_json,
+          'created_at' => feature_flag.created_at.as_json,
+          'scopes' => [],
+          'strategies' => [{
+            'id' => strategy.id,
+            'name' => 'gitlabUserList',
+            'parameters' => {},
+            'scopes' => [{
+              'id' => scope.id,
+              'environment_scope' => 'production'
+            }],
+            'user_list' => {
+              'id' => user_list.id,
+              'iid' => user_list.iid,
+              'name' => user_list.name,
+              'user_xids' => user_list.user_xids
+            }
           }]
         })
       end
@@ -224,6 +324,10 @@
     end
 
     context 'when creating a version 2 feature flag' do
+      let(:user_list) do
+        create(:operations_feature_flag_user_list, project: project)
+      end
+
       it 'creates a new feature flag' do
         params = {
           name: 'new-feature',
@@ -348,6 +452,32 @@
           environment_scope: 'staging'
         }])
       end
+
+      it 'creates a new feature flag with user list strategy', :aggregate_failures do
+        params = {
+          name: 'new-feature',
+          version: 'new_version_flag',
+          strategies: [{
+            name: 'gitlabUserList',
+            parameters: {},
+            user_list_id: user_list.id
+          }]
+        }
+
+        post api("/projects/#{project.id}/feature_flags", user), params: params
+
+        expect(response).to have_gitlab_http_status(:created)
+        expect(response).to match_response_schema('public_api/v4/feature_flag')
+
+        feature_flag = project.operations_feature_flags.last
+        expect(feature_flag.name).to eq(params[:name])
+        expect(feature_flag.version).to eq('new_version_flag')
+        expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{
+          name: 'gitlabUserList',
+          parameters: {}
+        }])
+        expect(feature_flag.strategies.first.user_list).to eq(user_list)
+      end
     end
 
     context 'when given invalid parameters' do
@@ -369,6 +499,10 @@
           project: project, active: true, name: 'feature1', description: 'old description')
       end
 
+      let(:user_list) do
+        create(:operations_feature_flag_user_list, project: project)
+      end
+
       it 'returns a 404 if the feature flag does not exist' do
         params = { description: 'new description' }
 
@@ -537,6 +671,30 @@
         }])
       end
 
+      it 'updates an existing feature flag strategy to be gitlab user list strategy', :aggregate_failures do
+        strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
+        params = {
+          strategies: [{
+            id: strategy.id,
+            name: 'gitlabUserList',
+            user_list_id: user_list.id,
+            parameters: {}
+          }]
+        }
+
+        put api("/projects/#{project.id}/feature_flags/feature1", user), params: params
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to match_response_schema('public_api/v4/feature_flag')
+        result = feature_flag.reload.strategies.map { |s| s.slice(:id, :name, :parameters).deep_symbolize_keys }
+        expect(result).to eq([{
+          id: strategy.id,
+          name: 'gitlabUserList',
+          parameters: {}
+        }])
+        expect(feature_flag.strategies.first.user_list).to eq(user_list)
+      end
+
       it 'adds a new gradual rollout strategy to a feature flag' do
         strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
         params = {
-- 
GitLab