Skip to content
quarry Artifact [059291d2c5]
Login

Artifact [059291d2c5]

Artifact 059291d2c50b14aa87d1ee2df983e595da5a379d4f1831f6a45db252629044f2:


package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/chat"
	"craftdesign.group/quarry/internal/ci"
	"craftdesign.group/quarry/internal/events"
	"craftdesign.group/quarry/internal/oltp"
)

// tokenEngine is dagnats' worker-token surface: a fixed list, and the
// ids it was asked to revoke. listStatus, when set, is what the list
// answers instead.
type tokenEngine struct {
	mu         sync.Mutex
	tokens     []ci.WorkerToken
	revoked    []string
	listStatus int
}

func (e *tokenEngine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	e.mu.Lock()
	defer e.mu.Unlock()
	switch {
	case req.Method == http.MethodGet && req.URL.Path == "/v1/tokens":
		if e.listStatus != 0 {
			w.WriteHeader(e.listStatus)
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"tokens": e.tokens})
	case req.Method == http.MethodDelete && strings.HasPrefix(req.URL.Path, "/v1/tokens/"):
		e.revoked = append(e.revoked, strings.TrimPrefix(req.URL.Path, "/v1/tokens/"))
		w.WriteHeader(http.StatusNoContent)
	default:
		http.NotFound(w, req)
	}
}

