@@ -0,0 +1,227 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user