Files
vault-plugin-secrets-arrstack/path_roles_test.go
T
unkin-agent 3418cfd8f6
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Scaffold vault-plugin-secrets-arrstack engine
Mint dynamic arrproxy machine tokens via arrproxy's bearer-gated admin API
so Terraform-driven *arr onboarding can issue and revoke per-role tokens
non-interactively.

- Add backend, config, roles, creds paths and the arrstack_token secret
- Call POST/DELETE /api/admin/tokens with a vault:arrstack:<role> subject
- Enforce apps as a non-empty subset of sonarr/radarr/prowlarr
- Cap lease renewal at the arrproxy token's fixed expiry
- Add table-driven unit tests against a fake arrproxy admin server
- Add Makefile, nfpm packaging, and pre-commit/build/test/release pipelines
2026-08-18 21:52:07 +10:00

184 lines
4.6 KiB
Go

package arrstack
import (
"context"
"testing"
"github.com/hashicorp/vault/sdk/logical"
)
func TestRole_WriteReadListDelete(t *testing.T) {
b, s := getTestBackend(t)
ctx := context.Background()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/media",
Storage: s,
Data: map[string]interface{}{
"apps": "sonarr,radarr",
"ttl": "1h",
"max_ttl": "24h",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("write role: err=%v resp=%v", err, resp)
}
resp, err = b.HandleRequest(ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "roles/media",
Storage: s,
})
if err != nil || resp == nil {
t.Fatalf("read role: err=%v resp=%v", err, resp)
}
apps := resp.Data["apps"].([]string)
// Stored de-duplicated and sorted.
if len(apps) != 2 || apps[0] != "radarr" || apps[1] != "sonarr" {
t.Fatalf("unexpected apps: %v", apps)
}
if resp.Data["ttl"].(int64) != 3600 {
t.Fatalf("unexpected ttl: %v", resp.Data["ttl"])
}
if resp.Data["max_ttl"].(int64) != 86400 {
t.Fatalf("unexpected max_ttl: %v", resp.Data["max_ttl"])
}
resp, err = b.HandleRequest(ctx, &logical.Request{
Operation: logical.ListOperation,
Path: "roles/",
Storage: s,
})
if err != nil {
t.Fatalf("list roles: %v", err)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 1 || keys[0] != "media" {
t.Fatalf("unexpected role list: %v", keys)
}
if _, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.DeleteOperation,
Path: "roles/media",
Storage: s,
}); err != nil {
t.Fatalf("delete role: %v", err)
}
role, _ := b.getRole(ctx, s, "media")
if role != nil {
t.Fatal("expected role to be gone after delete")
}
}
func TestRole_AllThreeApps(t *testing.T) {
b, s := getTestBackend(t)
ctx := context.Background()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/all",
Storage: s,
Data: map[string]interface{}{"apps": "sonarr,radarr,prowlarr"},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("write role: err=%v resp=%v", err, resp)
}
role, _ := b.getRole(ctx, s, "all")
if len(role.Apps) != 3 {
t.Fatalf("expected 3 apps, got %v", role.Apps)
}
}
func TestRole_UnknownAppRejected(t *testing.T) {
b, s := getTestBackend(t)
ctx := context.Background()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/bad",
Storage: s,
Data: map[string]interface{}{"apps": "sonarr,lidarr"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil || !resp.IsError() {
t.Fatal("expected an error for an app outside sonarr/radarr/prowlarr")
}
}
func TestRole_EmptyAppsRejected(t *testing.T) {
b, s := getTestBackend(t)
ctx := context.Background()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/empty",
Storage: s,
Data: map[string]interface{}{"ttl": "1h"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil || !resp.IsError() {
t.Fatal("expected an error when apps is empty")
}
}
func TestRole_TTLGreaterThanMaxTTLRejected(t *testing.T) {
b, s := getTestBackend(t)
ctx := context.Background()
resp, err := b.HandleRequest(ctx, &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/bad",
Storage: s,
Data: map[string]interface{}{
"apps": "sonarr",
"ttl": "48h",
"max_ttl": "1h",
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil || !resp.IsError() {
t.Fatal("expected error when ttl > max_ttl")
}
}
func TestValidateApps(t *testing.T) {
cases := []struct {
name string
in []string
want []string
wantErr bool
}{
{"single", []string{"sonarr"}, []string{"sonarr"}, false},
{"dedup and sort", []string{"radarr", "sonarr", "radarr"}, []string{"radarr", "sonarr"}, false},
{"all three", []string{"prowlarr", "sonarr", "radarr"}, []string{"prowlarr", "radarr", "sonarr"}, false},
{"empty", nil, nil, true},
{"unknown", []string{"sonarr", "lidarr"}, nil, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := validateApps(tc.in)
if (err != nil) != tc.wantErr {
t.Fatalf("validateApps err=%v wantErr=%v", err, tc.wantErr)
}
if tc.wantErr {
return
}
if len(got) != len(tc.want) {
t.Fatalf("got %v want %v", got, tc.want)
}
for i := range got {
if got[i] != tc.want[i] {
t.Fatalf("got %v want %v", got, tc.want)
}
}
})
}
}