1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "context"
7 "testing"
8 "time"
9
10 pb "bnema.dev/ermite-api/gen/ermite"
11 "bnema.dev/ermite/internal/domain"
12 "bnema.dev/ermite/internal/ports"
13 "bnema.dev/ermite/internal/ports/mocks"
14 "connectrpc.com/connect"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/mock"
17 "github.com/stretchr/testify/require"
18)
19
20func TestMirrorConfigPatchPresence(t *testing.T) {
21 service := mocks.NewMirrorService(t)
22 disabled := false
23 empty := ""
24 patch := ports.MirrorGlobalPatch{Enabled: &disabled, URLTemplate: &empty}
25 service.EXPECT().SetGlobalConfig(mock.Anything, domain.Principal{}, patch).Return(domain.MirrorGlobalConfig{}, nil).Once()
26 s := &mirrorService{service}
27 res, err := s.SetMirrorConfig(context.Background(), connect.NewRequest(&pb.SetMirrorConfigRequest{Enabled: &disabled, UrlTemplate: &empty}))
28 require.NoError(t, err)
29 assert.False(t, res.Msg.GetConfig().GetEnabled())
30}
31
32func TestMirrorSyncSelector(t *testing.T) {
33 s := &mirrorService{mocks.NewMirrorService(t)}
34 for _, req := range []*pb.SyncMirrorRequest{{}, {Repo: "repo", All: true}, {Repo: "../invalid"}} {
35 _, err := s.SyncMirror(context.Background(), connect.NewRequest(req))
36 assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
37 }
38}
39
40func TestMirrorSyncResponse(t *testing.T) {
41 service := mocks.NewMirrorService(t)
42 name, err := domain.ParseRepoName("repo")
43 require.NoError(t, err)
44 status := domain.MirrorStatus{Repo: name, State: domain.MirrorPaused, Class: domain.MirrorClassDiverged, Reason: "remote diverged", ConsecutiveFailures: 1}
45 target := domain.MirrorTarget{Repo: name, Enabled: true, Source: domain.MirrorSourceOverride}
46 service.EXPECT().Sync(mock.Anything, domain.Principal{}, name, true).Return(ports.MirrorSyncResult{Repo: name, Status: status}, nil).Once()
47 service.EXPECT().GetStatus(mock.Anything, domain.Principal{}, name).Return(ports.MirrorStatusView{Target: target, Status: status}, nil).Once()
48 s := &mirrorService{service}
49 res, err := s.SyncMirror(context.Background(), connect.NewRequest(&pb.SyncMirrorRequest{Repo: "repo", Force: true}))
50 require.NoError(t, err)
51 require.Len(t, res.Msg.Results, 1)
52 assert.Equal(t, "repo", res.Msg.Results[0].Repo)
53 assert.Equal(t, pb.MirrorState_MIRROR_STATE_PAUSED, res.Msg.Results[0].Status.State)
54 assert.Equal(t, "diverged", res.Msg.Results[0].Status.FailureClass)
55}
56
57func TestMirrorEvents(t *testing.T) {
58 name, err := domain.ParseRepoName("repo")
59 require.NoError(t, err)
60 now := time.Now().UTC()
61 failed := watchEventToPB(ports.WatchEvent{Event: domain.MirrorFailed{Repo: name, State: domain.MirrorPaused, Class: domain.MirrorClassDiverged, At: now}})
62 require.NotNil(t, failed)
63 assert.Equal(t, now, failed.Time.AsTime())
64 assert.Equal(t, &pb.MirrorFailed{Repo: "repo", State: pb.MirrorState_MIRROR_STATE_PAUSED, FailureClass: "diverged"}, failed.GetMirrorFailed())
65 recovered := watchEventToPB(ports.WatchEvent{Event: domain.MirrorRecovered{Repo: name, At: now}})
66 require.NotNil(t, recovered)
67 assert.Equal(t, "repo", recovered.GetMirrorRecovered().Repo)
68 // The public event messages deliberately expose neither URL nor stderr detail.
69 fields := failed.GetMirrorFailed().ProtoReflect().Descriptor().Fields()
70 assert.Nil(t, fields.ByName("url"))
71 assert.Nil(t, fields.ByName("detail"))
72 assert.Nil(t, fields.ByName("reason"))
73}
74
75func TestMirrorSyncAllReportsPerRepositoryErrors(t *testing.T) {
76 service := mocks.NewMirrorService(t)
77 good, bad := repoN(t, "good"), repoN(t, "bad")
78 service.EXPECT().SyncAll(mock.Anything, domain.Principal{}, false).Return([]ports.MirrorSyncResult{
79 {Repo: good, Status: domain.MirrorStatus{Repo: good, State: domain.MirrorOK}},
80 {Repo: bad, Status: domain.MirrorStatus{Repo: bad, State: domain.MirrorPaused}, Err: &domain.MirrorError{Class: domain.MirrorClassDiverged, Detail: "rejected"}},
81 }, nil).Once()
82 service.EXPECT().GetStatus(mock.Anything, domain.Principal{}, good).Return(ports.MirrorStatusView{Status: domain.MirrorStatus{Repo: good, State: domain.MirrorOK}}, nil).Once()
83 service.EXPECT().GetStatus(mock.Anything, domain.Principal{}, bad).Return(ports.MirrorStatusView{Status: domain.MirrorStatus{Repo: bad, State: domain.MirrorPaused}}, nil).Once()
84 res, err := (&mirrorService{service}).SyncMirror(context.Background(), connect.NewRequest(&pb.SyncMirrorRequest{All: true}))
85 require.NoError(t, err)
86 require.Len(t, res.Msg.Results, 2)
87 assert.Empty(t, res.Msg.Results[0].Error)
88 assert.Equal(t, "mirror diverged: see ermite mirror status", res.Msg.Results[1].Error)
89 assert.Equal(t, pb.MirrorState_MIRROR_STATE_PAUSED, res.Msg.Results[1].Status.State)
90}
91
92func TestMirrorRotateReturnsPreviousFingerprint(t *testing.T) {
93 service := mocks.NewMirrorService(t)
94 service.EXPECT().RotateKey(mock.Anything, domain.Principal{}).Return(ports.MirrorKeyRotation{
95 Key: domain.MirrorPublicKey{Fingerprint: "SHA256:new"}, Previous: domain.MirrorPublicKey{Fingerprint: "SHA256:old"},
96 }, nil).Once()
97 res, err := (&mirrorService{service}).RotateMirrorKey(context.Background(), connect.NewRequest(&pb.RotateMirrorKeyRequest{}))
98 require.NoError(t, err)
99 assert.Equal(t, "SHA256:new", res.Msg.Key.Fingerprint)
100 assert.Equal(t, "SHA256:old", res.Msg.PreviousFingerprint)
101}
102
103func repoN(t *testing.T, s string) domain.RepoName {
104 t.Helper()
105 n, err := domain.ParseRepoName(s)
106 require.NoError(t, err)
107 return n
108}