1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "context"
7 "slices"
8 "testing"
9
10 "net/http/httptest"
11
12 "connectrpc.com/connect"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15
16 pb "bnema.dev/ermite-api/gen/ermite"
17 "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
18 "bnema.dev/ermite/internal/domain"
19 "bnema.dev/ermite/internal/ports/mocks"
20)
21
22func TestServerServiceGetVersion(t *testing.T) {
23 t.Parallel()
24 advertised := []string{"one", "two"}
25 svc := &serverService{version: "v1.2.3", features: advertised, key: mocks.NewSigningKeyService(t)}
26 resp, err := svc.GetVersion(context.Background(), connect.NewRequest(&pb.GetVersionRequest{}))
27 require.NoError(t, err)
28 assert.Equal(t, "v1.2.3", resp.Msg.GetVersion())
29 assert.Equal(t, []string{"one", "two"}, resp.Msg.GetFeatures())
30
31 // The caller gets a copy: changing it does not change what is advertised.
32 resp.Msg.Features[0] = "tampered"
33 assert.Equal(t, []string{"one", "two"}, advertised)
34}
35
36func TestServerServiceGetSigningKey(t *testing.T) {
37 t.Parallel()
38 key := mocks.NewSigningKeyService(t)
39 key.EXPECT().Info().Return(domain.SigningKeyInfo{
40 Key: domain.SigningPublicKey{AuthorizedKey: "ssh-ed25519 AAAA", Fingerprint: "SHA256:xyz"},
41 Committer: domain.Identity{Name: "ermite", Email: "ermite@example.com"},
42 }).Once()
43 resp, err := (&serverService{key: key}).GetSigningKey(context.Background(), connect.NewRequest(&pb.GetSigningKeyRequest{}))
44 require.NoError(t, err)
45 assert.Equal(t, "ssh-ed25519 AAAA", resp.Msg.GetPublicKey())
46 assert.Equal(t, "SHA256:xyz", resp.Msg.GetFingerprint())
47 assert.Equal(t, "ermite", resp.Msg.GetCommitterName())
48 assert.Equal(t, "ermite@example.com", resp.Msg.GetCommitterEmail())
49 assert.Equal(t, "ermite@example.com ssh-ed25519 AAAA", resp.Msg.GetAllowedSigners())
50}
51
52// An API built without an OIDC key service answers unimplemented instead of
53// panicking, for the owner and for anyone else.
54func TestRotateOIDCKeyWithoutService(t *testing.T) {
55 t.Parallel()
56 for _, p := range []domain.Principal{domain.Owner(), {}} {
57 ctx := domain.ContextWithPrincipal(context.Background(), p)
58 _, err := (&serverService{}).RotateOIDCKey(ctx, connect.NewRequest(&pb.RotateOIDCKeyRequest{}))
59 assert.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
60 }
61}
62
63// GetVersion lists "passkeys" only when the API is built with passkeys.
64func TestAPIListsPasskeysFeatureWhenAvailable(t *testing.T) {
65 t.Parallel()
66 list := func(cfg Config) []string {
67 cfg.Sessions = mocks.NewSessionService(t)
68 cfg.SigningKey = mocks.NewSigningKeyService(t)
69 srv := httptest.NewServer(New(cfg).Handler())
70 t.Cleanup(srv.Close)
71 c := ermiteconnect.NewServerServiceClient(srv.Client(), srv.URL)
72 resp, err := c.GetVersion(context.Background(), connect.NewRequest(&pb.GetVersionRequest{}))
73 require.NoError(t, err)
74 return resp.Msg.GetFeatures()
75 }
76 assert.Equal(t, Features, list(Config{}))
77 got := list(Config{Passkeys: mocks.NewPasskeyService(t)})
78 assert.Equal(t, append(slices.Clone(Features), "passkeys"), got)
79 assert.NotContains(t, Features, "passkeys", "the shared list is not modified")
80}