mirror of https://github.com/coder/coder.git
346 lines
9.6 KiB
Go
346 lines
9.6 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/cli/clibase"
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
|
"github.com/coder/coder/v2/pty/ptytest"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
const (
|
|
fakeLicenseJWT = "test.jwt.sig"
|
|
testWarning = "This is a test warning"
|
|
)
|
|
|
|
func TestLicensesAddFake(t *testing.T) {
|
|
t.Parallel()
|
|
// We can't check a real license into the git repo, and can't patch out the keys from here,
|
|
// so instead we have to fake the HTTP interaction.
|
|
t.Run("LFlag", func(t *testing.T) {
|
|
t.Parallel()
|
|
inv := setupFakeLicenseServerTest(t, "licenses", "add", "-l", fakeLicenseJWT)
|
|
pty := attachPty(t, inv)
|
|
clitest.Start(t, inv)
|
|
pty.ExpectMatch("License with ID 1 added")
|
|
})
|
|
t.Run("Prompt", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
inv := setupFakeLicenseServerTest(t, "license", "add")
|
|
pty := attachPty(t, inv)
|
|
errC := make(chan error)
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
pty.ExpectMatch("Paste license:")
|
|
pty.WriteLine(fakeLicenseJWT)
|
|
require.NoError(t, <-errC)
|
|
pty.ExpectMatch("License with ID 1 added")
|
|
})
|
|
t.Run("File", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
dir := t.TempDir()
|
|
filename := filepath.Join(dir, "license.jwt")
|
|
err := os.WriteFile(filename, []byte(fakeLicenseJWT), 0o600)
|
|
require.NoError(t, err)
|
|
inv := setupFakeLicenseServerTest(t, "license", "add", "-f", filename)
|
|
pty := attachPty(t, inv)
|
|
errC := make(chan error)
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
require.NoError(t, <-errC)
|
|
pty.ExpectMatch("License with ID 1 added")
|
|
})
|
|
t.Run("StdIn", func(t *testing.T) {
|
|
t.Parallel()
|
|
inv := setupFakeLicenseServerTest(t, "license", "add", "-f", "-")
|
|
r, w := io.Pipe()
|
|
inv.Stdin = r
|
|
stdout := new(bytes.Buffer)
|
|
inv.Stdout = stdout
|
|
errC := make(chan error)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
_, err := w.Write([]byte(fakeLicenseJWT))
|
|
require.NoError(t, err)
|
|
err = w.Close()
|
|
require.NoError(t, err)
|
|
select {
|
|
case err = <-errC:
|
|
require.NoError(t, err)
|
|
case <-ctx.Done():
|
|
t.Error("timed out")
|
|
}
|
|
assert.Equal(t, "License with ID 1 added\n", stdout.String())
|
|
})
|
|
t.Run("DebugOutput", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
inv := setupFakeLicenseServerTest(t, "licenses", "add", "-l", fakeLicenseJWT, "--debug")
|
|
pty := attachPty(t, inv)
|
|
errC := make(chan error)
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
require.NoError(t, <-errC)
|
|
pty.ExpectMatch("\"f2\": 2")
|
|
})
|
|
}
|
|
|
|
func TestLicensesAddReal(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Fails", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
|
inv, conf := newCLI(
|
|
t,
|
|
"licenses", "add", "-l", fakeLicenseJWT,
|
|
)
|
|
clitest.SetupConfig(t, client, conf) //nolint:gocritic // requires owner
|
|
|
|
waiter := clitest.StartWithWaiter(t, inv)
|
|
var coderError *codersdk.Error
|
|
waiter.RequireAs(&coderError)
|
|
assert.Equal(t, 400, coderError.StatusCode())
|
|
assert.Contains(t, "Invalid license", coderError.Message)
|
|
})
|
|
}
|
|
|
|
func TestLicensesListFake(t *testing.T) {
|
|
t.Parallel()
|
|
// We can't check a real license into the git repo, and can't patch out the keys from here,
|
|
// so instead we have to fake the HTTP interaction.
|
|
t.Run("Mainline", func(t *testing.T) {
|
|
t.Parallel()
|
|
expectedLicenseExpires := time.Date(2024, 4, 6, 16, 53, 35, 0, time.UTC)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
inv := setupFakeLicenseServerTest(t, "licenses", "list", "-o", "json")
|
|
stdout := new(bytes.Buffer)
|
|
inv.Stdout = stdout
|
|
errC := make(chan error)
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
require.NoError(t, <-errC)
|
|
var licenses []codersdk.License
|
|
err := json.Unmarshal(stdout.Bytes(), &licenses)
|
|
require.NoError(t, err)
|
|
require.Len(t, licenses, 2)
|
|
assert.Equal(t, int32(1), licenses[0].ID)
|
|
assert.Equal(t, "claim1", licenses[0].Claims["h1"])
|
|
assert.Equal(t, int32(5), licenses[1].ID)
|
|
assert.Equal(t, "claim2", licenses[1].Claims["h2"])
|
|
expiresClaim := licenses[0].Claims["license_expires_human"]
|
|
expiresString, ok := expiresClaim.(string)
|
|
require.True(t, ok, "license_expires_human claim is not a string")
|
|
assert.NotEmpty(t, expiresClaim)
|
|
expiresTime, err := time.Parse(time.RFC3339, expiresString)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedLicenseExpires, expiresTime.UTC())
|
|
})
|
|
}
|
|
|
|
func TestLicensesListReal(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
|
inv, conf := newCLI(
|
|
t,
|
|
"licenses", "list", "-o", "json",
|
|
)
|
|
stdout := new(bytes.Buffer)
|
|
inv.Stdout = stdout
|
|
stderr := new(bytes.Buffer)
|
|
inv.Stderr = stderr
|
|
clitest.SetupConfig(t, client, conf) //nolint:gocritic // requires owner
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
errC := make(chan error)
|
|
go func() {
|
|
errC <- inv.WithContext(ctx).Run()
|
|
}()
|
|
require.NoError(t, <-errC)
|
|
assert.Equal(t, "[]\n", stdout.String())
|
|
assert.Contains(t, testWarning, stderr.String())
|
|
})
|
|
}
|
|
|
|
func TestLicensesDeleteFake(t *testing.T) {
|
|
t.Parallel()
|
|
// We can't check a real license into the git repo, and can't patch out the keys from here,
|
|
// so instead we have to fake the HTTP interaction.
|
|
t.Run("Mainline", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
inv := setupFakeLicenseServerTest(t, "licenses", "delete", "55")
|
|
pty := attachPty(t, inv)
|
|
|
|
clitest.Start(t, inv)
|
|
pty.ExpectMatch("License with ID 55 deleted")
|
|
})
|
|
}
|
|
|
|
func TestLicensesDeleteReal(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
|
inv, conf := newCLI(
|
|
t,
|
|
"licenses", "delete", "1")
|
|
clitest.SetupConfig(t, client, conf) //nolint:gocritic // requires owner
|
|
|
|
var coderError *codersdk.Error
|
|
clitest.StartWithWaiter(t, inv).RequireAs(&coderError)
|
|
assert.Equal(t, 404, coderError.StatusCode())
|
|
assert.Contains(t, "Unknown license ID", coderError.Message)
|
|
})
|
|
}
|
|
|
|
func setupFakeLicenseServerTest(t *testing.T, args ...string) *clibase.Invocation {
|
|
t.Helper()
|
|
s := httptest.NewServer(newFakeLicenseAPI(t))
|
|
t.Cleanup(s.Close)
|
|
|
|
inv, conf := newCLI(t, args...)
|
|
|
|
err := conf.URL().Write(s.URL)
|
|
require.NoError(t, err)
|
|
err = conf.Session().Write("sessiontoken")
|
|
require.NoError(t, err)
|
|
|
|
return inv
|
|
}
|
|
|
|
func attachPty(t *testing.T, inv *clibase.Invocation) *ptytest.PTY {
|
|
pty := ptytest.New(t)
|
|
inv.Stdin = pty.Input()
|
|
inv.Stdout = pty.Output()
|
|
return pty
|
|
}
|
|
|
|
func newFakeLicenseAPI(t *testing.T) http.Handler {
|
|
r := chi.NewRouter()
|
|
a := &fakeLicenseAPI{t: t, r: r}
|
|
r.NotFound(a.notFound)
|
|
r.Post("/api/v2/licenses", a.postLicense)
|
|
r.Get("/api/v2/licenses", a.licenses)
|
|
r.Get("/api/v2/buildinfo", a.noop)
|
|
r.Get("/api/v2/users/me", a.noop)
|
|
r.Delete("/api/v2/licenses/{id}", a.deleteLicense)
|
|
r.Get("/api/v2/entitlements", a.entitlements)
|
|
return r
|
|
}
|
|
|
|
type fakeLicenseAPI struct {
|
|
t *testing.T
|
|
r chi.Router
|
|
}
|
|
|
|
func (s *fakeLicenseAPI) notFound(_ http.ResponseWriter, r *http.Request) {
|
|
s.t.Errorf("unexpected HTTP call: %s", r.URL.Path)
|
|
}
|
|
|
|
func (*fakeLicenseAPI) noop(_ http.ResponseWriter, _ *http.Request) {}
|
|
|
|
func (s *fakeLicenseAPI) postLicense(rw http.ResponseWriter, r *http.Request) {
|
|
var req codersdk.AddLicenseRequest
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
require.NoError(s.t, err)
|
|
assert.Equal(s.t, "test.jwt.sig", req.License)
|
|
|
|
resp := codersdk.License{
|
|
ID: 1,
|
|
UploadedAt: time.Now(),
|
|
Claims: map[string]interface{}{
|
|
"h1": "claim1",
|
|
"features": map[string]int64{
|
|
"f1": 1,
|
|
"f2": 2,
|
|
},
|
|
},
|
|
}
|
|
rw.WriteHeader(http.StatusCreated)
|
|
err = json.NewEncoder(rw).Encode(resp)
|
|
assert.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *fakeLicenseAPI) licenses(rw http.ResponseWriter, _ *http.Request) {
|
|
resp := []codersdk.License{
|
|
{
|
|
ID: 1,
|
|
UploadedAt: time.Now(),
|
|
Claims: map[string]interface{}{
|
|
"license_expires": 1712422415,
|
|
"h1": "claim1",
|
|
"features": map[string]int64{
|
|
"f1": 1,
|
|
"f2": 2,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: 5,
|
|
UploadedAt: time.Now(),
|
|
Claims: map[string]interface{}{
|
|
"h2": "claim2",
|
|
"features": map[string]int64{
|
|
"f3": 3,
|
|
"f4": 4,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
err := json.NewEncoder(rw).Encode(resp)
|
|
assert.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *fakeLicenseAPI) deleteLicense(rw http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(s.t, "55", chi.URLParam(r, "id"))
|
|
rw.WriteHeader(200)
|
|
}
|
|
|
|
func (*fakeLicenseAPI) entitlements(rw http.ResponseWriter, r *http.Request) {
|
|
features := make(map[codersdk.FeatureName]codersdk.Feature)
|
|
for _, f := range codersdk.FeatureNames {
|
|
features[f] = codersdk.Feature{
|
|
Entitlement: codersdk.EntitlementEntitled,
|
|
Enabled: true,
|
|
}
|
|
}
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Entitlements{
|
|
Features: features,
|
|
Warnings: []string{testWarning},
|
|
HasLicense: true,
|
|
})
|
|
}
|