Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ func TestHTTPHubClientWhoAmI(t *testing.T) {
}
}

func TestHTTPHubClientWhoAmIBearer(t *testing.T) {
hub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != WhoAmIPath {
http.NotFound(w, r)
return
}
if got := r.Header.Get("Authorization"); got != "Bearer good-token" {
http.Error(w, `{"error":"expired"}`, http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(Identity{Username: "bear", DisplayName: "Bearer User"})
}))
defer hub.Close()

c := &HTTPHubClient{BaseURL: hub.URL}
id, err := c.WhoAmIBearer(context.Background(), "good-token")
if err != nil {
t.Fatalf("valid bearer token: %v", err)
}
if id.Username != "bear" || id.DisplayName != "Bearer User" {
t.Fatalf("identity mismatch: %+v", id)
}
if _, err := c.WhoAmIBearer(context.Background(), "bad-token"); !errors.Is(err, ErrUnauthenticated) {
t.Fatalf("bad bearer token: want ErrUnauthenticated, got %v", err)
}
if _, err := c.WhoAmIBearer(context.Background(), ""); !errors.Is(err, ErrUnauthenticated) {
t.Fatalf("empty bearer token: want ErrUnauthenticated, got %v", err)
}
}

func newTestMiddleware(hub HubClient) *Middleware {
return &Middleware{
Hub: hub,
Expand Down
144 changes: 144 additions & 0 deletions pkg/history/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"sync"
"testing"
"time"

"github.com/hivecommons/dibs/pkg/registry"
"github.com/hivecommons/dibs/pkg/settle"
)

Expand Down Expand Up @@ -262,3 +266,143 @@ func TestBackfillRateLimitDoesNotPersistPartialPulls(t *testing.T) {
t.Fatalf("rate-limited pulls should not persist partial history")
}
}

func TestNewBackfillerDefaults(t *testing.T) {
st, err := NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
b := NewBackfiller(st, "token")
if b.Store != st || b.Token != "token" {
t.Fatalf("backfiller dependencies not wired: %+v", b)
}
if b.Client == nil || b.Logf == nil {
t.Fatal("backfiller defaults missing client or logger")
}
if b.now().IsZero() {
t.Fatal("default clock returned zero time")
}
}

func TestStoreListCopiesAndSorts(t *testing.T) {
st, err := NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
if err := st.Upsert("z/repo", []DayActivity{{Date: "2026-08-18", MergedPRs: 1}}, fixedNow()); err != nil {
t.Fatalf("Upsert z/repo: %v", err)
}
if err := st.Upsert("a/repo", []DayActivity{{Date: "2026-08-18", MergedPRs: 2}}, fixedNow()); err != nil {
t.Fatalf("Upsert a/repo: %v", err)
}
list := st.List()
if len(list) != 2 || list[0].RepoID != "a/repo" || list[1].RepoID != "z/repo" {
t.Fatalf("List not sorted by repo: %+v", list)
}
list[0].Days[0].MergedPRs = 99
got, _ := st.Get("a/repo")
if got.Days[0].MergedPRs != 2 {
t.Fatalf("List returned mutable store internals: %+v", got.Days[0])
}
}

func TestRefreshAsyncRunsOnceAndSkipsFresh(t *testing.T) {
release := make(chan struct{})
issueCalls := make(chan string, 4)
var once sync.Once
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/repos/org/repo/issues":
issueCalls <- r.URL.RawQuery
once.Do(func() { <-release })
_ = json.NewEncoder(w).Encode([]map[string]any{})
case "/repos/org/repo/pulls":
_ = json.NewEncoder(w).Encode([]map[string]any{})
default:
http.NotFound(w, r)
}
}))
defer srv.Close()

st, err := NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
b := &Backfiller{Store: st, BaseURL: srv.URL, Client: srv.Client(), Now: fixedNow, Logf: t.Logf}
repos := []registry.RepoProfile{{RepoID: "org/repo"}}
b.RefreshAsync(repos)
select {
case <-issueCalls:
case <-time.After(5 * time.Second):
t.Fatal("first RefreshAsync never started")
}
b.RefreshAsync(repos)
select {
case q := <-issueCalls:
t.Fatalf("second RefreshAsync started while first was active: %s", q)
case <-time.After(50 * time.Millisecond):
}
close(release)

deadline := time.After(5 * time.Second)
for {
if h, ok := st.Get("org/repo"); ok && len(h.Days) == windowDays {
break
}
select {
case <-deadline:
t.Fatal("RefreshAsync never persisted history")
case <-time.After(10 * time.Millisecond):
}
}
b.RefreshAsync(repos)
select {
case q := <-issueCalls:
t.Fatalf("fresh history should not refresh again: %s", q)
case <-time.After(50 * time.Millisecond):
}
var nilBackfiller *Backfiller
nilBackfiller.RefreshAsync(repos)
(&Backfiller{}).RefreshAsync(repos)
}

