diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..b930642 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,30 @@ +name: Upload Go test results + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.13', '1.20', '1.21' ] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Install dependencies + run: go get . + + - name: Test with Go + run: go test -json > TestResults-${{ matrix.go-version }}.json + + - name: Upload Go test results + uses: actions/upload-artifact@v4 + with: + name: Go-results-${{ matrix.go-version }} + path: TestResults-${{ matrix.go-version }}.json diff --git a/extension.go b/extension.go index 7abb3f6..9b620bf 100644 --- a/extension.go +++ b/extension.go @@ -7,13 +7,15 @@ import "encoding/xml" type Extension struct { Type string `xml:"type,attr,omitempty"` CustomTracking []Tracking `xml:"CustomTracking>Tracking,omitempty" json:",omitempty"` - Data string `xml:",innerxml" json:",omitempty"` + // AdVerifications are IAB Open Measurement tags backported to VAST 2 and 3 as an extension + AdVerifications *[]Verification `xml:"AdVerifications>Verification,omitempty" json:",omitempty"` + Data string `xml:",innerxml" json:",omitempty"` } // the extension type as a middleware in the encoding process. type extension Extension -type extensionNoCT struct { +type extensionOnlyData struct { Type string `xml:"type,attr,omitempty"` Data string `xml:",innerxml" json:",omitempty"` } @@ -23,12 +25,12 @@ func (e Extension) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { // create a temporary element from a wrapper Extension, copy what we need to // it and return it's encoding. var e2 interface{} - // if we have custom trackers, we should ignore the data, if not, then we - // should consider only the data. - if len(e.CustomTracking) > 0 { - e2 = extension{Type: e.Type, CustomTracking: e.CustomTracking} + // if we have custom trackers or ad verifications, we should ignore the data, if not, then we + // should consider only the data + if len(e.CustomTracking) == 0 && (e.AdVerifications == nil || len(*e.AdVerifications) == 0) { + e2 = extensionOnlyData{Type: e.Type, Data: e.Data} } else { - e2 = extensionNoCT{Type: e.Type, Data: e.Data} + e2 = extension{Type: e.Type, CustomTracking: e.CustomTracking, AdVerifications: e.AdVerifications} } return enc.EncodeElement(e2, start) @@ -42,11 +44,14 @@ func (e *Extension) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error if err := dec.DecodeElement(&e2, &start); err != nil { return err } - // copy the type and the customTracking + + // copy the type, customTracking and adVerifications e.Type = e2.Type e.CustomTracking = e2.CustomTracking - // copy the data only of customTracking is empty - if len(e.CustomTracking) == 0 { + e.AdVerifications = e2.AdVerifications + + // copy the data only if customTracking and adVerifications are empty + if len(e.CustomTracking) == 0 && (e.AdVerifications == nil || len(*e.AdVerifications) == 0) { e.Data = e2.Data } return nil diff --git a/extension_test.go b/extension_test.go index 3f5abac..848de84 100644 --- a/extension_test.go +++ b/extension_test.go @@ -2,6 +2,7 @@ package vast import ( "encoding/xml" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -9,7 +10,10 @@ import ( var ( extensionCustomTracking = []byte(``) + extensionAdVerification = []byte(``) extensionData = []byte(`Generic`) + + multipleExtensions = []byte(fmt.Sprintf(`%s%s%s`, extensionCustomTracking, extensionAdVerification, extensionData)) ) func TestExtensionCustomTrackingMarshal(t *testing.T) { @@ -60,6 +64,32 @@ func TestExtensionCustomTracking(t *testing.T) { assert.Equal(t, string(extensionCustomTracking), string(xmlExtensionOutput)) } +func TestExtensionCustomAdVerification(t *testing.T) { + // unmarshal the Extension + var e Extension + assert.NoError(t, xml.Unmarshal(extensionAdVerification, &e)) + + // assert the resulting extension + assert.Equal(t, "AdVerifications", e.Type) + assert.Empty(t, e.Data) + if assert.NotNil(t, e.AdVerifications) && assert.Len(t, *e.AdVerifications, 1) { + assert.Equal(t, "doubleclickbygoogle.com-omid-video", (*e.AdVerifications)[0].Vendor) + if assert.Len(t, (*e.AdVerifications)[0].JavaScriptResource, 1) { + assert.Equal(t, JavaScriptResource{ + ApiFramework: "omid", + BrowserOptional: true, + URI: "https://example.com/verify.js", + }, (*e.AdVerifications)[0].JavaScriptResource[0]) + } + if assert.Len(t, (*e.AdVerifications)[0].TrackingEvents, 1) { + assert.Equal(t, Tracking{ + Event: "verificationNotExecuted", + URI: "https://pagead2.googlesyndication.com/pagead/interaction/?ai=Bt7src9CCZofvMqChiM0Pi8qQkAPFnbOVRgAAABABII64hW84AVjUt8DBgwRglfrwgYwHsgETZ29vZ2xlYWRzLmdpdGh1Yi5pb7oBCjcyOHg5MF94bWzIAQXaATRodHRwczovL2dvb2dsZWFkcy5naXRodWIuaW8vZ29vZ2xlYWRzLWltYS1odG1sNS92c2kvwAIC4AIA6gIlLzIxNzc1NzQ0OTIzL2V4dGVybmFsL3V0aWxpdHlfc2FtcGxlc_gC8NEegAMBkAPIBpgD4AOoAwHgBAHSBQYQj6GjiRagBiOoB7i-sQKoB5oGqAfz0RuoB5bYG6gHqpuxAqgHg62xAqgH4L2xAqgH_56xAqgH35-xAqgH-MKxAqgH-8KxAtgHAdIIMQiR4YBwEAEYHTIH64uA7r-AAToPgNCAgICAhAiAgICAgJQuSL39wTpY1cHtiZmGhwPYCAKACgWYCwGqDQJERdAVAfgWAYAXAQ&sigh=UTbooye19j8&label=active_view_verification_rejected&errorcode=%5BREASON%5D", + }, (*e.AdVerifications)[0].TrackingEvents[0]) + } + } +} + func TestExtensionGeneric(t *testing.T) { // unmarshal the Extension var e Extension @@ -77,3 +107,60 @@ func TestExtensionGeneric(t *testing.T) { // assert the resulting marshaled extension assert.Equal(t, string(extensionData), string(xmlExtensionOutput)) } + +func TestMultipleExtensions(t *testing.T) { + // unmarshal the Extensions + var inline InLine + assert.NoError(t, xml.Unmarshal(multipleExtensions, &inline)) + + extensions := *inline.Extensions + + // Check each extension + if assert.Len(t, extensions, 3) { + // Custom tracking + { + e := extensions[0] + assert.Equal(t, "testCustomTracking", e.Type) + assert.Empty(t, string(e.Data)) + if assert.Len(t, e.CustomTracking, 2) { + // first event + assert.Equal(t, "event.1", e.CustomTracking[0].Event) + assert.Equal(t, "http://event.1", e.CustomTracking[0].URI) + // second event + assert.Equal(t, "event.2", e.CustomTracking[1].Event) + assert.Equal(t, "http://event.2", e.CustomTracking[1].URI) + } + } + + // Ad verifications + { + e := extensions[1] + assert.Equal(t, "AdVerifications", e.Type) + assert.Empty(t, e.Data) + if assert.NotNil(t, e.AdVerifications) && assert.Len(t, *e.AdVerifications, 1) { + assert.Equal(t, "doubleclickbygoogle.com-omid-video", (*e.AdVerifications)[0].Vendor) + if assert.Len(t, (*e.AdVerifications)[0].JavaScriptResource, 1) { + assert.Equal(t, JavaScriptResource{ + ApiFramework: "omid", + BrowserOptional: true, + URI: "https://example.com/verify.js", + }, (*e.AdVerifications)[0].JavaScriptResource[0]) + } + if assert.Len(t, (*e.AdVerifications)[0].TrackingEvents, 1) { + assert.Equal(t, Tracking{ + Event: "verificationNotExecuted", + URI: "https://pagead2.googlesyndication.com/pagead/interaction/?ai=Bt7src9CCZofvMqChiM0Pi8qQkAPFnbOVRgAAABABII64hW84AVjUt8DBgwRglfrwgYwHsgETZ29vZ2xlYWRzLmdpdGh1Yi5pb7oBCjcyOHg5MF94bWzIAQXaATRodHRwczovL2dvb2dsZWFkcy5naXRodWIuaW8vZ29vZ2xlYWRzLWltYS1odG1sNS92c2kvwAIC4AIA6gIlLzIxNzc1NzQ0OTIzL2V4dGVybmFsL3V0aWxpdHlfc2FtcGxlc_gC8NEegAMBkAPIBpgD4AOoAwHgBAHSBQYQj6GjiRagBiOoB7i-sQKoB5oGqAfz0RuoB5bYG6gHqpuxAqgHg62xAqgH4L2xAqgH_56xAqgH35-xAqgH-MKxAqgH-8KxAtgHAdIIMQiR4YBwEAEYHTIH64uA7r-AAToPgNCAgICAhAiAgICAgJQuSL39wTpY1cHtiZmGhwPYCAKACgWYCwGqDQJERdAVAfgWAYAXAQ&sigh=UTbooye19j8&label=active_view_verification_rejected&errorcode=%5BREASON%5D", + }, (*e.AdVerifications)[0].TrackingEvents[0]) + } + } + } + + // Generic + { + e := extensions[2] + assert.Equal(t, "testCustomTracking", e.Type) + assert.Equal(t, "Generic", string(e.Data)) + assert.Empty(t, e.CustomTracking) + } + } +} diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..296194a --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vast_test.go b/vast_test.go index 833de2f..044798a 100644 --- a/vast_test.go +++ b/vast_test.go @@ -4,17 +4,19 @@ import ( "encoding/json" "encoding/xml" "fmt" - "github.com/pquerna/ffjson/ffjson" "io/ioutil" "os" "reflect" "testing" "time" + + "github.com/pquerna/ffjson/ffjson" ) func TestQuickStartComplex(t *testing.T) { skip := Duration(5 * time.Second) v := VAST{ + XMLName: xml.Name{Local: "VAST"}, Version: "4.2", Ads: []Ad{ { @@ -113,6 +115,7 @@ func TestQuickStartComplex(t *testing.T) { func TestQuickStart(t *testing.T) { d := Duration(5 * time.Second) v := VAST{ + XMLName: xml.Name{Local: "VAST"}, Mute: true, Version: "3.0", Ads: []Ad{ @@ -172,7 +175,7 @@ func TestQuickStart(t *testing.T) { }, } - want := []byte(`{"Version":"3.0","Ad":[{"ID":"123","Type":"front","InLine":{"AdSystem":{"Data":"DSP"},"AdTitle":{"Data":"adTitle"},"Impressions":[{"ID":"11111","URI":"http://impressionv1.track.com"},{"ID":"11112","URI":"http://impressionv2.track.com"}],"Creatives":[{"ID":"987","Linear":{"SkipOffset":"00:00:05","Duration":"00:00:15","TrackingEvents":[{"Event":"start","URI":"http://track.xxx.com/q/start?xx"},{"Event":"firstQuartile","URI":"http://track.xxx.com/q/firstQuartile?xx"},{"Event":"midpoint","URI":"http://track.xxx.com/q/midpoint?xx"},{"Event":"thirdQuartile","URI":"http://track.xxx.com/q/thirdQuartile?xx"},{"Event":"complete","URI":"http://track.xxx.com/q/complete?xx"}],"MediaFiles":{"MediaFile":[{"Delivery":"progressive","Type":"video/mp4","Width":1024,"Height":576,"URI":"http://mp4.res.xxx.com/new_video/2020/01/14/1485/335928CBA9D02E95E63ED9F4D45DF6DF_20200114_1_1_1051.mp4","Label":"123"}]}}}],"Extensions":[{"Type":"ClassName","Data":"AdsVideoView"},{"Type":"ExtURL","Data":"http://xxxxxxxx"}]}}],"Mute":true}`) + want := []byte(`{"XMLName":{"Space":"","Local":"VAST"},"Version":"3.0","Ad":[{"ID":"123","Type":"front","InLine":{"AdSystem":{"Data":"DSP"},"AdTitle":{"Data":"adTitle"},"Impressions":[{"ID":"11111","URI":"http://impressionv1.track.com"},{"ID":"11112","URI":"http://impressionv2.track.com"}],"Creatives":[{"ID":"987","Linear":{"SkipOffset":"00:00:05","Duration":"00:00:15","TrackingEvents":[{"Event":"start","URI":"http://track.xxx.com/q/start?xx"},{"Event":"firstQuartile","URI":"http://track.xxx.com/q/firstQuartile?xx"},{"Event":"midpoint","URI":"http://track.xxx.com/q/midpoint?xx"},{"Event":"thirdQuartile","URI":"http://track.xxx.com/q/thirdQuartile?xx"},{"Event":"complete","URI":"http://track.xxx.com/q/complete?xx"}],"MediaFiles":{"MediaFile":[{"Delivery":"progressive","Type":"video/mp4","Width":1024,"Height":576,"URI":"http://mp4.res.xxx.com/new_video/2020/01/14/1485/335928CBA9D02E95E63ED9F4D45DF6DF_20200114_1_1_1051.mp4","Label":"123"}]}}}],"Extensions":[{"Type":"ClassName","Data":"AdsVideoView"},{"Type":"ExtURL","Data":"http://xxxxxxxx"}]}}],"Mute":true}`) got, err := json.Marshal(v) t.Logf("%s", got) if err != nil { @@ -187,12 +190,13 @@ func TestQuickStart(t *testing.T) { func TestEmptyVast(t *testing.T) { v := VAST{ + XMLName: xml.Name{Local: "VAST"}, Version: "3.0", Errors: []CDATAString{ {CDATA: "http://xx.xx.com/e/error?e=__ERRORCODE__&co=__CONTENTPLAYHEAD__&ca=__CACHEBUSTING__&a=__ASSETURI__&t=__TIMESTAMP__&o=__OTHER__"}, }, } - want := []byte(`{"Version":"3.0","Errors":[{"Data":"http://xx.xx.com/e/error?e=__ERRORCODE__\u0026co=__CONTENTPLAYHEAD__\u0026ca=__CACHEBUSTING__\u0026a=__ASSETURI__\u0026t=__TIMESTAMP__\u0026o=__OTHER__"}]}`) + want := []byte(`{"XMLName":{"Space":"","Local":"VAST"},"Version":"3.0","Errors":[{"Data":"http://xx.xx.com/e/error?e=__ERRORCODE__\u0026co=__CONTENTPLAYHEAD__\u0026ca=__CACHEBUSTING__\u0026a=__ASSETURI__\u0026t=__TIMESTAMP__\u0026o=__OTHER__"}]}`) got, err := json.Marshal(v) if err != nil { t.Errorf("Marshal() error = %v", err) @@ -227,6 +231,7 @@ func createVastDemo() (*VAST, error) { mediaURI := "http://mp4.res.xxx.com/new_video/2020/01/14/1485/335928CBA9D02E95E63ED9F4D45DF6DF_20200114_1_1_1051.mp4" v := &VAST{ + XMLName: xml.Name{Local: "VAST"}, Version: "3.0", Ads: []Ad{ { @@ -318,7 +323,7 @@ func TestCreateVastJson(t *testing.T) { want []byte wantErr bool }{ - {name: "testCase1", want: []byte(`{"Version":"3.0","Ad":[{"ID":"123","Type":"front","InLine":{"AdSystem":{"Data":"DSP"},"AdTitle":{"Data":"ad title"},"Impressions":[{"ID":"456","URI":"http://impression.track.cn"}],"Creatives":[{"ID":"123456","Linear":{"Duration":"00:00:15","TrackingEvents":[{"Event":"start","URI":"http://track.xxx.com/q/start?xx"}],"MediaFiles":{"MediaFile":[{"Delivery":"progressive","Type":"video/mp4","Width":1024,"Height":576,"URI":"http://mp4.res.xxx.com/new_video/2020/01/14/1485/335928CBA9D02E95E63ED9F4D45DF6DF_20200114_1_1_1051.mp4","Label":"123"}]}}}]}}]}`), + {name: "testCase1", want: []byte(`{"XMLName":{"Space":"","Local":"VAST"},"Version":"3.0","Ad":[{"ID":"123","Type":"front","InLine":{"AdSystem":{"Data":"DSP"},"AdTitle":{"Data":"ad title"},"Impressions":[{"ID":"456","URI":"http://impression.track.cn"}],"Creatives":[{"ID":"123456","Linear":{"Duration":"00:00:15","TrackingEvents":[{"Event":"start","URI":"http://track.xxx.com/q/start?xx"}],"MediaFiles":{"MediaFile":[{"Delivery":"progressive","Type":"video/mp4","Width":1024,"Height":576,"URI":"http://mp4.res.xxx.com/new_video/2020/01/14/1485/335928CBA9D02E95E63ED9F4D45DF6DF_20200114_1_1_1051.mp4","Label":"123"}]}}}]}}]}`), wantErr: false}, } for _, tt := range tests {