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: "", 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(``)
}
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)
}
}
}