Skip to content

Commit 247470e

Browse files
committed
chore: add dynamic parameter unit test
1 parent 8152bb7 commit 247470e

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy