1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain_test
4
5import (
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15func TestIsOIDCRequestToken(t *testing.T) {
16 t.Parallel()
17 ok := domain.OIDCRequestTokenPrefix + strings.Repeat("aB3-_", 8) + "xyz"
18 assert.True(t, domain.IsOIDCRequestToken(ok))
19 assert.False(t, domain.IsRepoToken(ok), "not a repository token")
20 for _, bad := range []string{"", domain.OIDCRequestTokenPrefix, ok + "x", strings.Replace(ok, "ermiteoidc_", "ermitegh_", 1)} {
21 assert.False(t, domain.IsOIDCRequestToken(bad), bad)
22 }
23}
24
25// Only a trusted run with id-token: write gets ID tokens: GitHub's rule, plus
26// ermite's trust rule (an untrusted run's writes are lowered, and id-token has
27// no read).
28func TestIDTokenAllowed(t *testing.T) {
29 t.Parallel()
30 write := map[string]string{"id-token": "write", "contents": "read"}
31 assert.True(t, domain.IDTokenAllowed(write, true))
32 assert.False(t, domain.IDTokenAllowed(write, false), "untrusted")
33 assert.False(t, domain.IDTokenAllowed(map[string]string{"id-token": "read"}, true), "read is not a level of id-token")
34 assert.False(t, domain.IDTokenAllowed(nil, true), "the default has no id-token")
35 assert.False(t, domain.IDTokenAllowed(map[string]string{"contents": "write"}, true))
36 all := domain.ResolvePermissions(&domain.PermissionSpec{All: domain.PermissionsWriteAll}, nil)
37 assert.True(t, domain.IDTokenAllowed(all, true), "write-all")
38 readAll := domain.ResolvePermissions(&domain.PermissionSpec{All: domain.PermissionsReadAll}, nil)
39 assert.False(t, domain.IDTokenAllowed(readAll, true), "read-all")
40}
41
42func TestOIDCSubject(t *testing.T) {
43 t.Parallel()
44 tests := []struct{ event, env, ref, want string }{
45 {"push", "", "refs/heads/main", "repo:o/r:ref:refs/heads/main"},
46 {"push", "", "refs/tags/v1", "repo:o/r:ref:refs/tags/v1"},
47 {"workflow_dispatch", "", "refs/heads/dev", "repo:o/r:ref:refs/heads/dev"},
48 {"pull_request", "", "refs/pull/1/merge", "repo:o/r:pull_request"},
49 {"pull_request_target", "", "refs/heads/main", "repo:o/r:pull_request"},
50 {"pull_request", "prod", "refs/pull/1/merge", "repo:o/r:environment:prod"},
51 {"push", "Prod", "refs/heads/main", "repo:o/r:environment:Prod"},
52 }
53 for _, tt := range tests {
54 assert.Equal(t, tt.want, domain.OIDCSubject("o/r", tt.event, tt.env, tt.ref), tt)
55 }
56}
57
58func TestOIDCKeyPublished(t *testing.T) {
59 t.Parallel()
60 at := time.Date(2026, 10, 16, 0, 0, 0, 0, time.UTC)
61 assert.True(t, domain.OIDCKey{}.Published(at), "an active key")
62 k := domain.OIDCKey{RetiredAt: at}
63 assert.True(t, k.Published(at.Add(24*time.Hour-time.Second)))
64 assert.False(t, k.Published(at.Add(24*time.Hour)))
65 assert.Equal(t, "oidc/key/abc", domain.OIDCKeyPath("abc"))
66}
67
68// An ID token request token is nobody outside the token endpoint.
69func TestOIDCRequestTokenHasNoRights(t *testing.T) {
70 t.Parallel()
71 p := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "x", OIDC: true, Permissions: map[string]string{"contents": "write"}})
72 assert.True(t, p.IsAnonymous())
73 assert.Empty(t, p.Scopes())
74}