- ProviderAdapter 인터페이스를 정의하고 AWS/GitLab provider 구현체를 추가한다 - CRD model에 ProviderRef 및 status 필드를 추가하여 provider 관리를 표준화한다 - ControlPlane에 provider registry를 통합하여 runtime/periodic event routing을 provider별로 분기한다 - Router에 routeByProvider 메서드를 추가하여 provider별 event 라우팅을 지원한다 - Event 시스템에 outbound event 전송 기능을 추가한다 - 관련 테스트를 추가하여 provider별 동작을 검증한다
537 lines
17 KiB
Go
537 lines
17 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.toki-labs.com/toki/gito/services/core/internal/core"
|
|
)
|
|
|
|
func TestConfigNormalizedTrimsIdentityFields(t *testing.T) {
|
|
config := Config{
|
|
ID: ProviderID(" GitHub "),
|
|
Endpoint: " https://api.github.com ",
|
|
CredentialRef: " secret://gito/providers/github ",
|
|
Capabilities: CapabilityChangeRequest | CapabilityWebhook,
|
|
}
|
|
|
|
got := config.Normalized()
|
|
|
|
if got.ID != ProviderGitHub {
|
|
t.Fatalf("ID: got %q want %q", got.ID, ProviderGitHub)
|
|
}
|
|
if got.Endpoint != "https://api.github.com" {
|
|
t.Fatalf("Endpoint: got %q", got.Endpoint)
|
|
}
|
|
if got.CredentialRef != "secret://gito/providers/github" {
|
|
t.Fatalf("CredentialRef: got %q", got.CredentialRef)
|
|
}
|
|
}
|
|
|
|
func TestCapabilityFlags(t *testing.T) {
|
|
capabilities := CapabilityChangeRequest | CapabilityWebhook | CapabilityComment
|
|
|
|
if !capabilities.Has(CapabilityChangeRequest) {
|
|
t.Fatal("expected change request capability")
|
|
}
|
|
if !capabilities.Has(CapabilityWebhook) {
|
|
t.Fatal("expected webhook capability")
|
|
}
|
|
if !capabilities.Has(CapabilityComment) {
|
|
t.Fatal("expected comment capability")
|
|
}
|
|
if capabilities.Has(CapabilityChecks) {
|
|
t.Fatal("did not expect checks capability")
|
|
}
|
|
if capabilities.Has(0) {
|
|
t.Fatal("zero capability should not be supported")
|
|
}
|
|
}
|
|
|
|
func TestConfigSupportsCapability(t *testing.T) {
|
|
config := Config{
|
|
ID: ProviderForgejo,
|
|
Capabilities: CapabilityWebhook | CapabilityChecks,
|
|
}
|
|
|
|
if !config.Supports(CapabilityWebhook) {
|
|
t.Fatal("expected webhook support")
|
|
}
|
|
if !config.Supports(CapabilityChecks) {
|
|
t.Fatal("expected checks support")
|
|
}
|
|
if config.Supports(CapabilityChangeRequest) {
|
|
t.Fatal("did not expect change request support")
|
|
}
|
|
}
|
|
|
|
func TestChangeRequestMappingCandidatesCoverProviderDifferences(t *testing.T) {
|
|
mappings := ChangeRequestMappingCandidates()
|
|
if len(mappings) != 3 {
|
|
t.Fatalf("mapping count: got %d want 3", len(mappings))
|
|
}
|
|
|
|
seen := make(map[ProviderID]ChangeRequestMappingCandidate, len(mappings))
|
|
for _, mapping := range mappings {
|
|
seen[mapping.Provider] = mapping
|
|
assertChangeRequestMappingTargets(t, mapping)
|
|
assertChangeRequestMappingStates(t, mapping)
|
|
assertChangeRequestMappingActions(t, mapping)
|
|
if len(mapping.ExtensionFields) == 0 {
|
|
t.Fatalf("%s mapping should define provider extension fields", mapping.Provider)
|
|
}
|
|
}
|
|
|
|
for _, provider := range []ProviderID{ProviderGitHub, ProviderGitLab, ProviderGitea} {
|
|
if _, ok := seen[provider]; !ok {
|
|
t.Fatalf("missing %s change request mapping", provider)
|
|
}
|
|
}
|
|
if seen[ProviderGitHub].ProviderObject != "pull_request" {
|
|
t.Fatalf("github provider object: %q", seen[ProviderGitHub].ProviderObject)
|
|
}
|
|
if seen[ProviderGitLab].ProviderObject != "merge_request" {
|
|
t.Fatalf("gitlab provider object: %q", seen[ProviderGitLab].ProviderObject)
|
|
}
|
|
if seen[ProviderGitea].ProviderObject != "pull_request" {
|
|
t.Fatalf("gitea provider object: %q", seen[ProviderGitea].ProviderObject)
|
|
}
|
|
}
|
|
|
|
func TestChangeRequestProviderExtensionsStayOutOfCoreDTO(t *testing.T) {
|
|
crType := reflect.TypeOf(core.ChangeRequest{})
|
|
for _, field := range []string{"Extensions", "ExtensionFields", "ProviderFields", "Raw"} {
|
|
if _, ok := crType.FieldByName(field); ok {
|
|
t.Fatalf("provider-specific field %q must not live on core.ChangeRequest", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertChangeRequestMappingTargets(t *testing.T, mapping ChangeRequestMappingCandidate) {
|
|
t.Helper()
|
|
targets := make(map[string]bool, len(mapping.Fields))
|
|
for _, field := range mapping.Fields {
|
|
targets[field.Target] = true
|
|
if field.Source == "" {
|
|
t.Fatalf("%s field %q has empty source", mapping.Provider, field.Target)
|
|
}
|
|
}
|
|
for _, target := range []string{
|
|
"provider",
|
|
"external_id",
|
|
"repo_id",
|
|
"source_branch",
|
|
"target_branch",
|
|
"title",
|
|
"body",
|
|
"draft",
|
|
"state",
|
|
} {
|
|
if !targets[target] {
|
|
t.Fatalf("%s mapping missing target field %q", mapping.Provider, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertChangeRequestMappingStates(t *testing.T, mapping ChangeRequestMappingCandidate) {
|
|
t.Helper()
|
|
states := make(map[core.ChangeRequestState]bool, len(mapping.States))
|
|
for _, state := range mapping.States {
|
|
states[state.State] = true
|
|
if state.Source == "" {
|
|
t.Fatalf("%s state %q has empty source", mapping.Provider, state.State)
|
|
}
|
|
}
|
|
for _, state := range []core.ChangeRequestState{
|
|
core.ChangeRequestOpen,
|
|
core.ChangeRequestMerged,
|
|
core.ChangeRequestClosed,
|
|
} {
|
|
if !states[state] {
|
|
t.Fatalf("%s mapping missing state %q", mapping.Provider, state)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertChangeRequestMappingActions(t *testing.T, mapping ChangeRequestMappingCandidate) {
|
|
t.Helper()
|
|
actions := make(map[core.ChangeRequestAction]bool, len(mapping.Actions))
|
|
for _, action := range mapping.Actions {
|
|
actions[action] = true
|
|
}
|
|
for _, action := range []core.ChangeRequestAction{
|
|
core.ChangeRequestActionCreate,
|
|
core.ChangeRequestActionUpdate,
|
|
core.ChangeRequestActionComment,
|
|
core.ChangeRequestActionRequestReview,
|
|
core.ChangeRequestActionMerge,
|
|
core.ChangeRequestActionClose,
|
|
} {
|
|
if !actions[action] {
|
|
t.Fatalf("%s mapping missing action %q", mapping.Provider, action)
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeChangeRequestAdapter struct {
|
|
created core.ChangeRequestCreate
|
|
updatedID string
|
|
updated core.ChangeRequestUpdate
|
|
commentedID string
|
|
commented core.ChangeRequestComment
|
|
reviewedID string
|
|
reviewed core.ChangeRequestReviewRequest
|
|
mergedID string
|
|
merged core.ChangeRequestMerge
|
|
closedID string
|
|
closed core.ChangeRequestClose
|
|
changeRequest core.ChangeRequest
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) CreateChangeRequest(ctx context.Context, input core.ChangeRequestCreate) (core.ChangeRequest, error) {
|
|
f.created = input
|
|
return f.changeRequest, nil
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) UpdateChangeRequest(ctx context.Context, id string, input core.ChangeRequestUpdate) (core.ChangeRequest, error) {
|
|
f.updatedID = id
|
|
f.updated = input
|
|
return f.changeRequest, nil
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) CommentChangeRequest(ctx context.Context, id string, input core.ChangeRequestComment) error {
|
|
f.commentedID = id
|
|
f.commented = input
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) RequestChangeRequestReview(ctx context.Context, id string, input core.ChangeRequestReviewRequest) error {
|
|
f.reviewedID = id
|
|
f.reviewed = input
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) MergeChangeRequest(ctx context.Context, id string, input core.ChangeRequestMerge) (core.ChangeRequest, error) {
|
|
f.mergedID = id
|
|
f.merged = input
|
|
return f.changeRequest, nil
|
|
}
|
|
|
|
func (f *fakeChangeRequestAdapter) CloseChangeRequest(ctx context.Context, id string, input core.ChangeRequestClose) (core.ChangeRequest, error) {
|
|
f.closedID = id
|
|
f.closed = input
|
|
return f.changeRequest, nil
|
|
}
|
|
|
|
var _ ChangeRequestAdapter = (*fakeChangeRequestAdapter)(nil)
|
|
|
|
func TestChangeRequestAdapterContractActions(t *testing.T) {
|
|
title := "Updated title"
|
|
draft := true
|
|
fake := &fakeChangeRequestAdapter{
|
|
changeRequest: core.ChangeRequest{
|
|
Provider: string(ProviderGitHub),
|
|
ExternalID: "42",
|
|
RepoID: "repo-1",
|
|
SourceBranch: "feature/a",
|
|
TargetBranch: "main",
|
|
Title: "Add CR model",
|
|
Body: "body",
|
|
Draft: false,
|
|
State: core.ChangeRequestOpen,
|
|
},
|
|
}
|
|
|
|
created, err := fake.CreateChangeRequest(context.Background(), core.ChangeRequestCreate{
|
|
RepoID: "repo-1",
|
|
SourceBranch: "feature/a",
|
|
TargetBranch: "main",
|
|
Title: "Add CR model",
|
|
Draft: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateChangeRequest: %v", err)
|
|
}
|
|
if created.ExternalID != "42" || fake.created.Title != "Add CR model" {
|
|
t.Fatalf("create action: created=%+v input=%+v", created, fake.created)
|
|
}
|
|
|
|
updated, err := fake.UpdateChangeRequest(context.Background(), "42", core.ChangeRequestUpdate{
|
|
Title: &title,
|
|
Draft: &draft,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateChangeRequest: %v", err)
|
|
}
|
|
if updated.ExternalID != "42" || fake.updatedID != "42" || fake.updated.Title == nil || *fake.updated.Title != title {
|
|
t.Fatalf("update action: updated=%+v id=%q input=%+v", updated, fake.updatedID, fake.updated)
|
|
}
|
|
if fake.updated.Draft == nil || *fake.updated.Draft != draft {
|
|
t.Fatalf("update draft: %+v", fake.updated)
|
|
}
|
|
|
|
if err := fake.CommentChangeRequest(context.Background(), "42", core.ChangeRequestComment{Body: "looks good"}); err != nil {
|
|
t.Fatalf("CommentChangeRequest: %v", err)
|
|
}
|
|
if fake.commentedID != "42" || fake.commented.Body != "looks good" {
|
|
t.Fatalf("comment action: id=%q input=%+v", fake.commentedID, fake.commented)
|
|
}
|
|
|
|
if err := fake.RequestChangeRequestReview(context.Background(), "42", core.ChangeRequestReviewRequest{Reviewers: []string{"alice"}}); err != nil {
|
|
t.Fatalf("RequestChangeRequestReview: %v", err)
|
|
}
|
|
if fake.reviewedID != "42" || len(fake.reviewed.Reviewers) != 1 || fake.reviewed.Reviewers[0] != "alice" {
|
|
t.Fatalf("request review action: id=%q input=%+v", fake.reviewedID, fake.reviewed)
|
|
}
|
|
|
|
merged, err := fake.MergeChangeRequest(context.Background(), "42", core.ChangeRequestMerge{
|
|
CommitTitle: "Merge CR",
|
|
CommitMessage: "Merge provider-neutral CR",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MergeChangeRequest: %v", err)
|
|
}
|
|
if merged.ExternalID != "42" || fake.mergedID != "42" || fake.merged.CommitTitle != "Merge CR" {
|
|
t.Fatalf("merge action: merged=%+v id=%q input=%+v", merged, fake.mergedID, fake.merged)
|
|
}
|
|
|
|
closed, err := fake.CloseChangeRequest(context.Background(), "42", core.ChangeRequestClose{Reason: "superseded"})
|
|
if err != nil {
|
|
t.Fatalf("CloseChangeRequest: %v", err)
|
|
}
|
|
if closed.ExternalID != "42" || fake.closedID != "42" || fake.closed.Reason != "superseded" {
|
|
t.Fatalf("close action: closed=%+v id=%q input=%+v", closed, fake.closedID, fake.closed)
|
|
}
|
|
}
|
|
|
|
// --- Fake adapter for testing ---
|
|
|
|
// fakeWebhookAdapter is a test implementation of WebhookAdapter.
|
|
type fakeWebhookAdapter struct {
|
|
verified bool
|
|
verifyErr error
|
|
candidates []WebhookCandidate
|
|
normalizeErr error
|
|
|
|
// Recorded requests for verifier and normalizer.
|
|
verifyReq WebhookRequest
|
|
normalizeReq WebhookRequest
|
|
}
|
|
|
|
func (f *fakeWebhookAdapter) VerifyWebhook(ctx context.Context, req WebhookRequest) error {
|
|
if f.verifyErr != nil {
|
|
return f.verifyErr
|
|
}
|
|
f.verified = true
|
|
f.verifyReq = req
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWebhookAdapter) NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error) {
|
|
if f.normalizeErr != nil {
|
|
return nil, f.normalizeErr
|
|
}
|
|
f.normalizeReq = req
|
|
return f.candidates, nil
|
|
}
|
|
|
|
// verifyWebhookAdapter ensures WebhookAdapter compile-time assertion.
|
|
var _ WebhookAdapter = (*fakeWebhookAdapter)(nil)
|
|
|
|
func TestWebhookAdapterContractCompileTime(t *testing.T) {
|
|
// fakeWebhookAdapter is already assigned to var _ WebhookAdapter.
|
|
// This test passes if the file compiles.
|
|
}
|
|
|
|
func TestWebhookAdapterContractRequestDelivery(t *testing.T) {
|
|
revision := &core.RevisionEvent{
|
|
RepoID: "test-repo",
|
|
Branch: "main",
|
|
Before: "abc123",
|
|
After: "def456",
|
|
ObservedAt: time.Now().UTC(),
|
|
}
|
|
now := time.Now().UTC()
|
|
|
|
req := WebhookRequest{
|
|
Provider: ProviderForgejo,
|
|
EventType: "push",
|
|
ExternalID: "delivery-123",
|
|
Payload: []byte(`{"ref":"refs/heads/main"}`),
|
|
Headers: map[string][]string{
|
|
"X-Forgejo-Signature": {"sha256=abc"},
|
|
},
|
|
Query: map[string][]string{
|
|
"repo_id": {"test-repo"},
|
|
},
|
|
ReceivedAt: now,
|
|
}
|
|
|
|
fake := &fakeWebhookAdapter{
|
|
candidates: []WebhookCandidate{
|
|
{
|
|
Type: "branch.updated",
|
|
Provider: ProviderForgejo,
|
|
ExternalID: "delivery-123",
|
|
Revision: revision,
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Verify delivery
|
|
if err := fake.VerifyWebhook(ctx, req); err != nil {
|
|
t.Fatalf("VerifyWebhook: %v", err)
|
|
}
|
|
if !fake.verified {
|
|
t.Fatal("expected verified to be true")
|
|
}
|
|
|
|
// Normalize delivery
|
|
candidates, err := fake.NormalizeWebhook(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeWebhook: %v", err)
|
|
}
|
|
if len(candidates) != 1 {
|
|
t.Fatalf("expected 1 candidate, got %d", len(candidates))
|
|
}
|
|
|
|
c := candidates[0]
|
|
if c.Type != "branch.updated" {
|
|
t.Fatalf("Type: got %q want %q", c.Type, "branch.updated")
|
|
}
|
|
if c.Provider != ProviderForgejo {
|
|
t.Fatalf("Provider: got %q want %q", c.Provider, ProviderForgejo)
|
|
}
|
|
if c.ExternalID != "delivery-123" {
|
|
t.Fatalf("ExternalID: got %q want %q", c.ExternalID, "delivery-123")
|
|
}
|
|
if c.Revision == nil {
|
|
t.Fatal("expected Revision to be set")
|
|
}
|
|
if c.Revision.RepoID != "test-repo" {
|
|
t.Fatalf("Revision.RepoID: got %q want %q", c.Revision.RepoID, "test-repo")
|
|
}
|
|
if c.Revision.Branch != "main" {
|
|
t.Fatalf("Revision.Branch: got %q want %q", c.Revision.Branch, "main")
|
|
}
|
|
|
|
// Verify that the request was actually delivered to the verifier.
|
|
if fake.verifyReq.Provider == "" {
|
|
t.Fatal("expected verifyReq.Provider to be set")
|
|
}
|
|
if fake.verifyReq.Provider != ProviderForgejo {
|
|
t.Fatalf("verifyReq.Provider: got %q want %q", fake.verifyReq.Provider, ProviderForgejo)
|
|
}
|
|
if fake.verifyReq.EventType != "push" {
|
|
t.Fatalf("verifyReq.EventType: got %q want %q", fake.verifyReq.EventType, "push")
|
|
}
|
|
if fake.verifyReq.ExternalID != "delivery-123" {
|
|
t.Fatalf("verifyReq.ExternalID: got %q want %q", fake.verifyReq.ExternalID, "delivery-123")
|
|
}
|
|
if string(fake.verifyReq.Payload) != `{"ref":"refs/heads/main"}` {
|
|
t.Fatalf("verifyReq.Payload: got %q", fake.verifyReq.Payload)
|
|
}
|
|
if len(fake.verifyReq.Headers) != 1 {
|
|
t.Fatalf("verifyReq.Headers: got %d entries, want 1", len(fake.verifyReq.Headers))
|
|
}
|
|
if sigs, ok := fake.verifyReq.Headers["X-Forgejo-Signature"]; !ok {
|
|
t.Fatal("verifyReq.Headers['X-Forgejo-Signature'] not found")
|
|
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
|
t.Fatalf("verifyReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
|
}
|
|
if len(fake.verifyReq.Query) != 1 {
|
|
t.Fatalf("verifyReq.Query: got %d entries, want 1", len(fake.verifyReq.Query))
|
|
}
|
|
if repos, ok := fake.verifyReq.Query["repo_id"]; !ok {
|
|
t.Fatal("verifyReq.Query['repo_id'] not found")
|
|
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
|
t.Fatalf("verifyReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
|
}
|
|
if !fake.verifyReq.ReceivedAt.Equal(now) {
|
|
t.Fatalf("verifyReq.ReceivedAt: got %v, want %v", fake.verifyReq.ReceivedAt, now)
|
|
}
|
|
|
|
// Verify that the request was actually delivered to the normalizer.
|
|
if fake.normalizeReq.Provider != ProviderForgejo {
|
|
t.Fatalf("normalizeReq.Provider: got %q want %q", fake.normalizeReq.Provider, ProviderForgejo)
|
|
}
|
|
if fake.normalizeReq.EventType != "push" {
|
|
t.Fatalf("normalizeReq.EventType: got %q want %q", fake.normalizeReq.EventType, "push")
|
|
}
|
|
if fake.normalizeReq.ExternalID != "delivery-123" {
|
|
t.Fatalf("normalizeReq.ExternalID: got %q want %q", fake.normalizeReq.ExternalID, "delivery-123")
|
|
}
|
|
if string(fake.normalizeReq.Payload) != `{"ref":"refs/heads/main"}` {
|
|
t.Fatalf("normalizeReq.Payload: got %q", fake.normalizeReq.Payload)
|
|
}
|
|
if len(fake.normalizeReq.Headers) != 1 {
|
|
t.Fatalf("normalizeReq.Headers: got %d entries, want 1", len(fake.normalizeReq.Headers))
|
|
}
|
|
if sigs, ok := fake.normalizeReq.Headers["X-Forgejo-Signature"]; !ok {
|
|
t.Fatal("normalizeReq.Headers['X-Forgejo-Signature'] not found")
|
|
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
|
t.Fatalf("normalizeReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
|
}
|
|
if len(fake.normalizeReq.Query) != 1 {
|
|
t.Fatalf("normalizeReq.Query: got %d entries, want 1", len(fake.normalizeReq.Query))
|
|
}
|
|
if repos, ok := fake.normalizeReq.Query["repo_id"]; !ok {
|
|
t.Fatal("normalizeReq.Query['repo_id'] not found")
|
|
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
|
t.Fatalf("normalizeReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
|
}
|
|
if !fake.normalizeReq.ReceivedAt.Equal(now) {
|
|
t.Fatalf("normalizeReq.ReceivedAt: got %v, want %v", fake.normalizeReq.ReceivedAt, now)
|
|
}
|
|
}
|
|
|
|
func TestWebhookRequestEmptyPayloadPreservation(t *testing.T) {
|
|
req := WebhookRequest{
|
|
Provider: ProviderGitHub,
|
|
EventType: "pull_request",
|
|
ExternalID: "",
|
|
Payload: []byte{},
|
|
Headers: nil,
|
|
Query: nil,
|
|
ReceivedAt: time.Time{},
|
|
}
|
|
|
|
if len(req.Payload) != 0 {
|
|
t.Fatalf("expected empty payload, got %d bytes", len(req.Payload))
|
|
}
|
|
if req.ExternalID != "" {
|
|
t.Fatalf("expected empty ExternalID, got %q", req.ExternalID)
|
|
}
|
|
if !req.ReceivedAt.IsZero() {
|
|
t.Fatalf("expected zero ReceivedAt")
|
|
}
|
|
}
|
|
|
|
func TestWebhookCandidateNoSecretRequirement(t *testing.T) {
|
|
// WebhookCandidate must not expose any secret-related fields.
|
|
// This is verified by the struct definition itself:
|
|
// - Type string
|
|
// - Provider ProviderID
|
|
// - ExternalID string
|
|
// - Revision *core.RevisionEvent
|
|
// No secret, signature, or token fields.
|
|
c := WebhookCandidate{
|
|
Type: "branch.updated",
|
|
Provider: ProviderForgejo,
|
|
ExternalID: "delivery-1",
|
|
}
|
|
|
|
// Verify the struct has no secret-related fields by construction.
|
|
// If any secret field were added, compilation would still pass,
|
|
// but the test ensures the current minimal metadata contract.
|
|
if c.Type == "" {
|
|
t.Fatal("Type should be settable")
|
|
}
|
|
if c.Provider == "" {
|
|
t.Fatal("Provider should be settable")
|
|
}
|
|
}
|