// deleteFixture is a registrar holding one deletable repository, "gone",
// on project code c0de, beside a served neighbour "kept" on beef, with
// real chat and event planes.
func deleteFixture(t *testing.T, eng *tokenEngine) (*registrar, *chat.Room, *events.Server, string) {
	t.Helper()
	rg := newTestRegistrar(t, 25)
	rg.dir, rg.reposRoot = t.TempDir(), t.TempDir()
	store := filepath.Join(rg.reposRoot, "gone")
	if err := os.MkdirAll(store, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.AddRepo("gone", store, "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	var mu sync.Mutex
	rg.codes, rg.codeMu = map[string]string{"gone": "c0de", "kept": "beef"}, &mu

	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	room, err := chat.New(ev.Conn(), ev.JS())
	if err != nil {
		t.Fatal(err)
	}
	rg.ev, rg.deps = ev, &api.Deps{Repos: api.NewRepos(nil), Chat: room}

	srv := httptest.NewServer(eng)
	t.Cleanup(srv.Close)
	rg.engine = ci.New(srv.URL)
	rg.engine.SetAdminToken("admin")
	return rg, room, ev, store
}

// Deleting a repository takes what it held outside the forge database
// too: its worker tokens are revoked at the engine, its chat channel is
// emptied, and nobody's inbox still carries it — so a repository created
// later under the same name starts with none of them (quarry#294).
func TestDeleteForgetsTheRepositoryEverywhere(t *testing.T) {
	gone, kept := ci.GroupFor("gone"), ci.GroupFor("kept")
	eng := &tokenEngine{tokens: []ci.WorkerToken{
		{ID: "own", WorkerGroups: []string{gone}},
		{ID: "shared", WorkerGroups: []string{kept, gone}},
		{ID: "neighbour", WorkerGroups: []string{kept}},
		{ID: "fleet"},
		{ID: "done", WorkerGroups: []string{gone}, RevokedAt: "2026-09-01T00:00:00Z"},
	}}
	rg, room, ev, store := deleteFixture(t, eng)
	for _, code := range []string{"c0de", "beef"} {
		if _, err := room.Post(code, chat.Message{User: "dan", Text: "hello " + code}); err != nil {
			t.Fatal(err)
		}
	}
	for _, repo := range []string{"gone", "kept"} {
		if _, err := ev.Attend("ann", events.AttentionEvent{Kind: "mention", Repo: repo}); err != nil {
			t.Fatal(err)
		}
	}
	tok, err := rg.odb.MintReporterToken("gone", "ci")
	if err != nil {
		t.Fatal(err)
	}

	if err := rg.Delete("gone", "gone"); err != nil {
		t.Fatal(err)
	}

	if strings.Join(eng.revoked, ",") != "own,shared" {
		t.Errorf("revoked %v; want exactly the live tokens scoped to the deleted repository's group", eng.revoked)
	}
	if msgs, err := room.Range("c0de", 0, 0); err != nil || len(msgs) != 0 {
		t.Errorf("the deleted repository's chat holds %d messages, %v", len(msgs), err)
	}
	if msgs, err := room.Range("beef", 0, 0); err != nil || len(msgs) != 1 {
		t.Errorf("the neighbour's chat holds %d messages, %v; want 1", len(msgs), err)
	}
	evs, err := ev.RecentAttention("ann", 50)
	if err != nil {
		t.Fatal(err)
	}
	if len(evs) != 1 || evs[0].Repo != "kept" {
		t.Errorf("ann's attention after the delete = %+v; want only kept's", evs)
	}
	if _, err := os.Stat(store); !os.IsNotExist(err) {
		t.Errorf("the store survived: %v", err)
	}

	// Somebody else takes the name.
	if err := rg.odb.AddAccount("eve", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.AddRepo("gone", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.SetOwner("gone", "eve"); err != nil {
		t.Fatal(err)
	}
	if rg.odb.CheckReporterToken("gone", tok) {
		t.Error("the deleted repository's reporter token authenticates for its successor")
	}
}

// A chat channel another served repository also answers to — the same
// project code registered twice — is not the deleted one's to empty.
func TestDeleteLeavesASharedChannel(t *testing.T) {
	rg, room, _, _ := deleteFixture(t, &tokenEngine{})
	rg.codes["twin"] = "c0de"
	if _, err := room.Post("c0de", chat.Message{User: "dan", Text: "ours too"}); err != nil {
		t.Fatal(err)
	}
	if err := rg.Delete("gone", "gone"); err != nil {
		t.Fatal(err)
	}
	if msgs, err := room.Range("c0de", 0, 0); err != nil || len(msgs) != 1 {
		t.Fatalf("the twin's channel holds %d messages, %v; want 1", len(msgs), err)
	}
}

// An engine that cannot say which tokens exist refuses the delete
// before anything is destroyed: a deletion that left a live worker
// token behind is the bug, and the owner can retry.
func TestDeleteRefusesWhenWorkerTokensCannotBeRevoked(t *testing.T) {
	rg, _, _, store := deleteFixture(t, &tokenEngine{listStatus: http.StatusUnauthorized})
	if err := rg.Delete("gone", "gone"); err == nil {
		t.Fatal("the delete went ahead with the worker tokens unaccounted for")
	}
	if _, found, _ := rg.odb.RepoByName("gone"); !found {
		t.Error("the registration went although the delete failed")
	}
	if _, err := os.Stat(store); err != nil {
		t.Errorf("the store went although the delete failed: %v", err)
	}
}

// An engine that manages no worker tokens at all has none to revoke,
// and does not stand in the way of a delete.
func TestDeleteProceedsWhenTheEngineHasNoTokens(t *testing.T) {
	for _, status := range []int{http.StatusServiceUnavailable, http.StatusNotFound} {
		rg, _, _, _ := deleteFixture(t, &tokenEngine{listStatus: status})
		if err := rg.Delete("gone", "gone"); err != nil {
			t.Errorf("engine answering %d: %v", status, err)
		}
	}
	// Nor does one quarry holds no admin token for: quarry could not
	// have minted a token there, and cannot see one to revoke.
	rg, _, _, _ := deleteFixture(t, &tokenEngine{listStatus: http.StatusUnauthorized})
	rg.engine.SetAdminToken("")
	if err := rg.Delete("gone", "gone"); err != nil {
		t.Errorf("an engine quarry holds no admin token for: %v", err)
	}
}