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 {