310 lines
9.4 KiB
Go
310 lines
9.4 KiB
Go
package improvekandev
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/kandev/kandev/internal/auth/authn"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/system/logbundle"
|
|
taskmodels "github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// fakeGitHubInfo is a configurable GitHubInfo for unit tests. Each method
|
|
// returns the value of the corresponding field; if the err counterpart is
|
|
// non-nil, the value is ignored and the error is returned instead.
|
|
type fakeGitHubInfo struct {
|
|
login string
|
|
loginErr error
|
|
hasWrite bool
|
|
hasWriteErr error
|
|
hasFork bool
|
|
hasForkErr error
|
|
calledHasFork bool
|
|
}
|
|
|
|
func (f *fakeGitHubInfo) GetAuthenticatedLogin(_ context.Context) (string, error) {
|
|
return f.login, f.loginErr
|
|
}
|
|
|
|
func (f *fakeGitHubInfo) HasRepoWriteAccess(_ context.Context, _, _ string) (bool, error) {
|
|
return f.hasWrite, f.hasWriteErr
|
|
}
|
|
|
|
func (f *fakeGitHubInfo) UserHasFork(_ context.Context, _, _ string) (bool, error) {
|
|
f.calledHasFork = true
|
|
return f.hasFork, f.hasForkErr
|
|
}
|
|
|
|
func newTestHandler(gh GitHubInfo) *Handler {
|
|
return &Handler{gh: gh, log: logger.Default()}
|
|
}
|
|
|
|
func TestCanonicalWorkspaceID(t *testing.T) {
|
|
const canonical = "123e4567-e89b-12d3-a456-426614174000"
|
|
got, err := canonicalWorkspaceID("123E4567-E89B-12D3-A456-426614174000")
|
|
if err != nil {
|
|
t.Fatalf("canonicalWorkspaceID() error = %v", err)
|
|
}
|
|
if got != canonical {
|
|
t.Fatalf("canonicalWorkspaceID() = %q, want %q", got, canonical)
|
|
}
|
|
if _, err := canonicalWorkspaceID("../workspace"); err == nil {
|
|
t.Fatal("canonicalWorkspaceID() expected invalid UUID error")
|
|
}
|
|
}
|
|
|
|
type fakeDiagnosticBundles struct {
|
|
path string
|
|
owner string
|
|
id string
|
|
}
|
|
|
|
func (f *fakeDiagnosticBundles) OpenArchive(owner, id string) (*os.File, logbundle.JobView, error) {
|
|
f.owner, f.id = owner, id
|
|
file, err := os.Open(f.path)
|
|
return file, logbundle.JobView{ID: id, Status: logbundle.StatusReady, Sources: []string{"backend", "frontend"}}, err
|
|
}
|
|
|
|
func TestLeaseBundleChecksMarkerOwnerAndCopiesArchive(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
dir, err := createBundleDir("user-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
|
source := filepath.Join(t.TempDir(), "source.zip")
|
|
if err := os.WriteFile(source, []byte("zip bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bundles := &fakeDiagnosticBundles{path: source}
|
|
handler := &Handler{log: logger.Default(), logBundles: bundles}
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
authn.SetOnGin(c, authn.Identity{UserID: "user-1"})
|
|
c.Next()
|
|
})
|
|
router.POST("/lease", handler.httpLeaseBundle)
|
|
|
|
body, _ := json.Marshal(leaseBundleRequest{BundleDir: dir, BundleID: "bundle-1"})
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/lease", bytes.NewReader(body)))
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if bundles.owner != "user-1" || bundles.id != "bundle-1" {
|
|
t.Fatalf("OpenArchive identity = %q/%q", bundles.owner, bundles.id)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, diagnosticFileName))
|
|
if err != nil || string(data) != "zip bytes" {
|
|
t.Fatalf("leased archive = %q, err=%v", data, err)
|
|
}
|
|
|
|
otherDir, err := createBundleDir("user-2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(otherDir) })
|
|
body, _ = json.Marshal(leaseBundleRequest{BundleDir: otherDir, BundleID: "bundle-1"})
|
|
recorder = httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/lease", bytes.NewReader(body)))
|
|
if recorder.Code != http.StatusForbidden {
|
|
t.Fatalf("other owner status = %d, want 403", recorder.Code)
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_Writable(t *testing.T) {
|
|
gh := &fakeGitHubInfo{login: "alice", hasWrite: true}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.login != "alice" || !access.hasWrite {
|
|
t.Fatalf("login/write mismatch: %+v", access)
|
|
}
|
|
if access.forkStatus != ForkStatusWritable {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusWritable)
|
|
}
|
|
if gh.calledHasFork {
|
|
t.Errorf("writable users should short-circuit before the fork check")
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_ForkAlreadyExists(t *testing.T) {
|
|
gh := &fakeGitHubInfo{login: "bob_corp", hasWrite: false, hasFork: true}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.forkStatus != ForkStatusReady {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusReady)
|
|
}
|
|
if access.forkMessage != "" {
|
|
t.Errorf("ready status must not include a fork_message even for EMU-shaped logins: %q", access.forkMessage)
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_BlockedEMU(t *testing.T) {
|
|
gh := &fakeGitHubInfo{login: "eve_corp", hasWrite: false, hasFork: false}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.forkStatus != ForkStatusBlockedEMU {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusBlockedEMU)
|
|
}
|
|
if access.forkMessage == "" {
|
|
t.Errorf("blocked_emu must include a fork_message for the dialog")
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_UnknownOnLoginError(t *testing.T) {
|
|
gh := &fakeGitHubInfo{loginErr: errors.New("auth failed")}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.login != "" || access.hasWrite {
|
|
t.Errorf("login error must yield empty access: %+v", access)
|
|
}
|
|
if access.forkStatus != ForkStatusUnknown {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusUnknown)
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_UnknownOnForkLookupError(t *testing.T) {
|
|
gh := &fakeGitHubInfo{login: "carol_corp", hasWrite: false, hasForkErr: errors.New("network")}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.forkStatus != ForkStatusUnknown {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusUnknown)
|
|
}
|
|
if access.forkMessage != "" {
|
|
t.Errorf("fork lookup failures must not produce an EMU message even for underscore logins")
|
|
}
|
|
}
|
|
|
|
func TestResolveGitHubAccess_NoForkNotEMU(t *testing.T) {
|
|
gh := &fakeGitHubInfo{login: "frank", hasWrite: false, hasFork: false}
|
|
access := newTestHandler(gh).resolveGitHubAccess(context.Background())
|
|
if access.forkStatus != ForkStatusUnknown {
|
|
t.Errorf("fork status = %q, want %q", access.forkStatus, ForkStatusUnknown)
|
|
}
|
|
if access.forkMessage != "" {
|
|
t.Errorf("non-EMU users should not get a fork_message")
|
|
}
|
|
}
|
|
|
|
func TestIsEMULogin(t *testing.T) {
|
|
cases := []struct {
|
|
login string
|
|
want bool
|
|
}{
|
|
{"alice", false},
|
|
{"alice-bob", false},
|
|
{"", false},
|
|
{"alice_corp", true},
|
|
{"Carlos-Florencio-ii3_nbcuni", true},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := isEMULogin(tc.login); got != tc.want {
|
|
t.Errorf("isEMULogin(%q) = %v, want %v", tc.login, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindKandevRepoByLocalRemote(t *testing.T) {
|
|
resolver := func(path string) (string, string, string) {
|
|
switch path {
|
|
case "/home/u/kandev":
|
|
return "github", "kdlbs", "kandev"
|
|
case "/home/u/fork":
|
|
return "github", "alice", "kandev"
|
|
case "/home/u/other":
|
|
return "github", "kdlbs", "other"
|
|
}
|
|
return "", "", ""
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
repos []*taskmodels.Repository
|
|
want string // matched repo ID, or "" for no match
|
|
}{
|
|
{name: "empty list", repos: nil, want: ""},
|
|
{
|
|
name: "skips entries without local path",
|
|
repos: []*taskmodels.Repository{
|
|
{ID: "no-path", LocalPath: ""},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "matches remote owner/name with no provider info",
|
|
repos: []*taskmodels.Repository{
|
|
{ID: "match", LocalPath: "/home/u/kandev"},
|
|
},
|
|
want: "match",
|
|
},
|
|
{
|
|
name: "skips fork (different owner)",
|
|
repos: []*taskmodels.Repository{
|
|
{ID: "fork", LocalPath: "/home/u/fork"},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "skips repo already wired to a different provider",
|
|
repos: []*taskmodels.Repository{
|
|
{ID: "wired", LocalPath: "/home/u/kandev", Provider: "github", ProviderOwner: "someone", ProviderName: "kandev"},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "returns first match when multiple candidates",
|
|
repos: []*taskmodels.Repository{
|
|
{ID: "first", LocalPath: "/home/u/kandev"},
|
|
{ID: "second", LocalPath: "/home/u/kandev"},
|
|
},
|
|
want: "first",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := findKandevRepoByLocalRemote(tc.repos, resolver)
|
|
if tc.want == "" {
|
|
if got != nil {
|
|
t.Errorf("expected no match, got %q", got.ID)
|
|
}
|
|
return
|
|
}
|
|
if got == nil || got.ID != tc.want {
|
|
t.Errorf("got = %v, want id = %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindKandevRepoByLocalRemote_NilResolver(t *testing.T) {
|
|
repos := []*taskmodels.Repository{{ID: "x", LocalPath: "/p"}}
|
|
if got := findKandevRepoByLocalRemote(repos, nil); got != nil {
|
|
t.Errorf("nil resolver must return nil, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestIsGHNotFound(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{"nil", nil, false},
|
|
{"unrelated", errors.New("network unreachable"), false},
|
|
{"http 404", errors.New("gh api: exit status 1: HTTP 404: Not Found"), true},
|
|
{"http 403", errors.New("gh api: HTTP 403: Forbidden"), false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := isGHNotFound(tc.err); got != tc.want {
|
|
t.Errorf("isGHNotFound(%v) = %v, want %v", tc.err, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|