228 lines
8.0 KiB
Go
228 lines
8.0 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"oci-portal/internal/oci"
|
|
)
|
|
|
|
type idpIconStubClient struct {
|
|
*fakeClient
|
|
uploadedNames []string
|
|
}
|
|
|
|
type idpCreateStubClient struct {
|
|
*fakeClient
|
|
setting oci.IdentitySettingInfo
|
|
settingErr error
|
|
settingCalls int
|
|
createCalls int
|
|
createdInput oci.CreateIdpInput
|
|
}
|
|
|
|
func (c *idpIconStubClient) UploadDomainImage(_ context.Context, _ oci.Credentials, _, _, fileName string, _ []byte) (string, string, error) {
|
|
c.uploadedNames = append(c.uploadedNames, fileName)
|
|
return "https://images.example/" + fileName, "images/" + fileName, nil
|
|
}
|
|
|
|
func (c *idpCreateStubClient) GetIdentitySetting(_ context.Context, _ oci.Credentials, _, _ string) (oci.IdentitySettingInfo, error) {
|
|
c.settingCalls++
|
|
return c.setting, c.settingErr
|
|
}
|
|
|
|
func (c *idpCreateStubClient) CreateSamlIdentityProvider(_ context.Context, _ oci.Credentials, _, _ string, in oci.CreateIdpInput) (oci.IdentityProviderInfo, error) {
|
|
c.createCalls++
|
|
c.createdInput = in
|
|
return oci.IdentityProviderInfo{Name: in.Name}, nil
|
|
}
|
|
|
|
type failingIconRandom struct{}
|
|
|
|
func (failingIconRandom) Read([]byte) (int, error) {
|
|
return 0, errors.New("entropy unavailable")
|
|
}
|
|
|
|
func newIdpIconTestService(t *testing.T) (*OciConfigService, *idpIconStubClient, uint) {
|
|
t.Helper()
|
|
base := &fakeClient{tenancy: oci.TenancyInfo{Name: "t", HomeRegionKey: "FRA"}}
|
|
client := &idpIconStubClient{fakeClient: base}
|
|
service := newTestService(t, client)
|
|
return service, client, importAliveConfig(t, service).ID
|
|
}
|
|
|
|
func newIdpCreateTestService(t *testing.T) (*OciConfigService, *idpCreateStubClient, uint) {
|
|
t.Helper()
|
|
base := &fakeClient{tenancy: oci.TenancyInfo{Name: "t", HomeRegionKey: "FRA"}}
|
|
client := &idpCreateStubClient{fakeClient: base}
|
|
service := newTestService(t, client)
|
|
return service, client, importAliveConfig(t, service).ID
|
|
}
|
|
|
|
func testCreateIdpInput(jitEnabled bool) oci.CreateIdpInput {
|
|
return oci.CreateIdpInput{
|
|
Name: "test-idp", Metadata: "<EntityDescriptor/>", JitEnabled: jitEnabled,
|
|
}
|
|
}
|
|
|
|
// iconFixture 生成带正确文件头魔数的最小样本;精简校验只核对魔数,不做深度解码。
|
|
func iconFixture(ext string) []byte {
|
|
switch ext {
|
|
case ".png":
|
|
return append([]byte("\x89PNG\r\n\x1a\n"), 0, 0, 0, 0)
|
|
case ".jpg", ".jpeg":
|
|
return []byte{0xff, 0xd8, 0xff, 0xe0, 0, 0}
|
|
case ".gif":
|
|
return []byte("GIF89a\x00\x00")
|
|
case ".webp":
|
|
return []byte("RIFF\x00\x00\x00\x00WEBPVP8 ")
|
|
case ".ico":
|
|
return []byte{0, 0, 1, 0, 1, 0}
|
|
case ".svg":
|
|
return []byte(`<svg xmlns="http://www.w3.org/2000/svg"/>`)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestValidateIdpIconAcceptsMatchingContent(t *testing.T) {
|
|
for _, ext := range []string{".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".svg"} {
|
|
t.Run(ext, func(t *testing.T) {
|
|
fileName := "icon" + ext
|
|
got, err := validateIdpIcon(fileName, iconFixture(ext))
|
|
if err != nil || got != fileName {
|
|
t.Errorf("validateIdpIcon = %q, %v; want %q, nil", got, err, fileName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateIdentityProviderForcesEmailMapping(t *testing.T) {
|
|
service, client, configID := newIdpCreateTestService(t)
|
|
client.setting.PrimaryEmailRequired = true
|
|
_, err := service.CreateIdentityProvider(context.Background(), configID, "domain", testCreateIdpInput(true))
|
|
if err != nil {
|
|
t.Fatalf("CreateIdentityProvider: %v", err)
|
|
}
|
|
if client.settingCalls != 1 || client.createCalls != 1 {
|
|
t.Fatalf("calls = setting:%d create:%d; want 1, 1", client.settingCalls, client.createCalls)
|
|
}
|
|
if !client.createdInput.JitMapEmail {
|
|
t.Error("JitMapEmail = false, want true for primary-email-required domain")
|
|
}
|
|
}
|
|
|
|
func TestCreateIdentityProviderStopsWhenSettingFails(t *testing.T) {
|
|
service, client, configID := newIdpCreateTestService(t)
|
|
client.settingErr = errors.New("setting unavailable")
|
|
_, err := service.CreateIdentityProvider(context.Background(), configID, "domain", testCreateIdpInput(true))
|
|
if err == nil || !strings.Contains(err.Error(), "get identity setting") {
|
|
t.Fatalf("CreateIdentityProvider error = %v, want setting failure", err)
|
|
}
|
|
if client.settingCalls != 1 || client.createCalls != 0 {
|
|
t.Errorf("calls = setting:%d create:%d; want 1, 0", client.settingCalls, client.createCalls)
|
|
}
|
|
}
|
|
|
|
func TestCreateIdentityProviderSkipsSettingWithoutJIT(t *testing.T) {
|
|
service, client, configID := newIdpCreateTestService(t)
|
|
client.settingErr = errors.New("must not be called")
|
|
_, err := service.CreateIdentityProvider(context.Background(), configID, "domain", testCreateIdpInput(false))
|
|
if err != nil {
|
|
t.Fatalf("CreateIdentityProvider: %v", err)
|
|
}
|
|
if client.settingCalls != 0 || client.createCalls != 1 {
|
|
t.Errorf("calls = setting:%d create:%d; want 0, 1", client.settingCalls, client.createCalls)
|
|
}
|
|
}
|
|
|
|
func TestUploadIdpIconUsesUniqueServerNames(t *testing.T) {
|
|
service, client, configID := newIdpIconTestService(t)
|
|
data := iconFixture(".png")
|
|
randomData := append(bytes.Repeat([]byte{0x11}, idpIconRandomBytes), bytes.Repeat([]byte{0x22}, idpIconRandomBytes)...)
|
|
random := bytes.NewReader(randomData)
|
|
storedNames := make([]string, 2)
|
|
for index := range storedNames {
|
|
_, storedName, err := service.uploadIdpIcon(context.Background(), configID, "domain", "Logo.PNG", data, random)
|
|
if err != nil {
|
|
t.Fatalf("upload %d: %v", index, err)
|
|
}
|
|
storedNames[index] = storedName
|
|
}
|
|
wants := []string{"idp-icon-" + strings.Repeat("11", idpIconRandomBytes) + ".png", "idp-icon-" + strings.Repeat("22", idpIconRandomBytes) + ".png"}
|
|
for index, want := range wants {
|
|
if client.uploadedNames[index] != want || storedNames[index] != "images/"+want {
|
|
t.Errorf("upload %d = %q, %q; want %q, %q", index, client.uploadedNames[index], storedNames[index], want, "images/"+want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUploadIdpIconRandomFailureSkipsUpstream(t *testing.T) {
|
|
service, client, configID := newIdpIconTestService(t)
|
|
_, _, err := service.uploadIdpIcon(context.Background(), configID, "domain", "logo.png", iconFixture(".png"), failingIconRandom{})
|
|
if err == nil || !strings.Contains(err.Error(), "generate storage name") {
|
|
t.Fatalf("UploadIdpIcon error = %v, want random source failure", err)
|
|
}
|
|
if len(client.uploadedNames) != 0 {
|
|
t.Errorf("upstream upload calls = %d, want 0", len(client.uploadedNames))
|
|
}
|
|
}
|
|
|
|
type iconRejectCase struct {
|
|
name, fileName string
|
|
data []byte
|
|
want error
|
|
}
|
|
|
|
func assertIconRejected(t *testing.T, cases []iconRejectCase) {
|
|
t.Helper()
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := validateIdpIcon(tc.fileName, tc.data)
|
|
if !errors.Is(err, tc.want) {
|
|
t.Errorf("err = %v, want errors.Is(%v)", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateIdpIconRejectsSpoofedContent(t *testing.T) {
|
|
assertIconRejected(t, []iconRejectCase{
|
|
{"extension mismatch", "icon.jpg", iconFixture(".png"), ErrIdpIconBadType},
|
|
{"random bytes as png", "icon.png", []byte("not an image"), ErrIdpIconBadType},
|
|
{"unsupported extension", "icon.bmp", iconFixture(".png"), ErrIdpIconBadType},
|
|
})
|
|
}
|
|
|
|
func TestValidateIdpIconRejectsBadNames(t *testing.T) {
|
|
assertIconRejected(t, []iconRejectCase{
|
|
{"control name", "bad\r\n.png", iconFixture(".png"), ErrIdpIconBadName},
|
|
{"long name", strings.Repeat("a", 252) + ".png", iconFixture(".png"), ErrIdpIconBadName},
|
|
})
|
|
}
|
|
|
|
func TestValidateDomainImageFileName(t *testing.T) {
|
|
token := strings.Repeat("ab", idpIconRandomBytes)
|
|
cases := []struct {
|
|
name, fileName string
|
|
valid bool
|
|
}{
|
|
{"generated image", "images/idp-icon-" + token + ".png", true},
|
|
{"empty", "", false}, {"wrong prefix", "icons/a.png", false},
|
|
{"traversal", "images/a/../secret", false}, {"backslash", `images\a.png`, false},
|
|
{"control", "images/a\x00.png", false}, {"directory", "images/", false},
|
|
{"unrelated domain image", "images/company-brand.png", false},
|
|
{"nested image", "images/generated/idp-icon-" + token + ".png", false},
|
|
{"uppercase token", "images/idp-icon-" + strings.ToUpper(token) + ".png", false},
|
|
{"wrong token length", "images/idp-icon-ab.png", false},
|
|
}
|
|
for _, tc := range cases {
|
|
err := validateDomainImageFileName(tc.fileName)
|
|
if (err == nil) != tc.valid {
|
|
t.Errorf("%s: err = %v, valid = %v", tc.name, err, tc.valid)
|
|
}
|
|
}
|
|
}
|