Skip to content

Commit bcfada2

Browse files
committed
chore: add dynamic parameter unit test
1 parent b613955 commit bcfada2

File tree

8 files changed

+389
-64
lines changed

8 files changed

+389
-64
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package coderdtest
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/google/uuid"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/v2/coderd/util/ptr"
12+
"github.com/coder/coder/v2/coderd/util/slice"
13+
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/coder/v2/provisioner/echo"
15+
"github.com/coder/coder/v2/provisionersdk/proto"
16+
)
17+
18+
type DynamicParameterTemplateParams struct {
19+
MainTF string
20+
Plan json.RawMessage
21+
ModulesArchive []byte
22+
23+
// StaticParams is used if the provisioner daemon version does not support dynamic parameters.
24+
StaticParams []*proto.RichParameter
25+
}
26+
27+
func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UUID, args DynamicParameterTemplateParams) (codersdk.Template, codersdk.TemplateVersion) {
28+
t.Helper()
29+
30+
files := echo.WithExtraFiles(map[string][]byte{
31+
"main.tf": []byte(args.MainTF),
32+
})
33+
files.ProvisionPlan = []*proto.Response{{
34+
Type: &proto.Response_Plan{
35+
Plan: &proto.PlanComplete{
36+
Plan: args.Plan,
37+
ModuleFiles: args.ModulesArchive,
38+
Parameters: args.StaticParams,
39+
},
40+
},
41+
}}
42+
43+
version := CreateTemplateVersion(t, client, org, files)
44+
AwaitTemplateVersionJobCompleted(t, client, version.ID)
45+
tpl := CreateTemplate(t, client, org, version.ID)
46+
47+
var err error
48+
tpl, err = client.UpdateTemplateMeta(t.Context(), tpl.ID, codersdk.UpdateTemplateMeta{
49+
UseClassicParameterFlow: ptr.Ref(false),
50+
})
51+
require.NoError(t, err)
52+
53+
return tpl, version
54+
}
55+
56+
type ParameterAsserter struct {
57+
Name string
58+
Params []codersdk.PreviewParameter
59+
t *testing.T
60+
}
61+
62+
func AssertParameter(t *testing.T, name string, params []codersdk.PreviewParameter) *ParameterAsserter {
63+
return &ParameterAsserter{
64+
Name: name,
65+
Params: params,
66+
t: t,
67+
}
68+
}
69+
70+
func (a *ParameterAsserter) find(name string) *codersdk.PreviewParameter {
71+
a.t.Helper()
72+
for _, p := range a.Params {
73+
if p.Name == name {
74+
return &p
75+
}
76+
}
77+
78+
assert.Fail(a.t, "parameter not found", "expected parameter %q to exist", a.Name)
79+
return nil
80+
}
81+
82+
func (a *ParameterAsserter) NotExists() *ParameterAsserter {
83+
a.t.Helper()
84+
85+
names := slice.Convert(a.Params, func(p codersdk.PreviewParameter) string {
86+
return p.Name
87+
})
88+
89+
assert.NotContains(a.t, names, a.Name)
90+
return a
91+
}
92+
93+
func (a *ParameterAsserter) Exists() *ParameterAsserter {
94+
a.t.Helper()
95+
96+
names := slice.Convert(a.Params, func(p codersdk.PreviewParameter) string {
97+
return p.Name
98+
})
99+
100+
assert.Contains(a.t, names, a.Name)
101+
return a
102+
}
103+
104+
func (a *ParameterAsserter) Value(expected string) *ParameterAsserter {
105+
a.t.Helper()
106+
107+
p := a.find(a.Name)
108+
if p == nil {
109+
return a
110+
}
111+
112+
assert.Equal(a.t, expected, p.Value.Value)
113+
return a
114+
}
115+
116+
func (a *ParameterAsserter) Options(expected ...string) *ParameterAsserter {
117+
a.t.Helper()
118+
119+
p := a.find(a.Name)
120+
if p == nil {
121+
return a
122+
}
123+
124+
optValues := slice.Convert(p.Options, func(p codersdk.PreviewParameterOption) string {
125+
return p.Value.Value
126+
})
127+
assert.ElementsMatch(a.t, expected, optValues, "parameter %q options", a.Name)
128+
return a
129+
}

