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) } } }