func TestFetchMergedPullRequestsFiltersSortsAndAuthenticates(t *testing.T) {
newer := time.Date(2026, 8, 18, 12, 0, 0, 0, time.UTC)
older := time.Date(2026, 8, 17, 12, 0, 0, 0, time.UTC)
var sawAuth, sawVersion string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/repos/org/repo/pulls" {
t.Fatalf("unexpected path %s", r.URL.Path)
}
q, _ := url.ParseQuery(r.URL.RawQuery)
if q.Get("state") != "closed" || q.Get("sort") != "updated" || q.Get("direction") != "desc" {
t.Fatalf("unexpected query %s", r.URL.RawQuery)
}
sawAuth = r.Header.Get("Authorization")
sawVersion = r.Header.Get("X-GitHub-Api-Version")
_ = json.NewEncoder(w).Encode([]map[string]any{
{"title": " older merge ", "merged_at": older, "user": map[string]string{"login": "alice"}},
{"title": "closed only", "merged_at": nil, "user": map[string]string{"login": "bob"}},
{"title": "newer merge", "merged_at": newer, "user": map[string]string{"login": "carol"}},
})
}))
defer srv.Close()

b := &Backfiller{BaseURL: srv.URL, Token: "token", Client: srv.Client(), Now: fixedNow}
got, err := b.FetchMergedPullRequests(context.Background(), "org/repo")
if err != nil {
t.Fatalf("FetchMergedPullRequests: %v", err)
}
if sawAuth != "Bearer token" || sawVersion != "2022-11-28" {
t.Fatalf("headers Authorization=%q X-GitHub-Api-Version=%q", sawAuth, sawVersion)
}
if len(got) != 2 {
t.Fatalf("got %d merged PRs: %+v", len(got), got)
}
if got[0].Title != "newer merge" || got[0].Author != "carol" || !got[0].MergedAt.Equal(newer) {
t.Fatalf("newest PR not first/trimmed: %+v", got[0])
}
if got[1].Title != "older merge" || strings.TrimSpace(got[1].Title) != got[1].Title {
t.Fatalf("older PR not trimmed: %+v", got[1])
}
}
22 changes: 22 additions & 0 deletions pkg/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ func TestSyncAndList(t *testing.T) {
}
}

func TestListByOwnerFiltersAndCopies(t *testing.T) {
r, _ := newTestRegistry(t)
if err := r.Merge(sampleRepos()); err != nil {
t.Fatalf("Merge: %v", err)
}
alice := r.ListByOwner("alice")
if len(alice) != 1 || alice[0].RepoID != "kubestellar/kubestellar" {
t.Fatalf("alice repos = %+v", alice)
}
alice[0].Description = "mutated"
rp, err := r.Get("kubestellar/kubestellar")
if err != nil {
t.Fatalf("Get: %v", err)
}
if rp.Description == "mutated" {
t.Fatal("ListByOwner returned mutable registry internals")
}
if got := r.ListByOwner("nobody"); len(got) != 0 {
t.Fatalf("unknown owner repos = %+v, want none", got)
}
}

// TestSyncPreservesLocalEdits: a hub re-sync must not clobber owner toggles.
func TestSyncPreservesLocalEdits(t *testing.T) {
r, _ := newTestRegistry(t)
Expand Down
27 changes: 27 additions & 0 deletions pkg/server/wave2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,33 @@ func TestPrivateIdeaNeverSurfacesBeforeOffer(t *testing.T) {
}
}

func TestIdeaMatchesSeenMarksCurrentSuggestionsForAuthor(t *testing.T) {
f := newWave2Server(t, &settle.Fake{})
idea := f.createIdea(t, "bob-session", "Kubernetes marketplace boost",
"A kubernetes marketplace for ideas.", "public")
matchesUpdatedAt := time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)
if _, err := f.store.Mutate(idea.ID, false, func(i *store.Idea) error {
i.MatchesUpdatedAt = matchesUpdatedAt
i.Matches = []store.Match{{RepoID: "kubestellar/dibs", Score: 91, RepoHash: "hash"}}
return nil
}); err != nil {
t.Fatalf("seed matches: %v", err)
}

rec := doJSON(t, f.h, "POST", "/api/ideas/"+idea.ID+"/matches/seen", "alice-session", nil)
if rec.Code != http.StatusForbidden {
t.Fatalf("non-author matches/seen status = %d, want 403", rec.Code)
}
rec = doJSON(t, f.h, "POST", "/api/ideas/"+idea.ID+"/matches/seen", "bob-session", nil)
if rec.Code != http.StatusOK {
t.Fatalf("matches/seen: %d %s", rec.Code, rec.Body.String())
}
got := decode[store.Idea](t, rec)
if !got.SuggestionsSeenAt.Equal(matchesUpdatedAt) {
t.Fatalf("SuggestionsSeenAt = %v, want %v", got.SuggestionsSeenAt, matchesUpdatedAt)
}
}

// TestOfferAcceptSettleFlow: the LEGACY happy path end to end (a GitHub
// client is configured, so accept still opens the credited issue
// server-side) — match, offer, accept, credited issue, notifications.
Expand Down
Loading