coderd/coderdtest/stream.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package coderdtest
2+
3+
import "github.com/coder/coder/v2/codersdk/wsjson"
4+
5+
// SynchronousStream returns a function that assumes the stream is synchronous.
6+
// Meaning each request sent assumes exactly one response will be received.
7+
// The function will block until the response is received or an error occurs.
8+
//
9+
// This should not be used in production code, as it does not handle edge cases.
10+
// The second function `pop` can be used to retrieve the next response from the
11+
// stream without sending a new request. This is useful for dynamic parameters
12+
func SynchronousStream[R any, W any](stream *wsjson.Stream[R, W]) (do func(W) (R, error), pop func() R) {
13+
rec := stream.Chan()
14+
15+
return func(req W) (R, error) {
16+
err := stream.Send(req)
17+
if err != nil {
18+
return *new(R), err
19+
}
20+
21+
return <-rec, nil
22+
}, func() R {
23+
return <-rec
24+
}
25+
}

coderd/dynamicparameter_test.go

Lines changed: 0 additions & 21 deletions
This file was deleted.

coderd/parameters_test.go

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -368,28 +368,12 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
368368
owner := coderdtest.CreateFirstUser(t, ownerClient)
369369
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
370370

371-
files := echo.WithExtraFiles(map[string][]byte{
372-
"main.tf": args.mainTF,
371+
tpl, version := coderdtest.DynamicParameterTemplate(t, templateAdmin, owner.OrganizationID, coderdtest.DynamicParameterTemplateParams{
372+
MainTF: string(args.mainTF),
373+
Plan: args.plan,
374+
ModulesArchive: args.modulesArchive,
375+
StaticParams: args.static,
373376
})
374-
files.ProvisionPlan = []*proto.Response{{
375-
Type: &proto.Response_Plan{
376-
Plan: &proto.PlanComplete{
377-
Plan: args.plan,
378-
ModuleFiles: args.modulesArchive,
379-
Parameters: args.static,
380-
},
381-
},
382-
}}
383-
384-
version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files)
385-
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID)
386-
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
387-
388-
var err error
389-
tpl, err = templateAdmin.UpdateTemplateMeta(t.Context(), tpl.ID, codersdk.UpdateTemplateMeta{
390-
UseClassicParameterFlow: ptr.Ref(false),
391-
})
392-
require.NoError(t, err)
393377

394378
ctx := testutil.Context(t, testutil.WaitShort)
395379
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, codersdk.Me, version.ID)

coderd/testdata/parameters/dynamic/main.tf

Lines changed: 0 additions & 22 deletions
This file was deleted.

