259 lines
7.7 KiB
Go
259 lines
7.7 KiB
Go
package handlers
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/go-chi/chi/v5"
|
||
|
|
|
||
|
|
"github.com/Silo-Server/silo-server/internal/access"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAccessGroupHandlerIsDefaultRoundTrips(t *testing.T) {
|
||
|
|
store := newAccessGroupHandlerTestStore()
|
||
|
|
handler := NewAccessGroupHandler(store)
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/access-groups", strings.NewReader(`{
|
||
|
|
"name": "Users",
|
||
|
|
"is_default": true
|
||
|
|
}`))
|
||
|
|
handler.HandleCreate(rec, req)
|
||
|
|
if rec.Code != http.StatusCreated {
|
||
|
|
t.Fatalf("HandleCreate status = %d, body %s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
created := decodeAccessGroupResponse(t, rec)
|
||
|
|
if !created.IsDefault {
|
||
|
|
t.Fatalf("created is_default = false, want true")
|
||
|
|
}
|
||
|
|
|
||
|
|
rec = httptest.NewRecorder()
|
||
|
|
req = accessGroupRequestWithID(http.MethodGet, "/api/v1/admin/access-groups/1", nil, "1")
|
||
|
|
handler.HandleGet(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("HandleGet status = %d, body %s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
got := decodeAccessGroupResponse(t, rec)
|
||
|
|
if !got.IsDefault {
|
||
|
|
t.Fatalf("get is_default = false, want true")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAccessGroupHandlerUpdateDefaultUnsetsPrevious(t *testing.T) {
|
||
|
|
store := newAccessGroupHandlerTestStore()
|
||
|
|
store.groups[1] = access.Group{ID: 1, Name: "Group A", DownloadAllowed: true, RequestsAllowed: true, IsDefault: true}
|
||
|
|
store.groups[2] = access.Group{ID: 2, Name: "Group B", DownloadAllowed: true, RequestsAllowed: true}
|
||
|
|
store.nextID = 3
|
||
|
|
handler := NewAccessGroupHandler(store)
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
req := accessGroupRequestWithID(http.MethodPut, "/api/v1/admin/access-groups/2", strings.NewReader(`{
|
||
|
|
"is_default": true
|
||
|
|
}`), "2")
|
||
|
|
handler.HandleUpdate(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("HandleUpdate status = %d, body %s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
rec = httptest.NewRecorder()
|
||
|
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/access-groups", nil)
|
||
|
|
handler.HandleList(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("HandleList status = %d, body %s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
var groups []accessGroupResponse
|
||
|
|
if err := json.NewDecoder(rec.Body).Decode(&groups); err != nil {
|
||
|
|
t.Fatalf("decode list: %v", err)
|
||
|
|
}
|
||
|
|
defaults := map[int64]bool{}
|
||
|
|
for _, group := range groups {
|
||
|
|
defaults[group.ID] = group.IsDefault
|
||
|
|
}
|
||
|
|
if defaults[1] {
|
||
|
|
t.Fatalf("group A remained default after setting group B")
|
||
|
|
}
|
||
|
|
if !defaults[2] {
|
||
|
|
t.Fatalf("group B is_default = false, want true")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAccessGroupHandlerDefaultGroupGuards(t *testing.T) {
|
||
|
|
store := newAccessGroupHandlerTestStore()
|
||
|
|
store.groups[1] = access.Group{ID: 1, Name: "Default", DownloadAllowed: true, RequestsAllowed: true, IsDefault: true}
|
||
|
|
store.nextID = 2
|
||
|
|
handler := NewAccessGroupHandler(store)
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
req := accessGroupRequestWithID(http.MethodDelete, "/api/v1/admin/access-groups/1", nil, "1")
|
||
|
|
handler.HandleDelete(rec, req)
|
||
|
|
if rec.Code != http.StatusConflict {
|
||
|
|
t.Fatalf("HandleDelete(default) status = %d, want %d, body %s", rec.Code, http.StatusConflict, rec.Body.String())
|
||
|
|
}
|
||
|
|
if _, ok := store.groups[1]; !ok {
|
||
|
|
t.Fatalf("default group removed despite conflict response")
|
||
|
|
}
|
||
|
|
|
||
|
|
rec = httptest.NewRecorder()
|
||
|
|
req = accessGroupRequestWithID(http.MethodPut, "/api/v1/admin/access-groups/1", strings.NewReader(`{
|
||
|
|
"is_default": false
|
||
|
|
}`), "1")
|
||
|
|
handler.HandleUpdate(rec, req)
|
||
|
|
if rec.Code != http.StatusConflict {
|
||
|
|
t.Fatalf("HandleUpdate(demote default) status = %d, want %d, body %s", rec.Code, http.StatusConflict, rec.Body.String())
|
||
|
|
}
|
||
|
|
if !store.groups[1].IsDefault {
|
||
|
|
t.Fatalf("default group demoted despite conflict response")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type accessGroupHandlerTestStore struct {
|
||
|
|
nextID int64
|
||
|
|
groups map[int64]access.Group
|
||
|
|
}
|
||
|
|
|
||
|
|
func newAccessGroupHandlerTestStore() *accessGroupHandlerTestStore {
|
||
|
|
return &accessGroupHandlerTestStore{
|
||
|
|
nextID: 1,
|
||
|
|
groups: map[int64]access.Group{},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) List(context.Context) ([]access.Group, error) {
|
||
|
|
groups := make([]access.Group, 0, len(s.groups))
|
||
|
|
for _, group := range s.groups {
|
||
|
|
groups = append(groups, group)
|
||
|
|
}
|
||
|
|
sort.Slice(groups, func(i, j int) bool {
|
||
|
|
return groups[i].ID < groups[j].ID
|
||
|
|
})
|
||
|
|
return groups, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) Get(_ context.Context, id int64) (*access.Group, error) {
|
||
|
|
group, ok := s.groups[id]
|
||
|
|
if !ok {
|
||
|
|
return nil, access.ErrGroupNotFound
|
||
|
|
}
|
||
|
|
return &group, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) Create(_ context.Context, input access.CreateGroupInput) (*access.Group, error) {
|
||
|
|
now := time.Unix(1, 0).UTC()
|
||
|
|
group := access.Group{
|
||
|
|
ID: s.nextID,
|
||
|
|
Name: input.Name,
|
||
|
|
Description: input.Description,
|
||
|
|
LibraryIDs: append([]int(nil), input.LibraryIDs...),
|
||
|
|
MaxPlaybackQuality: input.MaxPlaybackQuality,
|
||
|
|
DownloadAllowed: input.DownloadAllowed,
|
||
|
|
DownloadTranscodeAllowed: input.DownloadTranscodeAllowed,
|
||
|
|
MaxStreams: input.MaxStreams,
|
||
|
|
MaxTranscodes: input.MaxTranscodes,
|
||
|
|
AllowedPermissions: append([]string(nil), input.AllowedPermissions...),
|
||
|
|
RequestsAllowed: input.RequestsAllowed,
|
||
|
|
IsDefault: input.IsDefault,
|
||
|
|
CreatedAt: now,
|
||
|
|
UpdatedAt: now,
|
||
|
|
}
|
||
|
|
if input.IsDefault {
|
||
|
|
s.clearDefault()
|
||
|
|
}
|
||
|
|
s.groups[group.ID] = group
|
||
|
|
s.nextID++
|
||
|
|
return &group, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) Update(_ context.Context, id int64, input access.UpdateGroupInput) (*access.Group, error) {
|
||
|
|
group, ok := s.groups[id]
|
||
|
|
if !ok {
|
||
|
|
return nil, access.ErrGroupNotFound
|
||
|
|
}
|
||
|
|
if input.Name != nil {
|
||
|
|
group.Name = *input.Name
|
||
|
|
}
|
||
|
|
if input.Description != nil {
|
||
|
|
group.Description = *input.Description
|
||
|
|
}
|
||
|
|
if input.LibraryIDs != nil {
|
||
|
|
group.LibraryIDs = append([]int(nil), (*input.LibraryIDs)...)
|
||
|
|
}
|
||
|
|
if input.MaxPlaybackQuality != nil {
|
||
|
|
group.MaxPlaybackQuality = *input.MaxPlaybackQuality
|
||
|
|
}
|
||
|
|
if input.DownloadAllowed != nil {
|
||
|
|
group.DownloadAllowed = *input.DownloadAllowed
|
||
|
|
}
|
||
|
|
if input.DownloadTranscodeAllowed != nil {
|
||
|
|
group.DownloadTranscodeAllowed = *input.DownloadTranscodeAllowed
|
||
|
|
}
|
||
|
|
if input.MaxStreams != nil {
|
||
|
|
group.MaxStreams = *input.MaxStreams
|
||
|
|
}
|
||
|
|
if input.MaxTranscodes != nil {
|
||
|
|
group.MaxTranscodes = *input.MaxTranscodes
|
||
|
|
}
|
||
|
|
if input.AllowedPermissions != nil {
|
||
|
|
group.AllowedPermissions = append([]string(nil), (*input.AllowedPermissions)...)
|
||
|
|
}
|
||
|
|
if input.RequestsAllowed != nil {
|
||
|
|
group.RequestsAllowed = *input.RequestsAllowed
|
||
|
|
}
|
||
|
|
if input.IsDefault != nil {
|
||
|
|
if *input.IsDefault {
|
||
|
|
s.clearDefault()
|
||
|
|
} else if group.IsDefault {
|
||
|
|
return nil, access.ErrDefaultGroupRequired
|
||
|
|
}
|
||
|
|
group.IsDefault = *input.IsDefault
|
||
|
|
}
|
||
|
|
group.UpdatedAt = time.Unix(2, 0).UTC()
|
||
|
|
s.groups[id] = group
|
||
|
|
return &group, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) Delete(_ context.Context, id int64) error {
|
||
|
|
group, ok := s.groups[id]
|
||
|
|
if !ok {
|
||
|
|
return access.ErrGroupNotFound
|
||
|
|
}
|
||
|
|
if group.IsDefault {
|
||
|
|
return access.ErrDefaultGroupRequired
|
||
|
|
}
|
||
|
|
delete(s.groups, id)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *accessGroupHandlerTestStore) clearDefault() {
|
||
|
|
for id, group := range s.groups {
|
||
|
|
group.IsDefault = false
|
||
|
|
s.groups[id] = group
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func accessGroupRequestWithID(method, path string, body *strings.Reader, id string) *http.Request {
|
||
|
|
var reader io.Reader
|
||
|
|
if body != nil {
|
||
|
|
reader = body
|
||
|
|
}
|
||
|
|
req := httptest.NewRequest(method, path, reader)
|
||
|
|
routeCtx := chi.NewRouteContext()
|
||
|
|
routeCtx.URLParams.Add("id", id)
|
||
|
|
return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
|
||
|
|
}
|
||
|
|
|
||
|
|
func decodeAccessGroupResponse(t *testing.T, rec *httptest.ResponseRecorder) accessGroupResponse {
|
||
|
|
t.Helper()
|
||
|
|
var response accessGroupResponse
|
||
|
|
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
||
|
|
t.Fatalf("decode response: %v", err)
|
||
|
|
}
|
||
|
|
return response
|
||
|
|
}
|