coder/enterprise/cli/licenses_test.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,
})
}