coderd/util/slice/slice.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,16 @@ func CountConsecutive[T comparable](needle T, haystack ...T) int {
217217

218218
return max(maxLength, curLength)
219219
}
220+
221+
// Convert converts a slice of type F to a slice of type T using the provided function f.
222+
func Convert[F any, T any](a []F, f func(F) T) []T {
223+
if a == nil {
224+
return nil
225+
}
226+
227+
tmp := make([]T, 0, len(a))
228+
for _, v := range a {
229+
tmp = append(tmp, f(v))
230+
}
231+
return tmp
232+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package coderd_test
2+
3+
import (
4+
_ "embed"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/rbac"
13+
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
15+
"github.com/coder/coder/v2/enterprise/coderd/license"
16+
"github.com/coder/coder/v2/testutil"
17+
"github.com/coder/websocket"
18+
)
19+
20+
// TestDynamicParameterTemplate uses a template with some dynamic elements, and
21+
// tests the parameters, values, etc are all as expected.
22+
func TestDynamicParameterTemplate(t *testing.T) {
23+
t.Parallel()
24+
25+
owner, _, api, first := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
26+
Options: &coderdtest.Options{IncludeProvisionerDaemon: true},
27+
LicenseOptions: &coderdenttest.LicenseOptions{
28+
Features: license.Features{
29+
codersdk.FeatureTemplateRBAC: 1,
30+
},
31+
},
32+
})
33+
34+
orgID := first.OrganizationID
35+
36+
_, userData := coderdtest.CreateAnotherUser(t, owner, orgID)
37+
templateAdmin, templateAdminData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgTemplateAdmin(orgID))
38+
userAdmin, userAdminData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgUserAdmin(orgID))
39+
_, auditorData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAuditor(orgID))
40+
41+
coderdtest.CreateGroup(t, owner, orgID, "developer", auditorData, userData)
42+
coderdtest.CreateGroup(t, owner, orgID, "admin", templateAdminData, userAdminData)
43+
coderdtest.CreateGroup(t, owner, orgID, "auditor", auditorData, templateAdminData, userAdminData)
44+
45+
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/dynamic/main.tf")
46+
require.NoError(t, err)
47+
48+
_, version := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
49+
MainTF: string(dynamicParametersTerraformSource),
50+
Plan: nil,
51+
ModulesArchive: nil,
52+
StaticParams: nil,
53+
})
54+
55+
var _ = userAdmin
56+
57+
ctx := testutil.Context(t, testutil.WaitLong)
58+
59+
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, userData.ID.String(), version.ID)
60+
require.NoError(t, err)
61+
defer func() {
62+
_ = stream.Close(websocket.StatusNormalClosure)
63+
64+
// Wait until the cache ends up empty. This verifies the cache does not
65+
// leak any files.
66+
require.Eventually(t, func() bool {
67+
return api.AGPL.FileCache.Count() == 0
68+
}, testutil.WaitShort, testutil.IntervalFast, "file cache should be empty after the test")
69+
}()
70+
71+
// Initial response
72+
preview, pop := coderdtest.SynchronousStream(stream)
73+
init := pop()
74+
coderdtest.AssertParameter(t, "isAdmin", init.Parameters).
75+
Exists().Value("false")
76+
coderdtest.AssertParameter(t, "adminonly", init.Parameters).
77+
NotExists()
78+
coderdtest.AssertParameter(t, "groups", init.Parameters).
79+
Exists().Options(database.EveryoneGroup, "developer")
80+
require.Len(t, init.Diagnostics, 0, "no top level diags")
81+
82+
// Switch to an admin
83+
resp, err := preview(codersdk.DynamicParametersRequest{
84+
ID: 1,
85+
Inputs: map[string]string{
86+
"colors": `["red"]`,
87+
"thing": "apple",
88+
},
89+
OwnerID: userAdminData.ID,
90+
})
91+
require.NoError(t, err)
92+
require.Equal(t, resp.ID, 1)
93+
94+
coderdtest.AssertParameter(t, "isAdmin", resp.Parameters).
95+
Exists().Value("true")
96+
coderdtest.AssertParameter(t, "adminonly", resp.Parameters).
97+
Exists()
98+
coderdtest.AssertParameter(t, "groups", resp.Parameters).
99+
Exists().Options(database.EveryoneGroup, "admin", "auditor")
100+
coderdtest.AssertParameter(t, "colors", resp.Parameters).
101+
Exists().Value(`["red"]`)
102+
coderdtest.AssertParameter(t, "thing", resp.Parameters).
103+
Exists().Value("apple").Options("apple", "ruby")
104+
require.Len(t, init.Diagnostics, 0, "no top level diags")
105+
106+
// Try some other colors
107+
resp, err = preview(codersdk.DynamicParametersRequest{
108+
ID: 2,
109+
Inputs: map[string]string{
110+
"colors": `["yellow", "blue"]`,
111+
"thing": "banana",
112+
},
113+
OwnerID: userAdminData.ID,
114+
})
115+
require.NoError(t, err)
116+
require.Equal(t, resp.ID, 2)
117+
118+
coderdtest.AssertParameter(t, "isAdmin", resp.Parameters).
119+
Exists().Value("true")
120+
coderdtest.AssertParameter(t, "colors", resp.Parameters).
121+
Exists().Value(`["yellow", "blue"]`)
122+
coderdtest.AssertParameter(t, "thing", resp.Parameters).
123+
Exists().Value("banana").Options("banana", "ocean", "sky")
124+
}

0 commit comments

Comments
 (0)